Get started

Engineering NotesTools and execution

Parent-picked subagent tools skip approval, so code decides them

The usual way to add subagents is to give the parent model a tool that launches them and let it decide the prompt, tools, and model each time. In a business system, that freedom breaks three things. Subagents run where no human is watching, so if the parent hands one a tool that needs approval, that tool runs without anyone confirming it. If the parent invents a model name, the subagent runs on a model with no known price and its spend lands in the usage ledger as zero. And if a subagent never finishes, neither does the parent's turn.

In OwlAI, the only place that launches subagents is deep research (“One agent loop for every feature, with the differences in hooks”). The lead writes a plan and launches researchers and a verifier, each taking one branch of the investigation. A definition fixed in code sets each subagent's prompt, report format, tool-selection rules, and caps. The lead model passes only a task and a tool-call budget. This post covers what the definition fixes, where a researcher's tools, identity, and model come from, and how long the lead waits for its researchers.

Parent-built subagents fit when a human is watching

In developer tools, a subagent definition usually lists its tools, and an empty list means it inherits every tool the parent has. That is enough when a developer sits in front of the approval prompt: whatever the subagent calls, a person confirms the risky calls.

Deep research researchers are different. Several run at once, and their tool calls do not go through chat's approval prompt. Under that condition we have to separate what can be left to the parent model from what cannot.

If the parent model decides If code decides
Tools Approval-required tools can slip through Only tools usable without approval go
Model Can run under a name outside the catalog Uses the conversation's model as is
Identity An argument appears for launching under another role The person who asked, unchanged
Waiting The model decides when it ends There is a cap; past it, the subagent is cancelled

The lead passes only a task and a tool budget

The lead has four subagent tools.

Tool What it does
Launch Starts several researchers in one call and returns a handle for each right away
Send Sends a running researcher more instructions; it reads them at the head of its next round
Wait Waits until one researcher finishes
Stop Sets a cancellation signal on one researcher

The launch tool takes five fields per researcher: which definition to launch (researcher or verifier), a short title for the progress card, the task, the tool-call budget, and which item of the approved plan it covers. There is no field for the prompt, tool list, model, or reasoning effort. The definition name is a choice between two values, so the model cannot write in a different one.

The tool-call budget is only a request. The server clips it to the configured range (agentLoop.researcherToolBudgetMin and researcherToolBudgetMax, 3–25 by default) and sets it as the cap on the researcher loop's tool calls. If the budget is missing, the server uses 14, the middle of the range. The minimum would cut a broad task off after a few calls, and the maximum would make the researcher whose budget was forgotten the most expensive one. The clipped number is appended to the task as one line, so it is the value the researcher relies on. The iteration cap sits one round above the budget, because a round must remain after the budget is spent for writing the report without tools.

If any entry in one launch call has an empty task, a wrong definition name, or permissions beyond the parent's, nothing launches and the reason comes back. Launching only some would make the lead fix the rest and resend, launching the ones already running a second time.

Three sources of a researcher: the lead's task, the code's definition, the parent's identity and model
Figure 1. Three sources of a researcher: the lead's task, the code's definition, the parent's identity and model

A researcher's tools are filtered from the lead's tools

A researcher's tool list is chosen, at launch, from the tools the lead has right then. What stays are the tools used for investigation: web search and page reading, every tool in the data-query category, product help, and remote MCP tools from connectors. Two groups are then removed:

  • Tools that run through the user's PC
  • Tools the execution policy does not plainly allow (approval required or blocked)

The second check uses the same code as the permission check chat runs before executing a tool. Only tools that check answers "allow" for go to the researcher. The tool that asks the user a question, the tool that gives the final answer, and the tool that launches subagents are never candidates.

Data-query tools are passed as a whole category rather than picked by name. Researchers get the same tool-usage rules as chat, and those rules say to call the join-path tool before writing a join. If a hand-picked list left that tool out, a model that follows the rules would never run SQL and would end with "cannot query."

Remote tools are not loaded into the request up front; only their names and summaries appear until they are needed (“185 tool definitions cost 99K tokens, so we list names and load picks”). The lead's job is planning and dispatch, so it rarely turns remote tools on. Remote tools the lead hasn't turned on therefore go through the same check and are listed by name for the researcher, which turns them on itself. A tool a researcher turns on stays inside that researcher.

The definition gets one more check. Before launch, the runtime confirms that the definition's external sources are within the parent's, and that if the definition holds approval-required tools, the parent does too. If not, the launch is refused. The comparison is by external source and approval requirement, not by tool name, because giving a subagent data tools the parent doesn't have is one of the reasons definitions exist separately.

Identity and model are the lead's, unchanged

