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.
Add a Commands []string field to api.Agent so clients can enumerate
the slash commands defined on the root of each loaded config.
Populated from cfg.Agents.First().Commands (already merged with any
UseCommands references by config.Load), sorted alphabetically, and
omitted from the JSON when empty for backward compatibility.
Add chat.Usage.Add helper; validate/normalize summary attribution in
server; thread model/usage through client API via session.Item; deep-copy
usage pointer in persistence observer and snapshotItems; document
last-model-wins in runUsage.
Assisted-By: Claude
Adds Model+Usage to compactor.Result and session.Item (migration 25),
threads them through SessionSummaryEvent, persistence, and the server
AddSummary path so the TUI /cost By Model breakdown includes compaction
spend instead of silently dropping it.
Introduces a middle-ground session mode between 'ask for everything'
(strict) and 'auto-approve everything' (unsafe/--yolo). Under
safe-auto, safer_shell auto-approves classifier-verified safe shell
calls (docker ps, ls, git status, ...) and still prompts for
destructive and unknown commands.
The classifier now emits blast_radius=safe metadata on the safe
branch too, so embedders can render a SAFE badge even under strict
mode and offer a one-click opt-in via the new approve-safe resume
type that flips the session into safe-auto.
- pkg/session: SafetyPolicySafeAuto + SetSafetyPolicy helper
- pkg/hooks/builtins/safer_shell: emit safe verdict; auto-Allow under
safe-auto
- pkg/runtime/toolexec: ResumeTypeApproveSafe + user_approved_safe
approval source
- pkg/api + pkg/server: safety_policy on session responses + snapshot;
new PATCH /api/sessions/:id/safety-policy endpoint
- pkg/tui: safe case for blast-radius badge with a green reassuring
heading
- docs + examples: describe the four-mode matrix
GET /snapshot of a session the server does not know now 404s with
{"code":"unknown_session"} in the body. Clients that own their session
ids (the board) can finally tell the two 404s apart: with the code, the
server is current but the session is not created/attached yet — keep
waiting; without it, the route itself is missing (a binary that predates
/snapshot) — relaunch to pick up the current binary. Previously both
looked identical and the board could only guess.
The new server tests cover heartbeats, the aggregate snapshot cost, and
this error code.
The snapshot now carries the session's total cost (TotalCost, which
includes sub-session and item-level costs such as compaction). External
consumers like the board no longer need to fetch every message and sum
per-message costs — which undercounted — to display what a session has
spent.
POST /api/sessions/:id/fork now takes a 0-based ordinal counting only
user-role messages instead of an index into the flat all-roles list.
Clients only have to know which of the user's turns they clicked on;
the server translates that into the underlying item index.
This decouples the contract from the precise layout of tool responses
and assistant turns, which a streaming client cannot fully observe
until persistence completes.
Add POST /api/sessions/:id/fork that deep-copies a session up to (but
excluding) a chosen user message, returning a new session with a
fork-numbered title. The fork point must be a user message; assistant
turns, tool responses, and sub-session items are rejected with 400.
session.ForkSession wraps the existing BranchSession with the fork
title generator so the TUI's branch-from-edit flow keeps its own
(branched) / (branch N) titling scheme.
A client whose POST /followup times out can't tell whether the
follow-up landed. Retrying risked delivering the prompt twice; not
retrying risked losing it.
Accept an optional Idempotency-Key header on /followup. A repeat with a
key already seen for the session is acknowledged (status "duplicate",
202) without delivering the message again, so retries are safe. The
reservation is per-session, bounded (recent keys only), made atomically
to settle concurrent retries to a single delivery, and rolled back if
delivery fails so a genuine failure stays retryable.
A client rebuilding a session's view previously had to stitch together
/status, /sessions/:id and replayed events, with no way to know the
exact event-stream position the assembled state corresponds to \u2014 so a
transition occurring between the reads could be missed.
Add a single snapshot endpoint returning the session's stored state
(messages, tokens, permissions), its live runtime state (streaming,
current agent), and last_event_seq: the sequence number of the most
recent event on its /events stream. The resync pattern is then exact and
gapless: read the snapshot, connect to /events?since=<last_event_seq>,
and any event in between is replayed from the buffer.
Replace the dedicated PATCH/POST /api/sessions/:id/model endpoints with a model field on the runAgent request. The override is applied before user messages are persisted, preventing orphaned messages on validation failure. RemoteRuntime queues overrides internally and forwards them on the next turn.
- Remove SetSessionModelRequest/Response types
- Add RunAgentRequest with messages and optional model field
- Update Client.RunAgent() to accept model parameter
- Remove Client.SetAgentModel()
- Update RemoteRuntime to queue pending overrides
- Fold model logic into SessionManager.RunSession()
- Remove dedicated /model endpoints and handler
- Update API documentation and tests
Add GET /api/sessions/:id/models to list available models and the
current override (if any), and PATCH/POST /api/sessions/:id/model to
apply a model override for the session's current agent.
Extend ModelChoice with JSON tags for wire format and add
SessionModelsResponse type. Both endpoints return 422 if the runtime
doesn't support model switching.
Add a lightweight status endpoint that returns a snapshot of the
session's current runtime state: title, streaming flag, active agent,
token counts, and message count.
This lets late-joining SSE consumers (boards, dashboards) immediately
know the session's state without waiting for the next event transition.
The streaming flag is probed via the existing per-session mutex so no
additional bookkeeping is needed.
Addresses #2223.
Allow API clients to inject user messages into an active agent session
without waiting for the current turn to finish. This is a common pattern
in agentic coding tools where the user can steer or provide follow-up
context while the agent is executing tool calls.
New API endpoint: POST /sessions/:id/steer
Runtime changes:
- SteeredMessage type + buffered channel on LocalRuntime
- Steer() enqueues, DrainSteeredMessages() batch-drains
- Agent loop injects steered messages after tool execution and before
the stop-condition check; emits user_message events so clients know
when the LLM actually picks them up
- Messages wrapped in <system-reminder> tags for clear LLM attribution
Server changes:
- POST /sessions/:id/steer endpoint (202 Accepted)
- SteerSession() on SessionManager with GetLocalRuntime() helper for
PersistentRuntime unwrapping
- Concurrent stream guard on RunSession (rejects if already streaming)
- Proper defer ordering: streaming flag cleared before channel close
No behavioral change to the TUI — the existing client-side message
queue continues to work as before. The TUI can adopt mid-turn steering
in a future change by calling LocalRuntime.Steer() directly.
Thinking is now solely driven by the model's thinking_budget config.
If a model has thinking_budget set or requires thinking (e.g. o-series),
it uses thinking. There is no longer a per-session toggle.
Removed:
- /think slash command from TUI
- Session.Thinking field and WithThinking option
- options.WithThinking model option and applyOverrides
- Thinking toggle from sidebar, server API, session state
- ThinkingConfigured from agent and teamloader
- All related handlers, messages, and tests
Assisted-By: docker-agent
There are now two modes in the oauth flow
- Managed mode
In this mode, cagent handles, in process, everything oauth related:
asking for user consent, starting the oauth callback server, resuming
once the flow is done
- Unmanged mode
This mode is more for the remote case in cagent, in this mode the remote
cagent will _only_ ask for user consent via an elicitation, up to the
client to do the oauth flow and send the token back in the elicitation
response.
Signed-off-by: Djordje Lukic <djordje.lukic@docker.com>