Expand description
A fluent, typed builder for a graph Graph document.
The document model in crate::document is the wire format: strict,
adjacently tagged, and easy to get wrong by hand (a stray key, a payload
nested under the wrong tag, an edge that names a field instead of a node).
This builder is the author-facing front door. It never invents a new format;
it constructs the same Graph the model already defines, so whatever this
builder emits parses and validates exactly as a hand-written document would.
§What the types buy you
Each node kind has its own spec type (AgentSpec, ToolSpec,
GateSpec, BranchSpec, MapSpec, FoldSpec) whose constructor demands the
fields that kind cannot do without: an agent needs its hash, a tool needs its
name, a gate needs its approval schema. A field that belongs to one kind is
not reachable on another, so “a gate with an agent_hash” is not a runtime
error, it is a shape you cannot write. The optional fields are chained
methods, present only where the model allows them. The result is that a
STRUCTURALLY malformed document is hard to express.
§Where the builder stops
Typed construction stops at structure. It does NOT check that an agent hash
is 64 hex digits, that a map’s concurrency is positive, that edges name real
nodes, or that the graph is acyclic. Those are SEMANTIC rules, and they stay
with [crate::validate], which runs over the built Graph the same way it
runs over a parsed one. Build to get a well-shaped document; validate to
learn whether it is a legal one.
§Authoring the canonical flow
use salvor_graph::{AgentSpec, GateSpec, GraphBuilder, ToolSpec};
use serde_json::json;
let draft = json!({
"type": "object",
"properties": { "draft": { "type": "string" } },
"required": ["draft"]
});
let graph = GraphBuilder::new()
.agent(
AgentSpec::new("research", format!("sha256:{}", "1".repeat(64)))
.output_schema(draft.clone()),
)
.agent(
AgentSpec::new("review", format!("sha256:{}", "2".repeat(64)))
.input_schema(draft.clone())
.output_schema(draft),
)
.gate(
GateSpec::new(
"approve",
json!({
"type": "object",
"properties": { "approved": { "type": "boolean" } },
"required": ["approved"]
}),
)
.prompt("Approve this draft for publication?"),
)
.tool(
ToolSpec::new("publish", "http_post")
.input("body", "approve.draft")
.input("url", "config.publish_url"),
)
.edge("research", "review")
.edge("review", "approve")
.edge("approve", "publish")
.build();
// Structure is done; semantics are a separate pass.
let summary = salvor_graph::validate(&graph).expect("the canonical flow is valid");
assert_eq!(summary.entry_nodes, ["research"]);
assert_eq!(summary.terminal_nodes, ["publish"]);Structs§
- Agent
Spec - The spec for an
agentnode: a full agent loop referenced by content hash. - Branch
Spec - The spec for a
branchnode: routes on a typed output. - Fold
Spec - The spec for a
foldnode: bounded iteration that accumulates across passes. - Gate
Spec - The spec for a
gatenode: human approval that suspends the run. - Graph
Builder - Accumulates nodes and edges, then freezes them into a
Graph. - MapSpec
- The spec for a
mapnode: fan-out a sub-run per element of a typed list, with a concurrency cap. - Tool
Spec - The spec for a
toolnode: one direct tool invocation.