In OwlAI, the identity of an agent run binds four things into one value: the person whose usage limit is checked, the person recorded in the usage ledger, the user the sandbox is partitioned by, and the role that sets data permissions. The value rejects empty fields when it is created. A researcher receives the lead's identity as is, and no argument overrides it. SQL a researcher runs stays within the tables the asking person's role opens, and the tokens a researcher uses come out of that person's limit.

The model and reasoning effort are also inherited from what the conversation fixed. If the launch tool took a model argument, a model name the lead made up would become the call target. A model outside the catalog has no price, so its cost stays at zero in the ledger, and nobody knows whether it even accepts tool calls.

Each researcher runs on its own thread and takes a copy of the lead's execution context. That context holds which conversation and user the usage is charged to, which credential (a personal key or a shared key) makes the calls, and when the turn's deadline is. Without the copy, a researcher's model calls would leave no row in the ledger, or tokens spent on a personal key would be recorded against the shared key. Each researcher's ledger rows carry a subagent marker, the parent unit, and depth 1, so you can trace which researcher in which turn used what.

The four subagent tools are given only to the lead, at depth 0. Researchers are not refused those tools; they never see them. Their prompt head carries no tools they can't use, and no round is spent on a researcher trying to launch a sub-researcher and being refused.

The lead waits with the wait tool, and the wait has a cap

The launch tool puts researchers on a dedicated thread pool and returns immediately. One lead runs up to 4 researchers at a time by default (agentLoop.maxParallelSubagents); the rest show as Waiting on the progress card and start when a slot opens. Researchers run on threads because the lead's tool execution opens and closes an event loop every round, so a researcher built as an async task would die when its round ended. The pool is separate to avoid thread-starvation deadlock. If the lead, its researchers, and tools shared one pool, whoever waited on a researcher would block while holding a thread, leaving no thread for the researcher to run on.

A researcher's result goes into the lead's mailbox (a queue that holds outside input for the head of the next round) and is carried into the lead's next round as a tool-call and tool-result pair (“One agent loop for every feature, with the differences in hooks”). While any researcher's result has not yet been carried in, the lead's final answer is refused before it runs. Accepting it would mean that researcher's result reaches no one.

Waiting is a tool because of how models behave. If waiting is the implicit act of "answer without calling a tool," some models call send or stop instead. In our measurements, one model used stop on a live researcher 30 seconds after launching it and failed two runs out of three. The wait tool blocks until the researcher finishes and returns only its status. A single wait is cut shorter than the per-tool execution time limit and returns "still running, wait again," because a model that gets an error from hitting that limit goes back to impatient calls. If the user sends a new message during the wait, the wait is released so the lead reads it first (“A 400 that sticks: add tool results on write, fix shapes on send”).

The wait cap defaults to 1,800 seconds (agentLoop.subagentJoinTimeoutSeconds). Without a cap, one researcher that dies and never reports back would hold the whole turn. A 600-second cap would cut off a healthy run that spends 11 minutes over 34 rounds writing complex SQL. The same value is also one researcher's run-time cap: a researcher runs within the shorter of 1,800 seconds and "the turn's remaining time minus the report-writing allowance." The writing allowance defaults to 600 seconds (agentLoop.researcherWriterReserveSeconds). Without that allowance, researchers would use up the turn and the report would go out empty. The deadline is set from launch time, so a researcher whose deadline passes while it waits for a pool slot ends as out of time without starting.

Stop sets a cancellation signal, and the researcher checks it both at the round head and while a tool is running. The SQL tool carries that signal through to a query cancel on the database side (“Read-only and query cancellation have to reach the database server”). When the user stops the turn, every running researcher is stopped and the waiting lead is woken right away.

Design choices

We chose definitions in code over freedom for the parent model. The cost is that every new kind of subagent needs a new definition in code, and the lead cannot narrow or widen a researcher's tools to suit a task. In return, what a researcher can do is answerable from the deployed code and the administrator's execution policy alone.

Researchers get only tools usable without approval. Work that needs approval, such as sending mail or creating a ticket, can't happen inside deep research; the user has to turn deep research off and ask in a regular chat. Showing an approval prompt per researcher would keep approval intact, but with four researchers waiting on approval at once, the user would struggle to tell which call belongs to which investigation.

Work is split one level deep. If researchers could launch researchers, a bigger investigation could break into finer pieces, but tool budgets and deadlines would be sliced thinner at each branch and plan review would scatter (“Deep research returns reports with coverage and gaps, not raw text”). Identity, model, and permission checks also need to apply to only one level, under one set of rules.

The wait cap exists to cut off failures. When a researcher is cut off, the lead sees its share as a gap in the pre-report review and either reassigns it or states it in the report. Raising the cap would cut off fewer slow but healthy investigations, and it would also lengthen how long one dead researcher can hold the turn.

If the work you hand to subagents is independent and no human watches it run, let the subagents' tools, model, and identity come from definitions in code and policy set by an administrator, not from arguments the parent model chooses.