Skip to content

[EPIC-Y][LOU-Y4.2] Background sub-agents are cancelled or awaited when the lead run ends - #92

Merged
LinuxDevil merged 1 commit into
mainfrom
lou-y4-2-background-run-end
Oct 1, 2026
Merged

LinuxDevil merged 1 commit into
mainfrom
lou-y4-2-background-run-end

Conversation

@LinuxDevil

Copy link
Copy Markdown
Owner

What

  • ExecuteOptions.onRunEnd?: ({ result } | { error }) => void | Promise<void>: a small, generic run-lifecycle hook. AgentExecutor calls it exactly once per execute() / stream() run, in a finally around withExtensions() + runAgentLoop(). That covers every finishReason (aborted, awaiting-approval, max-steps, ...), a rejected run, and a sub-agent catalog that fails before the loop starts. The run settles after the hook returns; if the hook throws, the run rejects. AgentExecutor.ts has about 20 lines of code changes. resume.ts and toolCallExecution.ts are untouched.
  • withSubagents() takes the run's onRunEnd and wraps it with the background-task end, which BackgroundTasks.finish() handles:
    • Default: tasks still queued or running are cancelled. The run then waits until their runs have stopped, so no child event reaches onEvent after the run resolves.
    • awaitBackgroundOnFinish: true: the run waits for those tasks before it resolves. An aborted or failed lead run still cancels them.
    • In both cases result.backgroundTasks lists every background task of the run, in the agent_status shape ({ taskId, agent, status, elapsedMs, error?, approvalId?, toolName? }). The field is absent when the run started none. The user's own onRunEnd runs after this, so it sees the field.
  • SubagentOptions.awaitBackgroundOnFinish is new. createAgent({ subagentOptions: { maxConcurrent, awaitBackgroundOnFinish } }) is accepted and forwarded: the option type plus the one runOptions.subagents line in createAgent.ts. It uses subagentsWithOptions(), which copies the subagents record or catalog and puts the new options over any already attached with withSubagentOptions(). The caller's value is not mutated, so it can be shared between agents.
  • Approval pause: a lead run that pauses for approval cancels its background tasks (or awaits them, with awaitBackgroundOnFinish). The resumed run is a separate execute(), possibly in another process. Without touching resume.ts it has no link to the paused run's tasks, so keeping them alive could not be done simply. This is documented.

Why

LOU-Y4 (#91) left background children running after the lead run finished, with their events still reaching onEvent. The executor had no run-end hook that the sub-agent runtime could use. This completes LOU-Y4.

Acceptance criteria

  • Generic run-lifecycle hook in the executor (ExecuteOptions.onRunEnd), called exactly once per execute()/stream() run in a finally, including on abort, error, 'awaiting-approval' and 'max-steps'. The AgentExecutor.ts change is small, and resume.ts / toolCallExecution.ts are untouched.
  • By default, queued and running background children are cancelled when the run ends, and result.backgroundTasks: [{ taskId, agent, status, ... }] reports them (documented).
  • withSubagentOptions({ awaitBackgroundOnFinish: true }) waits for them before the run resolves and includes their final statuses.
  • A run pausing for approval cancels its children rather than keeping them alive across the resume (not simple without editing resume.ts); documented.
  • createAgent({ subagentOptions: { maxConcurrent, awaitBackgroundOnFinish } }) is accepted and forwarded (options type + the one subagents line in createAgent.ts).
  • Tests: parent finish cancels remaining children (running and queued, and no events come after the run resolves); awaitBackgroundOnFinish waits and reports; the hook runs once on abort, error, max-steps, awaiting-approval and stream; createAgent({ subagentOptions }) forwards and does not mutate subagents; lead failure and lead approval pause cancel children.
  • Docs: the "Background sub-agents" section of docs/sub-agents.md is updated (new "When the lead run ends" subsection, limits revised), llms files are regenerated, and CHANGELOG has Added and Changed entries (the Changed entry has a migration note).

How verified

  • npx tsc --noEmit: no errors
  • npm run lint: 0 errors, 405 warnings. The changed files add no warnings: the only one in them is the existing any in ExecutionEvent.
  • npm run test:types: Type Errors no errors
  • npx vitest run: Test Files 145 passed (145), Tests 2102 passed | 3 skipped (2105)
  • npm run build && npm run build --workspace=packages/create-loushy-agent: success
  • npm run docs:verify-snippets -- --skip-build: all 108 snippet(s) type-check against src/; 6 also run cleanly
  • npm run docs:llms:check: pass
  • npm run test:coverage: 145 files / 2102 tests passed, exit 0
  • npm run fallow: exit 0. Dead code: no issues; 0 above the complexity threshold. The one clone group it lists is unrelated to this PR: AnthropicProvider.ts / OpenAIProvider.ts.

🤖 Generated with Claude Code

https://claude.ai/code/session_016bY29ibQgv3MDdjAbUT4YR


Generated by Claude Code

…n the lead run ends

Add ExecuteOptions.onRunEnd, called exactly once per execute()/stream()
run in a finally (any finishReason, or the error). The subagent runtime
wraps it: background tasks still queued or running when the run ends are
cancelled (or awaited with awaitBackgroundOnFinish), the run resolves once
their runs have stopped, and result.backgroundTasks reports them.
createAgent({ subagentOptions }) forwards maxConcurrent and
awaitBackgroundOnFinish without mutating the caller's subagents value.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bY29ibQgv3MDdjAbUT4YR
@LinuxDevil
LinuxDevil merged commit 43751f4 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.

2 participants