96 Commits
Author SHA1 Message Date
Djordje Lukic bd5067a322 refactor(runtime)!: extract model discovery and picker catalog
Model discovery and picker enrichment do not belong to agent execution. Move configured, gateway, DMR, and models.dev choices and discovery caches into runtime/modelcatalog. Callers import catalog types directly instead of using runtime aliases, so the execution package no longer owns presentation metadata. Model override application stays in the local runtime.
2026-10-26 12:40:21 +01:00
David Gageot a2f028e6b3 lint: detect unnecessarily bidirectional channels 2026-10-15 21:55:31 +01:00
David Gageot 98bedcf5d9 fix(cli): separate reasoning from response in exec text mode
Insert a blank line between thinking and response chunks so exec output doesn't run them together, while avoiding a redundant separator after tool calls, warnings, or TTY headers.
2026-10-05 09:58:19 +02:00
David Gageot 6276c6b9f6 feat: add --last flag for exec to print only the final answer
Buffers output across turns and emits just the last response, as text
or a JSON value, declining prompts like --json does. Also redirects
automatic model-pull diagnostics to stderr and surfaces empty-response/
tool-call metadata on remote completion events so --last can tell a
real answer from a partial one.

Assisted-By: docker-agent
2026-10-04 16:04:46 +02:00
hjeddad 9eb3205a6e test: cover remaining hook routing acceptance gaps
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.
2026-10-01 15:43:57 +02:00
David Gageot caa9b0e9e1 test: migrate slice-copy test helpers to slices.Clone
Mechanical follow-up to the SlicesClone cop: updates recorder/snapshot
helpers across the test suite to use slices.Clone instead of
append(nil, ...) or make+copy. No behavior change, same nil vs empty
results as before.

Signed-off-by: David Gageot <david.gageot@docker.com>
2026-09-25 14:20:07 +02:00
David Gageot 601ba843da test(cli): use synctest for runner tests
Signed-off-by: David Gageot <david.gageot@docker.com>
2026-09-24 11:43:45 +02:00
David Gageot e933b3872e test(cli): use synctest for runner drain tests
Signed-off-by: David Gageot <david.gageot@docker.com>
2026-09-24 11:43:45 +02:00
David Gageot d0b69ab672 feat: add skill_content_guard and prompt_file_guard hook events
Introduces two new fail-closed hook events that let operators screen
untrusted content before it reaches the model:

- `skill_content_guard` checks raw skill text (local, remote, inline)
  before command expansion or delivery, covering read_skill,
  read_skill_file, run_skill, slash commands, and command templates.
- `prompt_file_guard` screens add_prompt_files instructions before they
  enter session state or a model request, including stored initial/
  current values and historical updates on every turn.

Both events support the new `guard_decision` model-hook schema (allow/
deny + reason), an optional `system_prompt` on model hooks, and the
`FailedHooks` field on hook results. The executor redacts skill/file
content from logs and results so a judge cannot echo rejected
instructions. Every configured hook must explicitly approve; empty
output, invalid verdicts, and execution failures all reject in every
safety mode.

Adds `Runtime.ReadSkillContent`, `skillRuntime`, and
`InstructionContextSnapshot`/`GetMessagesWithInstructionContext` to
support policy checks in the slash-command path, native compaction, and
cache-stable prompt assembly. Slash-command errors now surface in the TUI
instead of silently falling through.

