GraphSchema¶
The agent topology view that a Session auto-populates as agents
register. Use it to inspect what your pipeline actually looks like
after construction, to render in an external diagramming tool, or
to persist as part of a run report.
GraphSchema is the descriptor layer — separate from the event
stream that exporters consume.
Signature¶
from lazybridge import GraphSchema, Session
from lazybridge.graph import NodeType, EdgeType
# Construction (rare — Session creates one for you).
GraphSchema(session_id="")
# Methods (auto-populated by Session; explicit calls are unusual).
graph.add_agent(agent)
graph.add_router(router)
graph.add_edge(from_id, to_id, *, label="", kind=EdgeType.TOOL)
# Inspection.
graph.nodes() # list[_BaseNode]
graph.edges() # list[Edge]
graph.edges_from(node_id) # outgoing edges
graph.edges_to(node_id) # incoming edges
# Serialisation.
graph.to_dict()
graph.to_json(indent=2)
graph.to_yaml() # requires lazybridge[yaml]
graph.save("topology.json") # extension chooses format
GraphSchema.from_dict(d)
GraphSchema.from_json(s)
GraphSchema.from_file("topology.yaml")
# Enums
class NodeType:
AGENT
ROUTER
class EdgeType:
TOOL # outer Agent uses inner via tools=[...]
CONTEXT # data dependency (e.g. shared Memory)
ROUTER # routing edge from a router node
Synopsis¶
A GraphSchema records three kinds of nodes and three kinds of edges:
AgentNode— everyAgentconstructed withsession=sessgets one. Carriesid,name,provider,model,system,engine_type(the engine class name —"LLMEngine","Plan","HumanEngine","SupervisorEngine", …), andtools(a list of names of plain Python-callable tools registered on the agent; agent-as-tool wrappings are captured as edges, not listed here).RouterNode— explicit router primitive (rare; most routing is encoded directly onStep(routes=...)and shows up as an edge instead)._ToolNode— auto-registered for every plain Python-callable tool an agent exposes (i.e. tools wherereturns_envelope=False). Lets the topology show every tool stub as a separate node before any execution starts._ToolNode.typeisNodeType.TOOL; the leading underscore on the class is a hint that the class itself is a small internal stub, not an extension surface, but the enum value is part of the publicNodeTypesurface.Edge(from_id, to_id, label, kind)— three flavours:TOOL— outer agent'stools=[...]includes inner agent or a plain callable.as_toolwrappings and the auto-registered_ToolNodestubs both produce these. Edges are idempotent: the same(from, to, kind, label)quadruple registered twice is a no-op, so a tool fired N times in a run shows up once in the graph.CONTEXT— data dependency (e.g. a sharedMemoryreferenced by another agent'ssources=).ROUTER— routing edges from explicit router nodes.
The graph is descriptor-only. Reconstructing a runnable
pipeline from a saved graph is the caller's job — from_dict /
from_json give you the topology; you wire the actual LLMEngine,
Agent, Plan instances yourself.
When to use it¶
- Debugging.
print(session.graph.to_json())after a run confirms the pipeline you built matches what executed. Especially useful when you suspect a typo or missingas_toolregistration. - External rendering. Save the graph and load it in a diagramming tool, your own UI renderer, or a GraphViz pipeline. YAML / JSON output keeps the format human-friendly.
- Run reports. Persist the graph alongside event logs to document what the run looked like — useful for audits and post-mortems.
- Pipeline diffs. Hash or normalise the graph to detect unintentional changes to topology between deployments.
When NOT to use it¶
- Live event streams. Use exporters instead; the graph captures structure, not behaviour.
- Reconstructing a live pipeline from JSON.
from_*methods return descriptors only — they don't instantiateAgent/LLMEngine/Planobjects. For round-trippable Plan serialisation see Plan.to_dict (Phase 3c — Advanced). - Cost / token / latency aggregation. That's
session.usage_summary(), not the graph.
Example¶
from lazybridge import Agent, LLMEngine, Session
sess = Session()
researcher = Agent(
engine=LLMEngine("gemini-3-flash-preview"),
name="researcher",
session=sess,
)
writer = Agent(
engine=LLMEngine("gemini-3-flash-preview"),
name="writer",
session=sess,
)
# 1) The orchestrator's tools=[...] auto-registers as_tool edges.
orchestrator = Agent(
engine=LLMEngine("gemini-3-flash-preview"),
name="orchestrator",
tools=[researcher, writer],
session=sess,
)
# 2) Inspect what was built.
import json
print(json.dumps(sess.graph.to_dict(), indent=2))
# {
# "session_id": "...",
# "nodes": [
# {"id": "researcher", "type": "agent", ...},
# {"id": "writer", "type": "agent", ...},
# {"id": "orchestrator", "type": "agent", ...},
# ],
# "edges": [
# {"from": "orchestrator", "to": "researcher", "label": "as_tool", "kind": "tool"},
# {"from": "orchestrator", "to": "writer", "label": "as_tool", "kind": "tool"},
# ]
# }
# 3) Persist + reload (descriptors only; not a runnable pipeline).
sess.graph.save("topology.yaml")
from lazybridge import GraphSchema
replay = GraphSchema.from_file("topology.yaml")
assert len(replay.nodes()) == 3
assert any(e.kind == "tool" for e in replay.edges())
# 4) Filter outgoing edges from the orchestrator.
for edge in sess.graph.edges_from("orchestrator"):
print(f"{edge.from_id} -> {edge.to_id} ({edge.label})")
# 5) Render with your tool of choice — descriptor-only output is
# tool-friendly. (graphviz is illustrative, not a dependency.)
import graphviz
g = graphviz.Digraph()
for node in sess.graph.nodes():
g.node(node.id, label=node.id)
for edge in sess.graph.edges():
g.edge(edge.from_id, edge.to_id, label=edge.label)
g.render("pipeline.gv")
Pitfalls¶
- An agent without
session=is not registered anywhere. If you pass it as a nested tool to an agent with a session, the outer agent propagates its session down and registers the nested one for you — but a top-level agent with no session produces no graph entry. - Duck-typed reads of
agent.engine.providerandagent.engine.model. A custom engine that doesn't expose those attributes leaves the correspondingAgentNodefields empty (None). If you need a custom name in the topology, set them explicitly on your engine. to_yamlrequires PyYAML. Install viapip install lazybridge[yaml].to_jsonis stdlib-only.from_dict/from_jsonreconstruct descriptors only. Theprovider/modelstrings onAgentNodeare inert metadata; there's no liveLLMEnginebehind them. Don't try toagent.run(...)against a reloaded graph — wire your own agents with the same names if you want to replay.- The graph captures registration time, not runtime. A
conditional route that could fire shows up as an edge whether
or not it actually fired in this run. For per-run "what
actually happened", read the event log via
session.events.query(...)instead. - Manual
add_edgeis rare. Most edges register automatically when an agent is wrapped viaas_tool(or passed directly into another agent'stools=[...]). Usesession.register_tool_edge(outer, inner, label="")only when wiring outside ofas_tool(custom routing primitives, etc.). - Engine class name is the fallback for
provider/model. When anAgentis built with a non-LLM engine (Plan,HumanEngine,SupervisorEngine),_derive_provider_modelfillsproviderandmodelwith the engine's class name so non-LLM nodes don't render as empty strings in dumps. If you rely on theproviderfield being a real LLM provider name, filter onengine_type == "LLMEngine"first.
See also¶
- Session — owns the graph; populates it as agents register.
- Exporters — the live event stream; complements the graph's static topology view.
- As tool — every
as_toolwrapping records oneEdgeType.TOOLedge automatically. - Guides → Advanced → Plan.to_dict (Phase 3c) — round-trippable Plan serialisation, the runnable counterpart to graph descriptors.