Agent Tools
const front = orchestration.tools('front') // 3 tools
const authoring = orchestration.tools('authoring') // 21 toolsThe split between them is a threat-model boundary, not an organisational one. Tiers that exist for tidiness get merged the first time someone finds them inconvenient. This one does not merge.
Tier A — what a conversational agent sees
Three tools, and no graph mechanics whatsoever:
| Tool | What it does |
|---|---|
list_templates | Reports each registered template's id, summary and declared params. |
instantiate_plan | Mints a plan from a template with args filled in. |
author_plan | Authors a plan from a description. |
An agent holding tier A cannot add_node, cannot freeze_plan, and cannot rewire a graph. It fills in declared parameters, or it describes what it wants. That is the whole surface, and it is deliberate on two counts. A small model is measurably better at filling five declared holes than at authoring forty nodes. And an agent that cannot reach graph mechanics cannot misuse them — which is a stronger guarantee than instructing it not to, since a prompt is a suggestion and an absent tool is not.
Tier B — the authoring surface
Twenty-one tools covering the full graph: creation (create_plan, clone_plan), structure (add_node, remove_node, connect_nodes, disconnect_edge), configuration (set_node_config, set_node_field, set_node_phase, set_bounds), reading (get_plan, plan_outline, plan_read, plan_status, raw_plan, raw_ops, raw_diff), and lifecycle (validate_plan, submit_plan, freeze_plan, unfreeze_plan).
Two properties are worth knowing.
The node vocabulary reaches the model in the tool descriptions, rather than being left to guess. A model that cannot discover the seven node kinds from the tools it holds will confidently invent an eighth, and you will find out at freeze.
A mutation returns SCOPED prose naming what changed — not the whole projected plan. Echoing the entire graph back after every edit is defensible when the context window is large and the plan is small. Here the plan can be far larger than the budget for describing a single edit, so echoing everything spends the window on text the model already had.
Tier C is not a tool tier
This one reads oddly at first, so read it twice. Tier C is runtime.invocable — the allowlist of what a staged call may invoke. These are not tools the model calls. They are the tools the plan is permitted to stage.
The allowlist and the registry are the same object, and that closes a genuine trap. Keep them separate and author-time validation happily passes against a list of ten names while the registry behind it has zero callers. The plan then fails at fire time with "not registered" — after the operator approved it, mid-run, with whatever ran before it already done. Two sources of truth about what is callable is one source of truth too many.
const invocable = {
has: (tool) => registry.has(tool),
names: () => registry.names(),
// What each tool returns, so a downstream `transform` is validated at FREEZE.
returns: (tool) => registry.get(tool)?.artifactConstructor
? { kind: 'artifact', artifactClass: registry.get(tool).artifactConstructor() }
: undefined,
}returns() carries the class, not a descriptor array — because core's static toolMethods shadows rather than concatenates, so a consumer reading .toolMethods would hand over only the leaf set and freeze would refuse artifact_head on a JSON artifact the plan legitimately advertised. Handing over the class moves the union into the battery, where it is computed once.
References cross the wire as records
A tool call cannot transmit a class instance, and NodeRef is a class for reasons the Plan IR page explains. So the wire carries a record and the IR carries an instance, converted at one boundary:
// on the wire, from the model
{ "paths": { "$ref": { "node": "list_files", "select": "all" } } }// in the IR
{ paths: new NodeRef('list_files', 'all') }One conversion point means a model reads back the same shape it writes.
Plan ids are validated against /^plan-[A-Za-z0-9_-]+$/, so a model-authored id cannot be path-shaped — an id that looks like a path is an id that eventually gets treated like one.