The tools an agent has
Layover speaks MCP, which all three supported CLIs understand natively. An agent reaches Layover the same way it reaches any other tool server, and the tools below are what it finds there.
Why the list is short
Every tool is a thing an agent can do unattended, so each one has to earn its place. The test applied was whether an agent could do its job without it.
| Tool | What it does |
|---|---|
layover_send | Send work to another agent. The only way work moves — and sending is what starts the agent you send to, so there is no separate spawn. |
layover_peers | Who you may send to, and what each is for. Worth calling before deciding where work goes rather than guessing at names. |
layover_report | Say what you concluded. The account of a run that survives it. |
layover_help | Say something is in the way, and which kind of thing: blocker is one of access, tooling, ambiguity, environment, decision or other. The channel that stops a quiet failure travelling downstream. |
layover_memory_read | Read your own notes in full. |
layover_memory_write | Add to your own notes, for future runs of you. |
layover_status | What this chain has left: how many messages, how much budget. |
layover_learn | Propose something future runs should know. Applies at once; lapses unless rediscovered. |
layover_logbook_append | Add to the factory's shared memory, stamped with who wrote it. |
layover_wait | Set work down to be picked up later, by a pipeline that resumes layovers. |
All ten do something. There is no "declared but not connected" answer left; a tool that answered honestly about being unfinished was a promise to finish it.
There is deliberately no layover_spawn. A mode = "spawn" route already opens one itinerary
per flight, and a tool doing the same would be a second permission model over the same graph —
two places to look when asking what an agent may start, which is one too many.
What a run is given
A run is a fresh process that remembers nothing. What it knows comes entirely from its payload, in this order — instructions, memory, learnings, handover, who sent the flight, and the message that woke it last, because whatever arrives last reads as the current instruction.
| Memory | The tail of memory.md from this agent's Hangar, capped at 4 KB and saying so when it was cut |
| Learnings | What earlier runs of this agent worked out and that still applies |
| Sender | Who sent the flight, from the Tower's record of it — never from the body |
The sender is stated in a == WHO SENT THIS == section directly above the body, as one of four
kinds, because the same words mean different things from each:
| Sent by | The run is told |
|---|---|
| An agent | `reviewer` sent this — another agent in this factory, not a person. |
A person, from the dashboard or POST /flights | A person sent this, from the dashboard or the HTTP API. |
| A pipeline's schedule | Nobody sent this by hand: the `review-bot` pipeline's schedule fired, and nobody is watching this run. |
| A layover coming due | Nobody sent this just now: it is work set down earlier (`lay_…`) that has come due, … |
A flight released by a join carries no such section: its body already labels every flight it
folds together (## From `tester`), and one name above them would be wrong about the rest.
Both are injected, not fetched. An agent could call layover_memory_read when it wants its
notes — cheaper, explicit, and it fails silently: an agent that forgets to call simply has no
memory, and nothing anywhere reports that it forgot. Since fresh runs are what make memory
deliberate in the first place, a memory system that quietly does not work would undo the decision
it was built to serve.
The tail rather than the head because the end of the file is the most recent thing written; a memory that kept only its oldest entries would get less useful the longer an agent ran. The whole file stays one tool call away.
How a learning lives and dies
proposed ──> provisional ──(20 runs, unrediscovered)──> lapsed
│ │
│ rediscovered independently │
└────────────> confirmed <───────────────┘
A learning applies from the moment it is proposed. There is no approval queue: a sibling project built one and after 22 days held 88 learnings, none ever approved, so not one had ever reached a run.
Every run of an agent spends one of its provisional learnings' remaining runs, whatever the outcome — a learning that only decayed on success would be kept alive by the failures it was meant to prevent. Run out, and it lapses. Rediscovered independently by a later run, and it counts: enough times and it becomes permanent.
Repeating advice you were just given is an echo, not evidence, and is not counted. Otherwise a single fluke could confirm itself in three runs.
How a run reaches them
layover run binds an MCP endpoint on loopback for as long as it is draining, and gives each run
a token minted for it alone. The child is told about both in two ways:
LAYOVER_MCP_URL | The endpoint, in the child's environment |
LAYOVER_RUN_TOKEN | Its token, in the child's environment |
mcp.json (or mcp.toml) in the run's Hangar | The same two, in the shape the CLI's MCP-config flag expects, plus every MCP server the agent declares |
The declared servers are written beside Layover's own so the agent actually has them; their
credentials are named, never written — see MCP servers. The run
token itself is in the claude_json file, because that is where the CLI looks for a header; it is
minted for this run alone and revoked the moment the run ends. The codex_toml file names the
variable it is in instead (bearer_token_env_var).
Which file is written depends on the runner's mcp.format. The flag is appended to the command
unless the command places {mcp} itself:
[runners.copilot]
command = ["copilot", "--allow-all-tools", "--output-format", "json"]
mcp = { flag = "--additional-mcp-config", format = "claude_json", prefix = "@" }
# runs: copilot --allow-all-tools --output-format json --additional-mcp-config @<hangar>/mcp.json
[runners.codex]
command = ["codex", "exec", "--model", "{model}", "{mcp}", "-"]
mcp = { flag = "-c", format = "codex_toml" }
# runs: codex exec --model <model> -c <hangar>/mcp.toml -
Known not to work with the current Codex CLI.
codex exec -ctakes akey=valueoverride, not a path, so a Codex run wired this way is refused before it starts. The file Layover writes is the right shape — one[mcp_servers.<name>]table per server — but Codex has no flag that reads one. Recorded as an open question indecisions.md; Claude Code and Copilot CLI are unaffected.
prefix is prepended to the path. Copilot CLI's --additional-mcp-config takes either a JSON
string or a file path and tells them apart by a leading @; without it the path is parsed as JSON
and the run dies complaining about the factory's own configuration. Most CLIs take a plain path
and want no prefix.
codex exec … - reads its prompt from stdin, so the - has to stay last; that is what {mcp} is
for. Everything else can take the append.
The token is the identity
An agent never says which agent it is. The token does, and Layover holds the mapping — so the answer to "who is calling?" cannot be influenced by anything in the request, including a work item or another agent's output that is trying to talk the child into something.
A token is minted as a run starts and revoked the instant its process is gone, on every path out: a clean exit, a failure, a timeout, a Ground Stop. A call arriving on a revoked token is refused with HTTP 401 before any tool runs — not as a readable refusal like the others, because a call that cannot be charged to a run has no chain to spend from and no agent to be.
What the rails do while a run is live
layover_send is checked against the same route map and the same itinerary the supervisor uses:
- An edge the map does not draw is refused, and the agent is told to call
layover_peers. So is an edge only another workflow's routes draw: the check is against the caller's own chain's routes — the global ones and those scoped to its pipeline — andlayover_peerslists exactly those. Neither tool accepts a pipeline; one named in the arguments is ignored. - A chain with no Hops left is told to finish and report rather than send, while it can still do something about it.
- The flight it queues continues the caller's chain. It is not a new itinerary, so it spends the same Hops, the same Fuel and the same run cap. Two agents passing work back and forth are bounded by the budget the chain started with, not by a fresh one each time round.
- It carries the chain's pipeline and flags, taken from the Tower's record of the run and never from the agent, so the next run is composed with the flags the chain was triggered with. A flight over a spawn edge opens a new itinerary with a fresh budget, and still carries both — so the spawned chain may use the same workflow's routes, and no others.
Setting work down
The project is named after this. An agent that has opened a pull request and wants to react to comments over the following days calls:
{ "until": "6h", "because": "comments on pull request 41" }
and then finishes. Nothing stays alive in between: no process, no parked chain, no held budget.
Neither alternative worked. Keeping the chain alive and polling spends a Hop and real money on every tick, so Hops kills it long before a human replies — and the whole point of Hops is that it should. Re-triggering on a schedule works mechanically but arrives knowing nothing: which work item is this about, what was already tried, what did the earlier chain conclude.
until is how long to wait, in the same vocabulary as a pipeline's every: 30m, 2h, 3d. An
agent asked to wait "until the review lands" cannot know when that is, so it names an interval and
is brought back to look.
Coming back
A pipeline declares that it collects them:
[pipelines.follow_up]
entry = "publisher"
trigger = { every = "45m" }
resumes = true
A resuming pipeline does not open fresh work on its tick — it goes looking for layovers that are due. An ordinary pipeline never collects them, so a factory's hourly sweep cannot quietly start following up somebody else's work.
So a layover is picked up at the first tick of a resuming pipeline at or after its due time, not at the due time itself. The dashboard's Upcoming tab lists every layover waiting with both.
The resumed run gets a new chain with a fresh budget. The chain that booked the layover is over; its Hops and Fuel are spent, and reviving it would make the second follow-up cheaper than the first and the tenth refused. A layover is new work about an old subject, and it is priced that way.
It gets no new permissions, though. The new chain belongs to the resuming pipeline, but may use
a route only when the pipeline whose chain set the work down permits it too. Any agent may call
layover_wait, and a resuming pipeline collects whatever comes due, so without this a chain could
reach another workflow's agents by setting its work down and waiting to be woken there.
What carries over is context. The run is told which chain set this down, what it was waiting for,
and when — and it is composed with the flags the booking chain was triggered with, for every
flag the resuming pipeline declares, so a follow-up does not quietly revert to defaults the
operator had overridden. It is also handed the two things that say which work this is: the message that woke the run that set it down, and what that run reported
with layover_report, each quoted and cut to 2,000 characters:
## You are picking up work that was set down
An earlier chain (itn_01M2WH…) finished what it could and chose to come back to this later.
It was waiting for: comments on pull request 41
It was set down at 2026-09-19T09:56:18Z.
Nothing was left half-done: the earlier run ended cleanly. Your job is to see whether the thing
it was waiting for has happened, and to act on it if it has. If it has not, set the work down
again rather than waiting.
The message that woke the run that set this down:
> Publish work item 4821: the retry policy fix.
What that run reported before it finished:
> Opened draft pull request 41
>
> Branch fix/retry-4821; tests green.
The report is looked up when the work is picked up, not when it is set down, because a run usually reports after it books a layover. A run that never reported leaves that part out; how much the follow-up knows is exactly as much as the earlier run chose to write down.
That last paragraph is the opposite of what a recovered run is told, and deliberately so. A recovered run may have half-applied a side effect and is warned to check before repeating anything. A resumed layover was not interrupted — telling it to look for damage would send it hunting something that was never there.
When a wait becomes a leak
Nothing gives up on a layover by itself. A resumed run that finds nothing sets the work down
again with layover_wait, which books a new layover for whatever wait the agent chooses: the
delay is the agent's every time, and nothing limits how many times it does it. Each check is a run
on a fresh chain with fresh Fuel, so what bounds the spend across all of them is the factory's
Reserve, which counts only runs that report a cost.
So the decision belongs in the prompt of the agent that sets work down: how far apart its checks
should be, and when to stop and say nobody answered — "check hourly for a day, then daily; after a
week, report that the pull request has had no response and stop". A resumed run is told only when
this layover was set down, so an agent that should stop after a week has to carry the first date
forward itself: in each layover_report, which the next check is handed, or in its own notes.
examples/workitem-factory/'s
follower does this. layover doctor reports the layovers still waiting, and faults a factory where
no pipeline will ever collect them.
A prompt cannot name a tool that does not exist
layover validate reads every prompt, finds every layover_* name in it, and refuses a factory
that tells an agent to call something Layover does not offer:
error: agent `publisher`'s prompt tells it to call `layover_publish`, which is not a tool
Layover offers; an agent told to use a tool it does not have will improvise
This check exists because of a real failure. Eleven tool names were once documented across prompts and this book, and none of them existed — the names drifted apart because nothing could compare them. Improvising is precisely what a factory is meant not to do unattended.
Identity comes from the Tower, never from the agent
A tool call carries a token, and the token is the identity. Layover looks up which run, which agent and which itinerary it belongs to; the agent never states any of them.
This is not a formality. Every rail in the system — Hops, Fuel, the run cap, who may send to whom —
is indexed by the agent's name, so an agent that could name itself could claim another agent's
permissions and another agent's budget. There is no code path in which a field an agent sent
becomes an identity, and a test asserts that sending an agent field changes nothing.
A refused call is a successful answer
MCP distinguishes the call failed from the protocol failed, and Layover uses the distinction.
"You may not send to that agent" is a well-formed answer to a well-formed question, so it comes
back as a result marked isError, with text the agent can act on:
`analyst` may not send to `publisher`. Call layover_peers to see who you can reach.
Returning that as a protocol error would tell the CLI its connection had broken, rather than telling the agent it asked for something it is not allowed to have. The agent can read this, and try something else — which is the entire point of telling it.