delegation, memory, surfaces

Beyond one conversation

The grip contract describes what one conversation can do to itself. These are the parts that reach past it: children with their own context, notes that survive a restart, a tool surface sized to the task, shells on other machines, and a read-only door in Discord.

One level deep, on purpose

A sub-agent is a child session. It has its own event log, its own context window and its own turn loop, and it shares the parent's worker process, branch and checkout.

That split is deliberate in both directions. Isolating context is the whole point: a child explores a subproblem and hands back a summary, so the parent pays for the conclusion rather than for every step that led to it. Sharing the checkout is what makes delegation useful for code work, because a child that edited files in a worktree of its own would leave the parent unable to see, build or commit the result.

Running in one process also makes waiting cheap. A child settling rings a bell the parent is already sleeping on, instead of the parent polling the registry across an IPC boundary — a parent waiting half an hour would make thousands of those calls. Every wait still has a backstop poll, so a missed notification costs latency rather than correctness.

Depth

A sub-agent cannot spawn sub-agents. An agent that can delegate to a delegate can build an unbounded tree, and the literature on multi-agent failures names unbounded fan-out as one of the recurring ways such systems collapse. One level keeps the accounting, the cancellation semantics and the UI comprehensible, and the parent stays identifiable as the thing responsible for the work.

The verbs

ToolWhat it does
spawn_agentDelegate a self-contained piece of work. Takes a label of two or three words and a task that is the entire briefing; optionally a profile such as scout or worker, a model override for bulk reading, and a mode override, where plan gives a read-only child. Returns as soon as the child has started, so several calls fan out.
waitBlock until something happens instead of polling in a loop. Predicates: all, any, first_failure, and time for something outside the system such as a long build settling. Always returns the current state of every sub-agent, including their answers, and reports whether it timed out.
agent_statusList the sub-agents with their state, answer so far, cost and elapsed time.
agent_transcriptRead a child's own event log: what it did, not only what it concluded. For diagnosing a child that failed or answered oddly.
cancel_agentStop a child that is no longer worth finishing. Whatever it had produced is kept.
agent_profilesList the configured profiles, each a model, a mode and a standing brief, along with the delegation limits: how many children may run at once, the longest a wait may block, and how much of an answer reaches the parent.

wait is a core tool rather than part of the sub-agent group. The reason to block is not always delegation. A sub-agent is refused the delegation tools entirely, and it is exactly the kind of session that gets handed a long build to babysit, so a child whose only way to sleep travelled with that group would poll instead, burning an iteration and a slice of context each time.

The brief is everything

spawn_agent carries more instruction in its description than most tools do, because delegation is the surface where a vague call is most expensive: a badly briefed child burns a whole conversation's worth of tokens before anyone finds out. The research on multi-agent systems is consistent about which way it fails. The dominant failure is under-specification of the sub-task, not faulty execution of it.

So the tool demands the objective, what to return and in what shape, which files, paths or sources to use, and the boundaries: what the child must not touch and when to stop. It also asks for what the parent already knows, so the child does not rediscover it. The child cannot see the conversation, and a brief that assumes context produces confident, wrong work. A handful of well-briefed children beats many thin ones.

Notes that survive a restart

remember saves a durable keyed note for a conversation: a short identifier and the content to keep. The note survives restarts and self-modification, so what a conversation learned about itself is still there after the agent has rewritten the code it runs on.

recall reads one back by key. Called with no key, it lists everything remembered in that conversation.

What a long conversation sheds

The selection rules are ported from a MOO agent grip where they were worked out in anger, and they are the point of the module: deciding what to shed matters far more than the summary prompt does.

Thetis differs from the original in one way worth stating. There, originals were copied into a side offload store before being dropped. Here the event log is append-only and compaction never edits it. A compaction records which spans it stands for, and rehydration projects the log through those records. The originals are still there, so the store is unnecessary.

A tool surface the size of the task

Every tool belongs to exactly one group. A group is a task allocation rather than a capability one. Whether a tool is possible at all is already answered elsewhere, and that answer is a flat list of everything the machine can do. Groups answer a different question, which is whether a tool is plausibly wanted in this conversation, and a session doing code review has no use for BigQuery, Notion or the ssh registry.

Why

