Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions agent-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -801,7 +801,8 @@
"lsp",
"user_prompt",
"openapi",
"model_picker"
"model_picker",
"background_agents"
]
},
"instruction": {
Expand Down Expand Up @@ -998,7 +999,8 @@
"a2a",
"lsp",
"user_prompt",
"model_picker"
"model_picker",
"background_agents"
]
}
}
Expand Down Expand Up @@ -1082,6 +1084,13 @@
]
}
]
},
{
"properties": {
"type": {
"const": "background_agents"
}
}
}
]
},
Expand Down
44 changes: 44 additions & 0 deletions docs/concepts/multi-agent/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,50 @@ transfer_task(

</div>

## Parallel Delegation with Background Agents

`transfer_task` is **sequential** — the coordinator waits for the sub-agent to finish before continuing. When you need to fan out work to multiple agents at the same time, use the `background_agents` toolset instead.

Add it to your coordinator’s toolsets:

```yaml
agents:
root:
model: anthropic/claude-sonnet-4-0
description: Research coordinator
sub_agents: [researcher, analyst, writer]
toolsets:
- type: think
- type: background_agents
```

The coordinator can then:

1. **Dispatch** several tasks at once with `run_background_agent` — each returns a task ID immediately
2. **Monitor** progress with `list_background_agents` or `view_background_agent`
3. **Collect** results once tasks complete
4. **Cancel** tasks that are no longer needed with `stop_background_agent`

```bash
# Start two tasks in parallel
run_background_agent(agent="researcher", task="Find recent papers on LLM agents")
run_background_agent(agent="analyst", task="Analyze our current architecture")

# Check on all tasks
list_background_agents()

# Read results when ready
view_background_agent(task_id="agent_task_abc123")
```

<div class="callout callout-tip">
<div class="callout-title">💡 When to use which
</div>
<p><strong><code>transfer_task</code></strong> — simple, sequential delegation. Best when the coordinator needs the result before deciding what to do next.</p>
<p><strong><code>background_agents</code></strong> — parallel, async delegation. Best when multiple independent tasks can run simultaneously.</p>

</div>

## Example: Development Team

```yaml
Expand Down
25 changes: 25 additions & 0 deletions docs/configuration/tools/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,31 @@ toolsets:

The `transfer_task` tool is automatically available when an agent has `sub_agents`. Allows delegating tasks to sub-agents. No configuration needed — it's enabled implicitly.

### Background Agents

Dispatch work to sub-agents concurrently and collect results asynchronously. Unlike `transfer_task` (which blocks until the sub-agent finishes), background agent tasks run in parallel — the orchestrator can start several tasks, do other work, and check on them later.

```yaml
toolsets:
- type: background_agents
```

| Operation | Description |
| ------------------------ | ------------------------------------------------------------------------------------------------ |
| `run_background_agent` | Start a sub-agent task in the background; returns a task ID immediately |
| `list_background_agents` | List all background tasks with their status and runtime |
| `view_background_agent` | View live output or final result of a task by ID |
| `stop_background_agent` | Cancel a running task by ID |

No configuration options. Requires the agent to have `sub_agents` configured so the background tasks have agents to dispatch to.

<div class="callout callout-tip">
<div class="callout-title">💡 Tip
</div>
<p>Use <code>background_agents</code> when your orchestrator needs to fan out work to multiple specialists in parallel — for example, researching several topics simultaneously or running independent code analyses side by side.</p>

</div>

### LSP (Language Server Protocol)

Connect to language servers for code intelligence: go-to-definition, find references, diagnostics, and more.
Expand Down
42 changes: 21 additions & 21 deletions e2e/testdata/cassettes/TestA2AServer_MultiAgent.yaml

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,4 @@ These examples are groups of agents working together. Each of them is specialize
| [finance.yaml](finance.yaml) | Financial research and analysis | | | | ✓ | | [duckduckgo](https://hub.docker.com/mcp/server/duckduckgo/overview) | ✓ |
| [shared-todo.yaml](shared-todo.yaml) | Shared todo item manager | | | ✓ | | | | ✓ |
| [pr-reviewer-bedrock.yaml](pr-reviewer-bedrock.yaml) | PR review toolkit (Bedrock) | ✓ | ✓ | | | | | ✓ |
| [background_agents.yaml](background_agents.yaml) | Parallel research with background agents | | | | ✓ | | [duckduckgo](https://hub.docker.com/mcp/server/duckduckgo/overview) | ✓ |
49 changes: 49 additions & 0 deletions examples/background_agents.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
#!/usr/bin/env docker agent run

# This example demonstrates the background_agents toolset, which lets a
# coordinator dispatch work to multiple sub-agents in parallel rather than
# waiting for each one sequentially.
#
# The coordinator fans out research tasks to two specialists at the same time,
# monitors their progress, and synthesises a final answer once both are done.

agents:
root:
model: anthropic/claude-sonnet-4-0
description: Research coordinator that dispatches work in parallel
instruction: |
You are a research coordinator. When the user asks a question:

1. Break the question into independent research tasks.
2. Dispatch each task to the appropriate sub-agent using
`run_background_agent` so they run in parallel.
3. Use `list_background_agents` or `view_background_agent` to check
progress and collect results.
4. Synthesise the findings into a clear, well-structured answer.

Always run independent tasks concurrently — do not wait for one to
finish before starting the next.
sub_agents: [web_researcher, code_analyst]
toolsets:
- type: think
- type: background_agents

web_researcher:
model: openai/gpt-4o
description: Searches the web for information and summarises findings
instruction: |
You are a web researcher. Use the search tools to find relevant,
up-to-date information. Provide concise summaries with sources.
toolsets:
- type: mcp
ref: docker:duckduckgo

code_analyst:
model: anthropic/claude-sonnet-4-0
description: Analyses local code and provides technical insights
instruction: |
You are a code analyst. Read the codebase to answer technical
questions. Reference specific files and line numbers in your answers.
toolsets:
- type: filesystem
- type: shell
2 changes: 2 additions & 0 deletions pkg/config/latest/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,8 @@ func (t *Toolset) validate() error {
if len(t.Models) == 0 {
return errors.New("model_picker toolset requires at least one model in the 'models' list")
}
case "background_agents":
// no additional validation needed
}

return nil
Expand Down
18 changes: 6 additions & 12 deletions pkg/runtime/loop.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,27 +24,21 @@ import (
"github.com/docker/docker-agent/pkg/telemetry"
"github.com/docker/docker-agent/pkg/tools"
"github.com/docker/docker-agent/pkg/tools/builtin"
agenttool "github.com/docker/docker-agent/pkg/tools/builtin/agent"
)

// bgAgentHandler wraps a background-agent method as a ToolHandlerFunc.
func (r *LocalRuntime) bgAgentHandler(fn func(context.Context, *session.Session, tools.ToolCall) (*tools.ToolCallResult, error)) ToolHandlerFunc {
return func(ctx context.Context, sess *session.Session, tc tools.ToolCall, _ chan Event) (*tools.ToolCallResult, error) {
return fn(ctx, sess, tc)
}
}

// registerDefaultTools wires up the built-in tool handlers (delegation,
// background agents, model switching) into the runtime's tool dispatch map.
func (r *LocalRuntime) registerDefaultTools() {
r.toolMap[builtin.ToolNameTransferTask] = r.handleTaskTransfer
r.toolMap[builtin.ToolNameHandoff] = r.handleHandoff
r.toolMap[builtin.ToolNameChangeModel] = r.handleChangeModel
r.toolMap[builtin.ToolNameRevertModel] = r.handleRevertModel
r.toolMap[agenttool.ToolNameRunBackgroundAgent] = r.bgAgentHandler(r.bgAgents.HandleRun)
r.toolMap[agenttool.ToolNameListBackgroundAgents] = r.bgAgentHandler(r.bgAgents.HandleList)
r.toolMap[agenttool.ToolNameViewBackgroundAgent] = r.bgAgentHandler(r.bgAgents.HandleView)
r.toolMap[agenttool.ToolNameStopBackgroundAgent] = r.bgAgentHandler(r.bgAgents.HandleStop)

r.bgAgents.RegisterHandlers(func(name string, fn func(context.Context, *session.Session, tools.ToolCall) (*tools.ToolCallResult, error)) {
r.toolMap[name] = func(ctx context.Context, sess *session.Session, tc tools.ToolCall, _ chan Event) (*tools.ToolCallResult, error) {
return fn(ctx, sess, tc)
}
})
}

// finalizeEventChannel performs cleanup at the end of a RunStream goroutine:
Expand Down
6 changes: 6 additions & 0 deletions pkg/teamloader/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"github.com/docker/docker-agent/pkg/tools"
"github.com/docker/docker-agent/pkg/tools/a2a"
"github.com/docker/docker-agent/pkg/tools/builtin"
agenttool "github.com/docker/docker-agent/pkg/tools/builtin/agent"
"github.com/docker/docker-agent/pkg/tools/mcp"
)

Expand Down Expand Up @@ -77,6 +78,7 @@ func NewDefaultToolsetRegistry() *ToolsetRegistry {
r.Register("user_prompt", createUserPromptTool)
r.Register("openapi", createOpenAPITool)
r.Register("model_picker", createModelPickerTool)
r.Register("background_agents", createBackgroundAgentsTool)
return r
}

Expand Down Expand Up @@ -347,3 +349,7 @@ func createModelPickerTool(_ context.Context, toolset latest.Toolset, _ string,
}
return builtin.NewModelPickerTool(toolset.Models), nil
}

func createBackgroundAgentsTool(_ context.Context, _ latest.Toolset, _ string, _ *config.RuntimeConfig, _ string) (tools.ToolSet, error) {
return agenttool.NewToolSet(), nil
}
3 changes: 1 addition & 2 deletions pkg/teamloader/teamloader.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ import (
"github.com/docker/docker-agent/pkg/team"
"github.com/docker/docker-agent/pkg/tools"
"github.com/docker/docker-agent/pkg/tools/builtin"
agenttool "github.com/docker/docker-agent/pkg/tools/builtin/agent"
"github.com/docker/docker-agent/pkg/tools/codemode"
)

Expand Down Expand Up @@ -490,7 +489,7 @@ func getToolsForAgent(ctx context.Context, a *latest.AgentConfig, parentDir stri
}

if len(a.SubAgents) > 0 {
toolSets = append(toolSets, builtin.NewTransferTaskTool(), agenttool.NewToolSet())
toolSets = append(toolSets, builtin.NewTransferTaskTool())
}
if len(a.Handoffs) > 0 {
toolSets = append(toolSets, builtin.NewHandoffTool())
Expand Down
35 changes: 26 additions & 9 deletions pkg/tools/builtin/agent/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -384,23 +384,40 @@ func (h *Handler) StopAll() {
h.wg.Wait()
}

// toolSet is a lightweight ToolSet that returns just the tool definitions
// without requiring a Runner. Used by teamloader to register tool schemas.
type toolSet struct{}
// RegisterHandlers adds all background agent tool handlers to the given
// dispatch map, keyed by tool name.
func (h *Handler) RegisterHandlers(register func(name string, fn func(context.Context, *session.Session, tools.ToolCall) (*tools.ToolCallResult, error))) {
register(ToolNameRunBackgroundAgent, h.HandleRun)
register(ToolNameListBackgroundAgents, h.HandleList)
register(ToolNameViewBackgroundAgent, h.HandleView)
register(ToolNameStopBackgroundAgent, h.HandleStop)
}

// NewToolSet returns a ToolSet for registering background agent tool definitions.
// This does not require a Runner and is suitable for use in teamloader.
// NewToolSet returns a lightweight ToolSet for registering background agent
// tool definitions and instructions. It does not require a Runner and is
// suitable for use in the teamloader registry.
func NewToolSet() tools.ToolSet {
return &toolSet{}
}

func (t *toolSet) Tools(ctx context.Context) ([]tools.Tool, error) {
// toolSet provides tool definitions and instructions without a Runner.
type toolSet struct{}

func (t *toolSet) Tools(context.Context) ([]tools.Tool, error) {
return backgroundAgentTools()
}

// Tools returns the four background agent tool definitions.
func (h *Handler) Tools(ctx context.Context) ([]tools.Tool, error) {
return backgroundAgentTools()
func (t *toolSet) Instructions() string {
return `# Background Agent Tasks

Use background agent tasks to dispatch work to sub-agents concurrently.

- **run_background_agent**: Start a command, returns task ID. The sub-agent runs with all tools pre-approved — use only with trusted sub-agents and well-scoped tasks.
- **list_background_agents**: Show all tasks with status and runtime
- **view_background_agent**: Get output and status of a task by task_id
- **stop_background_agent**: Terminate a task by task_id

**Notes**: Output capped at 10MB per task. All tasks auto-terminate when the agent stops.`
}

func backgroundAgentTools() ([]tools.Tool, error) {
Expand Down
19 changes: 16 additions & 3 deletions pkg/tools/builtin/agent/agent_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -538,9 +538,9 @@ func TestHandler_ConcurrentAccess(t *testing.T) {

// --- Tools ---

func TestTools_ReturnsFourTools(t *testing.T) {
h := NewHandler(&mockRunner{})
toolsList, err := h.Tools(t.Context())
func TestNewToolSet_ReturnsFourTools(t *testing.T) {
ts := NewToolSet()
toolsList, err := ts.Tools(t.Context())
require.NoError(t, err)
assert.Len(t, toolsList, 4)

Expand All @@ -553,3 +553,16 @@ func TestTools_ReturnsFourTools(t *testing.T) {
assert.Contains(t, names, ToolNameViewBackgroundAgent)
assert.Contains(t, names, ToolNameStopBackgroundAgent)
}

func TestNewToolSet_Instructions(t *testing.T) {
ts := NewToolSet()
instructable, ok := ts.(tools.Instructable)
require.True(t, ok, "NewToolSet should implement Instructable")

instructions := instructable.Instructions()
assert.NotEmpty(t, instructions)
assert.Contains(t, instructions, "run_background_agent")
assert.Contains(t, instructions, "list_background_agents")
assert.Contains(t, instructions, "view_background_agent")
assert.Contains(t, instructions, "stop_background_agent")
}