Background
ADR-0026 records the architectural decision and full rationale:
docs/adr/0026-tool-first-chat-ingress.md
The status quo today carries two parallel routing dialects:
- The mature tool-calling backbone (
ToolCallLoop + IAgentToolSource, 30+ live implementations including Scripting, Skills, NyxID, Lark) already drives "LLM decides → dispatcher executes → result flows back" — and /v1/responses already composes it on top of caller scope (nyxid-responses-direct.md §5).
- The remaining forward variants (
ForwardToGAgent, ForwardToTeam, ForwardToWorkflow) on /v1/responses and /ws/voice are an alternative dispatch chain (ResponsesEndpoints.cs:779-927, AgentRunGAgent.cs:1108-1141, voice direct-bind to actor) that could be re-expressed as the LLM choosing a tool.
The cost is paid as: parallel adapter chain on /v1/responses and /ws/voice, TargetActorId override plumbing in NyxidChat, and /v1/messages returning HTTP 501 for the dialect it cannot host.
This is a CLAUDE.md §"统一投影链路 / 单一主干" violation surfaced. ADR-0026 collapses ChatRouteAction to two variants — Reject and ForwardToModel (with tool_set_ref + tool_choice_hint) — and reframes GAgent/Team/Workflow invocation as IAgentToolSource tools.
Use case validating the shape
A user calls /v1/responses directly through NyxID (no Aevatar UI session), says "push X to my Lark", and the LLM dispatches lark_message_send under the caller's NyxID bearer; the message lands in the caller's Lark account. The additive-tools pattern already in production for use_skill / ornn_search_skills shows this is mechanically supported; the orchestration tools simply join the same category.
What this issue is
The umbrella tracking the 5-stage execution plan in ADR-0026. Each stage ships independently; sub-issues are opened as each stage starts.
Stage 1 — Tool sources land, no router changes
Stage 2 — Policy carries tool injection
Stage 3 — Deprecate legacy actions
Stage 4 — Delete legacy code paths (breaking for stragglers)
Stage 5 — Voice convergence
Verification (when full epic is done)
/v1/responses policy + tool set drives GAgent invocation end-to-end; no ForwardToGAgent adapter touched
/v1/messages no longer returns HTTP 501 for any case the legacy policy would have routed
/ws/voice model can call aevatar_invoke_gagent mid-conversation; result lands as backchannel and is spoken
- NyxID-direct caller hits
/v1/responses, says "push X to my Lark", message arrives in caller's Lark
- CI guards block emission of removed action variants
Out of scope
IActorDispatchPort / IActorRuntime / direct member invoke surface — internal actor-to-actor dispatch is untouched
/api/scopes/.../workflows workflow definition/editing endpoints
- New readmodel design —
aevatar_observe_run reads through existing projections
- VoicePresence.OpenAI GA migration itself — tracked as prerequisite issue
References
- ADR-0026 (this epic's source of truth)
- ADR-0024 (chat route policy form, partially superseded §D5)
- ADR-0025 (voice v1, superseded by D6)
- ADR-0018 (per-user NyxID binding)
docs/canon/nyxid-responses-direct.md
Background
ADR-0026 records the architectural decision and full rationale:
docs/adr/0026-tool-first-chat-ingress.mdThe status quo today carries two parallel routing dialects:
ToolCallLoop+IAgentToolSource, 30+ live implementations including Scripting, Skills, NyxID, Lark) already drives "LLM decides → dispatcher executes → result flows back" — and/v1/responsesalready composes it on top of caller scope (nyxid-responses-direct.md§5).ForwardToGAgent,ForwardToTeam,ForwardToWorkflow) on/v1/responsesand/ws/voiceare an alternative dispatch chain (ResponsesEndpoints.cs:779-927,AgentRunGAgent.cs:1108-1141, voice direct-bind to actor) that could be re-expressed as the LLM choosing a tool.The cost is paid as: parallel adapter chain on
/v1/responsesand/ws/voice,TargetActorIdoverride plumbing in NyxidChat, and/v1/messagesreturning HTTP 501 for the dialect it cannot host.This is a CLAUDE.md §"统一投影链路 / 单一主干" violation surfaced. ADR-0026 collapses
ChatRouteActionto two variants —RejectandForwardToModel(withtool_set_ref+tool_choice_hint) — and reframes GAgent/Team/Workflow invocation asIAgentToolSourcetools.Use case validating the shape
A user calls
/v1/responsesdirectly through NyxID (no Aevatar UI session), says "push X to my Lark", and the LLM dispatcheslark_message_sendunder the caller's NyxID bearer; the message lands in the caller's Lark account. The additive-tools pattern already in production foruse_skill/ornn_search_skillsshows this is mechanically supported; the orchestration tools simply join the same category.What this issue is
The umbrella tracking the 5-stage execution plan in ADR-0026. Each stage ships independently; sub-issues are opened as each stage starts.
Stage 1 — Tool sources land, no router changes
aevatar_invoke_gagentIAgentToolSource(payload schema = proto-derived strict mode)aevatar_invoke_teamIAgentToolSourceaevatar_start_workflowIAgentToolSourceaevatar_observe_runIAgentToolSourceaevatar_query_readmodelIAgentToolSourceAgentToolRequestContext(D7 prerequisite Feature/cqrs projection suite #2)/v1/responsescallsaevatar_invoke_gagent, sub-run streams events back through SSE without anyForwardToGAgentinvolvementStage 2 — Policy carries tool injection
ForwardToModel.tool_set_ref(typed sub-message)ForwardToModel.tool_choice_hint(typed sub-message)ChatRouteResolvertranslates rules that previously emittedForwardToGAgent/TeamintoForwardToModel + tool_choice_hintChatRunActor(session-scoped) for SSE sessions; sub-run tracking in actor State, not middleware dictionaryworkspace.default,lark.self_notify,voice.realtime, …)Stage 3 — Deprecate legacy actions
ForwardToGAgent/ForwardToTeam/ForwardToWorkflowForwardToModel + tool_choice_hintStage 4 — Delete legacy code paths (breaking for stragglers)
ResponsesEndpoints.cs:779-927(legacy forward-to-actor branches)AgentRunGAgent.cs:1108-1141(TargetActorId override)ForwardToGAgent/ForwardToTeam/ForwardToWorkflowdeprecated (kept on wire; tag reuse forbidden)/v1/messages501 fallback for these actions removedForwardToModelorRejectStage 5 — Voice convergence
VoiceSessionActorimplementation (session-scoped, owns Realtime WS + tool subscriptions)/ws/voiceno longer binds to actor at upgrade; resolver returnsForwardToModel(tool_set_ref=...)session.updatedeclares resolved tool setconversation.item.create+response.createfor async tool results/ws/voice/{actorId}dev bypass unchanged (ADR-0024 D4)Verification (when full epic is done)
/v1/responsespolicy + tool set drives GAgent invocation end-to-end; noForwardToGAgentadapter touched/v1/messagesno longer returns HTTP 501 for any case the legacy policy would have routed/ws/voicemodel can callaevatar_invoke_gagentmid-conversation; result lands as backchannel and is spoken/v1/responses, says "push X to my Lark", message arrives in caller's LarkOut of scope
IActorDispatchPort/IActorRuntime/ direct member invoke surface — internal actor-to-actor dispatch is untouched/api/scopes/.../workflowsworkflow definition/editing endpointsaevatar_observe_runreads through existing projectionsReferences
docs/canon/nyxid-responses-direct.md