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:

KindTriggerTargetMechanism
Internal (bot-to-bot)Router decisionA specialist (A2A endpoint)createAgentGraphTransport
External (bot-to-human)Router decision or user request or sentiment escalationA human agentHandoffController.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:

  1. Routers decide. They evaluate declarative rules against a user turn (optionally augmented with an IntentSignal from the signals subsystem) and dispatch to exactly one downstream node.
  2. Specialists answer. Each specialist is a remote A2A endpoint reached via createA2AAgentClient({ agentCardUrl }).
  3. 'human' is a reserved escalation target. Routing to 'human' delegates to the existing HandoffController — 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's HandoffController (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 IntentSignal that routers can match against.
  • Showcase /agent-graph — minimal pedagogy with live inspector.
  • Applied Retail /support — production-style triage graph with returns / billing / order specialists.
Source: docs/concepts/agents.md