Titah’s own model can run several of its own configured agents as sub-agents, in parallel, inside one turn — without leaving the process or spawning an external CLI.
{
"agent": {
"explore": {
"mode": "subagent",
"description": "Codebase explorer — read only",
"permission": { "edit": "deny", "write": "deny", "bash": "deny" }
},
"qc-developer": {
"mode": "all",
"permission": { "bash": "allow" }
}
}
}
mode decides who may be dispatched
mode decides whether the coordinator’s own model may hand an agent work:
| Value | Meaning |
|---|---|
"primary" | The default. Never offered to the task tool; refused outright if something tries to dispatch it anyway |
"subagent" | Dispatchable, and still selectable as your own top-level agent |
"all" | Both |
The default is "primary", not "all", deliberately. Flipping that default
would have quietly handed every agent already in your config — including ones
with a wide-open permission — to the model the moment this feature shipped,
without you writing a single line asking for it.
mode does not otherwise change an agent. A "subagent" agent is still
selectable with Tab or --agent, exactly as before sub-agents existed.
delegate routes a sub-agent through an external CLI
delegate sends a sub-agent’s work through an external CLI listed in
externalAgent instead of Titah’s own loop — the same engine @claude uses,
reached from task / /tim instead of a mention.
It is mutually exclusive with model: an agent has one engine, and a config
setting both is rejected when it loads.
!!! warning “permission does nothing to a delegating agent”
The external CLI runs under its own policy and never asks Titah for anything,
so Titah’s edit / write / bash settings are not applied to it, and no
permission dialog appears on its behalf. Because Titah cannot know what that
CLI will touch, a delegating agent is always treated as a writer,
whatever its permission block says. To restrain it, restrain it in that
CLI’s own configuration.
Readers run together, writers take turns
Whether a sub-agent may run at the same time as others is read from its
permission, never from tools.
An agent counts as a reader only when edit, write and bash are all
explicitly "deny", and it does not delegate. Everything else is a writer —
including an agent whose config never mentions permission at all, and every
agent with delegate set.
bash counts as writing on purpose: an allowed shell can run sed -i just as
well as the edit tool can, and treating it as read-only would open exactly the
hole this rule exists to close.
- Readers run with no limit on how many are concurrent.
- Writers are serialised on a queue keyed by working directory, so two sub-agents editing the same project never race over the same shadow-git snapshot. One waits its turn while the panel shows it “waiting for a turn”.
This is why /tim tells the model not to order writers itself: Titah already
does.
task and /tim
The coordinator hands work to a sub-agent with the task tool; several calls
in the same step run concurrently.
The prompt asks the model for one call per agent, but nothing enforces it: the same agent named twice in one step is two sub-agents, and if it can write, the second waits for the first on the write lock.
/tim <task> is not a separate orchestration engine. It is an ordinary turn
with one extra system-prompt section, listing the current dispatchable roster and
instructing the model to split the work and dispatch it with task, doing any
leftover work itself rather than inventing an agent for it. The model does the
coordinating; Titah only tells it who is on the roster.
Running /tim with no dispatchable agent configured does not hang or guess — it
answers directly with what to add: an agent block with "mode": "subagent"
(or "all") in titah.json.
A sub-agent never gets the task tool itself, no matter its own mode.
Dispatch depth is capped at exactly one level, so nothing can spawn a tree of
sub-agents that burns through your provider quota with no way to stop it.
A sub-agent inherits the coordinator’s ceiling
A dispatched sub-agent’s permission is computed from its parent, not from the global config, and then clamped by the stricter of parent and child on every axis. One sentence: a parent can never hand out more than it holds.
That is what makes Plan mode a real boundary. Under a plan coordinator
whose write and edit are "deny", a sub-agent that declares nothing
inherits the deny, and a sub-agent that declares "write": "allow" is clamped
back down to deny. Earlier this leaked: task carried no check of its own and
the child fell through to its own config, so /tim in Plan mode could run a
qc-developer with "bash": "allow" and get a shell command executed with no
confirmation. That hole is closed.
It fixed the mirror-image annoyance at the same time. build-auto opens every
axis so it never interrupts, but sub-agents used to fall back to the user’s
global block — usually ask everywhere — so the mode kept its “no
confirmations” promise exactly until it delegated, and then asked about ls.
Inheriting from the parent removes the dialog without widening anything: the
child still cannot do what the parent could not have done itself, directly.
rules and allowlist are merged rather than intersected. They work at a
different layer — the axis decides the class of action, the rule decides the
argument — and a child’s allow rule cannot open an axis that is already
deny, because the axis is checked first.
One rule differs from top level, deliberately, and in both directions:
“Always” from a sub-agent lasts the turn, not the session. Answering
ato a dialog raised by a sub-agent adds the pattern to an allowlist discarded when the coordinator’s turn ends — unlike the same answer at top level, which lasts the whole session. And for that turn it covers every sub-agent, not only the one that asked.
Watching them work
Ctrl+X then ↓ toggles a panel listing every sub-agent of the current
turn, with each row’s status: queued, running, done, failed, stopped.
Rows that have started show a running clock. A queued row shows none, on purpose — a clock on something that has not begun reads exactly like something stuck.
The list is cleared when you send your next message, so it is always about the
work in front of you. It lives in the TUI’s memory, not in the session: quit and
come back, or switch away with /session and back, and the panel starts empty
even though the sub-agents’ own child sessions are still on disk.
While the panel is open it owns the keyboard — ↑ / ↓ select, Esc
closes, and everything else is swallowed rather than typed into your prompt
behind it. Ctrl+X chords, Ctrl+C and Ctrl+D still reach through.
Reading a sub-agent’s transcript
A sub-agent runs in its own child session, so what it did is a real
transcript rather than a summary. Enter on a panel row — or a click — opens it
as a full page, streaming along live while the work is still going. Ctrl+X S
opens the same page for every sub-agent this session has run, finished turns
included: the panel is cleared on each new prompt, so without that list the work
of an earlier turn sat complete in the database with no way in from the screen.
The page is read-only, and Esc returns to the conversation. A prompt box
there would promise messages you could send into a session the coordinator is
already driving.
A super agent is the exception. It runs a CLI outside Titah and only sends its final answer back, so its page says that instead of drawing an empty screen.
Child sessions stay hidden from GET /session and from /session in the
TUI. Picking one there would swap the active session, and your next prompt would
land inside the sub-agent’s own conversation. They are reachable on their own
route — GET /session/:id/children, see HTTP API.
Cancelling one
x cancels the selected sub-agent, and asks first: the first press arms that
row and says so in the panel’s title, a second x on the same row does it, and
any other key — including moving the selection — disarms it. Stopping cannot be
undone, so a single keystroke is not allowed to do it.
A cancelled sub-agent reports rather than fails. Its task call returns
STOPPED BY USER after 48s. as an ordinary tool result, the coordinator reads it
like any other result and carries on with the rest of the team, and the history
line is marked ⊘, not ✓.
This holds for both engines: a sub-agent running an external CLI has that CLI killed, exactly as an internal one has its turn stopped.
Undo after a /tim turn
Read Permissions & undo before relying on it. In short: a
sub-agent’s writes snapshot on its own session, so a coordinator that wrote
nothing has no snapshot, and undo walks back to an earlier turn — reverting
that as well, silently, in the same operation.