Skip to content
Draft
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
6 changes: 3 additions & 3 deletions agent-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -631,21 +631,21 @@
},
"sub_agents": {
"type": "array",
"description": "List of sub-agents. Can be names of agents defined in this config, external references (OCI images like 'namespace/repo' or URLs), or named external references using 'name:reference' syntax (e.g. 'reviewer:myorg/review-pr'). External agents without an explicit name are named after their last path segment. Pin external OCI references to an immutable digest (e.g. 'namespace/repo@sha256:...') to serve them from cache; tag references (including the implicit ':latest') are re-resolved against the registry on every run, even when the sub-agent is never invoked.",
"description": "List of sub-agents. Can be names of agents defined in this config, external references (OCI images like 'namespace/repo', URLs, or local config file paths like './team.yaml'), or named external references using 'name:reference' syntax (e.g. 'reviewer:myorg/review-pr' or 'specialists:./team.yaml'). External agents without an explicit name are named after their last path segment (file references after the file name without extension). A local file reference (.yaml, .yml or .hcl, resolved relative to this config file) exposes only that team's default agent ('root' if present, otherwise the first declared), which keeps its own sub-agents so it orchestrates its own team; transfer_task blocks until it returns. Pin external OCI references to an immutable digest (e.g. 'namespace/repo@sha256:...') to serve them from cache; tag references (including the implicit ':latest') are re-resolved against the registry on every run, even when the sub-agent is never invoked.",
"items": {
"type": "string"
}
},
"handoffs": {
"type": "array",
"description": "List of agents this agent can hand off the conversation to. Can be names of agents defined in this config, external references (OCI images like 'namespace/repo' or URLs), or named external references using 'name:reference' syntax (e.g. 'reviewer:myorg/review-pr'). External agents without an explicit name are named after their last path segment. Pin external OCI references to an immutable digest (e.g. 'namespace/repo@sha256:...') to serve them from cache; tag references are re-resolved against the registry on every run.",
"description": "List of agents this agent can hand off the conversation to. Can be names of agents defined in this config, external references (OCI images like 'namespace/repo', URLs, or local config file paths like './team.yaml'), or named external references using 'name:reference' syntax (e.g. 'reviewer:myorg/review-pr'). External agents without an explicit name are named after their last path segment (file references after the file name without extension). Pin external OCI references to an immutable digest (e.g. 'namespace/repo@sha256:...') to serve them from cache; tag references are re-resolved against the registry on every run.",
"items": {
"type": "string"
}
},
"force_handoff": {
"type": "string",
"description": "Name of an agent that unconditionally receives the conversation whenever this agent produces a final response, bypassing the LLM's tool-calling entirely. Guarantees deterministic routing for strict pipelines (e.g. extractor -> summarizer). Can be the name of an agent defined in this config or an external reference. Pin an external OCI reference to an immutable digest (e.g. 'namespace/repo@sha256:...') to avoid a per-run registry lookup. Must not reference the agent itself, and force_handoff chains must not form a cycle."
"description": "Name of an agent that unconditionally receives the conversation whenever this agent produces a final response, bypassing the LLM's tool-calling entirely. Guarantees deterministic routing for strict pipelines (e.g. extractor -> summarizer). Can be the name of an agent defined in this config or an external reference (OCI image, URL, or local config file path). Pin an external OCI reference to an immutable digest (e.g. 'namespace/repo@sha256:...') to avoid a per-run registry lookup. Must not reference the agent itself, and force_handoff chains must not form a cycle."
},
"add_date": {
"type": "boolean",
Expand Down
1 change: 1 addition & 0 deletions cmd/root/payload.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ func (f *runExecFlags) loadTeamRequest(agentSource config.Source) runtime.LoadTe
Source: agentSource,
ModelOverrides: f.modelOverrides,
PromptFiles: f.promptFiles,
ExternalTeams: f.teams,
RunConfig: &f.runConfig,
}
}
Expand Down
8 changes: 8 additions & 0 deletions cmd/root/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ type runExecFlags struct {
remoteAddress string
modelOverrides []string
promptFiles []string
teams []string
dryRun bool
runConfig config.RuntimeConfig
sessionDB string
Expand Down Expand Up @@ -143,6 +144,7 @@ func newRunCmd() *cobra.Command {
Long: "Run an agent with the specified configuration and prompt",
Example: ` docker-agent run ./agent.yaml
docker-agent run ./team.yaml --agent root
docker-agent run ./primary.yaml --team "Research team=./secondary.yaml"
docker-agent run # project config or built-in default agent
docker-agent run coder # built-in coding agent
docker-agent run ./echo.yaml "INSTRUCTIONS"
Expand All @@ -168,6 +170,7 @@ func addRunOrExecFlags(cmd *cobra.Command, flags *runExecFlags) {
cmd.PersistentFlags().BoolVar(&flags.hideToolResults, "hide-tool-results", false, "Hide tool call results")
cmd.PersistentFlags().StringVar(&flags.attachmentPath, "attach", "", "Attach an image file to the message")
cmd.PersistentFlags().StringArrayVar(&flags.promptFiles, "prompt-file", nil, "Append file contents to the prompt (repeatable)")
cmd.PersistentFlags().StringArrayVar(&flags.teams, "team", nil, "Attach a local YAML/HCL team to the primary lead: 'Team name=path' (repeatable)")
cmd.PersistentFlags().StringArrayVar(&flags.modelOverrides, "model", nil, "Override agent model: [agent=]provider/model (repeatable)")
cmd.PersistentFlags().BoolVar(&flags.dryRun, "dry-run", false, "Initialize the agent without executing anything")
cmd.PersistentFlags().StringVar(&flags.remoteAddress, "remote", "", "Use remote runtime with specified address")
Expand Down Expand Up @@ -210,6 +213,8 @@ func addRunOrExecFlags(cmd *cobra.Command, flags *runExecFlags) {
cmd.PersistentFlags().BoolVar(&flags.sessionReadOnly, "session-read-only", false, "Open the session in read-only mode (view conversation history but prevent new messages)")
cmd.MarkFlagsMutuallyExclusive("fake", "record")
cmd.MarkFlagsMutuallyExclusive("remote", "sandbox")
cmd.MarkFlagsMutuallyExclusive("remote", "team")
cmd.MarkFlagsMutuallyExclusive("sandbox", "team")
cmd.MarkFlagsMutuallyExclusive("remote", "session-db")
cmd.MarkFlagsMutuallyExclusive("remote", "session")
cmd.MarkFlagsMutuallyExclusive("remote", "record")
Expand Down Expand Up @@ -852,6 +857,9 @@ func (f *runExecFlags) loadAgentFrom(ctx context.Context, req runtime.LoadTeamRe
if len(req.PromptFiles) > 0 {
opts = append(opts, teamloader.WithPromptFiles(req.PromptFiles))
}
if len(req.ExternalTeams) > 0 {
opts = append(opts, teamloader.WithExternalTeams(req.ExternalTeams))
}
return teamloader.LoadWithConfig(ctx, req.Source, req.RunConfig, opts...)
}

Expand Down
33 changes: 33 additions & 0 deletions cmd/root/run_team_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package root

import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestRunTeamFlagIsRepeatable(t *testing.T) {
cmd := newRunCmd()
require.NoError(t, cmd.ParseFlags([]string{
"--team", "Research team=./secondary.yaml",
"--team", "QA team=./qa.hcl",
}))

flag := cmd.Flags().Lookup("team")
require.NotNil(t, flag)
assert.Equal(t, "[Research team=./secondary.yaml,QA team=./qa.hcl]", flag.Value.String())
}

func TestLoadTeamRequestCarriesExternalTeams(t *testing.T) {
flags := &runExecFlags{teams: []string{"Research team=./secondary.yaml", "QA team=./qa.hcl"}}
req := flags.loadTeamRequest(nil)
assert.Equal(t, flags.teams, req.ExternalTeams)
}

func TestRunSecondPositionalRemainsMessage(t *testing.T) {
flags := &runExecFlags{}
args := []string{"./primary.yaml", "./secondary.yaml"}
assert.Equal(t, "./primary.yaml", flags.resolveRunAgentFileName(args))
assert.Equal(t, "./secondary.yaml", args[1], "the second positional remains a message; use --team to compose teams")
}
55 changes: 55 additions & 0 deletions docs/concepts/multi-agent/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,61 @@ External references in `handoffs` and `force_handoff` carry the same per-run cos
>
> See [`examples/sub-agents-from-registry.yaml`](https://github.com/docker/docker-agent/blob/main/examples/sub-agents-from-registry.yaml) for a complete example mixing local and external sub-agents.

## External Teams from Local Files

`sub_agents` also accepts local file paths (`.yaml`, `.yml`, or `.hcl`), resolved relative to the importing file. This lets you split a large system across files: only the referenced team's **default agent** (the one named `root`, or the first declared) is exposed to the parent — the rest of that team stays private and keeps reporting to its own lead.

```yaml
# primary-team.yaml
agents:
root:
model: openai/gpt-5
description: Primary lead
instruction: Delegate specialist work to the secondary team lead.
sub_agents:
- specialists:./secondary-team.yaml # exposed as "specialists"
```

```yaml
# secondary-team.yaml
agents:
root:
model: openai/gpt-5
description: Secondary team lead
instruction: Coordinate your own specialists and report the result.
sub_agents: [researcher]

researcher:
model: openai/gpt-5
description: Researcher
instruction: Research topics and return concise notes.
```

The imported lead keeps its own `sub_agents` (here `researcher`), so it still orchestrates its own team. Without an alias, `- ./secondary-team.yaml` is exposed as `secondary-team` (the file name without extension). Delegation stays synchronous either way: the primary lead's `transfer_task` call blocks until the secondary lead returns its result.

### Compose teams from the CLI

The same relationship can be added at launch time without editing the primary YAML. Use the repeatable `--team` flag; the second positional argument remains a user message for backward compatibility.

```bash
docker agent run ./primary-team.yaml \
--team "Research team=./secondary-team.yaml"

# Add more teams
docker agent run ./primary-team.yaml \
--team "Research team=./secondary-team.yaml" \
--team "QA team=./qa-team.hcl"
```

`--team` accepts `Team name=path`, where the text before `=` is the human-readable TUI title. Paths may point to local `.yaml`, `.yml`, or `.hcl` files and resolve relative to the primary manifest. Routing IDs are generated separately, so the secondary lead keeps its normal display name (for example `root`) under the `Research team` title. This option is currently local-only and cannot be combined with `--remote` or `--sandbox`.

The TUI keeps the usual agent cards and uses each team name as that card section's title. Imported members are shown under their lead so their activity is visible during nested transfers, but they have no shortcut and cannot be selected directly.

Only the referenced team's agents are imported: an imported file must not declare top-level `permissions`, `budget`, `budgets`, or `runtime.safety` — those policies cannot be preserved across the import, so loading fails with an error asking you to declare them in the importing (main) manifest. Per-agent settings such as `agents.<name>.safety` travel with the agent and keep working.

> [!TIP]
> See [`examples/local-team/`](https://github.com/docker/docker-agent/tree/main/examples/local-team) for a runnable pair of files.

## Harness-Backed Sub-Agents

Sub-agents can be backed by external coding CLIs — Claude Code, Codex, opencode, or pi — instead of a model API. Add a `harness:` block in place of a `model:` field to create a harness sub-agent:
Expand Down
2 changes: 1 addition & 1 deletion docs/configuration/agents/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ agents:
| `description` | string | ✓ | Brief description of the agent's purpose. Used by coordinators to decide delegation. |
| `instruction` | string | ✓ | System prompt that defines the agent's behavior, personality, and constraints. Required unless `instruction_file` is set. |
| `instruction_file` | string \| array | ✗ | Path(s) to a file or files (relative to the config file's directory) whose contents become the agent's instruction, loaded at startup. Accepts a single path or a list; multiple files are concatenated in order, separated by a blank line. Mutually exclusive with `instruction`. Each path must be a local relative path inside the config directory (absolute paths and `..` traversal are rejected). Only supported for local file-based configs, not OCI/URL sources. See [External Instruction Files](#external-instruction-files) below. |
| `sub_agents` | array | ✗ | List of agent names or external OCI references this agent can delegate to. Supports local agents, registry references (e.g., `myorg/agent:tag`), and named references (`name:reference`). Automatically enables the `transfer_task` tool. Pin external OCI references to a digest (`name@sha256:…`) to skip the per-run registry lookup that tag references incur. See [External Sub-Agents](../../concepts/multi-agent/index.md#external-sub-agents-from-registries). |
| `sub_agents` | array | ✗ | List of agent names or external references this agent can delegate to. Supports local agents, registry references (e.g., `myorg/agent:tag`), local YAML/HCL file paths (e.g., `./team.yaml`, resolved relative to this file; only that team's lead/default agent is exposed and it keeps its own team), and named references (`name:reference`). Automatically enables the `transfer_task` tool. Pin external OCI references to a digest (`name@sha256:…`) to skip the per-run registry lookup that tag references incur. See [External Sub-Agents](../../concepts/multi-agent/index.md#external-sub-agents-from-registries) and [External Teams from Local Files](../../concepts/multi-agent/index.md#external-teams-from-local-files). |
| `toolsets` | array | ✗ | List of tool configurations. See [Tool Config](../tools/index.md). |
| `fallback` | object | ✗ | Automatic model failover configuration. |
| `add_date` | boolean | ✗ | When `true`, injects the current date into the agent's context. |
Expand Down
4 changes: 4 additions & 0 deletions docs/features/cli/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ $ docker agent run [config] [message...] [flags]
| `--json` | Output results as newline-delimited JSON (use with `--exec`) |
| `--hide-tool-calls` | Hide tool calls in the output |
| `--hide-tool-results` | Hide tool call results in the output |
| `--prompt-file <path>` | Append file contents to every agent prompt (repeatable) |
| `--team <name=path>` | Attach a local YAML/HCL team to the primary lead (repeatable). The text before `=` is the TUI section title; a unique routing ID is generated separately. Not available with `--remote` or `--sandbox`. |
| `--sandbox` | Run the agent inside a Docker sandbox (see [Sandbox](../../configuration/sandbox/index.md)) |
| `--template <image>` | Template image for the sandbox (default: `docker/docker-agent-sbx-templates:latest`) |
| `--sbx` | Prefer the `sbx` CLI backend when available (default `true`; set `--sbx=false` to force `docker sandbox`) |
Expand Down Expand Up @@ -84,6 +86,8 @@ $ docker agent run agent.yaml --model "dev=openai/gpt-4o,reviewer=anthropic/clau
$ docker agent run agent.yaml --session -1 # resume last session
$ docker agent run agent.yaml --session -1 --session-read-only # review last session without sending messages
$ docker agent run agent.yaml --prompt-file ./context.md # include file as context
$ docker agent run primary.yaml --team "Research team=./secondary.yaml"
$ docker agent run primary.yaml --team "Research team=./secondary.yaml" --team "QA team=./qa.hcl"

# Add hooks from the command line
$ docker agent run agent.yaml --hook-session-start "./scripts/setup-env.sh"
Expand Down
1 change: 1 addition & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ remote MCP endpoints.
| [`coding_harnesses.yaml`](coding_harnesses.yaml) | Orchestrator delegating coding tasks to external harness-backed sub-agents. |
| [`coding_harness_background_agents.yaml`](coding_harness_background_agents.yaml) | Orchestrator running external coding harnesses concurrently via background agents. |
| [`dev-team.yaml`](dev-team.yaml) | Product-manager-led team (designer + engineer) with shared memory. |
| [`local-team/primary-team.yaml`](local-team/primary-team.yaml) | Imports the lead of a second team defined in [`local-team/secondary-team.yaml`](local-team/secondary-team.yaml) through declarative `sub_agents`; [`local-team/primary-cli.yaml`](local-team/primary-cli.yaml) demonstrates the same composition with `--team "Research team=./secondary-team.yaml"`. |
| [`multi-code.yaml`](multi-code.yaml) | Tech-lead routing tasks to a frontend and a Go expert. |
| [`coder.yaml`](coder.yaml) | Coding agent with planner, implementer, and librarian sub-agents. |
| [`pr-reviewer-bedrock.yaml`](pr-reviewer-bedrock.yaml) | PR review toolkit pinned to Bedrock models. |
Expand Down
18 changes: 18 additions & 0 deletions examples/local-team/primary-cli.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Minimal primary manifest for demonstrating CLI team composition.
# Run from the repository root:
#
# docker agent run ./examples/local-team/primary-cli.yaml \
# --team "Research team=./secondary-team.yaml"

models:
model:
provider: openai
model: gpt-4o

agents:
root:
model: model
description: Primary lead that delegates specialist work to attached teams
instruction: |
Coordinate the work. Delegate research or writing tasks to an attached
team lead, wait for its result, and present it to the user.
32 changes: 32 additions & 0 deletions examples/local-team/primary-team.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# This example demonstrates composing teams from local files: a lead agent
# can list another team's YAML file in `sub_agents`. Only that team's default
# agent (the one named "root", or the first declared) is exposed to the
# parent — here under the "specialists" alias — and it keeps its own
# sub-agents, so it orchestrates its own team behind the scenes.
#
# Without an alias, `- ./secondary-team.yaml` would be exposed as
# "secondary-team" (the file name without extension). Relative paths are
# resolved against this file's directory. Local HCL files (.hcl) work too.
#
# Delegation stays synchronous: `transfer_task` blocks the primary lead until
# the secondary lead returns its result.
#
# Instead of declaring `sub_agents` below, use the dedicated primary file:
# docker agent run ./examples/local-team/primary-cli.yaml \
# --team "Research team=./secondary-team.yaml"

models:
model:
provider: openai
model: gpt-4o

agents:
root:
model: model
description: Primary lead that delegates specialist work to a second team
instruction: |
You coordinate the work. For any research or writing task, delegate to
the "specialists" sub-agent (the lead of a second team defined in
./secondary-team.yaml), wait for its result, and present it to the user.
sub_agents:
- specialists:./secondary-team.yaml
Loading
Loading