Agent Graphs
An agent graph is a typed composition of router nodes and specialist agents that lets one assistant decompose into many. Customers running production with gecx-chat are hitting the limits of one monolithic system prompt and want a sanctioned pattern for splitting work across specialists — returns, billing, order tracking, scheduling — without building their own routing layer.
The SDK implements this as a ChatTransport wrapper. ChatSession itself is unchanged: the graph composes with any existing transport, recovery, memory, and governance wiring. For the public API surface and an end-to-end walkthrough, see Agent Graph.
Two kinds of handoff
The graph cleanly separates two kinds of handoff that used to be conflated:
| Kind | Trigger | Target | Mechanism |
|---|---|---|---|
| Internal (bot-to-bot) | Router decision | A specialist (A2A endpoint) | createAgentGraphTransport |
| External (bot-to-human) | Router decision or user request or sentiment escalation | A human agent | HandoffController.requestTransfer() |
Internal handoff is fast and observable: routing decisions happen in-process in under 200 ms (p99), and every decision emits an analytics event. External handoff is unchanged — it goes through the existing HandoffController FSM, HandoffStatus history, and AgentTransferPart-bearing wire flow.
The wire format adds one value to the existing TransferType enum: 'bot_to_bot'. The optional routeDecision and graphPath fields on AgentTransferPart and HandoffStatusEvent let downstream consumers (cockpits, debug bundles, analytics) attribute a turn to a particular specialist.
Why A2A
The natural protocol for specialist agents is Google's Agent-to-Agent (A2A) spec. Customer specialists are usually maintained by different teams; A2A's Agent Card + JSON-RPC + SSE contract is what those teams already speak.
The SDK ships an A2A client. It does not host specialists. Your team brings up the specialist however it likes — Cloud Run, a Lambda, a long-running Node process — and exposes an Agent Card at a well-known URL. The SDK consumes the card, caches it, and routes turns there.
For unit tests and offline development, createMockA2AClient provides the same shape against in-process scripts.
Three node kinds
A graph has three kinds of node:
- Routers decide. They evaluate declarative rules against a user turn (optionally augmented with an
IntentSignalfrom the signals subsystem) and dispatch to exactly one downstream node. - Specialists answer. Each specialist is a remote A2A endpoint reached via
createA2AAgentClient({ agentCardUrl }). 'human'is a reserved escalation target. Routing to'human'delegates to the existingHandoffController— there is no new path for human handoff.
user turn ──▶ router ──▶ specialist (A2A) ──▶ TransportEvents ──▶ ChatSession
│
└─▶ 'human' ──▶ HandoffController.requestTransfer(...)
Routes are typed at the call site. routeTo: 'returns' is a literal-string type tied to the specialist registry — a wrong value is a compile error, not a runtime lookup miss.
Routing as a transport wrapper
The graph runtime composes with the host's existing transport via createAgentGraphTransport(...). From ChatSession's perspective, nothing changes: it sends a turn to a transport and receives normalized events. The graph transport intercepts the turn, asks its router to pick a destination, and either:
- Streams the specialist's A2A SSE output back as ordinary
TransportEvents (internal handoff), or - Calls
requestTransfer()on the host'sHandoffController(external handoff).
Because the graph is a transport wrapper, every other SDK subsystem (recovery, memory, governance, analytics, identity) keeps working unchanged.
Observability
A graph emits six new ProductAnalyticsEvent variants — agent_graph_entered, agent_routed, agent_specialist_started, agent_specialist_completed, agent_specialist_failed, agent_graph_exited — so routing decisions are auditable in the same stream as message impressions and tool approvals. The DebugBundle gains an agentGraph snapshot with recent decisions for in-product diagnostics.
For graphs with up to ~5 nodes the bundled inspector (used by the showcase /agent-graph route and the applied-retail /support route) renders the live topology with active-node highlighting and a scrolling event feed. Larger customer graphs will need a real layout engine; out of scope for v1.
Sequence: one routed turn
sequenceDiagram
participant User
participant Session as ChatSession
participant Graph as AgentGraphTransport
participant Router as Router
participant Specialist as Returns specialist (A2A)
participant HC as HandoffController
User->>Session: "I want a refund on order 1234"
Session->>Graph: stream(send request)
Graph->>Router: evaluate(turn, intent?)
Router-->>Graph: routeTo: 'returns'
Graph->>Specialist: A2A JSON-RPC + SSE
Specialist-->>Graph: stream events
Graph-->>Session: TransportEvent stream<br/>(AgentTransferPart routeDecision: 'returns')
Session-->>User: streaming answer
Note over Router,HC: If routeTo were 'human':<br/>Graph→HC.requestTransfer(...)<br/>bot_to_human flow unchanged
Where to go next
- Agent Graph — the public API:
agentGraph,defineRouter,defineSpecialist,createA2AAgentClient,createAgentGraphTransport. - Handoff — the FSM that human escalation reuses unchanged.
- Signals — supplies the optional
IntentSignalthat routers can match against. - Showcase
/agent-graph— minimal pedagogy with live inspector. - Applied Retail
/support— production-style triage graph with returns / billing / order specialists.
docs/concepts/agents.md