Graph Architecture¶
AgentPool compiles agents and teams into pydantic-graph workflows. This provides step-by-step execution, fork/join parallelism, and graph-level observability while preserving the existing MessageNode public API.
flowchart TB
subgraph MessageNode["MessageNode (User-Facing)"]
run["run() / run_stream()"]
signals["Signals:<br/>message_received<br/>message_sent"]
end
subgraph Step["Step (pydantic-graph v2)"]
call["step.call(StepContext)"]
queue["event_queue"]
end
subgraph GraphRun["GraphRun"]
iter["Graph.iter()"]
fork["Fork / Join"]
decision["Decision"]
end
MessageNode -->|"_step property"| Step
Step -->|"wired by GraphBuilder"| GraphRun
GraphRun -->|"yields GraphTask / EndMarker"| MessageNode
Agents as Steps¶
Every MessageNode (including agents and teams) exposes an internal _step property that wraps its execution logic as a pydantic-graph Step:
class MessageNode[TDeps, TResult](ABC):
@property
@abstractmethod
def _step(self) -> Step[AgentPoolState, TDeps, ChatMessage[Any], ChatMessage[TResult]]: ...
The Step receives a StepContext containing:
- state: AgentPoolState with prompts, kwargs, and event queue
- deps: Node dependencies (e.g., database connections)
- inputs: Input message (or None for root runs)
For single-node execution, MessageNode.run() builds a one-node graph and runs it via Graph.run(). MessageNode.run_stream() drives the same graph via Graph.iter(), draining the event queue after each step to yield RichAgentStreamEvent tokens.
Teams as Graphs¶
Sequential teams compile to chained Steps:
flowchart LR
start((start)) --> agent1[analyzer]
agent1 --> agent2[reviewer]
agent2 --> agent3[formatter]
agent3 --> end_node((end))
Parallel teams compile to Fork + Join:
flowchart TB
start((start)) --> fork{Fork}
fork --> agent1[claude]
fork --> agent2[goose]
agent1 --> join{Join}
agent2 --> join
join --> end_node((end))
The AgentPool lazily builds the graph from registered nodes and Talk connections. The graph rebuilds automatically when nodes are added or removed.
New graph: YAML Syntax¶
The graph: section maps directly to pydantic-graph's GraphBuilder API:
graph:
name: review_pipeline
steps:
- id: analyzer
agent: analyzer
- id: reviewer
agent: reviewer
- id: formatter
agent: formatter
# Implicit edges: start -> analyzer -> reviewer -> formatter -> end
Parallel execution uses list syntax for to: and from::
graph:
name: parallel_analysis
steps:
- id: researcher
agent: research_agent
- id: analyst
agent: analysis_agent
- id: summarizer
agent: summary_agent
edges:
- from: start
to: [researcher, analyst]
- from: [researcher, analyst]
to: summarizer
- from: summarizer
to: end
Conditional branching via condition::
graph:
steps:
- id: classifier
agent: classifier_agent
- id: handle_error
agent: error_agent
- id: handle_success
agent: success_agent
edges:
- from: classifier
to: handle_error
condition:
type: match
field: sentiment
value: negative
- from: classifier
to: handle_success
condition:
type: match
field: sentiment
value: positive
Edge transforms via transform::
graph:
steps:
- id: extractor
agent: extract_agent
- id: formatter
agent: format_agent
edges:
- from: extractor
to: formatter
transform: mymodule.prepare_input
Map (iterable fan-out):
graph:
steps:
- id: url_fetcher
agent: fetch_agent
- id: page_processor
agent: process_agent
- id: result_aggregator
agent: aggregate_agent
edges:
- from: url_fetcher
to: page_processor
map: true
- from: page_processor
to: result_aggregator
join: true
Signal Behavior¶
AgentPool signals are emulated at pydantic-graph step boundaries via SignalEmittingGraphRun:
| Signal | Emission Point |
|---|---|
MessageNode.message_received |
When GraphTask is yielded (step about to run) |
MessageNode.message_sent |
On the next yield (step completed) |
Talk.connection_processed |
When edge traversal produces a new GraphTask |
Talk.message_forwarded |
When a transform is applied before continuing |
The wrapper intercepts GraphRun.__anext__() without subclassing, tracks previous tasks across yields, and maps (source_node_id, destination_node_id) tuples back to Talk instances.
Streaming Behavior¶
Graph-based streaming uses Graph.iter() and maps yields to existing event types:
| Graph Yield | Event |
|---|---|
Sequence[GraphTask] |
PartStartEvent (one per task) |
| Step-internal streaming | PartDeltaEvent via StepEventCollector |
| Tool call invocation | ToolCallStartEvent + ToolCallCompleteEvent |
EndMarker |
StreamCompleteEvent with final ChatMessage |
ErrorMarker |
RunErrorEvent then re-raise |
A background task drives Graph.iter() and pushes events into an async queue, which run_stream() drains. This matches the existing native agent streaming pattern.
Migration Guide: teams: to graph:¶
Sequential team (legacy):
# Legacy syntax — still supported
teams:
review_pipeline:
mode: sequential
members: [analyzer, reviewer, formatter]
Equivalent graph syntax:
graph:
name: review_pipeline
steps:
- id: analyzer
agent: analyzer
- id: reviewer
agent: reviewer
- id: formatter
agent: formatter
Parallel team (legacy):
Equivalent graph syntax:
graph:
name: parallel_coders
steps:
- id: claude
agent: claude
- id: goose
agent: goose
edges:
- from: start
to: [claude, goose]
- from: [claude, goose]
to: end
Agent connections (legacy):
Equivalent graph syntax:
graph:
steps:
- id: picker
agent: picker
- id: analyzer
agent: analyzer
edges:
- from: picker
to: analyzer
Old configs with teams: or connections: are automatically translated to GraphConfig at load time. You can mix graph: with legacy sections, or migrate incrementally.