Framework-agnostic agent graph schema for AI agent workflows as directed acyclic graphs
Project description
quartermaster-graph
Framework-agnostic graph schema for defining AI agent workflows as directed graphs.
Features
- Pydantic-based models:
Agent,GraphSpec,GraphNode,GraphEdgewith full validation - GraphBuilder fluent API for programmatic graph construction (
Graphconvenience alias) - 40+ node types covering LLM, control flow, data, user interaction, memory, and utility
- YAML/JSON serialization with round-trip fidelity
- Graph validation: start/end nodes, cycle detection, orphan detection, edge label checks
- Graph traversal: topological sort, path finding, successor/predecessor queries
- Pre-built templates: simple chat, decision tree, multi-agent supervisor, and more
- Typed metadata schemas for each node type
New in v0.5.0
program_runner(program=<callable>)-- pass a@tool()-decorated function directly instead of its name string; the graph builder auto-registers it. Parity with.agent(tools=[...]).
Installation
pip install quartermaster-graph
Dependencies: pydantic>=2.0, pyyaml>=6.0
Quick Start
Build a Graph with the Fluent API
from quartermaster_graph import Graph
graph = (
Graph("Customer Support Agent")
.user("How can I help you?")
.instruction("Classify intent", model="gpt-4o", system_instruction="Classify the user's intent.")
.decision("Route by intent", options=["billing", "technical", "general"])
.on("billing").instruction("Handle billing", model="gpt-4o").end()
.on("technical").instruction("Handle technical", model="gpt-4o").end()
.on("general").instruction("Handle general", model="gpt-4o").end()
.end()
)
Graph is a convenience alias for GraphBuilder. Both .build() and .to_graph() return a GraphSpec:
from quartermaster_graph import GraphBuilder
builder = GraphBuilder("My Agent")
builder.instruction("Process", model="gpt-4o").end()
spec = builder.build() # returns GraphSpec
spec = builder.to_graph() # same thing
Load a Graph from YAML
# agent.yaml
agent_id: "550e8400-e29b-41d4-a716-446655440000"
start_node_id: "00000000-0000-0000-0000-000000000001"
nodes:
- id: "00000000-0000-0000-0000-000000000001"
type: "Start1"
name: "Start"
- id: "00000000-0000-0000-0000-000000000002"
type: "User1"
name: "Input"
- id: "00000000-0000-0000-0000-000000000003"
type: "Instruction1"
name: "Process"
metadata:
llm_system_instruction: "Analyze user input"
llm_model: "gpt-4o"
- id: "00000000-0000-0000-0000-000000000004"
type: "End1"
name: "End"
edges:
- source_id: "00000000-0000-0000-0000-000000000001"
target_id: "00000000-0000-0000-0000-000000000002"
- source_id: "00000000-0000-0000-0000-000000000002"
target_id: "00000000-0000-0000-0000-000000000003"
- source_id: "00000000-0000-0000-0000-000000000003"
target_id: "00000000-0000-0000-0000-000000000004"
from quartermaster_graph import from_yaml
with open("agent.yaml") as f:
spec = from_yaml(f.read())
API Reference
Core Models
| Model | Description |
|---|---|
Agent |
Top-level agent definition (name, description, tags) |
GraphSpec |
Graph definition: nodes, edges, start node, features |
GraphNode |
A node with type, metadata, traversal config, error handling |
GraphEdge |
Directed edge with optional label and routing points |
NodePosition |
Visual position for editor rendering |
AgentGraphandAgentVersionboth exist as deprecated backward-compatibility aliases forGraphSpec.
GraphSpec Methods
| Method | Return Type | Description |
|---|---|---|
get_node(node_id) |
GraphNode | None |
Find a node by ID |
get_start_node() |
GraphNode | None |
Get the start node |
get_successors(node_id) |
list[GraphNode] |
All successor nodes |
get_predecessors(node_id) |
list[GraphNode] |
All predecessor nodes |
get_edges_from(node_id) |
list[GraphEdge] |
All edges from a node |
get_edges_to(node_id) |
list[GraphEdge] |
All edges to a node |
GraphBuilder Methods
Start / End
| Method | Returns | Description |
|---|---|---|
.start() |
GraphBuilder |
Add a Start node (auto-inserted since v0.2.0 — rarely needed explicitly) |
.end() |
GraphBuilder |
Add an End node (or close a branch) |
LLM Nodes
| Method | Returns | Description |
|---|---|---|
.instruction(name, model, provider, temperature, system_instruction, **kwargs) |
GraphBuilder |
LLM text generation, no tools, streams response |
.decision(name, model, provider, temperature, prefix_message, suffix_message, options, **kwargs) |
GraphBuilder |
LLM picks one path via pick_path tool (non-streaming) |
| .summarize(name, model, provider, temperature, system_instruction, **kwargs) | GraphBuilder | LLM condenses conversation history |
| .agent(name, model, provider, system_instruction, tools, max_iterations, **kwargs) | GraphBuilder | Agentic loop with tools, up to max_iterations |
| .vision(name, model, provider, system_instruction, **kwargs) | GraphBuilder | Image vision/analysis node |
| .merge(name, model, provider, temperature, system_instruction, prefix_message, suffix_message, **kwargs) | GraphBuilder | LLM merges parallel branch outputs |
Control Flow
| Method | Returns | Description |
|---|---|---|
.on(label) |
BranchBuilder |
Start a named branch for a decision/if/switch |
.if_node(name, expression) |
GraphBuilder |
Conditional branch via expression, no LLM |
.static_decision(name, expression) |
GraphBuilder |
Expression-based branching, no LLM |
.user_decision(name) |
GraphBuilder |
User picks which path to follow |
.switch(name, cases, default_edge_id) |
GraphBuilder |
Multi-way switch, first matching case wins |
.parallel(name) |
GraphBuilder |
Start a parallel fan-out |
.branch() |
BranchBuilder |
Start a parallel branch |
.static_merge(name, text) |
GraphBuilder |
Merge parallel branches without LLM |
.break_node(name, targets) |
GraphBuilder |
Stop backward message collection |
User Interaction
| Method | Returns | Description |
|---|---|---|
.user(name, prompts) |
GraphBuilder |
Pause flow for user input |
.user_form(name, parameters) |
GraphBuilder |
Show a structured form to the user |
Data Nodes
| Method | Returns | Description |
|---|---|---|
.static(name, text) |
GraphBuilder |
Fixed text content, no LLM |
.code(name, code, filename) |
GraphBuilder |
Code execution node |
.text(name, template) |
GraphBuilder |
Jinja2 template rendering |
.var(name, variable, expression) |
GraphBuilder |
Evaluate expression, store as variable |
.text_to_variable(name, variable, source) |
GraphBuilder |
Convert text output to a variable |
.program_runner(name, program, **kwargs) |
GraphBuilder |
Run a program/tool inline |
Memory
| Method | Returns | Description |
|---|---|---|
.read_memory(name, memory_name, memory_type, variable_names) |
GraphBuilder |
Read from persistent memory |
.write_memory(name, memory_name, memory_type, variables) |
GraphBuilder |
Write to persistent memory |
.update_memory(name, memory_name, memory_type, variables) |
GraphBuilder |
Update existing memory variables |
.flow_memory(name, memory_name, initial_data) |
GraphBuilder |
Define flow-scoped memory |
.user_memory(name, memory_name, initial_data) |
GraphBuilder |
Define user-scoped persistent memory |
Composition / Utility
| Method | Returns | Description |
|---|---|---|
.sub_agent(name, graph_id) |
GraphBuilder |
Call another agent graph synchronously |
.use(sub_graph) |
GraphBuilder |
Inline a sub-graph (accepts GraphSpec or GraphBuilder) |
.comment(name, text) |
GraphBuilder |
Documentation-only node, no runtime logic |
.allowed_agents(*agent_ids) |
GraphBuilder |
Restrict which sub-agents can be spawned |
.node(node_type, name, metadata, **kwargs) |
GraphBuilder |
Add any node type generically |
.edge(source_id, target_id, label, is_main) |
GraphBuilder |
Manually add an edge |
.connect(from_name, to_name, label) |
GraphBuilder |
Create an edge between two nodes by name |
Build / Export
| Method | Returns | Description |
|---|---|---|
.build(validate=True) |
GraphSpec |
Build the graph, optionally validate |
.to_graph(validate=True, agent_id=None) |
GraphSpec |
Build with optional explicit agent ID |
.to_agent(validate=True) |
Agent |
Export as a full Agent model |
Node Configuration Kwargs
All builder methods accept optional keyword arguments:
| Kwarg | Type | Description |
|---|---|---|
traverse_in |
TraverseIn |
When to execute: AWAIT_FIRST or AWAIT_ALL |
traverse_out |
TraverseOut |
Which successors to trigger |
thought_type |
ThoughtType |
How to build conversation context |
message_type |
MessageType |
What message role to use |
show_output |
bool |
Whether to display this node's output (default True) |
error_handling |
ErrorStrategy |
What to do on failure |
Loops and Cycles
Use connect() to create back-edges for iterative flows:
agent = (
Graph("Refiner")
.user("Input")
.var("Init", variable="round", expression="1")
.text("Header", template="Round {{round}}", traverse_in=TraverseIn.AWAIT_FIRST)
.instruction("Process", system_instruction="Improve the text")
.var("Increment", variable="round", expression="round + 1")
.if_node("Done?", expression="round > 3")
.on("true").text("Done", template="Complete").end()
.on("false").text("Continue", template="", show_output=False).end()
.end()
)
agent.connect("Continue", "Header", label="loop")
graph = agent.build(validate=False) # skip validation for intentional cycles
Validation
from quartermaster_graph import validate_graph
errors = validate_graph(agent_graph)
for err in errors:
print(f"[{err.severity}] {err.code}: {err.message}")
Validation checks:
- Exactly one Start node, at least one End node
- All edge source/target IDs reference existing nodes
- Orphan detection (unreachable from Start)
- Cycle detection (DAG property)
- Decision/If/Switch nodes have proper edge labels
Serialization
from quartermaster_graph import to_json, from_json, to_yaml, from_yaml, json_schema
# JSON round-trip
data = to_json(agent_graph)
restored = from_json(data)
# YAML round-trip
yaml_str = to_yaml(agent_graph)
restored = from_yaml(yaml_str)
# JSON Schema for cross-language validation
schema = json_schema()
Traversal Utilities
from quartermaster_graph import (
get_start_node, get_successors, get_predecessors,
get_path, topological_sort, find_merge_points, find_decision_points,
)
start = get_start_node(agent_graph)
ordered = topological_sort(agent_graph)
path = get_path(agent_graph, start_id, end_id)
Enums
| Enum | Values |
|---|---|
NodeType |
40+ types: START, END, INSTRUCTION, DECISION, IF, SWITCH, AGENT, USER, USER_FORM, USER_DECISION, CODE, MERGE, STATIC, STATIC_MERGE, STATIC_DECISION, VAR, TEXT, SUMMARIZE, SUB_ASSISTANT, BREAK, COMMENT, etc. |
TraverseIn |
AWAIT_ALL, AWAIT_FIRST |
TraverseOut |
SPAWN_ALL, SPAWN_NONE, SPAWN_START, SPAWN_PICKED |
ThoughtType |
SKIP, NEW, NEW_HIDDEN, NEW_COLLAPSED, INHERIT, CONTINUE, EDIT_OR_NEW, EDIT_SAME, APPEND, USE_PREVIOUS, etc. |
MessageType |
AUTOMATIC, USER, ASSISTANT, SYSTEM, TOOL, VARIABLE |
ErrorStrategy |
STOP, RETRY, SKIP, CONTINUE, CUSTOM |
ExceptionResolution |
RETRY, BREAK, CONTINUE |
Integration with Sibling Packages
# Build a graph (quartermaster-graph)
from quartermaster_graph import Graph
graph = Graph("Agent").user("Input").instruction("Process").end()
# Execute it (quartermaster-engine)
from quartermaster_engine import FlowRunner
runner = FlowRunner(graph=graph.build(), node_registry=registry)
result = runner.run("Hello!")
Contributing
See CONTRIBUTING.md for guidelines.
License
Apache License 2.0 -- see LICENSE for details.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file quartermaster_graph-0.6.3.tar.gz.
File metadata
- Download URL: quartermaster_graph-0.6.3.tar.gz
- Upload date:
- Size: 66.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.15
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
887bb5b247af29a255008202cd73e58f9282d2d3e404b244c85c2e7565b54442
|
|
| MD5 |
9a483a002d0b8724837ccbeb87233c0b
|
|
| BLAKE2b-256 |
fc8c8fdb20a9c1ed10dbc65abb602c0ca5a8ee94d1f05a8f0449da194748d0e1
|
File details
Details for the file quartermaster_graph-0.6.3-py3-none-any.whl.
File metadata
- Download URL: quartermaster_graph-0.6.3-py3-none-any.whl
- Upload date:
- Size: 43.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.15
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e9b04987b9872ee4bd78b1a6490a658fe63d9c6055147f07915cb72f3d83c734
|
|
| MD5 |
2fb5cc2f00d7429bd04454f2a5524956
|
|
| BLAKE2b-256 |
f99d50e96240278dbb4fb4237f8db7e67c24de35d37f0912b2a8fa82aaa51392
|