Skip to content

LOU-Y1, LOU-Y3: sub-agents inherit the parent runtime; subagents option with a task tool - #71

Merged
LinuxDevil merged 5 commits into
mainfrom
lou-y1-y3-subagents
Oct 1, 2026
Merged

LinuxDevil merged 5 commits into
mainfrom
lou-y1-y3-subagents

Conversation

@LinuxDevil

Copy link
Copy Markdown
Owner

What

LOU-Y3: subagents option and the task tool

const researcher = createAgent({ provider, instructions: 'You research...', tools: [webSearch], description: 'Finds and summarizes sources' });
const lead = createAgent({
  provider, instructions: 'You coordinate...',
  subagents: { researcher, writer },   // or a catalog: { list(), resolve(name) }
  maxSubagentDepth: 2,                 // default 1
});
  • New description on createAgent(); required for an agent used as a sub-agent (clear error if missing, or if the value is not a createAgent() agent).
  • subagents / maxSubagentDepth on createAgent() and AgentExecutor.execute()/stream(). Registers ONE task tool ({ agent: enum, prompt, description }) plus an "Available sub-agents" prompt block. A catalog is list()ed at the start of each run. A user tool already named task throws.
  • The tool result is the sub-agent's final text plus a footer ([sub-agent 'researcher': 3 step(s), finish reason 'stop']). An error, maxSteps running out, an abort or an unknown name each become an error result that says why.
  • Several task calls in one turn run in parallel (LOU-V3). Depth is a budget: each sub-agent gets its parent's budget minus one, and a run at 0 is not offered task.
  • task and createDelegateTool() share one delegation core (src/execution/delegation.ts). createDelegateTool() keeps its API and { text, usage } result.

LOU-Y1: delegation inherits the parent runtime. Unless the child sets its own, a child run gets the parent's:

  • abort signal
  • exporter: the child's invoke_agent span is parented to the parent's execute_tool span (new ExecuteOptions.parentSpanId)
  • captureContent / redactContent
  • hooks, with the new ctx.subagent ({ name, depth, toolCallId, description?, parent? }) so a hook can opt out. A hook that throws inside a sub-agent halts the whole run.
  • approvalStore (see below)
  • toolConcurrency and sandbox
  • onEvent, with events tagged event.subagent

Usage rolls up through LOU-V5's onDelegatedUsage. On a streaming parent (agent.stream()), the child's steps, text deltas, tool events and errors are streamed inline with a subagent field. run.start and run.done still mark only the top-level run, once each. The child keeps its own instructions, model, tools, skills and maxSteps, and sees only the task prompt.

Approval inside a child: full durable nested resume, not the simple fallback.

  • A child call that needsApproval pauses the WHOLE run.
  • The child's snapshot is stored inside the parent's single approval record (ExecutionSnapshot.subagent, PendingApproval.subagentPath), so any ApprovalStore works.
  • The parent transcript keeps a placeholder result for the paused task call, so sibling results from the same turn are kept and never re-run.
  • resumeAfterApproval() re-enters the parent's tool call and resumes the child with the decision. The child's final answer becomes the tool result, and the parent then continues. If the child needs another approval, the run pauses again with a new id. This works at any depth and for createDelegateTool() children.
  • If two children pause in one turn, the run pauses on the first. The second gets an error result saying its call was not run.

Why

The audit found that child agents lose hooks, tracing and approvals. Competitors (open-harness, eve) offer a task-style sub-agent catalog with isolated context and depth limits.

How verified

Ran on the merge with origin/main at a343df5 (LOU-V5). The last merge, 90aa72b (agent-forge fix plus one export), was re-checked with tsc, the execution/subagents/createAgent tests and docs:llms:check.

  • npx tsc --noEmit: clean
  • npm run lint: 0 errors, 131 warnings, all pre-existing. The files this PR touches have only their 2 existing any warnings.
  • npm run build && npm run build --workspace=packages/create-loushy-agent: success
  • npm run test:coverage: Test Files 132 passed (132), Tests 1951 passed | 4 skipped (1955). On one earlier run, http.test.ts > rejects near the configured timeout failed on timing (2371ms vs 2000ms) under load. It passed on rerun and is unrelated.
  • npm run fallow: exit 0. No issues found, 0 above threshold. It still reports a duplicate (AnthropicProvider/OpenAIProvider) that is already on main.
  • npm run test:types: 4 files passed, no type errors
  • npm run docs:verify-snippets: all 84 snippet(s) type-check against src/; 6 also run cleanly
  • npm run docs:llms regenerated; docs:llms:check passes
  • git diff --diff-filter=D --name-only origin/main HEAD: empty

