1
Build a fresh world
createState() is called. It must return a newly created object, normally via structuredClone, because Node caches imported JSON modules for the life of the process.2
Snapshot it
A copy is kept as the initial state. This is what the verifier later compares against, and what it derives expected answers from.
3
Bind the tools
bindTools(state) produces the callable tools, closed over the live world.4
Load the agent
The module at
--agent is imported. It must default-export a function, or the run fails immediately.5
Hand over control
The agent is called once with the task instruction, the tool schemas,
callTool, and an AbortSignal. Everything it does happens inside this call.6
Take the output
Whatever it returns as
output is captured. A string is used as-is; anything else is JSON-stringified.7
Diff the world
The final state is compared against the snapshot to produce
state-diff.json.8
Grade
The task’s verifier receives the final world, the initial world, and the agent’s output, and returns checks.
9
Write artifacts
The trace, result, diff and run metadata are written to
.silo/runs/<runId>/.Isolation
The agent holds no reference to the world. It receives tool schemas — name, description, input shape — not the tools themselves, and certainly not state. Two consequences worth internalizing: What no tool exposes, the agent cannot know. If a task requires knowing which users are inactive and no tool reports that, the task is unsolvable regardless of how capable the model is. Missing tools look like agent failures. Tool output is cloned on the way out. A tool returning a slice of state hands the agent a copy, so nothing the agent does with that value can reach back into the world. The only way to change state is to call a tool that changes it.A tool call, step by step
EachcallTool goes through the same path:
- Abort check. If the run has already exceeded a limit, the call does not execute.
- Lookup. An unknown name returns
tool_not_foundas a value. - Validation. Arguments are checked against
inputSchema. A hallucinated property is rejected beforerunexecutes, with a message naming the real parameters. - Execution.
run(state, input)operates on the live world. - Cloning. The return value is cloned.
- Recording. A
tool_calland atool_resultare written to the trace, sharing acallId, with the call’sdurationMs.
Reading a trace
Because the sequence is fixed, a trace tells you exactly where things went wrong:
That last one is the failure Silo exists to catch: a confident answer with nothing behind it.
Timing
run.json records startedAt, finishedAt and durationMs, and each tool_result carries its own durationMs. Because environments are in-memory simulations, tool calls are typically sub-millisecond — so almost all of a real run’s wall time is the model thinking, which makes the split useful when tuning.
Silo