Add background agent task tools for concurrent sub-agent dispatch (#863)#1908
Draft
jfrey wants to merge 4 commits intodocker:mainfrom
Draft
Add background agent task tools for concurrent sub-agent dispatch (#863)#1908jfrey wants to merge 4 commits intodocker:mainfrom
jfrey wants to merge 4 commits intodocker:mainfrom
Conversation
Introduces four new built-in tools available to any agent with sub_agents, mirroring the existing run_background_job/view_background_job shell tool pattern: - run_background_agent: dispatch a sub-agent task asynchronously, returns a task ID - list_background_agents: list all background agent tasks with status and runtime - view_background_agent: view live output or final result of a task by ID - stop_background_agent: cancel a running task via context cancellation This enables an orchestrating agent to fan out work to multiple sub-agents concurrently, check in on progress, and collect results when ready — without blocking on each sub-agent sequentially. Closes docker#863 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
R001 - Fix data race on r.currentAgent: add currentAgentMu RWMutex, thread-safe CurrentAgentName()/setCurrentAgent() accessors, update all 26 read/write sites in runtime.go. R002 - Add 21 tests covering task lifecycle, all four handler methods, input validation, concurrency cap enforcement, and goroutine cleanup. R003 - Remove ReadOnlyHint:true from run_background_agent; the tool spawns agents that can execute write operations. R004 - Fix completed task memory leak: pruneCompleted() collects IDs in a first pass then deletes to avoid holding Range lock during Delete. R005 - Enforce maxConcurrentAgentTasks (20) and maxTotalAgentTasks (100) caps with clear error messages. R006 - Add sync.WaitGroup to backgroundAgentHandler; stopAll() now waits for all goroutines to exit before returning. R007 - Background tasks run with WithToolsApproved(true); they execute asynchronously with no user present to respond to approval prompts. R008 - Call sess.AddSubSession(s) on completion for cost/token tracking. Also: use crypto/rand for non-guessable task IDs; store actual error message from ErrorEvent in task.errMsg. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
SEC-003: set currentAgent to sub-agent before RunStream in the background goroutine so it uses the correct model, tools, and RAG config rather than the parent agent's identity. SEC-002: guard setElicitationEventsChannel / clearElicitationEventsChannel with !sess.ToolsApproved so background tasks (which never need elicitation) don't overwrite or null out the parent session's pending MCP auth channel. Tests: add concurrent-access test (race detector), output buffer truncation test, and dead-code documentation test for the non-prunable total cap path. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Lint: - context.Background() → t.Context() in all test functions (forbidigo) - for loops converted to integer range form (intrange, Go 1.22+) - toolCall() pre-computed in main goroutine before spawning goroutines to avoid require.FailNow from non-test goroutines (testifylint) - newRuntimeWithSubAgent unused *agent.Agent return removed (unparam) - gofmt: align map keys to longest entry in registerDefaultTools - teamloader: combine two consecutive appends into one (gocritic) E2E: - Update TestA2AServer_MultiAgent cassette to include the four new background agent tools in the recorded request body; the root agent in multi.yaml has sub_agents configured so it now receives all five tools Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
dgageot
reviewed
Mar 4, 2026
Member
dgageot
left a comment
There was a problem hiding this comment.
LGTM in general! Sorry, I left a few nit-picky comments. Feel free to ignore.
One of the things that could be better is that most of the code lives in the runtime package that we wish would be simpler. Do you think most of it could be extracted?
Comment on lines
+31
to
+36
| const ( | ||
| agentTaskRunning int32 = iota | ||
| agentTaskCompleted int32 = iota | ||
| agentTaskStopped int32 = iota | ||
| agentTaskFailed int32 = iota | ||
| ) |
Member
There was a problem hiding this comment.
Suggested change
| const ( | |
| agentTaskRunning int32 = iota | |
| agentTaskCompleted int32 = iota | |
| agentTaskStopped int32 = iota | |
| agentTaskFailed int32 = iota | |
| ) | |
| const ( | |
| agentTaskRunning int32 = iota | |
| agentTaskCompleted | |
| agentTaskStopped | |
| agentTaskFailed | |
| ) |
|
|
||
| // agent task status constants — mirrors backgroundJob in shell.go | ||
| const ( | ||
| agentTaskRunning int32 = iota |
Member
There was a problem hiding this comment.
We should maybe introduce a type for the enum rather than int32?
| } | ||
|
|
||
| // newTaskID generates a unique, non-guessable ID for a background agent task. | ||
| func newTaskID() (string, error) { |
Member
There was a problem hiding this comment.
Maybe use github.com/google/uuid?
|
|
||
| // runningTaskCount returns the number of currently running tasks. | ||
| func (h *backgroundAgentHandler) runningTaskCount() int { | ||
| count := 0 |
Member
There was a problem hiding this comment.
Suggested change
| count := 0 | |
| var count int |
| @@ -0,0 +1,179 @@ | |||
| # Plan: Background Agent Tasks (Issue #863) | |||
Member
There was a problem hiding this comment.
FWIW, we usually try to avoid committing those plans
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements concurrent sub-agent orchestration from #863 by adding a family of four background agent tools that mirror the existing shell background job pattern (
run_background_job/list_background_jobs/ etc.):run_background_agent— dispatch a named sub-agent task asynchronously, returns a task ID immediatelylist_background_agents— show all tasks with status and runtimeview_background_agent— inspect live buffered output or final result by task IDstop_background_agent— cancel a running taskTools are registered automatically for any agent that has
sub_agentsconfigured, alongside the existingtransfer_tasktool.Key design decisions
view_background_agent, matching the UX of shell background jobs the LLM already knowsToolsApproved=truefor background sessions — no user present to answer approval prompts; documented with a TODO for per-session permission scopingNotable fixes applied during review
setCurrentAgent(params.Agent)beforeRunStreamso the background task uses the sub-agent's model/tools/RAG rather than the parent'ssetElicitationEventsChannel/clearElicitationEventsChannelare now guarded with!sess.ToolsApprovedso background task teardown doesn't null out the parent session's pending MCP auth channelcurrentAgentdata race: addedcurrentAgentMu sync.RWMutexwith getter/setter; all 26 read sites updatedconcurrent.MapRange/Delete deadlock: two-pass delete inpruneCompletedCompareAndSwap(running, completed)prevents overwriting a stopped statussync.WaitGroupinstopAllfor clean goroutine shutdownKnown limitations (tracked as TODOs in code)
currentAgentfield — proper fix requires passing the agent through the session rather than shared runtime stateRunStreamhas other shared fields (registerDefaultToolsmap,resumeChan) that are unsafe for truly concurrent calls — documented in codeTest plan
pkg/runtime/background_agent_test.gocovering all four handlers, caps, error paths, and concurrent accessgo test -race ./pkg/runtime/ -run TestHandler_Concurrent./cagentand exercise background agent dispatch in a team with sub-agents configured🤖 Generated with Claude Code