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:
silo.agent.ts in the directory you run from and the flag is optional.
"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:
"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: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:signal aborts and further callTool calls stop. A long-running loop should check signal.aborted and return what it has rather than spinning.
Silo