New tests (32): src/subagents/subagents.test.ts and src/execution/subagentInheritance.test.ts. They cover:

  • isolated context
  • parallel tasks
  • the depth limit (default 1 / 2 / 0)
  • catalog, unknown agent and configuration errors
  • max-steps and failure results
  • usage roll-up
  • hooks with ctx.subagent, and a hook halting the run
  • span parenting
  • abort propagation
  • onEvent and stream forwarding, including nested chains
  • toolConcurrency inheritance and override
  • approval: approve, reject, re-pause, depth 2, a sibling kept, two pauses in one turn, createDelegateTool, no store, and a streaming lead. A JSON round-trip store proves snapshots are serializable.

Notes for reviewer

  • Context passing: the parent runtime reaches the child through the tool's execute(args, options) options object (a WeakMap-bound scope). It does not use AsyncLocalStorage: node:async_hooks on the AgentExecutor path breaks the Cloudflare Worker bundle. I hit that first; the wrangler test caught it.
  • Shared-file edits:
    • AgentExecutor.ts: extensions, parentSpanId, the scope, suspension handling in settleToolBatch, and savePause, split out of pauseForApproval
    • toolCallExecution.ts
    • sandboxGuard.ts
    • resume.ts: the nested branch lives in a new resumeSubagent.ts
    • agentRun.ts / agentEvents.ts: the subagent tag, and a per-sink step result so parallel children do not clobber each other
  • Snapshots now store the agent as configured (before skills/sub-agents are applied), so resume can re-apply them. Before this, resuming a run that had skills threw "load_skill already registered".
  • Not inherited: sessionId / checkpointStore. A child is not checkpointed on its own, so a crash mid-child re-runs that task call on resume. Also not inherited: the single-run callbacks (onLLMRequest, ...). There is no logger on ExecuteOptions to inherit.
  • Approvals need AgentExecutor for the lead, because createAgent() has no approvalStore yet (LOU-D21).
  • Follow-up: loadAgentDir (LOU-Y5) still turns subagents/<name>/ into delegate_to_<name> tools. It could switch to the native subagents option.

🤖 Generated with Claude Code

LinuxDevil and others added 5 commits October 1, 2026 11:01
…on with a task tool

LOU-Y3: `subagents` (a record of createAgent() agents with a new
`description` field, or a `{ list, resolve }` catalog) on createAgent()
and AgentExecutor.execute() registers ONE `task` tool
({ agent, prompt, description }) and an "Available sub-agents" prompt
block. A sub-agent runs on the task prompt alone (isolated context); its
final text plus a steps/finish-reason footer becomes the tool result, and
failures (error, maxSteps, abort, unknown name) become error results that
say why. `maxSubagentDepth` (default 1) bounds nesting: a run at the limit
is not offered `task`. Parallel task calls run concurrently (LOU-V3).

LOU-Y1: the `task` tool and createDelegateTool() share one delegation
core (src/execution/delegation.ts). The child inherits the parent run's
abort signal, trace exporter and span parent (invoke_agent under
execute_tool), hooks (ctx.subagent set; a throwing hook halts the whole
run), approval store, toolConcurrency (unless the child sets its own),
sandbox, captureContent/redactContent and onEvent (events tagged with
event.subagent); child token usage rolls into the parent's result.usage.
The executor passes this through the tool's execute options object (no
async-context globals, so the Worker bundle stays free of node:*).

Approvals: a child call that needs approval pauses the WHOLE run. The
child's paused snapshot is stored inside the parent's single approval
record (ExecutionSnapshot.subagent, PendingApproval.subagentPath); the
parent transcript keeps a placeholder result for the task call, so
sibling results of the same turn are kept. resumeAfterApproval()
re-enters the parent's tool call, resumes the child with the decision,
and returns the child's final answer as the tool result (re-pausing if
the child needs another approval), at any depth. If two children pause
in one turn the run pauses on the first and the second gets an error
result. Snapshots now store the agent as configured (before skills /
sub-agents were applied) so resume can re-apply them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Resolve conflicts with LOU-V2 (agent.stream), LOU-Y5 (agent directories)
and LOU-W5; stream sub-agent events inside the parent's AgentRun with a
subagent field.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sub-agent usage rolls up through LOU-V5's onDelegatedUsage (the task tool
too, and a sub-agent resumed after approval adds only what it spends after
the resume). Streamed sub-agent events no longer include their own
run.start/run.done, so those still mark the top-level run exactly once.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@LinuxDevil
LinuxDevil merged commit a03b1a3 into main Oct 1, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant