examples/custom-graph.ts (excerpt)
compileGraph(graphType, definition) validates the definition before it
calls the graph type, and the compiled graph’s definition field holds a
frozen copy with its canonical JSON and a SHA-256 digest. A definition
needs:
- A stable
idand a positivedefinitionVersion. - Graph
data, whatever your type reads. - Nodes with stable
idvalues anddata, and edges with stableid,source,targetanddata.
Implement the graph type
A graph type has a stablekind, a positive version and one compile
method, which receives the validated, frozen definition and a GraphKernel
for node and neighbour lookups:
examples/custom-graph.ts (excerpt)
compile returns pure operations:
reduce and decide receive frozen copies of their inputs and must return
JSON data; the same definition and events must give the same state and
commands. decide returns zero or more dispatch commands, or exactly one
pause, complete or fail. An empty decision means start nothing new,
and the executor accepts it only while a recorded attempt is in flight;
without in-flight work it fails instead of asking forever. position is the
stable identity of one requested node occurrence, unique within a decision;
once the history records a dispatch, decide must not return it again, and
a later event can make the same node dispatchable as a new occurrence with
its own position.
An optional validateNodeResult(nodeId, result) runs before the executor
saves a completion, with a frozen copy of the result. null accepts it; an
issue with string code, path and message records node-failed with
RESULT_INVALID. A malformed return or an exception propagates as an error.
GraphBindings<Requirements> maps a graph requirement to a host binding
type: a required memory requirement to a required Memory binding, an
unused one to a type with no memory binding. This layer never creates,
injects or executes a binding.
Describe the graph
describe() declares what a host inspects before anything runs: phases,
nodes, edges, input and output contracts, policies, execution lanes,
requested permissions and bounds:
examples/custom-graph.ts (excerpt)
compileGraph supplies the edges list in the
definition’s edge order. Each node’s lane must exist in executionLanes,
and a node’s phaseId must name the phase that lists it. Each bound is
known or unknown: use a known bound only when the graph can calculate the
value, and an unknown bound with a reason when it can’t. Don’t invent a
maximum or a concurrency value. validateGraphDescription(unknown) validates
a description that didn’t come from compileGraph and returns it frozen;
invalid data, including JSON nested more than 256 levels, throws
GraphValidationError.
Prove conformance
runGraphTypeConformance checks a fixture without a test framework. The
fixture declares the expected initial state and command, then the expected
state and command after every event prefix:
examples/custom-graph.ts (excerpt)
maxConcurrency, since the
trace doesn’t show which attempts overlap; the graph declares that cap or
reports it unknown. Each expected decision must hold only new requests, and
when the final decision is exactly complete, the observed total must meet
a known dispatch minimum. Run the file with npx tsx custom-graph.ts:
Output
done, the next decision is
complete, and the plan’s bounds are the two dispatches the description
declared.
Build a callable team
team(config) returns a Job: callable work for several agents with one
task and separate answers. It doesn’t use the stored graph executor, and
passing it to run() doesn’t give it replay. For saved room messages and
turns requested by mentions, compile teamGraphType as
Team conversation shows. The config has:
The result,
TeamResult in the outcome’s data, holds the task, each
member’s name, role and outcome, whether the work was integrated,
and the review’s outcome where there was one. While a callback review
waits, it also holds the question’s requestId.
In a Git workspace, each agent gets the task and its standing brief in its
own worktree through isolated(), and when an agent passes its changes
merge into your branch under the merge lock all isolated() jobs share.
Outside a Git repository, isolated() warns and runs in the shared
workspace, so a passing team result doesn’t prove a merge happened; team()
adds no merge or lease of its own. review takes a review panel or a
callback gate. A panel receives the completed team result as its previous
outcome, so its reviewers see the task and each member’s outcome. A callback
gate posts its question to the run’s callbacks client and pauses the team
until someone answers. The question’s input holds the gate’s own input,
where the team sits in the graph, and the task with each member’s outcome.
So a team that runs again, for example in the next turn of a loop, asks a
new question, and two teams never share an answer. Start the run again with
recordTo and resume: true over the same stored callbacks client. Members
that finished don’t run again, and the team reads the answer. An answer with approved set to false
fails the review, with its note as the summary when it has one. Any other
answer passes the review.
The team returns the usual Outcome, with a TeamResult in its data.
The status is pass only when every agent passes and the configured review
passes; a member or review failure returns fail with all member results
preserved, a pause returns paused with its reason, and a merge conflict
returns fail. Before it runs, the team checks that task is a non-empty
string, that agents has at least one member with a unique name, that
every member has a non-empty role, brief and engine binding, and that a
configured review follows the panel or callback contract.
Failure
GraphValidationErrorfromcompileGraph: an empty identifier, a duplicate node or edge id, or an edge that names a node that doesn’t exist. No node can run from this layer.RESULT_INVALIDfromvalidateNodeResult: the executor recordsnode-failedand saves no completion.
Limits
- An outside graph type is trusted package code. It can import modules and cause side effects on its own; the conformance kit checks public behaviour and doesn’t stop side effects.
- This contract doesn’t schedule nodes, store runs or execute work. The graph executor runs nodes and stores runs.
Full file
Full file
examples/custom-graph.ts
Next steps
- Built-in pipeline: the dag form that ships, as a stored pipeline.
- Plan admission: binding a description to a host’s admission record before a run.
- Graph executor: how a dispatch runs and how a run resumes.