Work handed to a second model, which may not ask

subagent_run gives a self-contained piece of work to a helper carrying the
parent's connection, directory, model and effort, and hands its answer back as
the tool result. The mechanism is the one scheduled runs already use -- a hidden
chat, one turn, wake_chat, and a poll -- so tools, rounds, budgets, metrics and
steps all work with no second implementation. The two alternatives were
rejected where they had already been rejected once: a nested Generation is two
replies writing one transcript, and a one-shot complete() has no tools, which
schedule/runner.py records as useless for exactly this case.

Every restriction is a property of the child's row, applied by resolve_tools
after the gates, because a rule that lives in a system message is one a page the
model just read can argue with. No questions, no recursion, nothing that writes
unless the call asked for it and the parent's own mode would not have stopped
first, and commands only from a fixed read-only list -- in every mode including
Auto, because the task text can have come from a page.

Withdrawing ask_user turned out to be half of "nobody is watching". An approval
still built a card nobody could see and parked the reply until approval_timeout,
which from every screen is the feature not working. Chat.unattended is the
question now, and not the kind: _authorise answers with a refusal instead. A
scheduled task's chat had the same hole and is covered by the same flag.

Three bounds, counted where each is knowable: per reply on the parent's
Generation, instance-wide in a set a restart clears, and per helper in settings
of its own so one runs out of room long before the reply that asked. Past the
clock the helper is stopped rather than abandoned, so a partial answer comes
back with a sentence saying so.

Also: four gates had shipped into the scope menu with no name, taking the first
tool's label instead -- the canvas switch read "Canvas written". There is a test
that refuses a family without one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jaroslav Beneš
2026-08-06 15:05:30 +02:00
co-authored by Claude Opus 5
parent 0fa05c88b2
commit 46066150d9
17 changed files with 1850 additions and 13 deletions
+99
View File
@@ -136,6 +136,15 @@ VARIABLES: tuple[Variable, ...] = (
"Schedule",
"In a scheduled task's chat: how often it runs, in words.",
),
Variable(
"subagent",
"Is a helper",
"Set inside the chat of a helper another model sent, and empty "
"everywhere else — so it is the gate on the guidance a helper reads "
"about being one. It carries no text worth printing; it is a flag "
"wearing a variable's clothes, because `requires` is how a fragment "
"gates itself and a flag has nowhere else to live.",
),
Variable(
"timezone",
"Timezone",
@@ -1262,6 +1271,96 @@ BUILTIN: tuple[Fragment, ...] = (
"before the first run."
),
),
Fragment(
key="tool.subagent",
label="Helpers",
group=GROUP_TOOLS,
order=252,
families=("subagent",),
hint="Appears when subagent_run is offered. Two things a model gets "
"wrong about delegation and neither is in the schema. It under-uses it "
"— answering four independent questions one after another when they "
"could have run at once — and then over-uses it, sending a helper to "
"do a single search. The dividing line is whether the pieces are "
"independent, so that is what the wording is built around.",
default=(
"- You can delegate. subagent_run hands one self-contained piece of work to "
"another model that runs on its own and gives you its answer. Several calls "
"in the same turn run at the same time, which is the point of it: four "
"questions that do not depend on each other take as long as the slowest, "
"not as long as all four.\n"
"- Delegate when the work splits into independent parts, each worth more "
"than one lookup — different sources to read, different areas to survey, "
"two approaches to compare. Do it yourself when it is one search, one page "
"or one file: a helper costs a whole reply, so using one to save a single "
"call is slower than not.\n"
"- Write each task as if to somebody who has just walked in. A helper starts "
"with none of this conversation, and cannot ask you or the reader anything "
"— so say what is wanted, what a good answer contains, and any name, path "
"or decision it could not look up. Half a task produces half an answer with "
"no sign that anything was missing.\n"
"- Give each helper a different piece. Two with the same task come back with "
"the same answer twice, at twice the cost.\n"
"- What comes back is another model's work. Read it, say where it disagrees "
"with what you already had, and do not repeat a claim you cannot check just "
"because a helper made it."
),
),
Fragment(
key="tool.subagent_agent",
label="Helpers on a machine",
group=GROUP_TOOLS,
order=253,
families=("subagent",),
requires=("agent_target",),
hint="The agent-chat half, gated on `agent_target` so it appears only "
"where there is a machine. What it has to say is what a helper cannot "
"do there, because the failure otherwise is a model planning a whole "
"phase around a helper that will refuse every step of it — a helper "
"reads and may run a short list of read-only commands, and nothing "
"else, whatever mode this chat is in.",
default=(
"- A helper on this machine reads and reports. It can list and read files "
"and run the ordinary read-only commands — ls, cat, grep, find, git status, "
"git log, git diff — and nothing else, in every mode, because there is "
"nobody there to approve anything. Send one to find out where something "
"lives, to read a subsystem and describe it, or to check whether a pattern "
"holds across a tree; make the changes yourself once it reports.\n"
"- Do not send one to build, test, install or run anything: it will be "
"refused a step in and come back having done nothing.\n"
"- Ask for what you want back, not for a summary. “The three files that "
"define X and what each does” is usable; “look into X” comes back as prose "
"you have to read the codebase to check."
),
),
Fragment(
key="core.subagent",
label="You are the helper",
group=GROUP_CORE,
order=36,
requires=("subagent",),
hint="Only inside a helper's own chat. The three things it cannot work "
"out for itself: nobody is reading, there is exactly one reply, and "
"the thing that asked is a model rather than a person — so the usual "
"moves of asking what was meant, or promising to carry on afterwards, "
"both end the run having done nothing. This is the prompt half; the "
"enforcement is that ask_user and subagent_run are not offered here at "
"all, and that everything which writes has been withdrawn unless the "
"task was sent as a writing one.",
default=(
"- You are answering a request from another model, and you get one reply. "
"Nobody is reading this: you cannot ask a question, and there is no next "
"turn to carry on in. Do the work now and put everything into this answer.\n"
"- Answer the task as asked and stop. Do not open questions beyond it, "
"propose next steps, or address the reader — the model that asked will "
"decide what happens next, and anything you write to a person here is read "
"by nobody.\n"
"- Say what you actually found, with the file, the page or the command it "
"came from. Where you could not find something, say so plainly rather than "
"filling the gap: the model reading this cannot tell a careful answer from "
"a confident one, and will act on either."
),
),
Fragment(
key="context.knowledge_scope",
label="Which knowledge bases",