The secondary GenerateA2UITool previously hardcoded openai.OpenAI() + gpt-4.1,
forcing non-OpenAI users to keep an OPENAI_API_KEY around just for the A2UI
dashboard demo even when LANGROID_MODEL pointed at another provider.
Route the planner through lm.OpenAIGPT (langroid's universal LLM abstraction)
keyed on A2UI_MODEL (override) → LANGROID_MODEL (inherit) → gpt-4.1 (default).
Default uses the bare model name because `openai/` is NOT a langroid-native
prefix — langroid passes it literally to the OpenAI SDK which rejects it.
Additional hardening:
- _A2uiError TypedDict with _A2uiErrorKind enum, factory with non-empty-string
validation that raises ValueError (not assert, so python -O can't strip it)
- _ARGS_MISSING sentinel distinguishes "no tool call" from "degraded shape"
so the frontend gets correct remediation
- Narrow re-raise tuple propagates programmer errors (AttributeError, TypeError,
NameError, ImportError, ModuleNotFoundError, AssertionError, NotImplementedError,
pydantic.ValidationError) while wrapping recoverable SDK errors into _a2ui_error
- _ToolErrorKind enum for backend tool error codes — typo-safe parity with _A2uiErrorKind
- Boundary validation after build_a2ui_operations_from_tool_call return
- Backend tool handlers wrap impls in try/except so exceptions don't escape
to langroid's tool machinery
- @lru_cache(maxsize=4) on _get_a2ui_llm keyed on resolved model string
- FRONTEND_TOOL_NAMES canary raises RuntimeError at import time on drift
- No top-level import of openai (AST walker test pins this contract)
Covered by 109 tests (integration test gated by LANGROID_INTEGRATION_TESTS=1).
- Narrow the mid-stream except around agent.llm_response_async from a
broad Exception to (openai.APIError, httpx.HTTPError,
asyncio.TimeoutError, pydantic.ValidationError). Programmer bugs
(AttributeError, NameError, TypeError) now propagate instead of
being sanitized as 'Agent run failed: <ClassName>' — keeps real
bugs visible in server logs and stack traces, where they belong.
Expected runtime errors still get the sanitized TEXT_MESSAGE +
RUN_FINISHED treatment so the UI never hangs mid-stream.
- Update _parse_tool_args call-site comment in handle_run: it used
to say 'returns None' when args could not be parsed; the function
now returns ParsedArgs with status in {ok, empty, malformed}, and
the caller already pattern-matches on .usable.
- Fix stale _try_parse_tool docstring that referenced the old
agent.agent_response() API; the adapter has used
llm_response_async() for a while now.
Tests: add test_llm_response_async_programmer_bug_propagates — mocks
llm_response_async to raise AttributeError('typo') and asserts the
exception propagates out of the SSE generator (not sanitized).
Update the existing mid-stream-failure test to raise
asyncio.TimeoutError (a covered exception) instead of RuntimeError
so it continues to exercise the sanitization path.
- _try_parse_tool function_call path now accepts (str, bytes, bytearray)
for arguments, matching _parse_tool_args. Real OpenAI/httpx stacks can
deliver bytes; the old narrow isinstance(args, str) guard fell through
to tool_cls(**args) with raw bytes and silently TypeError'd.
- Wrap agent.llm_response_async in try/except inside event_stream. On
failure after RUN_STARTED, emit a sanitized TEXT_MESSAGE triple and
RUN_FINISHED so the frontend never hangs waiting for stream closure.
- Wrap request.json() and RunAgentInput(**body) with explicit handlers
that return structured JSONResponse errors carrying an errorId for
client-side correlation (400 for bad JSON, 422 for schema mismatch).
- Defensive guard on run_input.messages: skip entries whose role or
content are not strings, with a warning. Prevents f-string coercion
of None/complex values into garbage prompts.
- Wrap the content-JSON-path asyncio.to_thread in try/except that emits
a sanitized error + RUN_FINISHED before re-raising, so scheduler-level
failures do not kill the SSE generator mid-stream.
- Duplicate-tool RuntimeError now includes fully qualified
module.Class identities for each colliding ToolMessage, instead of
just bare request names.
Tests: 30/30 pass (added 5 new tests covering bytes args, mid-stream
failure, malformed JSON body, invalid RunAgentInput, non-string
role/content, collision error identity).
Changes to src/agents/agui_adapter.py:
- Extract _execute_backend_tool / _run_backend_tool helpers so the
oai_tool_calls path and the content-JSON path share one sanitization
contract. The _try_parse_tool backend-tool path no longer runs
.handle() barefoot — it goes through _execute_backend_tool which
catches the narrowed (pydantic.ValidationError, ValueError), logs
the traceback server-side, and returns a sanitized JSON error payload.
- Narrow tool-execution except from
(RuntimeError, ValueError, TypeError, KeyError, pydantic.ValidationError)
to (pydantic.ValidationError, ValueError). RuntimeError/KeyError from
unrelated libraries or real config drift now propagate as real bugs
rather than being misclassified as user-facing tool failures.
- Replace dict | None three-way return of _parse_tool_args with a
ParsedArgs dataclass (args + status: 'ok' / 'empty' / 'malformed' +
.usable helper). Call sites pattern-match on status explicitly.
- Treat empty-string raw_args (legacy function_call path) as 'malformed'
instead of 'ok with {}' — firing a tool with empty args produces a
meaningless UI card, same rationale as the oai path.
- Drop TypeError from the json.loads except in _try_parse_tool (it
signals a programmer bug, not plain text). Add an
isinstance(content, (str, bytes, bytearray)) guard that logs a
WARNING rather than silencing the case.
- Drop pydantic.errors.PydanticUserError from tool-instantiation except
lists — it signals malformed model *definitions*, not runtime data
errors, and must surface loudly at startup.
- Freeze _TOOL_BY_NAME behind types.MappingProxyType so post-import
mutation raises TypeError (e.g. accidental monkeypatch / shadowing).
Changes to tests/python/conftest.py:
- Tighten the docstring cross-reference to src/agent_server.py (the
real entry point confirmed to import agents.agui_adapter identically).
Changes to tests/python/test_agui_adapter.py:
- Update helper tests to assert on ParsedArgs.status / .usable / .args.
- Flip empty-string expectation to 'malformed' per the new contract.
- Add: bytes-input parse, unknown-type 'empty' status, MappingProxyType
frozen-ness of _TOOL_BY_NAME, unified-helper happy path, helper-level
sanitization + traceback capture, helper propagates non-narrowed
RuntimeError, _try_parse_tool non-str/bytes guard warns and returns
None, and str(exc) never appears anywhere in the SSE stream bytes.
- Switch sanitized-error test's raise to ValueError (narrowed except
covers it) — previously RuntimeError leaked through by accident.
24/24 tests pass (16 existing + 8 new).
- agui_adapter: compute thread_id once so RUN_STARTED/RUN_FINISHED never
disagree when the caller omits it.
- Narrow silent `except Exception: pass` sites, log parse/tool failures
with sanitized context, and drop the dead `agent.agent_response` path
(returns ChatDocument, not ToolMessage).
- Extract `_emit_text_block` helper DRYing the empty-delta guard, and a
`_parse_tool_args` helper with explicit warning on malformed input.
- Assert tool-class request-name uniqueness at import so collisions fail
loudly instead of silently shadowing.
- Replace `str(response)` fallback with documented empty default.
- Add unit tests covering the OAI tool-calls happy path, malformed args,
legacy function_call shape, empty-content skip, thread_id stability,
and tool-class uniqueness (red-green verified).