Skip to main content

Module builder

Module builder 

Source
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§

AgentSpec
The spec for an agent node: a full agent loop referenced by content hash.
BranchSpec
The spec for a branch node: routes on a typed output.
FoldSpec
The spec for a fold node: bounded iteration that accumulates across passes.
GateSpec
The spec for a gate node: human approval that suspends the run.
GraphBuilder
Accumulates nodes and edges, then freezes them into a Graph.
MapSpec
The spec for a map node: fan-out a sub-run per element of a typed list, with a concurrency cap.
ToolSpec
The spec for a tool node: one direct tool invocation.