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
6 changes: 6 additions & 0 deletions docs/community/troubleshooting/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,12 @@ A few things that catch people out:
> [!WARNING]
> Raising `--max-request-size` increases how much memory an unauthenticated or malicious client can force the server to buffer per request. Pick a value with your deployment's exposure in mind, and pair any non-loopback listener with `--auth-token` (API server) or `--api-key`/`--api-key-env` (chat server). A `--listen` control plane has neither flag — keep it on loopback, a unix socket, or behind an authenticating reverse proxy if it must be reachable from elsewhere.

### Delegated task appears stalled before its first response

A direct `transfer_task` delegation automatically retries once when the child model stream is silent before sending any response payload. It emits the existing warning event, and the retry is immediate and applies once to the complete child run, including later turns and fallback models. Each nested native `transfer_task` starts a new child run with its own fresh retry allowance. The retry does not apply after partial output, to `background_agents`, or when cancellation or a run budget blocks it.

Run with `--debug` and look for `Delegated model stream idle before response; retrying immediately`. If the child still fails, check provider connectivity, fallback configuration, cancellation, and run-budget events; Docker Agent does not retry when the context is canceled or the run budget is exhausted.

## Performance Issues

### High memory usage
Expand Down
4 changes: 2 additions & 2 deletions docs/configuration/models/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ models:
token_key: string # Optional: env var for API token
thinking_budget: string|int # Optional: reasoning effort
task_budget: int|object # Optional: total task token budget (Anthropic)
parallel_tool_calls: boolean # Optional: allow parallel tool calls
parallel_tool_calls: boolean # Optional: allow parallel tool calls. Omit to use the provider/API default.
track_usage: boolean # Optional: track token usage
routing: [list] # Optional: rule-based model routing
capabilities: # Optional: override attachment (input) capabilities
Expand Down Expand Up @@ -72,7 +72,7 @@ models:
| `token_key` | string | ✗ | Environment variable name containing the API token (overrides provider default) |
| `thinking_budget` | string/int | ✗ | Reasoning effort control |
| `task_budget` | int/object | ✗ | Total token budget for an agentic task (forwarded to Anthropic; see [Task Budget](#task-budget)). |
| `parallel_tool_calls` | boolean | ✗ | Allow model to call multiple tools at once |
| `parallel_tool_calls` | boolean | ✗ | Allow model to call multiple tools at once. When omitted, Docker Agent leaves the setting unset so the selected provider or API can apply its own default. |
| `track_usage` | boolean | ✗ | Track and report token usage for this model |
| `routing` | array | ✗ | Rule-based routing to different models. See [Model Routing](../routing/index.md). |
| `capabilities` | object | ✗ | Override attachment (input) capabilities for this model. See [Attachment Capability Overrides](#attachment-capability-overrides). |
Expand Down
2 changes: 1 addition & 1 deletion docs/configuration/overview/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -485,7 +485,7 @@ agents:
| `top_p` | Default top-p sampling parameter. |
| `frequency_penalty` | Default frequency penalty. |
| `presence_penalty` | Default presence penalty. |
| `parallel_tool_calls` | Enable parallel tool calls by default. |
| `parallel_tool_calls` | Enable or disable parallel tool calls by default. If omitted, the provider/API default is used. |
| `track_usage` | Track token usage by default. |
| `provider_opts` | Provider-specific options. |

Expand Down
2 changes: 1 addition & 1 deletion docs/features/cli/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ $ docker agent run [config] [message...] [flags]
| `-a, --agent <name>` | Run a specific agent from the config |
| `--yolo` | Auto-approve tool calls (unless explicitly denied). Legacy alias for `--safety autonomous`. |
| `--safety <mode>` | Safety mode for tool approval: `strict` (ask for everything), `balanced` (auto-approve safe calls), `restricted` (auto-approve safe calls, deny the rest — fail-closed for unattended runs), or `autonomous` (approve everything). Wins over `--yolo` when both are given. Without the flag, the mode falls back to alias/user-config defaults, then the agent YAML's `agents.<name>.safety` / `runtime.safety`; a resumed session keeps its stored mode unless `--safety`/`--yolo` is passed explicitly. See [Safety Modes](../../configuration/permissions/index.md#safety-modes). |
| `--model <ref>` | Override model(s). Use `provider/model` for all agents, or `agent=provider/model` for specific agents. Comma-separate multiple overrides. |
| `--model <ref>` | Override model(s). Use `provider/model` for all agents, or `agent=provider/model` for specific agents. Comma-separate multiple overrides. Inline overrides inherit each agent's explicit `parallel_tool_calls` setting only from a concrete, provider-qualified configured model. Existing named targets are authoritative on every invocation, including an omitted setting; harness, router, alloy/comma-list, `first_available`, providerless, and unresolved source models do not inherit it. |
| `--session <id>` | Resume a previous session. Supports relative refs (`-1` = newest by creation time, `-2` = second-newest, … — creation order, not last-used). An explicit ID that does not exist yet is created with that ID, so a supervisor can own the session ID upfront and reuse it across runs. |
| `-s, --session-db <path>` | Path to the SQLite session database (default: `<data-dir>/session.db`, so `~/.cagent/session.db` unless `--data-dir` is set) |
| `--session-read-only` | Open the TUI in read-only mode: conversation history is displayed but no new messages can be sent to the LLM. Cannot be used with `--exec`. |
Expand Down
2 changes: 1 addition & 1 deletion docs/providers/custom/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ agents:
| `top_p` | float | Default nucleus sampling threshold (0.0–1.0). | — |
| `frequency_penalty` | float | Default frequency penalty (-2.0–2.0). | — |
| `presence_penalty` | float | Default presence penalty (-2.0–2.0). | — |
| `parallel_tool_calls` | boolean | Whether to enable parallel tool calls by default. | — |
| `parallel_tool_calls` | boolean | Whether to enable parallel tool calls by default. When omitted, the provider/API default is used. | — |
| `track_usage` | boolean | Whether to track token usage by default. | — |
| `thinking_budget` | string/int | Default reasoning effort/budget. | — |
| `task_budget` | int/object | Default total token budget for an agentic task (forwarded to Anthropic; honored by Claude Opus 4.7+ today). Integer shorthand or `{type: tokens, total: N}`. | — |
Expand Down
4 changes: 4 additions & 0 deletions docs/tools/transfer-task/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ The `transfer_task` tool allows an agent to delegate tasks to specialized sub-ag

**You don't need to add it manually** — it's automatically available when an agent has `sub_agents` configured.

## Idle stream recovery

When a delegated model connection becomes idle before producing any response payload, `transfer_task` retries that stream once and emits a warning. The retry allowance belongs to the complete direct delegated child run: it can be consumed only once across all of that child's turns and fallback models. A nested native `transfer_task` starts its own child run with a fresh allowance rather than inheriting the caller's; background delegation has no allowance. The retry is skipped when cancellation or a run budget blocks it and is never attempted after partial content, reasoning, media, or tool-call data has arrived, preventing duplicate output or tool execution.

## Configuration

The tool is enabled implicitly when `sub_agents` is set:
Expand Down
8 changes: 0 additions & 8 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -262,14 +262,6 @@ func validateConfig(cfg *latest.Config) error {
cfg.Models = map[string]latest.ModelConfig{}
}

for name := range cfg.Models {
if cfg.Models[name].ParallelToolCalls == nil {
m := cfg.Models[name]
m.ParallelToolCalls = new(true)
cfg.Models[name] = m
}
}

if err := ensureModelsExist(cfg); err != nil {
return err
}
Expand Down
18 changes: 18 additions & 0 deletions pkg/config/latest/internal_state.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package latest

// InternalModelOverrideState returns opaque loader-owned state. It is not part
// of the configuration schema and must not affect serialization.
func (t *Config) InternalModelOverrideState() any {
if t == nil {
return nil
}
return t.modelOverrideState
}

// SetInternalModelOverrideState stores opaque loader-owned state outside the
// public configuration schema.
func (t *Config) SetInternalModelOverrideState(state any) {
if t != nil {
t.modelOverrideState = state
}
}
2 changes: 2 additions & 0 deletions pkg/config/latest/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ type Config struct {
// only carries the section so it round-trips — see config.applyFlavors
// for the merge semantics.
Flavors map[string]map[string]any `json:"flavors,omitempty"`

modelOverrideState any
}

// BudgetConfig caps what a single run may consume before the agent is
Expand Down
147 changes: 138 additions & 9 deletions pkg/config/overrides.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,31 +3,163 @@ package config
import (
"errors"
"fmt"
"reflect"
"slices"
"strings"

"github.com/docker/docker-agent/pkg/config/latest"
)

// ApplyModelOverrides applies CLI model overrides to the configuration
type modelOverrideReceipt struct {
sourceModel string
sourcePolicy *bool
eligible bool
requestedModel string
expectedPostModel string
expectedPostValue latest.ModelConfig
}

type modelOverridePolicy struct {
modelRef string
parallelToolCalls *bool
}

type modelOverrideState struct {
receipts map[string]modelOverrideReceipt
policies map[string]modelOverridePolicy
lastOverrides []string
}

func overrideState(cfg *latest.Config) *modelOverrideState {
state, _ := cfg.InternalModelOverrideState().(*modelOverrideState)
if state == nil {
state = &modelOverrideState{
receipts: make(map[string]modelOverrideReceipt),
policies: make(map[string]modelOverridePolicy),
}
cfg.SetInternalModelOverrideState(state)
}
return state
}

func cloneBool(value *bool) *bool {
if value == nil {
return nil
}
return new(*value)
}

func isConcreteModelConfig(model latest.ModelConfig) bool {
return model.Provider != "" && model.Model != "" && !strings.Contains(model.Model, ",") &&
len(model.Routing) == 0 && !model.IsFirstAvailable()
}

func sourceReceipt(cfg *latest.Config, state *modelOverrideState, agent latest.AgentConfig) modelOverrideReceipt {
if receipt, ok := state.receipts[agent.Name]; ok {
current, exists := cfg.Models[agent.Model]
if agent.Model == receipt.expectedPostModel && exists && reflect.DeepEqual(current, receipt.expectedPostValue) {
return receipt
}
delete(state.receipts, agent.Name)
}

model, exists := cfg.Models[agent.Model]
return modelOverrideReceipt{
sourceModel: agent.Model,
sourcePolicy: cloneBool(model.ParallelToolCalls),
eligible: agent.Harness == nil && exists && isConcreteModelConfig(model),
}
}

// ApplyModelOverridePolicy applies an inherited per-agent CLI override policy
// to a copied model config. The manifest model map remains authoritative and
// unmodified.
func ApplyModelOverridePolicy(cfg *latest.Config, agentName, modelRef string, model *latest.ModelConfig) {
if cfg == nil || model == nil {
return
}
state, _ := cfg.InternalModelOverrideState().(*modelOverrideState)
if state == nil {
return
}
policy, ok := state.policies[agentName]
if !ok || policy.modelRef != modelRef {
return
}
model.ParallelToolCalls = cloneBool(policy.parallelToolCalls)
}

// ApplyModelOverrides applies CLI model overrides to the configuration.
func ApplyModelOverrides(cfg *latest.Config, overrides []string) error {
if len(overrides) == 0 {
return nil
}

state := overrideState(cfg)
if slices.Equal(overrides, state.lastOverrides) && receiptsIntact(cfg, state) {
return nil
}
invocationTargets := make(map[string]struct{}, len(cfg.Models))
for name := range cfg.Models {
invocationTargets[name] = struct{}{}
}
receipts := make(map[string]modelOverrideReceipt, len(cfg.Agents))
for _, agent := range cfg.Agents {
receipts[agent.Name] = sourceReceipt(cfg, state, agent)
}

for _, override := range overrides {
if err := applySingleOverride(cfg, override); err != nil {
return err
}
}

// After applying overrides, ensure new models are added to cfg.Models
return ensureModelsExist(cfg)
if err := ensureModelsExist(cfg); err != nil {
return err
}
policies := make(map[string]modelOverridePolicy)
for _, agent := range cfg.Agents {
receipt := receipts[agent.Name]
requestedRef := agent.Model
target := cfg.Models[requestedRef]
_, targetExisted := invocationTargets[requestedRef]
if receipt.eligible && receipt.sourcePolicy != nil && !targetExisted {
policies[agent.Name] = modelOverridePolicy{
modelRef: requestedRef,
parallelToolCalls: cloneBool(receipt.sourcePolicy),
}
}
receipt.requestedModel = requestedRef
receipt.expectedPostModel = requestedRef
receipt.expectedPostValue = target
state.receipts[agent.Name] = receipt
}
state.policies = policies
state.lastOverrides = slices.Clone(overrides)
return nil
}

func receiptsIntact(cfg *latest.Config, state *modelOverrideState) bool {
for _, agent := range cfg.Agents {
receipt, ok := state.receipts[agent.Name]
if !ok || agent.Model != receipt.expectedPostModel {
return false
}
model, exists := cfg.Models[agent.Model]
if !exists || !reflect.DeepEqual(model, receipt.expectedPostValue) {
return false
}
}
return len(state.receipts) == len(cfg.Agents)
}

// applySingleOverride processes a single model override string
// applySingleOverride processes a single model override string.
func applySingleOverride(cfg *latest.Config, override string) error {
override = strings.TrimSpace(override)
if override == "" {
return nil // Skip empty overrides
return nil
}

// Handle comma-separated format: "agent1=model1,agent2=model2"
if strings.Contains(override, ",") {
for part := range strings.SplitSeq(override, ",") {
if err := applySingleOverride(cfg, part); err != nil {
Expand All @@ -37,7 +169,6 @@ func applySingleOverride(cfg *latest.Config, override string) error {
return nil
}

// Check if this is an agent-specific override (contains '=')
agentName, modelSpec, ok := strings.Cut(override, "=")
if ok {
agentName = strings.TrimSpace(agentName)
Expand All @@ -50,15 +181,13 @@ func applySingleOverride(cfg *latest.Config, override string) error {
return fmt.Errorf("empty model specification in override: %s", override)
}

// Apply to specific agent
ok := cfg.Agents.Update(agentName, func(a *latest.AgentConfig) {
a.Model = modelSpec
})
if !ok {
return fmt.Errorf("unknown agent '%s'", agentName)
}
} else {
// Global override: apply to all agents
modelSpec := strings.TrimSpace(override)
if modelSpec == "" {
return errors.New("empty model specification")
Expand Down
Loading
Loading