6 min read
Three sub agent designs. How opencode, pi and deepseek-harness isolate, restrict and stop recursion
We read the sub agent implementations in three open source agent projects and compared their choices on context isolation, permission derivation, recursion guards, concurrency, timeouts and return contracts. All three are equally narrow, and none has a timeout or a retry. We close with what this means for digital employees delegating to each other.
- sub agent
- survey
- agent
- secretary
In August 2026 we read the sub agent implementations in three open source projects: opencode, pi and deepseek-harness. Conclusions come from code, with documentation as reference only. The three take very different positions, yet agree on several key points by accident.
Three positions
| Position | Shape | |
|---|---|---|
| opencode | Sub agents are a first class built-in | One built-in task tool. A child is an ordinary session with a parent pointer |
| pi | The core explicitly refuses | The README says No sub-agents. The only implementation is a thousand-line example extension that spawns a child process |
| deepseek-harness | A service interface | Eleven packages, six providers, one-shot and continuable lifecycles, cold recovery |
pi's refusal is not laziness. It pushes sub agents, plan mode, todos, background bash, MCP and approval popups out of the core as workflow layer, betting that the bottleneck in complex tasks is context and traceability rather than orchestration. Its effort goes into compaction, session trees and tool output truncation.
Isolation: logical, physical, or a dimension
- opencode uses logical isolation. Child sessions share storage, events and the permission stack with the parent and form a tree through the parent pointer. Zero new infrastructure, at the cost of filtering children everywhere.
- pi uses physical isolation. A separate process, nothing persisted, and only three channels between parent and child: argv, stdout and a kill signal. The cleanest isolation, and nothing survives the run.
- deepseek-harness makes isolation level a provider dimension. In process there is fresh start and fork from the parent log prefix. Across processes there are four external agents. The most flexible and the heaviest.
Should the model see the agent type
This is the widest split.
opencode gives the model a type parameter. The candidate list is trimmed by the caller's permissions before it is written into the tool description, so a forbidden agent vanishes and the model never knows it exists. pi also lets the model pick by name, with definitions of only four fields: name, description, tools and model, rescanned from disk on every call.
deepseek-harness goes the other way. The model-facing tool takes only a description and a task. No type. To expose several kinds of sub agent you load the same plugin several times in configuration. The design note says: the service holds the multi-provider registry, the tool picks one, the schema carries no type.
One side lets the model choose the type. The other binds it at deployment and lets the model fill in only the task. Opposite directions.
Context passing: all three are narrow
Everything a parent passes to a child is one prompt string. No conversation history, no file list. The child's intermediate steps never enter the parent's context, and the parent receives only the text of the final reply.
The differences are at the edges. opencode expands @file mentions and can resume an old child by task id. pi's chain mode hands off through string substitution. deepseek-harness's fork can seed from a balanced prefix of the parent log.
Recursion guards
| Mechanism | Default depth | |
|---|---|---|
| opencode | Walk the parent chain to compute depth, fail before the permission prompt, then inject a no-delegation rule into the child | 1, no nesting |
| pi | None. A child that declares no tools loads every extension, including the sub agent tool itself, and can nest without limit | none |
| deepseek-harness | Delegation depth is written into the session header, persistent and monotonic. Neither restart nor cold recovery can demote a child to top level | 3 |
The deepseek-harness point is the one to remember: depth must be persisted. A computed depth breaks across process recovery.
Concurrency, timeouts, retries
- opencode has no concurrency cap and relies on the prompt to encourage multiple tool calls per message. Cancelling the parent recursively cancels all descendants.
- pi caps parallel tasks at eight and concurrency at four with a hand-written worker pool. Cancellation is a real kill: SIGTERM, then SIGKILL after five seconds.
- deepseek-harness lets the agent loop synthesize a parallel pool with no dedicated cap.
None of the three has a timeout. None has a retry. That agreement was unexpected.
Return contracts and billing
- None of the three rolls child tokens up to the parent. Each session is billed separately and the parent pays only for the returned text. pi does not even count child spend in its cost view.
- Only deepseek-harness has structured output. It avoids JSON mode and instead registers a mandatory tool inside the child scope with two-phase commit.
- Truncation: pi hard-truncates each task at 50KB and keeps the full content for the UI. opencode does not truncate at all, so a long child reply lands in the parent context unchanged.
Where permissions are pinned
opencode's parent-to-child permission derivation is asymmetric. The child inherits only the parent session's deny rules and external directory rules. The parent agent's own capability limits do not leak down. That boundary was fixed twice in opposite directions before being frozen into a dedicated regression test. Children cannot ask the user questions by default.
deepseek-harness pins the approval policy at the delegation boundary. Whatever the parent had, the child never asks for approval, because nobody is watching a child's approval popup. The policy is written into the child's own log as events, and cold recovery replays those events instead of copying from the parent again. The model also gets a fixed line: your permission scope was fixed when you were started, do not retry a denied operation, state the limitation in your reply.
pi has no tool approval. Its trust boundary sits on the agent's origin. Only definitions in the user's home directory load by default, and repository definitions require an explicit request from the model plus user confirmation. The threat model treats a repository-controlled prompt as executable code.
What this means for digital employees delegating to each other
In createrole the secretary can assign a task to another digital employee from the contact list. The assignee works on its own computer in a dedicated session and returns the result and deliverables. Reading the three implementations gives us these comparisons:
- Timeouts and restart recovery we already have. A delegated task has a thirty minute timeout and is actively interrupted when it expires. After a service restart, unfinished delegations recover from the ledger, and reported results are deduplicated by id. None of the three has either.
- The deliverable channel is specific to this kind of product. All three return contracts are text only. Digital employees produce files, so we have an agreed directory, snapshots and at-least-once delivery with deduplication. That is a product difference between digital employees and coding agents, not a gap.
- Depth must be persisted. The assignee may itself be a secretary, and we have no depth concept yet. The deepseek-harness approach is the only one that holds across process recovery.
- Child output must be truncated. opencode's lack of truncation is a known trap. pi's 50KB plus full content for the UI is the clean answer.
- A delegated employee asking a human needs explicit semantics. It would be asking in a dedicated session nobody is watching. Either forbid by default like opencode, or pin to never-approve and tell the model, like deepseek-harness.
On whether agent type should be a model parameter, we lean toward the deepseek-harness direction. If the model has no freedom over type, no new profession requires a change to the tool schema.