Skip to main content
Silo does not give you an agent. It gives you a world and a boundary, and you plug your agent into it. That boundary is one function. Silo calls it once, hands it the job and the means to do it, and waits for an answer. Everything in between is yours — a model loop, a framework, or a hard-coded line of calls.

The contract

Your agent module default-exports a function. Silo calls it with one object. What Silo hands you: What you return: an object with an output. A string is used as-is; anything else is JSON-stringified. That text is what an answer-producing verifier grades.
The agent never sees the environment’s state. It sees the instruction, the tool schemas, and whatever tools return. If it should be able to look something up, a tool exists for it — and if no tool exposes it, the agent cannot know it.

Pointing Silo at it

Your agent is an ordinary file in your project. Name it whatever suits you and pass --agent:
Because it is just a path, you can keep several agents side by side and run the same task against each to compare them against the same seeded world. There is one shortcut: name it silo.agent.ts in the directory you run from and the flag is optional.
That shortcut matches ./silo.agent.ts only — the .ts extension specifically. A silo.agent.js still needs --agent ./silo.agent.js, or the run fails with Cannot find module.
Agents are plain ES modules, so your project needs "type": "module" in its package.json. Without it the file loads as CommonJS, its exports end up nested under .default, and you get a misleading error about a missing export.

Failed tool calls come back as values

callTool does not throw when a tool rejects the call. It returns isError: true alongside a machine-readable code and a message written to be handed straight back to a model:
That message names both the mistake and the real parameter, which is what gives a model a chance to correct itself on the next turn rather than failing silently. Unknown tools come back the same way, with "code": "tool_not_found". This is deliberate. A harness that throws on a bad tool call ends the rollout and scores a failure; returning the error as a value turns a typo into a recoverable turn. Feed it back into your loop and let the model try again.

Where the model goes

Swap the straight line for your own loop. The shape is the same in any framework:
Silo does not care which model or library you use. It only cares that something calls callTool and eventually returns an output. Note that the loop pushes result.output back into the conversation without checking isError — a rejected call is just another observation for the model to read and act on.

Respecting limits

A run can be capped:
When either limit trips, signal aborts and further callTool calls stop. A long-running loop should check signal.aborted and return what it has rather than spinning.