From 5d09b4b92c4d1d03455a6de3e8eb5555eb25755d Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 4 May 2026 01:35:20 +0100 Subject: [PATCH] feat(agents): add tool progress detail modes --- CHANGELOG.md | 1 + docs/.generated/config-baseline.sha256 | 4 +-- docs/concepts/progress-drafts.md | 33 ++++++++++++++++-- docs/gateway/config-agents.md | 2 ++ docs/gateway/configuration-examples.md | 1 + docs/tools/thinking.md | 5 ++- .../src/app-server/event-projector.test.ts | 33 ++++++++++++++++++ .../codex/src/app-server/event-projector.ts | 34 +++++++++++++------ src/agents/pi-embedded-runner/run.ts | 1 + src/agents/pi-embedded-runner/run/params.ts | 2 ++ .../pi-embedded-subscribe.handlers.tools.ts | 8 ++++- .../pi-embedded-subscribe.handlers.types.ts | 1 + .../pi-embedded-subscribe.shared-types.ts | 1 + src/agents/pi-embedded-subscribe.types.ts | 13 +++++-- src/agents/pi-embedded-utils.ts | 8 +++-- src/agents/tool-display-common.ts | 7 ++-- src/agents/tool-display-exec.ts | 14 ++++++-- src/agents/tool-display.test.ts | 12 +++++++ src/agents/tool-display.ts | 3 ++ .../reply/agent-runner-execution.ts | 2 ++ src/auto-reply/reply/agent-runner.ts | 3 ++ src/auto-reply/reply/get-reply-run.ts | 7 ++++ src/config/schema.base.generated.ts | 20 +++++++++++ src/config/types.agent-defaults.ts | 6 ++++ src/config/types.agents.ts | 2 ++ src/config/zod-schema.agent-defaults.ts | 1 + src/config/zod-schema.agent-runtime.ts | 2 ++ src/plugin-sdk/agent-harness-runtime.ts | 10 ++++-- src/plugin-sdk/channel-streaming.ts | 11 ++++-- 29 files changed, 217 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cb1bce5ab24..d488e1d5153 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ Docs: https://docs.openclaw.ai ### Changes - Channels/streaming: add unified `streaming.mode: "progress"` drafts with auto single-word status labels and shared progress configuration across Discord, Telegram, Matrix, Slack, and Microsoft Teams. +- Agents/verbose: use compact explain-mode tool summaries for `/verbose` and progress drafts by default, with `agents.defaults.toolProgressDetail: "raw"` and per-agent overrides for debugging raw command/detail output. - Agents/commands: add `/steer ` for queue-independent steering of the active current-session run without starting a new turn when the session is idle. (#76934) - Tools/BTW: add `/side` as a text and native slash-command alias for `/btw` side questions. - Doctor/config: `doctor --fix` now commits safe legacy migrations even when unrelated validation issues (e.g. a missing plugin) prevent full validation from passing, so `agents.defaults.llm` and other known-legacy keys are always cleaned up by `doctor --fix` regardless of other config problems. Fixes #76798. (#76800) Thanks @hclsys. diff --git a/docs/.generated/config-baseline.sha256 b/docs/.generated/config-baseline.sha256 index 77830ef1385..ab535ee92a4 100644 --- a/docs/.generated/config-baseline.sha256 +++ b/docs/.generated/config-baseline.sha256 @@ -1,4 +1,4 @@ -34e7f2742624de44bfd1df7743e65ff33a04b0f6fe251bc417a6b33f85529772 config-baseline.json -5b5ebd95939d75496597d9858a375e27544812d0f79dc3b4bf87c794ada2ba08 config-baseline.core.json +3e7cbffbe3849b5201716f359dde9089d61d618c1a4206255c20887a855d85a9 config-baseline.json +31ec333df9f8b92c7656ac7107cecd5860dd02e08f7e18c7c674dc47a8811baa config-baseline.core.json 655d1309b70505e73198df20c5088784290b33098efd42027d3c09beeb3704a7 config-baseline.channel.json 055fae0d0067a751dc10125af7421da45633f73519c94c982d02b0c4eb2bdf67 config-baseline.plugin.json diff --git a/docs/concepts/progress-drafts.md b/docs/concepts/progress-drafts.md index d1c47edbe5a..7689bf69959 100644 --- a/docs/concepts/progress-drafts.md +++ b/docs/concepts/progress-drafts.md @@ -18,9 +18,9 @@ into the final answer when the channel can do that safely. ```text Shelling... -- reading recent channel context -- checking matching issues -- preparing reply +๐Ÿ“– Read: from docs/concepts/progress-drafts.md +๐Ÿ”Ž Web Search: for "discord edit message" +๐Ÿ› ๏ธ Exec: run tests ``` Use progress drafts when you want one tidy status message during tool-heavy work @@ -60,6 +60,9 @@ The label appears after the agent starts meaningful work and either remains busy for five seconds or emits a second work event. Plain text-only replies do not show a progress draft. Progress lines are added only when the agent emits useful work updates, for example `๐Ÿ› ๏ธ Exec`, `๐Ÿ”Ž Web Search`, or `โœ๏ธ Write: to /tmp/file`. +By default they use the same compact explain mode as `/verbose`; set +`agents.defaults.toolProgressDetail: "raw"` when debugging and you also want raw +commands/details appended. The final answer replaces the draft when possible; otherwise OpenClaw sends the final answer normally and cleans up or stops updating the draft according to the channel's transport. @@ -173,6 +176,30 @@ Progress lines are enabled by default in progress mode. They come from real run events: tool starts, item updates, task plans, approvals, command output, patch summaries, and similar agent activity. +OpenClaw uses the same formatter for progress drafts and `/verbose`: + +```json5 +{ + agents: { + defaults: { + toolProgressDetail: "explain", // explain | raw + }, + }, +} +``` + +`"explain"` is the default and keeps drafts stable with concise labels like +`๐Ÿ› ๏ธ Exec: check JS syntax for /tmp/app.js`. `"raw"` appends the underlying +command/detail when available, which is useful while debugging but noisier in +chat. + +For example, the same command appears differently depending on the detail mode: + +| Mode | Progress line | +| --------- | -------------------------------------------------------------------- | +| `explain` | `๐Ÿ› ๏ธ Exec: check JS syntax for /tmp/app.js` | +| `raw` | `๐Ÿ› ๏ธ Exec: check JS syntax for /tmp/app.js, node --check /tmp/app.js` | + Limit how many lines stay visible: ```json5 diff --git a/docs/gateway/config-agents.md b/docs/gateway/config-agents.md index a59d2c9c4fb..e2b7f347332 100644 --- a/docs/gateway/config-agents.md +++ b/docs/gateway/config-agents.md @@ -343,6 +343,7 @@ Time format in system prompt. Default: `auto` (OS preference). pdfMaxPages: 20, thinkingDefault: "low", verboseDefault: "off", + toolProgressDetail: "explain", reasoningDefault: "off", elevatedDefault: "on", timeoutSeconds: 600, @@ -383,6 +384,7 @@ Time format in system prompt. Default: `auto` (OS preference). - `pdfMaxBytesMb`: default PDF size limit for the `pdf` tool when `maxBytesMb` is not passed at call time. - `pdfMaxPages`: default maximum pages considered by extraction fallback mode in the `pdf` tool. - `verboseDefault`: default verbose level for agents. Values: `"off"`, `"on"`, `"full"`. Default: `"off"`. +- `toolProgressDetail`: detail mode for `/verbose` tool summaries and progress-draft tool lines. Values: `"explain"` (default, compact human labels) or `"raw"` (append raw command/detail when available). Per-agent `agents.list[].toolProgressDetail` overrides this default. - `reasoningDefault`: default reasoning visibility for agents. Values: `"off"`, `"on"`, `"stream"`. Per-agent `agents.list[].reasoningDefault` overrides this default. Configured reasoning defaults are only applied for owners, authorized senders, or operator-admin gateway contexts when no per-message or session reasoning override is set. - `elevatedDefault`: default elevated-output level for agents. Values: `"off"`, `"on"`, `"ask"`, `"full"`. Default: `"on"`. - `model.primary`: format `provider/model` (e.g. `openai/gpt-5.5` for API-key access or `openai-codex/gpt-5.5` for Codex OAuth). If you omit the provider, OpenClaw tries an alias first, then a unique configured-provider match for that exact model id, and only then falls back to the configured default provider (deprecated compatibility behavior, so prefer explicit `provider/model`). If that provider no longer exposes the configured default model, OpenClaw falls back to the first configured provider/model instead of surfacing a stale removed-provider default. diff --git a/docs/gateway/configuration-examples.md b/docs/gateway/configuration-examples.md index 7a1614d114b..97df79a67f1 100644 --- a/docs/gateway/configuration-examples.md +++ b/docs/gateway/configuration-examples.md @@ -249,6 +249,7 @@ Save to `~/.openclaw/openclaw.json` and you can DM the bot from that number. skills: ["github", "weather"], // inherited by agents that omit list[].skills thinkingDefault: "low", verboseDefault: "off", + toolProgressDetail: "explain", reasoningDefault: "off", elevatedDefault: "on", blockStreamingDefault: "off", diff --git a/docs/tools/thinking.md b/docs/tools/thinking.md index 82724c1543f..1b546bc2b09 100644 --- a/docs/tools/thinking.md +++ b/docs/tools/thinking.md @@ -80,9 +80,12 @@ title: "Thinking levels" - `/verbose off` stores an explicit session override; clear it via the Sessions UI by choosing `inherit`. - Inline directive affects only that message; session/global defaults apply otherwise. - Send `/verbose` (or `/verbose:`) with no argument to see the current verbose level. -- When verbose is on, agents that emit structured tool results (Pi, other JSON agents) send each tool call back as its own metadata-only message, prefixed with ` : ` when available (path/command). These tool summaries are sent as soon as each tool starts (separate bubbles), not as streaming deltas. +- When verbose is on, agents that emit structured tool results (Pi, other JSON agents) send each tool call back as its own metadata-only message, prefixed with ` : ` when available. These tool summaries are sent as soon as each tool starts (separate bubbles), not as streaming deltas. - Tool failure summaries remain visible in normal mode, but raw error detail suffixes are hidden unless verbose is `on` or `full`. - When verbose is `full`, tool outputs are also forwarded after completion (separate bubble, truncated to a safe length). If you toggle `/verbose on|full|off` while a run is in-flight, subsequent tool bubbles honor the new setting. +- `agents.defaults.toolProgressDetail` controls the shape of `/verbose` tool summaries and progress-draft tool lines. Use `"explain"` (default) for compact human labels such as `๐Ÿ› ๏ธ Exec: checking JS syntax`; use `"raw"` when you also want the raw command/detail appended for debugging. Per-agent `agents.list[].toolProgressDetail` overrides the default. + - `explain`: `๐Ÿ› ๏ธ Exec: check JS syntax for /tmp/app.js` + - `raw`: `๐Ÿ› ๏ธ Exec: check JS syntax for /tmp/app.js, node --check /tmp/app.js` ## Plugin trace directives (/trace) diff --git a/extensions/codex/src/app-server/event-projector.test.ts b/extensions/codex/src/app-server/event-projector.test.ts index 56fdf31b243..631f0228017 100644 --- a/extensions/codex/src/app-server/event-projector.test.ts +++ b/extensions/codex/src/app-server/event-projector.test.ts @@ -579,6 +579,38 @@ describe("CodexAppServerEventProjector", () => { ); expect(onToolResult).toHaveBeenCalledTimes(1); + expect(onToolResult).toHaveBeenCalledWith({ + text: "๐Ÿ› ๏ธ Bash: `run tests (in /workspace)`", + }); + }); + + it("can emit raw verbose tool summaries through onToolResult", async () => { + const onToolResult = vi.fn(); + const projector = await createProjector({ + ...(await createParams()), + verboseLevel: "on", + toolProgressDetail: "raw", + onToolResult, + }); + + await projector.handleNotification( + forCurrentTurn("item/started", { + item: { + type: "commandExecution", + id: "cmd-1", + command: "pnpm test extensions/codex", + cwd: "/workspace", + processId: null, + source: "agent", + status: "inProgress", + commandActions: [], + aggregatedOutput: null, + exitCode: null, + durationMs: null, + }, + }), + ); + expect(onToolResult).toHaveBeenCalledWith({ text: "๐Ÿ› ๏ธ Bash: `` run tests (in /workspace), `pnpm test extensions/codex` ``", }); @@ -589,6 +621,7 @@ describe("CodexAppServerEventProjector", () => { const projector = await createProjector({ ...(await createParams()), verboseLevel: "on", + toolProgressDetail: "raw", onToolResult, }); diff --git a/extensions/codex/src/app-server/event-projector.ts b/extensions/codex/src/app-server/event-projector.ts index bfe1477fa07..bae62e0ed02 100644 --- a/extensions/codex/src/app-server/event-projector.ts +++ b/extensions/codex/src/app-server/event-projector.ts @@ -16,6 +16,7 @@ import { type EmbeddedRunAttemptResult, type HeartbeatToolResponse, type MessagingToolSend, + type ToolProgressDetailMode, } from "openclaw/plugin-sdk/agent-harness-runtime"; import { readCodexTurn } from "./protocol-validators.js"; import { @@ -614,6 +615,7 @@ export class CodexAppServerEventProjector { if (!kind) { return; } + const meta = itemMeta(item, this.toolProgressDetailMode()); this.emitAgentEvent({ stream: "item", data: { @@ -623,7 +625,7 @@ export class CodexAppServerEventProjector { title: itemTitle(item), status: params.phase === "start" ? "running" : itemStatus(item), ...(itemName(item) ? { name: itemName(item) } : {}), - ...(itemMeta(item) ? { meta: itemMeta(item) } : {}), + ...(meta ? { meta } : {}), }, }); } @@ -641,7 +643,7 @@ export class CodexAppServerEventProjector { return; } this.toolResultSummaryItemIds.add(itemId); - const meta = itemMeta(item); + const meta = itemMeta(item, this.toolProgressDetailMode()); this.emitToolResultMessage({ itemId, text: formatToolSummary(toolName, meta), @@ -666,7 +668,7 @@ export class CodexAppServerEventProjector { } this.emitToolResultMessage({ itemId, - text: formatToolOutput(toolName, itemMeta(item), output), + text: formatToolOutput(toolName, itemMeta(item, this.toolProgressDetailMode()), output), finalOutput: true, }); } @@ -700,6 +702,10 @@ export class CodexAppServerEventProjector { : this.params.verboseLevel === "full"; } + private toolProgressDetailMode(): ToolProgressDetailMode { + return this.params.toolProgressDetail === "raw" ? "raw" : "explain"; + } + private recordToolMeta(item: CodexThreadItem | undefined): void { if (!item) { return; @@ -708,9 +714,10 @@ export class CodexAppServerEventProjector { if (!toolName) { return; } + const meta = itemMeta(item, this.toolProgressDetailMode()); this.toolMetas.set(item.id, { toolName, - ...(itemMeta(item) ? { meta: itemMeta(item) } : {}), + ...(meta ? { meta } : {}), }); } @@ -1047,19 +1054,26 @@ function itemName(item: CodexThreadItem): string | undefined { return undefined; } -function itemMeta(item: CodexThreadItem): string | undefined { +function itemMeta( + item: CodexThreadItem, + detailMode: ToolProgressDetailMode = "explain", +): string | undefined { if (item.type === "commandExecution" && typeof item.command === "string") { - return inferToolMetaFromArgs("exec", { - command: item.command, - cwd: typeof item.cwd === "string" ? item.cwd : undefined, - }); + return inferToolMetaFromArgs( + "exec", + { + command: item.command, + cwd: typeof item.cwd === "string" ? item.cwd : undefined, + }, + { detailMode }, + ); } if (item.type === "webSearch" && typeof item.query === "string") { return item.query; } const toolName = itemName(item); if ((item.type === "dynamicToolCall" || item.type === "mcpToolCall") && toolName) { - return inferToolMetaFromArgs(toolName, item.arguments); + return inferToolMetaFromArgs(toolName, item.arguments, { detailMode }); } return undefined; } diff --git a/src/agents/pi-embedded-runner/run.ts b/src/agents/pi-embedded-runner/run.ts index 7f65eb3b457..91d02918cc1 100644 --- a/src/agents/pi-embedded-runner/run.ts +++ b/src/agents/pi-embedded-runner/run.ts @@ -1128,6 +1128,7 @@ export async function runEmbeddedPiAgent( verboseLevel: params.verboseLevel, reasoningLevel: params.reasoningLevel, toolResultFormat: resolvedToolResultFormat, + toolProgressDetail: params.toolProgressDetail, execOverrides: params.execOverrides, bashElevated: params.bashElevated, timeoutMs: params.timeoutMs, diff --git a/src/agents/pi-embedded-runner/run/params.ts b/src/agents/pi-embedded-runner/run/params.ts index 6ad1a23a49a..756d75171b1 100644 --- a/src/agents/pi-embedded-runner/run/params.ts +++ b/src/agents/pi-embedded-runner/run/params.ts @@ -14,6 +14,7 @@ import type { AgentInternalEvent } from "../../internal-events.js"; import type { BlockReplyPayload } from "../../pi-embedded-payloads.js"; import type { BlockReplyChunking, + ToolProgressDetailMode, ToolResultFormat, } from "../../pi-embedded-subscribe.shared-types.js"; import type { SkillSnapshot } from "../../skills.js"; @@ -129,6 +130,7 @@ export type RunEmbeddedPiAgentParams = { verboseLevel?: VerboseLevel; reasoningLevel?: ReasoningLevel; toolResultFormat?: ToolResultFormat; + toolProgressDetail?: ToolProgressDetailMode; /** If true, suppress tool error warning payloads for this run (including mutating tools). */ suppressToolErrorWarnings?: boolean; /** Bootstrap context mode for workspace file injection. */ diff --git a/src/agents/pi-embedded-subscribe.handlers.tools.ts b/src/agents/pi-embedded-subscribe.handlers.tools.ts index 9b188741e77..8ef520fe41c 100644 --- a/src/agents/pi-embedded-subscribe.handlers.tools.ts +++ b/src/agents/pi-embedded-subscribe.handlers.tools.ts @@ -617,7 +617,13 @@ export function handleToolExecutionStart( } } - const meta = extendExecMeta(toolName, args, inferToolMetaFromArgs(toolName, args)); + const meta = extendExecMeta( + toolName, + args, + inferToolMetaFromArgs(toolName, args, { + detailMode: ctx.params.toolProgressDetail ?? "explain", + }), + ); ctx.state.toolMetaById.set(toolCallId, buildToolCallSummary(toolName, args, meta)); ctx.log.debug( `embedded run tool start: runId=${ctx.params.runId} tool=${toolName} toolCallId=${toolCallId}`, diff --git a/src/agents/pi-embedded-subscribe.handlers.types.ts b/src/agents/pi-embedded-subscribe.handlers.types.ts index 70065028126..3b807791afb 100644 --- a/src/agents/pi-embedded-subscribe.handlers.types.ts +++ b/src/agents/pi-embedded-subscribe.handlers.types.ts @@ -186,6 +186,7 @@ type ToolHandlerParams = Pick< | "sessionId" | "agentId" | "toolResultFormat" + | "toolProgressDetail" >; type ToolHandlerState = Pick< diff --git a/src/agents/pi-embedded-subscribe.shared-types.ts b/src/agents/pi-embedded-subscribe.shared-types.ts index 6592a141de6..c690ba5e523 100644 --- a/src/agents/pi-embedded-subscribe.shared-types.ts +++ b/src/agents/pi-embedded-subscribe.shared-types.ts @@ -1,5 +1,6 @@ import type { BlockReplyChunking } from "./pi-embedded-block-chunker.js"; export type ToolResultFormat = "markdown" | "plain"; +export type ToolProgressDetailMode = "explain" | "raw"; export type { BlockReplyChunking }; diff --git a/src/agents/pi-embedded-subscribe.types.ts b/src/agents/pi-embedded-subscribe.types.ts index d304084adce..e6899461878 100644 --- a/src/agents/pi-embedded-subscribe.types.ts +++ b/src/agents/pi-embedded-subscribe.types.ts @@ -6,8 +6,16 @@ import type { HookRunner } from "../plugins/hooks.js"; import type { AgentInternalEvent } from "./internal-events.js"; import type { BlockReplyPayload } from "./pi-embedded-payloads.js"; import type { EmbeddedRunReplayState } from "./pi-embedded-runner/replay-state.js"; -import type { BlockReplyChunking, ToolResultFormat } from "./pi-embedded-subscribe.shared-types.js"; -export type { BlockReplyChunking, ToolResultFormat } from "./pi-embedded-subscribe.shared-types.js"; +import type { + BlockReplyChunking, + ToolProgressDetailMode, + ToolResultFormat, +} from "./pi-embedded-subscribe.shared-types.js"; +export type { + BlockReplyChunking, + ToolProgressDetailMode, + ToolResultFormat, +} from "./pi-embedded-subscribe.shared-types.js"; export type SubscribeEmbeddedPiSessionParams = { session: AgentSession; @@ -18,6 +26,7 @@ export type SubscribeEmbeddedPiSessionParams = { reasoningMode?: ReasoningLevel; thinkingLevel?: ThinkLevel; toolResultFormat?: ToolResultFormat; + toolProgressDetail?: ToolProgressDetailMode; shouldEmitToolResult?: () => boolean; shouldEmitToolOutput?: () => boolean; onToolResult?: (payload: ReplyPayload) => void | Promise; diff --git a/src/agents/pi-embedded-utils.ts b/src/agents/pi-embedded-utils.ts index 5b27f0f10b6..59bb2514e6d 100644 --- a/src/agents/pi-embedded-utils.ts +++ b/src/agents/pi-embedded-utils.ts @@ -353,7 +353,11 @@ export function extractThinkingFromTaggedStream(text: string): string { return text.slice(start).trim(); } -export function inferToolMetaFromArgs(toolName: string, args: unknown): string | undefined { - const display = resolveToolDisplay({ name: toolName, args }); +export function inferToolMetaFromArgs( + toolName: string, + args: unknown, + options?: { detailMode?: "explain" | "raw" }, +): string | undefined { + const display = resolveToolDisplay({ name: toolName, args, detailMode: options?.detailMode }); return formatToolDetail(display); } diff --git a/src/agents/tool-display-common.ts b/src/agents/tool-display-common.ts index 5dca3f352ad..702dce06ce4 100644 --- a/src/agents/tool-display-common.ts +++ b/src/agents/tool-display-common.ts @@ -2,7 +2,7 @@ import { normalizeLowercaseStringOrEmpty, normalizeOptionalString, } from "../shared/string-coerce.js"; -import { resolveExecDetail } from "./tool-display-exec.js"; +import { resolveExecDetail, type ToolDetailMode } from "./tool-display-exec.js"; import { asRecord } from "./tool-display-record.js"; type ToolDisplayActionSpec = { @@ -71,6 +71,7 @@ export function resolveToolVerbAndDetailForArgs(params: { spec?: ToolDisplaySpec; fallbackDetailKeys?: string[]; detailMode: "first" | "summary"; + toolDetailMode?: ToolDetailMode; detailCoerce?: CoerceDisplayValueOptions; detailMaxEntries?: number; detailFormatKey?: (raw: string) => string; @@ -83,6 +84,7 @@ export function resolveToolVerbAndDetailForArgs(params: { spec: params.spec, fallbackDetailKeys: params.fallbackDetailKeys, detailMode: params.detailMode, + toolDetailMode: params.toolDetailMode, detailCoerce: params.detailCoerce, detailMaxEntries: params.detailMaxEntries, detailFormatKey: params.detailFormatKey, @@ -378,6 +380,7 @@ function resolveToolVerbAndDetail(params: { spec?: ToolDisplaySpec; fallbackDetailKeys?: string[]; detailMode: "first" | "summary"; + toolDetailMode?: ToolDetailMode; detailCoerce?: CoerceDisplayValueOptions; detailMaxEntries?: number; detailFormatKey?: (raw: string) => string; @@ -393,7 +396,7 @@ function resolveToolVerbAndDetail(params: { let detail: string | undefined; if (params.toolKey === "exec") { - detail = resolveExecDetail(params.args); + detail = resolveExecDetail(params.args, { detailMode: params.toolDetailMode }); } if (!detail && params.toolKey === "read") { detail = resolveReadDetail(params.args); diff --git a/src/agents/tool-display-exec.ts b/src/agents/tool-display-exec.ts index a67003faf74..1027de86658 100644 --- a/src/agents/tool-display-exec.ts +++ b/src/agents/tool-display-exec.ts @@ -385,7 +385,12 @@ function compactRawCommand(raw: string, maxLength = 120): string { return `${oneLine.slice(0, Math.max(0, maxLength - 1))}โ€ฆ`; } -export function resolveExecDetail(args: unknown): string | undefined { +export type ToolDetailMode = "explain" | "raw"; + +export function resolveExecDetail( + args: unknown, + options?: { detailMode?: ToolDetailMode }, +): string | undefined { const record = asRecord(args); if (!record) { return undefined; @@ -414,7 +419,12 @@ export function resolveExecDetail(args: unknown): string | undefined { } const displaySummary = cwd ? `${summary} (in ${cwd})` : summary; - if (compact && compact !== displaySummary && compact !== summary) { + if ( + options?.detailMode !== "explain" && + compact && + compact !== displaySummary && + compact !== summary + ) { return `${displaySummary} ยท \`${compact}\``; } diff --git a/src/agents/tool-display.test.ts b/src/agents/tool-display.test.ts index 19ef7652ffb..c9af286b4b2 100644 --- a/src/agents/tool-display.test.ts +++ b/src/agents/tool-display.test.ts @@ -115,6 +115,18 @@ describe("tool display details", () => { expect(detail).toBe("install dependencies (in ~/my-project), `cd ~/my-project && npm install`"); }); + it("omits raw command details in explain mode", () => { + const detail = formatToolDetail( + resolveToolDisplay({ + name: "exec", + args: { command: "cd ~/my-project && npm install" }, + detailMode: "explain", + }), + ); + + expect(detail).toBe("install dependencies (in ~/my-project)"); + }); + it("moves cd path to context suffix with multiple stages and raw command", () => { const detail = formatToolDetail( resolveToolDisplay({ diff --git a/src/agents/tool-display.ts b/src/agents/tool-display.ts index 248f581fa9f..21b42e7ac7f 100644 --- a/src/agents/tool-display.ts +++ b/src/agents/tool-display.ts @@ -9,6 +9,7 @@ import { resolveToolVerbAndDetailForArgs, } from "./tool-display-common.js"; import { TOOL_DISPLAY_CONFIG } from "./tool-display-config.js"; +import type { ToolDetailMode } from "./tool-display-exec.js"; type ToolDisplay = { name: string; @@ -45,6 +46,7 @@ export function resolveToolDisplay(params: { name?: string; args?: unknown; meta?: string; + detailMode?: ToolDetailMode; }): ToolDisplay { const name = normalizeToolName(params.name); const key = normalizeLowercaseStringOrEmpty(name); @@ -59,6 +61,7 @@ export function resolveToolDisplay(params: { spec, fallbackDetailKeys: FALLBACK.detailKeys, detailMode: "summary", + toolDetailMode: params.detailMode, detailMaxEntries: MAX_DETAIL_ENTRIES, detailFormatKey: (raw) => formatDetailKey(raw, DETAIL_LABEL_OVERRIDES), }); diff --git a/src/auto-reply/reply/agent-runner-execution.ts b/src/auto-reply/reply/agent-runner-execution.ts index 73d3eeb8de0..4ed5d086d1b 100644 --- a/src/auto-reply/reply/agent-runner-execution.ts +++ b/src/auto-reply/reply/agent-runner-execution.ts @@ -899,6 +899,7 @@ export async function runAgentTurnWithFallback(params: { activeSessionStore?: Record; storePath?: string; resolvedVerboseLevel: VerboseLevel; + toolProgressDetail?: "explain" | "raw"; replyMediaContext?: ReplyMediaContext; }): Promise { const TRANSIENT_HTTP_RETRY_DELAY_MS = 2_500; @@ -1465,6 +1466,7 @@ export async function runAgentTurnWithFallback(params: { } return isMarkdownCapableMessageChannel(channel) ? "markdown" : "plain"; })(), + toolProgressDetail: params.toolProgressDetail, suppressToolErrorWarnings: params.opts?.suppressToolErrorWarnings, disableTools: params.opts?.disableTools, enableHeartbeatTool: params.opts?.enableHeartbeatTool, diff --git a/src/auto-reply/reply/agent-runner.ts b/src/auto-reply/reply/agent-runner.ts index d9ebd9c2fd8..5581a00e79a 100644 --- a/src/auto-reply/reply/agent-runner.ts +++ b/src/auto-reply/reply/agent-runner.ts @@ -906,6 +906,7 @@ export async function runReplyAgent(params: { defaultModel: string; agentCfgContextTokens?: number; resolvedVerboseLevel: VerboseLevel; + toolProgressDetail?: "explain" | "raw"; isNewSession: boolean; blockStreamingEnabled: boolean; blockReplyChunking?: { @@ -943,6 +944,7 @@ export async function runReplyAgent(params: { defaultModel, agentCfgContextTokens, resolvedVerboseLevel, + toolProgressDetail, isNewSession, blockStreamingEnabled, blockReplyChunking, @@ -1263,6 +1265,7 @@ export async function runReplyAgent(params: { activeSessionStore, storePath, resolvedVerboseLevel, + toolProgressDetail, replyMediaContext, }); diff --git a/src/auto-reply/reply/get-reply-run.ts b/src/auto-reply/reply/get-reply-run.ts index d772730170b..b71a7e429c1 100644 --- a/src/auto-reply/reply/get-reply-run.ts +++ b/src/auto-reply/reply/get-reply-run.ts @@ -103,6 +103,10 @@ function normalizePromptRouteChannel(raw?: string | null): string | undefined { return normalized && normalized !== "none" ? normalized : undefined; } +function normalizeToolProgressDetail(value: unknown): "explain" | "raw" | undefined { + return value === "explain" || value === "raw" ? value : undefined; +} + function resolvePersistedPromptProvider(entry?: SessionEntry): string | undefined { return ( normalizePromptRouteChannel(entry?.origin?.provider) ?? @@ -1067,6 +1071,9 @@ export async function runPreparedReply( defaultModel, agentCfgContextTokens: agentCfg?.contextTokens, resolvedVerboseLevel: resolvedVerboseLevel ?? "off", + toolProgressDetail: + normalizeToolProgressDetail(agentCfg?.toolProgressDetail) ?? + normalizeToolProgressDetail(cfg.agents?.defaults?.toolProgressDetail), isNewSession, blockStreamingEnabled, blockReplyChunking, diff --git a/src/config/schema.base.generated.ts b/src/config/schema.base.generated.ts index 3c94eb83ae3..76e5df17662 100644 --- a/src/config/schema.base.generated.ts +++ b/src/config/schema.base.generated.ts @@ -5336,6 +5336,18 @@ export const GENERATED_BASE_CONFIG_SCHEMA: BaseConfigSchemaResponse = { }, ], }, + toolProgressDetail: { + anyOf: [ + { + type: "string", + const: "explain", + }, + { + type: "string", + const: "raw", + }, + ], + }, reasoningDefault: { anyOf: [ { @@ -6339,6 +6351,14 @@ export const GENERATED_BASE_CONFIG_SCHEMA: BaseConfigSchemaResponse = { description: "Optional per-agent default thinking level. Overrides agents.defaults.thinkingDefault for this agent when no per-message or session override is set.", }, + verboseDefault: { + type: "string", + enum: ["off", "on", "full"], + }, + toolProgressDetail: { + type: "string", + enum: ["explain", "raw"], + }, reasoningDefault: { type: "string", enum: ["on", "off", "stream"], diff --git a/src/config/types.agent-defaults.ts b/src/config/types.agent-defaults.ts index 08b847cc591..81de0a52bea 100644 --- a/src/config/types.agent-defaults.ts +++ b/src/config/types.agent-defaults.ts @@ -316,6 +316,12 @@ export type AgentDefaultsConfig = { thinkingDefault?: "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "adaptive" | "max"; /** Default verbose level when no /verbose directive is present. */ verboseDefault?: "off" | "on" | "full"; + /** + * Detail mode for user-visible tool progress in /verbose and editable progress drafts. + * - explain: compact human summary (default) + * - raw: include raw command/detail when available + */ + toolProgressDetail?: "explain" | "raw"; /** Default reasoning level when no /reasoning directive is present. */ reasoningDefault?: "off" | "on" | "stream"; /** Default elevated level when no /elevated directive is present. */ diff --git a/src/config/types.agents.ts b/src/config/types.agents.ts index 3ffb6bdbaaa..f6b5a46d3a1 100644 --- a/src/config/types.agents.ts +++ b/src/config/types.agents.ts @@ -90,6 +90,8 @@ export type AgentConfig = { thinkingDefault?: "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "adaptive" | "max"; /** Optional per-agent default verbosity level. */ verboseDefault?: "off" | "on" | "full"; + /** Optional per-agent tool progress detail mode. */ + toolProgressDetail?: AgentDefaultsConfig["toolProgressDetail"]; /** Optional per-agent default reasoning visibility. */ reasoningDefault?: "on" | "off" | "stream"; /** Optional per-agent default for fast mode. */ diff --git a/src/config/zod-schema.agent-defaults.ts b/src/config/zod-schema.agent-defaults.ts index eb43b954360..385f3aafbab 100644 --- a/src/config/zod-schema.agent-defaults.ts +++ b/src/config/zod-schema.agent-defaults.ts @@ -239,6 +239,7 @@ export const AgentDefaultsSchema = z ]) .optional(), verboseDefault: z.union([z.literal("off"), z.literal("on"), z.literal("full")]).optional(), + toolProgressDetail: z.union([z.literal("explain"), z.literal("raw")]).optional(), reasoningDefault: z.union([z.literal("off"), z.literal("on"), z.literal("stream")]).optional(), elevatedDefault: z .union([z.literal("off"), z.literal("on"), z.literal("ask"), z.literal("full")]) diff --git a/src/config/zod-schema.agent-runtime.ts b/src/config/zod-schema.agent-runtime.ts index c1c88c79521..ae1891dcaa8 100644 --- a/src/config/zod-schema.agent-runtime.ts +++ b/src/config/zod-schema.agent-runtime.ts @@ -839,6 +839,8 @@ export const AgentEntrySchema = z thinkingDefault: z .enum(["off", "minimal", "low", "medium", "high", "xhigh", "adaptive", "max"]) .optional(), + verboseDefault: z.enum(["off", "on", "full"]).optional(), + toolProgressDetail: z.enum(["explain", "raw"]).optional(), reasoningDefault: z.enum(["on", "off", "stream"]).optional(), fastModeDefault: z.boolean().optional(), skills: z.array(z.string()).optional(), diff --git a/src/plugin-sdk/agent-harness-runtime.ts b/src/plugin-sdk/agent-harness-runtime.ts index e7b3393605a..fc5a4c81ba6 100644 --- a/src/plugin-sdk/agent-harness-runtime.ts +++ b/src/plugin-sdk/agent-harness-runtime.ts @@ -154,8 +154,14 @@ export { /** * Derive the same compact user-facing tool detail that Pi uses for progress logs. */ -export function inferToolMetaFromArgs(toolName: string, args: unknown): string | undefined { - const display = resolveToolDisplay({ name: toolName, args }); +export type ToolProgressDetailMode = "explain" | "raw"; + +export function inferToolMetaFromArgs( + toolName: string, + args: unknown, + options?: { detailMode?: ToolProgressDetailMode }, +): string | undefined { + const display = resolveToolDisplay({ name: toolName, args, detailMode: options?.detailMode }); return formatToolDetail(display); } diff --git a/src/plugin-sdk/channel-streaming.ts b/src/plugin-sdk/channel-streaming.ts index e5209c87b84..8ac805c2b1b 100644 --- a/src/plugin-sdk/channel-streaming.ts +++ b/src/plugin-sdk/channel-streaming.ts @@ -128,6 +128,7 @@ export function isChannelProgressDraftWorkToolName(name: string | null | undefin type ChannelProgressLineOptions = { markdown?: boolean; + detailMode?: "explain" | "raw"; }; const EMOJI_PREFIX_RE = /^\p{Extended_Pictographic}/u; @@ -188,11 +189,15 @@ function compactStrings(values: readonly (string | undefined | null)[]): string[ return values.map((value) => value?.replace(/\s+/g, " ").trim()).filter(Boolean) as string[]; } -function inferToolMeta(name: string | undefined, args: Record | undefined) { +function inferToolMeta( + name: string | undefined, + args: Record | undefined, + detailMode: "explain" | "raw" = "explain", +) { if (!name || !args) { return undefined; } - return formatToolDetail(resolveToolDisplay({ name, args })); + return formatToolDetail(resolveToolDisplay({ name, args, detailMode })); } function formatNamedProgressLine( @@ -240,7 +245,7 @@ export function formatChannelProgressDraftLine( return formatNamedProgressLine( input.name, [ - inferToolMeta(input.name, input.args), + inferToolMeta(input.name, input.args, options?.detailMode), input.phase && !input.name ? input.phase : undefined, ], options,