A large flat tool block costs tokens and, less obviously, accuracy. The published numbers are consistent about both directions. LongFuncEval measured 7–85% drops in tool-calling accuracy as the catalog grew from 8K to 120K tokens. MCPGauge found that naive attachment of MCP tool definitions reduced accuracy by 9.5% on average across six commercial models, while multiplying input tokens by 3.25× to 236.5×. Anthropic's tool-search work reports Opus 4 rising from 49% to 74%, and Opus 4.5 from 79.5% to 88.1%, on a 58-tool surface when definitions are fetched on demand rather than preloaded, at about 85% fewer tokens.

Set against that, How Many Tools Should an LLM Agent See? found a fixed cut-off beating an adaptive one in aggregate on a 3,251-tool corpus, while scoring 0% on hard queries where the adaptive one got 16.7%. A candidate that never enters the pool cannot be recovered downstream.

The asymmetry

Withholding a group is cheap to get wrong in tokens and expensive to get wrong in capability, so the design leans one way on purpose. Routing is generous: one tag match admits a group. The groups that almost every task needs are always on. tool_search can pull in any group mid-session, append-only, and nothing is ever removed. And the whole mechanism is off by default until the accounting says it pays.

tool_search is always offered, even when grouping is off, so that the escape hatch is not itself something that can be scoped away. With grouping off it reports every group as loaded, which is true and harmless. It is not classed as mutating: it changes what a conversation can see rather than anything outside it, and a read-only session needs the escape hatch as much as any other. The mode filter still applies to whatever it admits.

Its description tells the agent the same thing in the same terms — capabilities it has may not be visible right now, so call this the moment a tool is suspected to exist but cannot be seen, rather than working around the gap.

Shells on other machines

A remote shell needs somewhere to keep where it connects: an address, a user, a key, whatever -o options a particular box needs. Two properties decide where that somewhere is.

It must not be publishable by accident. Host names, internal addresses and key paths are the shape of an infrastructure map, and thetis.toml is committed and pushed. So the registry lives in ssh-hosts.local.toml, which the existing *.local.toml rule in .gitignore already covers, and which the publish filter therefore never sees. It also never appears in list_config, which walks the committed file only: nothing in the registry is reachable by asking for a setting.

It must not be able to break startup. The config loader reads exactly <stem>.local.toml beside the config file, and a differently named file is invisible to it. A malformed registry can therefore fail its own read and nothing else, whereas a malformed thetis.local.toml is unbootable, and that is the one failure with no in-band cure.

The file is shared rather than per-branch. Hosts are facts about the world, not about a conversation's checkout, so a host added in one conversation is usable from every other one.

terminal_open takes a host naming a registered machine and runs the session there instead of locally; the remote session starts where its host says, so cwd is ignored. ssh_host_set, ssh_host_list, ssh_host_get, ssh_host_remove and ssh_host_rename manage the registry.

The one directory that outlives a conversation

/workspace is the one directory a WASI guest is handed as a preopen, and it is shared by every conversation and every branch. It is the opposite of the per-conversation checkout everything else operates on, which makes it the only place where work outlives a conversation.

It used to be invisible. Nothing in the prompt said it existed, so a turn that should have put a clone or a data file there put it in the branch instead, or spent tool calls rediscovering the directory from scratch.

So the system prompt carries two things: what the workspace is for, and one level of its contents. One level on purpose. It is what answers "what do I have access to", it is a handful of lines, and it is stable, because the top level of a shared directory changes when a project is added and not when a build runs. The prompt stays byte-identical between turns and the provider's cache keeps hitting. A deep tree would have neither property.

A read-only door in Discord

Discord's gateway is an outbound WebSocket that must be held open and heartbeated. A WebAssembly gateway component cannot do that: the gateway world is only ever called in response to something arriving, a fresh instance is made per call so nothing can be held open, and wasi:http has no socket upgrade. So the connector lives in the orchestrator, and reaches the agent through the same submit path the browser uses.

Every session created there is stamped with a read-only mode, and the agent withholds mutating tools for such a mode in two places: when it lists tools for the model, and again at dispatch. The connector therefore adds no tool policy of its own. It only has to make sure the mode is right, and that nothing exposed over Discord can change it. There is deliberately no command to switch modes.