Add tests for the routing behaviours that had code but no coverage:
- budgets: a real token budget, or one already spent, stops the run; an
assessment that exhausts the budget never falls back to the default agent,
for both entry and completion routing
- accounting: negative, NaN or infinite cost and negative or overflowing token
counts fail closed; unknown usage still routes and is flagged as unpriced
- structured output: an accepted tool-mode answer reaches after_agent_complete
with the validated JSON; a rejected one never routes
- evaluator scope: an agent's own evaluator wins over the team's, a bound scope
never borrows the team's, and an imported agent keeps its own bindings
- headless: cli.Run in text and JSON mode, including continuation, low
confidence fallback and a non-zero exit when a decision fails
- entry points: serve api (decision streamed and stored), A2A, the
OpenAI-compatible chat server and MCP all use unpinned sessions that route
- native TUI: a finished draft continues to the reviewer
Extract newRoutingFixtureFor so a fixture can use the completion event and
extra runtime options, and let scriptedProvider return a scripted stream.
Cancel abandoned runs and drain their event channels before returning.
Keep API streaming locks and ACP turn tokens held until teardown
finishes; apply the same lifecycle contract to A2A, CLI, and
synchronous runtime wrappers. Regression tests cover cancellation,
handler errors, and follow-up turns.
Signed-off-by: David Gageot <david.gageot@docker.com>
Assisted-By: Claude (Anthropic)
Every session created against a local workspace now records the absolute
workspace root that owns it, so generated files can later be resolved
against the owning workspace instead of whatever cwd the viewer happens
to run from:
- delegated/nested sub-sessions inherit parent.WorkingDir
- serve mcp / a2a / chatserver capture the server workspace once at
startup instead of reading os.Getwd per request (or not at all)
- embeddedchat conversations and `docker-agent new` capture the
configured root or the creation-time cwd
- --working-dir writes the absolutized path back into RuntimeConfig so a
relative flag value cannot leak into persisted provenance
- compaction runs inherit the compacted session's workspace
Remote/headless surfaces (remote backend template, API templates without
working_dir, ACP without client cwd, evaluation container transcripts)
intentionally keep an empty WorkingDir and now document that choice.
session.ResolveWorkingDir adds a bounded, cycle-safe, validated
parent-chain fallback for old persisted sub-sessions with an empty
WorkingDir; it rejects relative/malformed roots and never falls back to
the process cwd. It is the primitive the generated-media resolver will
build on.
- config.Load is now YAML-only. HCL support becomes a source decorator,
hcl.NewSource, applied by the resolver; embedders wrap sources that may
carry HCL themselves.
- The OCI source moves to pkg/config/ocisource (ocisource.New).
- Resolve, ResolveSources, ResolveAlias and BuiltinAgentNames move to
pkg/config/sources together with the embedded built-in agents. This is
the package that knows every source type.
- teamloader no longer hardwires the resolver for external sub-agents:
WithSourceResolver supplies it, and loaderdefaults.Opts wires
sources.Resolve so CLI behaviour is unchanged.
pkg/config drops hcl, go-cty, levenshtein, textseg, wordwrap,
klauspost/compress and image-spec, and only reaches
go-containerregistry/pkg/name for reference parsing.
finalizeEventChannel emitted the terminal StreamStoppedEvent non-blockingly
and dropped it whenever the runtime's 128-slot event buffer was full at
teardown. Under normal streaming back-pressure (a consumer, e.g. the
PersistenceObserver, doing a synchronous per-delta SQLite write) the buffer
is routinely full at that exact moment, so the TUI/supervisor/leantui never
saw the event and stayed stuck on "Working…" until Esc (#4136).
Replace the non-blocking emit with a bounded blocking send (~5s deadline,
event_sink.go's new boundedChannelSink/bounded()): a consumer still
draining the channel reliably receives it, even past context cancellation
(the TUI keeps draining after Esc); it is dropped only once nothing
accepts it within the deadline, i.e. the consumer has genuinely abandoned
the channel. Deliberately not unbounded: a consumer audit found several
call sites that legitimately stop reading before close (a2a, acp, cli
runner, LocalRuntime.Run, embeddedchat, server RunSession), and an
unbounded send there would reintroduce the #3070 teardown deadlock that
#3074/#3275 previously fixed by making the emit non-blocking in the first
place.
Also harden pkg/a2a/adapter.go: it only completed the ADK turn from the
StreamStoppedEvent case, so an ever-dropped event left the turn hanging.
Add a channel-close fallback so a closed events channel without a prior
StreamStopped still completes the turn.
Tool approval today is a coarse two-option choice: approve a call once,
or approve everything for the rest of the session. Prompt fatigue
pushes people to the blanket option without them realising it also
covers destructive commands. There is no middle ground, and no obvious
way back. The shell classifier (safer: true) was also entangled with
the approval verdict, which made its judgement impossible to reuse.
Introduce three explicit safety modes and split the classifier into a
pure labeller so the mode is what actually gates the call:
safe destructive unknown
strict ask ask ask
balanced ALLOW ask ask
autonomous ALLOW ALLOW ALLOW
Every call gets a safe / destructive / unknown label from a new
dependency-free pkg/safety package (shell commands via the pattern
taxonomy, other tools via their MCP annotations), and the pipeline
becomes: preempt hooks -> custom rules -> mode x label -> default
hooks -> ask user. Custom rules always win over the mode; session
ask rules always prompt while agent-author ask rules yield to a
user-chosen auto-approving mode.
Escalation is guided and reversible: confirmation prompts offer the
next rung, a new --safety flag and the PATCH safety-policy endpoint
set or undo the mode at any time, and downgrading genuinely revokes
the blanket approval (the yolo toggle restores the pre-escalation
mode, surviving branch and fork).
Compatibility: legacy policy values (unsafe / safer / safe-auto) and
resume verbs are accepted and normalized; safer maps to balanced,
which is deliberately narrower (unknown commands now prompt). Sessions
that never choose a mode keep the historical default. The shell
toolset's safer: flag stays parseable but is ignored; the safer_shell
builtin degrades to a metadata-only labeller and warns at config build
time. Confirmation events and the on_tool_approval_decision hook now
carry safety_label. Resuming a session stored as autonomous keeps
autonomous without re-passing --yolo; an explicit --safety on resume
overrides.
pkg/session mixed the in-memory store with the file-backed constructor,
so every embedder of pkg/runtime linked modernc.org/sqlite (large and
slow to compile) even when using purely in-memory sessions.
session.NewSQLiteSessionStore(ctx, path) becomes sqlitestore.New(ctx,
path); behavior (open, migrate, backup+retry recovery) is unchanged.
SQLiteSessionStore, NewSQLiteSessionStoreFromDB and the migrations stay
in pkg/session - they only use database/sql and don't drag the driver.
BREAKING CHANGE: callers of session.NewSQLiteSessionStore must import
github.com/docker/docker-agent/pkg/session/sqlitestore and call New.
Thread context through sqlite/session/RAG/memory DB setup so constructors and migrations no longer mint fresh roots. Fake proxy cleanup and askpass connection handling now use context providers derived from their startup context. LocalRuntime stores a context provider for tool-change refresh callbacks.
The remaining suppressions are intentionally detached event hooks/MCP supervisor callbacks or compatibility fallbacks.
Thread context into runtime.New/NewLocalRuntime so constructor-time model validation no longer needs context.TODO. Also add context to Runtime.TitleGenerator so title-model selection is correlated with the caller context.
Update runtime constructors across CLI/server/examples/tests and pass context through existing call sites.
- `pkg/a2a/server.go`: wrap the agent-card and JSON-RPC endpoints with `otelhttp.NewHandler` so inbound A2A requests extract `traceparent` / `tracestate` / `baggage` and emit a SERVER span. The outer `agent-a2a` server wrap covers any auxiliary routes
- `pkg/a2a/adapter.go`: in `runDockerAgent`, decorate the active SERVER span with `gen_ai.operation.name=invoke_agent`, `gen_ai.agent.name`, and `cagent.agent.name`. Wires the runtime tracer scope so per-invocation `runtime.session` / `runtime.stream` / `runtime.tool.call` chain onto the inbound A2A span instead of starting fresh trace ids per request
Add a --session-db flag to the a2a command and back the A2A server with
a SQLite session store. The A2A contextID is reused as the docker-agent
session ID so subsequent invocations resume the same conversation.
Signed-off-by: maxcleme <maxime.clement@docker.com>
Replace all slog.Debug/Info/Warn/Error calls with their *Context variants (slog.DebugContext, slog.InfoContext, etc.) throughout the codebase. This enables log correlation and tracing by threading context through all logging statements.
Key changes:
- Update all logging calls in runtime, agent, provider, and tool packages
- Pass context.TODO() in cases where no request context exists (e.g., config validation, debug commands)
- Update agent.Model() signature to accept context parameter for log correlation
- Maintain existing log levels and messages; only add context parameter
This change improves observability by allowing logs to be correlated with traces and requests, making debugging distributed operations easier.
- Add `NonInteractive` field to Session struct to explicitly mark sessions running without user interaction
- Update runtime loop to use `NonInteractive` instead of `ToolsApproved` for auto-stopping logic
- Set non-interactive mode in A2A adapter, evaluation framework, and MCP server
- This separates the concept of approved tools from non-interactive execution context
Add max_tool_call_tokens configuration option to control how much
historical tool call content is retained in the context. This helps
manage context size in long-running sessions.
Changes:
- Add maxOldToolCallTokens field to AgentConfig (pkg/config/latest/types.go)
- Update JSON schema with new max_old_tool_call_tokens property
- Add maxOldToolCallTokens field and getter to Agent struct
- Add maxOldToolCallTokens field to Session struct
- Update GetMessages() to use configured value or fall back to
DefaultMaxOldToolCallTokens (40000)
- Propagate maxOldToolCallTokens through all session creation points:
- A2A adapter
- ACP agent
- MCP server
- Runtime agent delegation (transfer_task, sub-sessions)
- HTTP/API server session manager
Configuration semantics:
- Not set or 0: Use default (40000 tokens)
- Positive value: Use that specific limit
- -1: Unlimited (no truncation)
When max_old_tool_call_tokens is exceeded, older tool calls have their
content replaced with "[content truncated]" to keep context size
manageable. Tokens are approximated as string_length/4.
Assisted-By: docker-agent
Signed-off-by: Guillaume Tardif <guillaume.tardif@gmail.com>
Add a consecutive duplicate tool call detector that terminates agents
when they issue N identical calls in a row without making progress.
This prevents runaway loops like the one observed in pinata run
23141381851, where a sub-agent called read_file 629 times on the same
file, consuming the entire 40-minute timeout.
Default threshold is 5, configurable via max_consecutive_tool_calls
in agent YAML.
Add modernize and perfsprint to .golangci.yml and resolve all lint
issues across 97 files:
- Replace fmt.Errorf with errors.New where no format verbs are used
- Replace fmt.Sprintf(%d) with strconv.Itoa for int conversions
- Replace redundant fmt.Sprintf(%s) with direct string values
- Use slices.Contains instead of manual loops (SkillsConfig.HasLocal)
- Use omitzero instead of omitempty on struct fields (time.Time, SkillsConfig)
- Inline boolPtr helpers to new(bool) and remove unused functions
- Replace string concatenation in loops with strings.Builder
- Use fmt.Fprintf instead of WriteString(fmt.Sprintf(...))
Assisted-By: docker-agent
Replace wildcard listen addresses (::, 0.0.0.0) with localhost in the
agent card URL so clients can actually reach the server. Also switch
TCP listeners to tcp4 to avoid binding to IPv6 on dual-stack systems.
Fixes#1679
Assisted-By: cagent
Update the Read() method signature to include TaskVersion as required
by the new API. Also add WriteVersioned() method to satisfy the Queue
interface.
Assisted-By: cagent