Assisted-By: Claude (Anthropic)
2026-09-19 14:56:17 +02:00
David Gageot db5746fd0d fix: drain runtime streams before releasing turn ownership
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)
2026-09-18 15:18:19 +02:00
Arnaud Héritier a28f32e663 feat: keep generated-media saves inside the owning workspace
Redirect absolute, traversal, home-relative and symlink-escaping targets to a sanitized workspace basename with a bounded warning. Generated media never elicits a path decision or writes externally; normal ACP and CLI turns complete without a form consumer. Preserve MIME correction, collision-safe atomic writes, partial success, owning-session provenance and workspace-only manifest authorization. Keep the pinned root-kind migration for database compatibility.
2026-09-10 10:51:39 +02:00
Arnaud Héritier 104caf4a54 feat(#3996): materialize generated media with no-resend history
Save accumulated media into the owning session's workspace, resolving
parent-session WorkingDir provenance. Persist owner-qualified workspace
references and MIME/name/size rather than generated base64 in session JSON.
Use generic naming fallback for refused provider names and per-item warnings
when no workspace/save is available; there is no alternate storage target
in this patch. Exempt media-bearing results from the empty-response error.

Replace saved generated parts in ordinary later provider turns with bounded
metadata placeholders and avoid counting mirrored text twice in compaction.
If all saves in a media-only reply fail, an empty assistant record may remain
alongside warnings; successful siblings and reply text are retained.

At a text-only stop, match phrases in the last user prompt for a nonfatal
missing-image notice. A terminal error skips this branch; current-agent
structured-output config suppresses it, but arbitrary per-call overrides
and reply content are not classified. Negated/question/context phrases can
match, capability is unchecked, and continuations are not tracked as one
submission. Characterization tests pin those limits. Use mockRuntime.events
for the independent CLI text-before-warning test, not a later helper.
2026-09-10 10:17:07 +02:00
Arnaud Héritier 1ec5fecd5d fix: resolve golangci-lint v2.13.1 / gofumpt v0.11.0 findings after linter bump
- Run golangci-lint fmt to apply gofumpt v0.11.0 formatting (v0.9.2→v0.11.0
  bundled in golangci-lint) across 30 files: purely mechanical whitespace
  and grouping changes, no logic changes.
- Remove 3 stale //nolint:recvcheck directives from pkg/config/latest/types.go
  (recvcheck in golangci-lint v2.13.1 no longer flags MarshalYAML/JSON patterns,
  making those suppression comments unused).
- Disable newly-activated modernize sub-checks in .golangci.yml:
  errorsastype, embedlit, stringscut, reflecttypeassert — all require Go 1.27
  APIs or x/tools ≥ v0.48; pre-existing violations in unrelated files.
  A follow-up PR should re-enable these and clean up the violations.
- Add .golangci.yml exclusions for pkg/config/v*/ (recvcheck + nolintlint):
  frozen versioned config types that cannot be modified; recvcheck changed
  behaviour between v2.12.2 and v2.13.1 leaving stale nolint directives.
2026-08-23 09:35:55 +00:00
Trung Nguyen 934784b7e3 feat: three-mode safety policy (strict / balanced / autonomous) with native shell classification
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.
2026-07-27 10:10:48 +02:00
David Gageot d55fc6aedb Merge pull request #3771 from dgageot/refactor/lean-embedder-imports
refactor: cut heavy transitive deps (goja, expr, openai-go, go-git, sqlite) from the code-built embedder surface
2026-07-21 16:52:06 +02:00
David Gageot b2233dc399 refactor(runtime): make the JS command evaluator pluggable to drop goja
pkg/runtime imported pkg/js solely to expand ${...} expressions in
slash-command instructions, linking the goja JavaScript engine into
every embedder of the runtime.

ResolveCommand now goes through a registered CommandEvaluator factory.
The new pkg/runtime/jscommands package registers the goja-backed
evaluator on import and is blank-imported by teamloader, the CLI and
embeddedchat/defaults, so every YAML/CLI path behaves exactly as
before. Code-built embedders that want JS command expressions opt in
with the same blank import; without it, ${...} is left unexpanded with
a warning pointing at the import.
2026-07-21 15:02:51 +02:00
Sayt-0 3e4f4b2602 chore: remove unreachable helpers 2026-07-21 13:37:43 +02:00
David Gageot 355654720f Merge pull request #3587 from docker/fix/a6-elicitation-concurrency
fix(runtime): reliable, correlation-safe, non-blocking elicitation delivery (#3584)
2026-07-15 16:42:53 +02:00
Sayt-0 ac18222479 fix(session): lock title and usage scalar access 2026-07-15 15:46:12 +02:00
Arnaud Héritier 811d91dd5f fix(runtime): reliable, correlation-safe, non-blocking elicitation delivery (#3584) 2026-07-13 16:39:20 +02:00
David Gageot 0caaa26465 Merge pull request #3452 from docker/feat/3442-actionable-model-credential-errors
feat(cli): actionable errors for missing models and credentials (phase 1 of #3442)
2026-07-06 18:41:34 +02:00
Sayt-0 549b076fd6 feat(cli): actionable errors for missing models and credentials
Phase 1 of #3442:

- Bare `docker agent` with empty non-TTY stdin fails with next steps
  instead of exiting 0 silently; piped prompts keep working
- DMR: "not installed" detection no longer depends on the exact docker
  CLI usage text; when `docker model status` fails, model availability
  is verified through the DMR HTTP API at client creation so a
  not-pulled model reports "docker model pull X" instead of a raw
  HTTP 404 at message time
- RequiredEnvError and the first_available variant name the secret
  sources able to supply arbitrary keys (shell environment, env file,
  pass, macOS Keychain) with a one-line example each and a link to
  the secrets guide; Docker Desktop and the credential helper are
  deliberately absent, they only resolve fixed Docker variables
  (DOCKER_TOKEN) and can never satisfy the reported missing keys
- When model credentials are missing, the error suggests running a
  local model instead (--model dmr/ai/qwen3, pulled on first use,
  `docker model ls` lists models already pulled)
- --env-from-file with an unreadable or malformed file aborts the run
  instead of being logged and skipped; parse errors name the file
- Exec mode prints the final error once (cobra), not twice
2026-07-06 18:22:00 +02:00
Sayt-0 18637507a4 feat(tui): per-sub-agent context accounting in the sidebar and agent inspector
Track the latest token-usage snapshot per agent (fed by TokenUsageEvents,
sub-sessions included) in the shared session state. Surface it as a
right-aligned context percentage on each agent's sidebar roster line and
as a "Context: <tokens> of <limit> tokens (N%)" line in the agent-details
dialog.

Background agent tasks (run_background_agent) previously dropped all
events; the runtime now forwards their TokenUsageEvents through a new
out-of-band Runtime.OnBackgroundEvent hook that the App bridges into the
TUI event stream, so background agents join per-agent accounting and
session cost totals. Token-usage events routed to inactive tabs no longer
move the current-agent marker.

Covered by unit tests at each layer and two e2e TUI scenarios driving the
full background-agent journey through scripted model providers.

Closes #3438
2026-07-06 13:41:25 +02:00
Sayt-0 4b63c67630 feat(tui): add /effort command to select the model's reasoning level 2026-07-02 10:47:27 +02:00
melmennaoui e689f31ab8 fix(cli): reject tool-call confirmations in JSON mode under --yolo
The JSON runner previously only rejected ToolCallConfirmationEvent
when AutoApprove was false; under --yolo it dropped the event
without any Resume, and the runtime's askUser blocked on a channel
that never received. Eval containers running `docker-agent run
--exec --yolo --json` hung whenever a preempt-yolo hook (safer_shell)
overrode yolo with an Ask verdict.

Reject unconditionally in JSON mode. A confirmation event under
--yolo can only fire when a preempt-yolo hook explicitly asked; the
hook said Ask, not Approve, so Reject is the safe answer and
mirrors what a machine consumer would do.
2026-07-01 15:41:12 +02:00
Sayt-0 6e57e03100 Merge remote-tracking branch 'origin/main' into fix/3145-surface-empty-reasoning-only-turns
# Conflicts:
#	pkg/model/provider/openai/client.go
2026-06-30 16:27:04 +02:00
David Gageot 69231c755e test: add t.Parallel() to eligible tests
Assisted-By: Claude
2026-06-29 13:39:47 +02:00
David Gageot ae5a574438 refactor(runtime): require context for runtime construction and title generators
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.
2026-06-29 10:17:44 +02:00
David Gageot 14d0ad9af0 refactor(runtime): thread context through Runtime interface methods
Add a context.Context parameter to the Runtime interface methods that
previously took none and minted a detached context.Background()/TODO()
internally: Steer, FollowUp, SetCurrentAgent, and CurrentAgentName.

- RemoteRuntime now uses the caller's context for its network calls
  (SteerSession/FollowUpSession/GetAgent) and its one-time default-agent
  resolution (via WithoutCancel, so a cancelled first caller can't poison
  the cached default).
- LocalRuntime keeps a private context-free currentAgentName() for its many
  internal callers (event callbacks, logging) that have no context; the
  exported method exists to satisfy the interface.
- Callers thread their existing context: app.App (TUI boundary mints a root
  in SwitchAgent), SessionManager, cli runner.
- Threading context into SetCurrentAgent surfaced a slog.Debug that should
  be slog.DebugContext (caught by Lint/SlogContextual); fixed.

Interface methods with fixed signatures and no caller context (e.g.
TitleGenerator) and internal leaf helpers below the context boundary
(getAgentModelID, startedToolNames, AgentsInfo \u2014 where the context only
feeds slog correlation) keep documented suppressions.
2026-06-29 10:16:55 +02:00
Sayt-0 9275d79317 fix(runtime): surface empty/reasoning-only turns instead of failing silently
A turn that produces no content and no tool calls is skipped by
recordAssistantMessage, which left the user with no output and no
explanation. Two known causes hit this:

- thinking-mode models (e.g. Qwen3 via openai_chatcompletions) that
  stream only reasoning tokens then stop or hit the output token limit
- an empty stream from a rate-limited or token-capped provider

Emit a Warning (mirroring the existing refusal path) that tells the user
the model produced only reasoning, or returned an empty response. Also
render WarningEvent in the non-TUI CLI runner so the notice is visible in
headless runs (the TUI already renders it); this surfaces the existing
refusal warning there too. No persistence change: reasoning is still not
used as the reply and the empty assistant message is still skipped.

Refs #3145
2026-06-25 22:25:03 +02:00
Djordje Lukic 36903dd4c6 Cycle model thinking level with shift+tab
Pressing Shift+Tab in the TUI now cycles the current model's
thinking-effort level through its provider-specific range (e.g.
off → minimal → low → medium → high → xhigh for OpenAI), wrapping
around. The selected level is applied as a runtime model override
for the active session.

The current level is shown inline in the sidebar's agent block as
`<provider>/<model> • <thinking>`. To avoid a tool-count flicker,
the change refreshes only agent/team info via a new lightweight
Runtime.EmitAgentInfo instead of re-running full startup info (which
re-discovers tools).

Models that don't support reasoning, and remote runtimes, are
no-ops with an informational message.

Signed-off-by: Djordje Lukic <djordje.lukic@docker.com>
2026-06-11 00:28:22 +02:00
Trung Nguyen dfca23c73c Rename OAuth elicitation meta keys from cagent/ to docker-agent/
The OAuth elicitation Meta map keys still carry the legacy product
name (cagent/type, cagent/server_url). The runtime was renamed to
docker-agent some time ago; align the meta keys with it so the
contract clients see matches the project name they're integrating
against.

Renamed:
  cagent/type        -> docker-agent/type
  cagent/server_url  -> docker-agent/server_url

Both writer sites (handleManagedOAuthFlow, handleUnmanagedOAuthFlow)
and every in-tree reader (CLI runner, RemoteRuntime OAuth mirror,
TUI elicitation handling) are updated together. Out-of-tree
consumers will need the same one-line rename.

Other cagent/* identifiers that are NOT in the elicitation Meta map
(user-config paths under ~/.cagent/, the User-Agent header, the
cagent/title meta key used by the user_prompt builtin) are
intentionally left alone -- the rename is scoped to OAuth meta.

Signed-off-by: Trung Nguyen <trung.nguyen@docker.com>
2026-05-28 15:23:53 +02:00
David Gageot 2a910ea477 feat(skills): run context: fork slash commands as sub-sessions
Typing `/<skill-name>` in the TUI for a skill declared with
`context: fork` used to inline the SKILL.md body into the parent
conversation as a regular user message and let the parent agent
answer it directly. The fork flag was silently ignored on this
path: no sub-session was created, the parent transcript was
polluted with a giant `<skill name="…">…</skill>` block, and the
skill's own `allowed-tools` / `description` had no effect.

Route fork-mode slash commands through the same machinery the
`run_skill` tool uses, so the skill runs in an isolated child
session and control returns to the parent when it ends:

- `Runtime.RunSkillFork` is now a public entry point shared by the
  `run_skill` tool and the new App-level slash dispatcher; it
  handles lookup, fork-mode validation, content+placeholder
  expansion, optional model override, and `runForwarding`.
- `App.SkillCommandFork` classifies user input as a fork-skill
  invocation; `App.RunSkillFork` drives the runtime and forwards
  events through the App's bus with the same context/cancellation
  discipline as `App.Run`.
- `App.ResolveSkillCommand` short-circuits for fork-mode skills so
  `ResolveInput` never returns the inlined body for them.
- The chat page calls `SkillCommandFork` first and falls back to
  the existing inline path for non-fork skills, agent commands and
  plain messages.

The skill body is now framed as the **first user message** of the
sub-session (matching the inline `<skill name="…">` envelope), not
as an extra system message — otherwise the agent's invariant system
prompt would drown it out and the model would ignore the
instructions. The YAML frontmatter is stripped because the runtime
has already consumed it; passing it through just adds noise.
`!`shell`` placeholders continue to be expanded by the existing
`ReadSkillContent` path, while plain backtick markdown such as
`` `git commit -q -m "<subject>"` `` passes through verbatim.

Tests:

- `pkg/app/skills_fork_test.go` covers slash-command detection,
  the fork dispatch contract (parent session not mutated, args
  forwarded), and the preserved inline path for non-fork skills.
- `pkg/tools/builtin/skills/build_user_message_test.go` pins the
  user-message envelope, frontmatter stripping, and the markdown
  pass-through.
2026-05-21 10:44:53 +02:00
David Gageot ac1c1a9a54 Fix gci linter issue: remove extra blank line in runner_test.go 2026-05-18 14:49:12 +02:00
David Gageot 68b8555527 Fix agent-switching command bugs in CLI and TUI paths
Address critical review feedback from docker-agent bot:

**CLI (runner.go) fixes:**
1. Resolve command content BEFORE switching agents - Previously,
   SetCurrentAgent was called before ResolveCommand, causing the
   command lookup to run against the new agent's command table instead
   of the original agent's. This resulted in raw slash-command strings
   being sent to the target agent when the command wasn't re-declared.

2. Return explicit error on agent-switch failure - Previously returned
   an empty UserMessage that was unconditionally added to the session,
   silently corrupting conversation history. Now returns (nil, "", error)
   so callers can handle the failure properly.

3. Handle agent-only commands with no trailing args - When a command
   like /plan has no instruction and no trailing text, return
   (nil, "", nil) to signal successful agent switch with no message
   to send, preventing empty messages from being forwarded to the LLM.

**TUI (handlers.go) fixes:**
Already implemented in the current code:
- Check if agent switch succeeded before sending resolved message
- Use comma-ok form for type assertion to prevent panics

**Test updates:**
- Update PrepareUserMessage signature to return error
- Fix test expectations to match new nil-message behavior
- Add expectError and expectNilMessage flags to test cases

All linters and tests passing.
2026-05-18 14:38:32 +02:00
David Gageot 202c498bd5 Address PR review feedback for agent-switching commands
This commit addresses all review feedback from PR #2790:

**Critical fixes (blocking issues):**

1. **Fix SetCurrentAgent error handling in CLI and TUI** (runner.go, handlers.go)
   - If SetCurrentAgent fails, we now return an empty message instead of
     proceeding to send the message to the wrong agent
   - Added checked type assertion in TUI to prevent potential panics
   - Both CLI and TUI now properly handle agent switch failures

2. **Add unit tests for PrepareUserMessage and agent switching** (runner_test.go)
   - Added comprehensive tests for agent-switching commands
   - Tests cover success cases, failure cases, and edge cases
   - Tests verify that empty messages are returned on switch failures
   - Tests verify that agent-only commands (no instruction) work correctly

**Non-blocking improvements:**

3. **Update agent-schema.json** (agent-schema.json)
   - Changed description from 'must be reachable from sub-agent graph' to
     'must be defined in team configuration' to match runtime behavior
   - The runtime validates agent existence but doesn't enforce graph reachability

4. **Add explanatory comment for fallback logic** (commands.go)
   - Added comment explaining why description falls back to instruction
   - Clarifies the shorthand command definition behavior

5. **Improve code quality**
   - Fixed unchecked type assertion in TUI handlers
   - Used slog.WarnContext instead of slog.Warn for proper context propagation
   - All linters pass, all tests pass

Addresses feedback from @aheritier and docker-agent bot.
2026-05-18 14:28:31 +02:00
David Gageot c59a279d34 fix: resolve command before switching agent to prevent lookup failures
Addresses review feedback from docker-agent bot:

1. In pkg/cli/runner.go (PrepareUserMessage):
   - Moved ResolveCommand call BEFORE SetCurrentAgent
   - This ensures the command is looked up in the original agent's command table
   - Prevents raw slash-command strings from being sent to target agents when
     the target agent doesn't have the command defined

2. In pkg/tui/handlers.go (handleAgentCommand):
   - Added switchSucceeded flag to track whether agent switch was successful
   - Only send resolved message if the agent switch succeeded
   - Prevents messages from being sent to the wrong agent when switching fails
2026-05-18 14:28:31 +02:00
David Gageot 50a288dc31 feat: add slash commands for agent switching
Add support for commands that switch the active agent via a new 'agent:' field
in the commands section of agent.yaml. Users can now use /plan to switch to a
planner sub-agent, /review to switch to a reviewer, etc. Trailing arguments
after the command are forwarded to the target agent as the first prompt.
2026-05-18 14:28:31 +02:00
David Gageot 770868cf2c refactor: standardize builtin toolset naming (ToolSet types, New constructors)
Signed-off-by: David Gageot <david@gageot.net>
Assisted-By: docker-agent
2026-05-18 10:37:24 +02:00
David Gageot 248e7b08be Fix linter issues
Signed-off-by: David Gageot <david.gageot@docker.com>
2026-05-12 14:39:42 +02:00
David Gageot 7556108d29 refactor: replace chan Event threading with EventSink interface 2026-05-12 09:34:06 +02:00
David Gageot fd856d70fe feat: queue depth endpoint and 429 backpressure for full queues 2026-05-11 17:12:57 +02:00
Simon Ferquel's Clanker a503698550 feat: wire TUI/CLI to emit Document parts and render attachments
Routes all binary attachments through chat.ProcessAttachmentWithMetadata
and emits MessagePartTypeDocument instead of the legacy MessagePartTypeImageURL
and MessagePartTypeFile parts.

pkg/app/app.go (processFileAttachment):
  - Replace separate image-resize + PDF-file-ref branches with a single
    ProcessAttachmentWithMetadata call → MessagePartTypeDocument
  - FormatDimensionNote still emitted for resized images via *ImageResizeResult
  - Removes encoding/base64 import (no longer needed)

pkg/cli/runner.go (CreateUserMessageWithAttachment):
  - Add ctx context.Context parameter; thread through to slog.WarnContext
  - Replace image-resize + PDF-file-ref branches with ProcessAttachmentWithMetadata
    → MessagePartTypeDocument
  - Removes encoding/base64 import

pkg/runtime/strip_modalities.go:
  - Add MessagePartTypeDocument case: drop parts whose Document.MimeType is
    an image type (consistent with existing ImageURL and File handling)

pkg/app/transcript/transcript.go:
  - writeUserMessage: always emit the ## User heading, then render each part
    separately so the heading is never omitted for attachment-only messages
  - MessagePartTypeDocument: image chip includes name/mime/size;
    binary/PDF chip includes name/mime/size; text attachment chip has name/mime
  - Backward-compat rendering for legacy MessagePartTypeImageURL and
    MessagePartTypeFile parts (stored sessions)
  - Add formatBytes helper (IEC-style: KB/MB/…)

Part of #2595

Assisted-By: docker-agent
2026-05-11 14:28:11 +00:00
David Gageot 73518a76d0 refactor(runtime): promote ToolsChangeSubscriber onto Runtime
OnToolsChanged is now a regular Runtime method rather than an
optional sub-interface. LocalRuntime keeps its existing emit
machinery; RemoteRuntime is a no-op (tool-list changes already flow
through the run-stream events server-side).

Drop the ToolsChangeSubscriber interface and ToolsChangeSubscriberOf
helper. capabilities.go is now empty save the package declaration;
the next step removes the file entirely.
2026-05-11 11:34:04 +02:00
David Gageot 4774cb7742 refactor(runtime): promote ModelSwitcher onto Runtime
SetAgentModel(ctx, agentName, modelRef), AvailableModels(ctx) and a
cheap SupportsModelSwitching() are now part of the Runtime interface.
LocalRuntime delegates to its existing implementation; RemoteRuntime
returns ErrUnsupported / nil / false.

Callers no longer scatter type assertions:
  - App.SetCurrentAgentModel  : runtime.ErrUnsupported -> 'not supported'
  - App.AvailableModels       : SupportsModelSwitching gate
  - App.applySessionModelOverrides
  - cmd/root.createLocalRuntimeAndSession (resume path)

Drop the ModelSwitcher interface and ModelSwitcherOf helper; the only
optional sub-interface left in capabilities.go is ToolsChangeSubscriber,
which the next step folds away the same way.
2026-05-11 11:34:04 +02:00
David Gageot 7eca8ead37 refactor(runtime): promote TogglePause onto the Runtime interface
TogglePause(ctx) (paused, err) is now part of Runtime. LocalRuntime
delegates to the existing implementation; RemoteRuntime returns
ErrUnsupported for now. App.TogglePause keys off the typed error
instead of a type assertion, and the optional Pauser interface goes
away.

Pattern for the remaining optional sub-interfaces (model switching,
snapshots, ...): each one becomes a copy of this commit.
2026-05-09 09:00:50 +02:00
Tomas Dabašinskas 5f0a5796cd refactor(logging): pass context to all slog calls for correlation
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.
2026-05-06 16:42:35 +03:00
David Gageot 35b1404413 split builtin tools into individual sub-packages 2026-05-06 14:03:10 +02:00
David Gageot a4c9f16c2e feat: foundation for hot-reload + /toolset-restart slash command
Lands two complementary pieces of the eventual file-watcher hot-reload:

1) The diff machinery (pkg/teamloader/diff.go) that classifies a
   freshly-loaded latest.Toolset slice against the running one as
   Added / Removed / Changed / Unchanged. ToolsetIdentity gives each
   entry a stable key (Type+Name when named, else Type+ref/cmd/url),
   and ToolsetSignature is a hex SHA-256 over the full Toolset
   JSON so adding new fields automatically participates in change
   detection without further wiring. HasChanges() is a fast-path for
   the future watcher.

2) An immediately-useful capability built on the supervisor: by-name
   restart. Most users hit a 'gopls is stuck' or 'I just OAuth'd this
   MCP and need it to reconnect' moment within minutes of using the
   agent; today they have to restart the whole agent.

   New tools.Restartable interface (Restart(ctx) error). Implemented
   by mcp.Toolset and *builtin.LSPTool, both of which delegate to
   supervisor.RestartAndWait under their respective timeouts.
   Runtime gains RestartToolset(ctx, name); the remote runtime
   refuses with a clear error since toolset lifecycle is server-side
   for that case. App is a thin pass-through.

   TUI: new RestartToolsetMsg, /toolset-restart slash command
   ('/toolset-restart gopls'), and a notification-toast handler that
   surfaces success or the supervisor's error.

Tests:
  * diff_test.go covers no-changes, added-only, removed-only,
    changed-only, mixed (preserves order), identity matching for
    named/unnamed toolsets across mcp/lsp/a2a, and signature
    stability/discrimination.
  * restart_test.go exercises name resolution and Restartable
    dispatch in isolation.

Note on full hot-reload (file watcher + diff-driven apply):
  Wiring the watcher into the agent so that Stop+Start of a single
  toolset works in a live runtime is non-trivial (Agent.toolsets
  needs a snapshot pattern, the tool dispatcher needs to swap
  references atomically, and the runtime loop needs a quiescent
  point). It is intentionally not in this commit. The pieces shipped
  here \u2014 diff machinery + per-toolset restart capability \u2014 are the
  primitives that flow needs and are independently useful.

Assisted-By: docker-agent
2026-04-28 18:23:42 +02:00
David Gageot dd1b74b718 feat(tui): add /toolsets command and supervisor-aware status surface
Until now the user could only learn that an MCP/LSP toolset failed via
ad-hoc warning toasts. /toolsets gives them a first-class view of every
toolset attached to the current agent, with the lifecycle state
(stopped / starting / ready / degraded / restarting / failed), the
last error, the restart count, and the human-readable description.

Wiring:

  * pkg/tools/status.go: ToolsetStatus DTO consumed by all status
    surfaces (TUI today, JSON status endpoints later).
  * pkg/tools/capabilities.go: Statable interface — any toolset that
    implements State() lifecycle.StateInfo plugs into the dashboard.
    Implemented by mcp.Toolset (already exposed State() in step 2)
    and *builtin.LSPTool (added here).
  * pkg/runtime: CurrentAgentToolsetStatuses() walks the active
    agent's toolsets, unwraps StartableToolSet, and asks each one
    for its state. Toolsets without a supervisor fall back to
    Ready/Stopped based on whether Start succeeded.
    The remote runtime returns nil — toolset state lives server-side.
  * pkg/app: thin pass-through.
  * pkg/tui: new ShowToolsetsDialogMsg, /toolsets slash command, and
    a small NewToolsetsDialog that renders each entry with a
    coloured state badge and a truncated last-error line.
  * Tests: toolsetstatus_test.go covers Statable / non-Statable /
    StartableToolSet-unwrap; toolsets_test.go covers empty,
    populated, and long-error truncation paths.

The dialog is a snapshot taken when /toolsets is opened. A live
subscription model is left for a follow-up commit; the current
snapshot is enough to tell the user 'gopls crashed and has been
restarting for 90s' which is the main pain point today.

Assisted-By: docker-agent
2026-04-28 18:23:42 +02:00