✨ Add structured response events to ResponseProtocol#39
Open
MFA-X-AI wants to merge 2 commits into
Open
Conversation
Adds a typed event union (tool_call, tool_result, usage) to the response models, a respond_event method on ResponseProtocol, turn-scoped event accumulation in ResponseHandler, and emission in SimpleToolOrchestrator around LLM usage and tool execution. UsageEvent subclasses LLMUsage so future usage fields flow through; tool results in events get the same json.dumps(default=repr) coercion as the conversation path. Duck-typed response handlers in the OpenAI/OpenAI-Responses/LiveKit adapters get a no-op respond_event. This gives adapters a structured surface for agent internals instead of final text only, as a prerequisite for an AG-UI adapter.
Response handlers in xaibo support what they support — partial implementations are the norm and nothing enforces the full protocol. Per standup discussion, respond_event follows the same rule rather than becoming a hard requirement: SimpleToolOrchestrator emits through an _emit_event guard that checks the handler and skips when the method is missing, so response handlers that predate it keep working unchanged. Handlers that want events extend to support them. A skipped emission never crosses the observability proxy and therefore cannot appear in the debug event trace, so the guard logs a debug line instead. The in-repo adapter handlers keep their no-op respond_event deliberately: an implemented-but-ignored event still crosses the proxy, keeping tool calls and usage visible in the debug trace when serving through these adapters. Adds a regression test running a legacy two-method response handler through the event-emitting orchestrator.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Step 2 of the AG-UI plan discussed (#38 is step 1). The goal is an
AGUIAdapterthat renders any xaibo agent in any AG-UI frontend with tool calls, steps, and reasoning visible. For that, adapters need something richer than final text to translate. The currentResponseProtocolcarries text and file attachments only — everything else an agent does is invisible at the response seam.Per the discussion, this extends the existing
ResponseProtocol/Responserather than introducing a parallel streaming protocol.If I Got It Correctly...
In practice, xaibo users write their own orchestrator modules. The ones we have now are starting templates, eg the SimpleOrchestrator and ReActOrchestrator.
ResponseProtocolis the one channel every orchestrator, shipped or custom, has to the frontend, and today it only carries text. So any orchestrator that wants to surface its internals has to invent an ad-hoc encoding inside the response text. TheReActOrchestratortemplate demonstrates exactly this workaround (🛠️ **EXECUTING TOOL:** ...emoji prose), and every downstream project would have to rebuild some variant of it, each unparseable by any standard frontend.This PR gives the seam a typed vocabulary instead, so custom orchestrators emit structured events once and every adapter can translate them uniformly.
Changes
core/models/response.py- a typed event union. The semantics are aligned with AG-UI's taxonomy so the future adapter mapping stays thin, but the classes are xaibo's own rather than imports from theag-ui-protocolSDK. Since the AG-UI is pre-1.0 and its spec still changes, keeping its types out of the core makes sense. Any future spec changes only needs to touch the adapter.ToolCallEvent(id, name, arguments)/ToolResultEvent(id, name, success, result?, error?)UsageEvent- subclasses the existingLLMUsage, so future usage fields (cached tokens, reasoning tokens) flow through without touching this model.Responsegains anevents: List[ResponseEvent]field.core/protocols/response.py- one new method:respond_event(event). Single entry point, so adding event types later doesn't grow the protocol surface a custom orchestrator programs against.primitives/modules/response.py-ResponseHandleraccumulates events into theResponse;respond(...)merges them. Events are turn-scoped:get_response()hands them off and resets, so a reused agent instance doesn't replay the previous turn's usage/tool events (text/attachment accumulation semantics are unchanged).orchestrator/simple_tool_orchestrator.py- updated as the reference for how an orchestrator uses the new surface:UsageEventafter each LLM call,ToolCallEventbefore each tool execution,ToolResultEventafter (success, failure, and exception paths converge on a single emission site). This is the template users copy when writing their own orchestrators, and it doubles as the real producer the API is tested against. Tool results in the event are coerced with ajson.dumps(..., default=repr)round-trip - the same treatment tool results already get on the conversation path in both orchestrator templates and that event payloads get in the debug trace listener (ui.py), so a raw copy here would be the inconsistency.openai.py,openai_responses.py, livekitllm.py) - their inline response handlers implementrespond_eventas a no-op: they acknowledge events but don't forward them, since their wire formats have no lane for agent internals. Because the call still happens, it crosses the observability proxy - tool calls and usage stay visible in the debug trace when serving through these adapters. The AG-UI adapter will be the first consumer that actually forwards them.Other technical details
respond_eventcalls go through the observability proxy like every module call, so delta-shaped methods would put per-token events into the event log (the log-volume concern). Whole events are one log entry per meaningful action. Token-level text deltas are a separate, later decision (needs the delta-aware event listener discussed in the standup).typediscriminator. Serializable for free, both in the event log and for future SSE adapters.respond_eventcall lands in the observability log with full envelope, so the debug UI sees tool calls and usage per agent run without any listener changes, for custom orchestrators too, automatically.ReActOrchestratortemplate is intentionally left as-is: its emoji-prose output is user-visible behavior that people may have copied, and converting it to events could be updated in another PR once this shape is agreed.Compatibility
Response/ResponseHandlerchanges are additive.ResponseProtocolimplementations: unaffected. Events are an optional capability (see below) — a handler that predatesrespond_eventkeeps working unchanged; it just doesn't receive events.Update 1: Events are an optional capability (per the discussion)
Response handlers in xaibo support what they support - partial implementations are the norm (the adapters' inline handlers implement only
respond_text+get_response, and nothing forces more). Per the discussion,respond_eventshould follow the same rule rather than becoming a hard requirement: the orchestrator template emits through a small guard (_emit_event) that checks the handler forrespond_eventand skips if absent. A handler that wants events extends to support them.One behavior note: a skipped emission never crosses the observability proxy, so it can't appear in the debug event trace - the guard logs a
logger.debugline instead so dropped events stay diagnosable. Handlers that implementrespond_event, even as a no-op like the in-repo adapters, keep full trace visibility since the call happens and is recorded before being ignored.A shared default base class (implementers get no-op event handling without writing it) is a natural follow-up once the AG-UI adapter exists as the first real event consumer to design it against.
Verification
New tests cover:
ResponseHandleraccumulation, merge, and per-turn event reset; a full agent run (MockLLM + stub tool provider through the real Exchange) asserting the event sequence[usage, tool_call, tool_result, usage]with payloads; failed-tool and exception paths; JSON-serializability of events carrying non-serializable tool results (datetime); a legacy response handler withoutrespond_eventrunning uncrashed through the event-emitting orchestrator.