Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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.

ToolWhat it does
layover_sendSend 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_peersWho you may send to, and what each is for. Worth calling before deciding where work goes rather than guessing at names.
layover_reportSay what you concluded. The account of a run that survives it.
layover_helpSay 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_readRead your own notes in full.
layover_memory_writeAdd to your own notes, for future runs of you.
layover_statusWhat this chain has left: how many messages, how much budget.
layover_learnPropose something future runs should know. Applies at once; lapses unless rediscovered.
layover_logbook_appendAdd to the factory's shared memory, stamped with who wrote it.
layover_waitSet 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.

MemoryThe tail of memory.md from this agent's Hangar, capped at 4 KB and saying so when it was cut
LearningsWhat earlier runs of this agent worked out and that still applies
SenderWho 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 byThe run is told
An agent`reviewer` sent this — another agent in this factory, not a person.
A person, from the dashboard or POST /flightsA person sent this, from the dashboard or the HTTP API.
A pipeline's scheduleNobody sent this by hand: the `review-bot` pipeline's schedule fired, and nobody is watching this run.
A layover coming dueNobody 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_URLThe endpoint, in the child's environment
LAYOVER_RUN_TOKENIts token, in the child's environment
mcp.json (or mcp.toml) in the run's HangarThe 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 -c takes a key=value override, 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 in decisions.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 — and layover_peers lists 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.