From 99dd4288622a3e93a573fc9c9c81722b5c110d8c Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 17 Jan 2026 05:33:27 +0000 Subject: [PATCH] feat: extend verbose tool feedback --- docs/cli/index.md | 2 +- docs/tools/agent-send.md | 2 +- docs/tools/slash-commands.md | 2 +- docs/tools/thinking.md | 5 +- docs/tui.md | 2 +- src/agents/pi-embedded-runner/run.ts | 1 + src/agents/pi-embedded-runner/run/attempt.ts | 1 + src/agents/pi-embedded-runner/run/params.ts | 1 + src/agents/pi-embedded-runner/run/payloads.ts | 4 +- src/agents/pi-embedded-runner/run/types.ts | 1 + .../pi-embedded-subscribe.handlers.tools.ts | 33 +++++++++++-- src/agents/pi-embedded-subscribe.handlers.ts | 2 +- .../pi-embedded-subscribe.handlers.types.ts | 2 + ...ompaction-retries-before-resolving.test.ts | 48 +++++++++++++++++++ src/agents/pi-embedded-subscribe.tools.ts | 18 +++++++ src/agents/pi-embedded-subscribe.ts | 28 ++++++++++- src/agents/pi-embedded-subscribe.types.ts | 5 +- src/agents/tool-display.json | 6 +++ ...rrent-verbose-level-verbose-has-no.test.ts | 2 +- .../reply/agent-runner-execution.ts | 2 + src/auto-reply/reply/agent-runner-helpers.ts | 27 +++++++++-- src/auto-reply/reply/agent-runner.ts | 12 ++++- .../reply/directive-handling.impl.ts | 8 ++-- src/auto-reply/reply/followup-runner.ts | 2 +- src/auto-reply/status.ts | 5 +- src/auto-reply/thinking.ts | 5 +- src/commands/agent.ts | 2 +- src/config/types.agent-defaults.ts | 2 +- src/config/zod-schema.agent-defaults.ts | 2 +- src/cron/isolated-agent/run.ts | 6 ++- src/infra/agent-events.ts | 4 +- 31 files changed, 208 insertions(+), 34 deletions(-) diff --git a/docs/cli/index.md b/docs/cli/index.md index 7a453a850..27eb5e797 100644 --- a/docs/cli/index.md +++ b/docs/cli/index.md @@ -459,7 +459,7 @@ Options: - `--to ` (for session key and optional delivery) - `--session-id ` - `--thinking ` (GPT-5.2 + Codex models only) -- `--verbose ` +- `--verbose ` - `--channel ` - `--local` - `--deliver` diff --git a/docs/tools/agent-send.md b/docs/tools/agent-send.md index 1c22c2be3..7f371a7b5 100644 --- a/docs/tools/agent-send.md +++ b/docs/tools/agent-send.md @@ -39,6 +39,6 @@ clawdbot agent --to +15555550123 --message "Summon reply" --deliver - `--deliver`: send the reply to the chosen channel (requires `--to`) - `--channel`: `whatsapp|telegram|discord|slack|signal|imessage` (default: `whatsapp`) - `--thinking `: persist thinking level (GPT-5.2 + Codex models only) -- `--verbose `: persist verbose level +- `--verbose `: persist verbose level - `--timeout `: override agent timeout - `--json`: output structured JSON diff --git a/docs/tools/slash-commands.md b/docs/tools/slash-commands.md index a55d36ac9..65279a198 100644 --- a/docs/tools/slash-commands.md +++ b/docs/tools/slash-commands.md @@ -74,7 +74,7 @@ Text + native (when enabled): - `/send on|off|inherit` (owner-only) - `/reset` or `/new` - `/think ` (dynamic choices by model/provider; aliases: `/thinking`, `/t`) -- `/verbose on|off` (alias: `/v`) +- `/verbose on|full|off` (alias: `/v`) - `/reasoning on|off|stream` (alias: `/reason`; when on, sends a separate message prefixed `Reasoning:`; `stream` = Telegram draft only) - `/elevated on|off` (alias: `/elev`) - `/model ` (alias: `/models`; or `/` from `agents.defaults.models.*.alias`) diff --git a/docs/tools/thinking.md b/docs/tools/thinking.md index 8b58046a7..40f358041 100644 --- a/docs/tools/thinking.md +++ b/docs/tools/thinking.md @@ -33,12 +33,13 @@ read_when: - **Embedded Pi**: the resolved level is passed to the in-process Pi agent runtime. ## Verbose directives (/verbose or /v) -- Levels: `on|full` or `off` (default). +- Levels: `on` (minimal) | `full` | `off` (default). - Directive-only message toggles session verbose and replies `Verbose logging enabled.` / `Verbose logging disabled.`; invalid levels return a hint without changing state. - `/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 result back as its own metadata-only message, prefixed with ` : ` when available (path/command); the tool output itself is not forwarded. These tool summaries are sent as soon as each tool finishes (separate bubbles), not as streaming deltas. If you toggle `/verbose on|off` while a run is in-flight, subsequent tool bubbles honor the new setting. +- 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 `full`, exec/process 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. ## Reasoning visibility (/reasoning) - Levels: `on|off|stream`. diff --git a/docs/tui.md b/docs/tui.md index 6c059d0cb..f10f20164 100644 --- a/docs/tui.md +++ b/docs/tui.md @@ -75,7 +75,7 @@ Core: Session controls: - `/think ` -- `/verbose ` +- `/verbose ` - `/reasoning ` - `/cost ` - `/elevated ` (alias: `/elev`) diff --git a/src/agents/pi-embedded-runner/run.ts b/src/agents/pi-embedded-runner/run.ts index 757c429fa..8f3001e23 100644 --- a/src/agents/pi-embedded-runner/run.ts +++ b/src/agents/pi-embedded-runner/run.ts @@ -213,6 +213,7 @@ export async function runEmbeddedPiAgent( runId: params.runId, abortSignal: params.abortSignal, shouldEmitToolResult: params.shouldEmitToolResult, + shouldEmitToolOutput: params.shouldEmitToolOutput, onPartialReply: params.onPartialReply, onAssistantMessageStart: params.onAssistantMessageStart, onBlockReply: params.onBlockReply, diff --git a/src/agents/pi-embedded-runner/run/attempt.ts b/src/agents/pi-embedded-runner/run/attempt.ts index 7677c8fd5..967a7f5f9 100644 --- a/src/agents/pi-embedded-runner/run/attempt.ts +++ b/src/agents/pi-embedded-runner/run/attempt.ts @@ -366,6 +366,7 @@ export async function runEmbeddedAttempt( verboseLevel: params.verboseLevel, reasoningMode: params.reasoningLevel ?? "off", shouldEmitToolResult: params.shouldEmitToolResult, + shouldEmitToolOutput: params.shouldEmitToolOutput, onToolResult: params.onToolResult, onReasoningStream: params.onReasoningStream, onBlockReply: params.onBlockReply, diff --git a/src/agents/pi-embedded-runner/run/params.ts b/src/agents/pi-embedded-runner/run/params.ts index 7b6349ad5..50504c95d 100644 --- a/src/agents/pi-embedded-runner/run/params.ts +++ b/src/agents/pi-embedded-runner/run/params.ts @@ -38,6 +38,7 @@ export type RunEmbeddedPiAgentParams = { runId: string; abortSignal?: AbortSignal; shouldEmitToolResult?: () => boolean; + shouldEmitToolOutput?: () => boolean; onPartialReply?: (payload: { text?: string; mediaUrls?: string[] }) => void | Promise; onAssistantMessageStart?: () => void | Promise; onBlockReply?: (payload: { diff --git a/src/agents/pi-embedded-runner/run/payloads.ts b/src/agents/pi-embedded-runner/run/payloads.ts index 082958df1..9743830f1 100644 --- a/src/agents/pi-embedded-runner/run/payloads.ts +++ b/src/agents/pi-embedded-runner/run/payloads.ts @@ -68,7 +68,9 @@ export function buildEmbeddedRunPayloads(params: { if (errorText) replyItems.push({ text: errorText, isError: true }); const inlineToolResults = - params.inlineToolResultsAllowed && params.verboseLevel === "on" && params.toolMetas.length > 0; + params.inlineToolResultsAllowed && + params.verboseLevel !== "off" && + params.toolMetas.length > 0; if (inlineToolResults) { for (const { toolName, meta } of params.toolMetas) { const agg = formatToolAggregate(toolName, meta ? [meta] : []); diff --git a/src/agents/pi-embedded-runner/run/types.ts b/src/agents/pi-embedded-runner/run/types.ts index 92b2ff9e2..190908e77 100644 --- a/src/agents/pi-embedded-runner/run/types.ts +++ b/src/agents/pi-embedded-runner/run/types.ts @@ -43,6 +43,7 @@ export type EmbeddedRunAttemptParams = { runId: string; abortSignal?: AbortSignal; shouldEmitToolResult?: () => boolean; + shouldEmitToolOutput?: () => boolean; onPartialReply?: (payload: { text?: string; mediaUrls?: string[] }) => void | Promise; onAssistantMessageStart?: () => void | Promise; onBlockReply?: (payload: { diff --git a/src/agents/pi-embedded-subscribe.handlers.tools.ts b/src/agents/pi-embedded-subscribe.handlers.tools.ts index 6abb4c05e..206ce4962 100644 --- a/src/agents/pi-embedded-subscribe.handlers.tools.ts +++ b/src/agents/pi-embedded-subscribe.handlers.tools.ts @@ -5,12 +5,28 @@ import { normalizeTextForComparison } from "./pi-embedded-helpers.js"; import { isMessagingTool, isMessagingToolSendAction } from "./pi-embedded-messaging.js"; import type { EmbeddedPiSubscribeContext } from "./pi-embedded-subscribe.handlers.types.js"; import { + extractToolResultText, extractMessagingToolSend, isToolResultError, sanitizeToolResult, } from "./pi-embedded-subscribe.tools.js"; import { inferToolMetaFromArgs } from "./pi-embedded-utils.js"; +const TOOL_OUTPUT_ALLOWLIST = new Set(["exec", "bash", "process"]); + +function extendExecMeta(toolName: string, args: unknown, meta?: string): string | undefined { + const normalized = toolName.trim().toLowerCase(); + if (normalized !== "exec" && normalized !== "bash") return meta; + if (!args || typeof args !== "object") return meta; + const record = args as Record; + const flags: string[] = []; + if (record.pty === true) flags.push("pty"); + if (record.elevated === true) flags.push("elevated"); + if (flags.length === 0) return meta; + const suffix = flags.join(" · "); + return meta ? `${meta} · ${suffix}` : suffix; +} + export async function handleToolExecutionStart( ctx: EmbeddedPiSubscribeContext, evt: AgentEvent & { toolName: string; toolCallId: string; args: unknown }, @@ -36,7 +52,7 @@ export async function handleToolExecutionStart( } } - const meta = inferToolMetaFromArgs(toolName, args); + const meta = extendExecMeta(toolName, args, inferToolMetaFromArgs(toolName, args)); ctx.state.toolMetaById.set(toolCallId, meta); ctx.log.debug( `embedded run tool start: runId=${ctx.params.runId} tool=${toolName} toolCallId=${toolCallId}`, @@ -53,8 +69,8 @@ export async function handleToolExecutionStart( args: args as Record, }, }); - // Await onAgentEvent to ensure typing indicator starts before tool summaries are emitted. - await ctx.params.onAgentEvent?.({ + // Best-effort typing signal; do not block tool summaries on slow emitters. + void ctx.params.onAgentEvent?.({ stream: "tool", data: { phase: "start", name: toolName, toolCallId }, }); @@ -185,4 +201,15 @@ export function handleToolExecutionEnd( ctx.log.debug( `embedded run tool end: runId=${ctx.params.runId} tool=${toolName} toolCallId=${toolCallId}`, ); + + if ( + ctx.params.onToolResult && + ctx.shouldEmitToolOutput() && + TOOL_OUTPUT_ALLOWLIST.has(toolName.trim().toLowerCase()) + ) { + const outputText = extractToolResultText(sanitizedResult); + if (outputText) { + ctx.emitToolOutput(toolName, meta, outputText); + } + } } diff --git a/src/agents/pi-embedded-subscribe.handlers.ts b/src/agents/pi-embedded-subscribe.handlers.ts index 92ed49111..9856b57e0 100644 --- a/src/agents/pi-embedded-subscribe.handlers.ts +++ b/src/agents/pi-embedded-subscribe.handlers.ts @@ -32,7 +32,7 @@ export function createEmbeddedPiSessionEventHandler(ctx: EmbeddedPiSubscribeCont handleMessageEnd(ctx, evt as never); return; case "tool_execution_start": - // Async handler - awaits typing indicator before emitting tool summaries. + // Async handler - best-effort typing indicator, avoids blocking tool summaries. // Catch rejections to avoid unhandled promise rejection crashes. handleToolExecutionStart(ctx, evt as never).catch((err) => { ctx.log.debug(`tool_execution_start handler failed: ${String(err)}`); diff --git a/src/agents/pi-embedded-subscribe.handlers.types.ts b/src/agents/pi-embedded-subscribe.handlers.types.ts index 1bc9c4af0..1800be109 100644 --- a/src/agents/pi-embedded-subscribe.handlers.types.ts +++ b/src/agents/pi-embedded-subscribe.handlers.types.ts @@ -56,7 +56,9 @@ export type EmbeddedPiSubscribeContext = { blockChunker: EmbeddedBlockChunker | null; shouldEmitToolResult: () => boolean; + shouldEmitToolOutput: () => boolean; emitToolSummary: (toolName?: string, meta?: string) => void; + emitToolOutput: (toolName?: string, meta?: string, output?: string) => void; stripBlockTags: ( text: string, state: { thinking: boolean; final: boolean; inlineCode?: InlineCodeState }, diff --git a/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.waits-multiple-compaction-retries-before-resolving.test.ts b/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.waits-multiple-compaction-retries-before-resolving.test.ts index 571781eee..7407a3581 100644 --- a/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.waits-multiple-compaction-retries-before-resolving.test.ts +++ b/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.waits-multiple-compaction-retries-before-resolving.test.ts @@ -131,4 +131,52 @@ describe("subscribeEmbeddedPiSession", () => { expect(payload.text).toContain("snapshot"); expect(payload.text).toContain("https://example.com"); }); + + it("emits exec output in full verbose mode and includes PTY indicator", async () => { + let handler: ((evt: unknown) => void) | undefined; + const session: StubSession = { + subscribe: (fn) => { + handler = fn; + return () => {}; + }, + }; + + const onToolResult = vi.fn(); + + subscribeEmbeddedPiSession({ + session: session as unknown as Parameters[0]["session"], + runId: "run-exec-full", + verboseLevel: "full", + onToolResult, + }); + + handler?.({ + type: "tool_execution_start", + toolName: "exec", + toolCallId: "tool-exec-1", + args: { command: "claude", pty: true }, + }); + + await Promise.resolve(); + + expect(onToolResult).toHaveBeenCalledTimes(1); + const summary = onToolResult.mock.calls[0][0]; + expect(summary.text).toContain("exec"); + expect(summary.text).toContain("pty"); + + handler?.({ + type: "tool_execution_end", + toolName: "exec", + toolCallId: "tool-exec-1", + isError: false, + result: { content: [{ type: "text", text: "hello\nworld" }] }, + }); + + await Promise.resolve(); + + expect(onToolResult).toHaveBeenCalledTimes(2); + const output = onToolResult.mock.calls[1][0]; + expect(output.text).toContain("hello"); + expect(output.text).toContain("```txt"); + }); }); diff --git a/src/agents/pi-embedded-subscribe.tools.ts b/src/agents/pi-embedded-subscribe.tools.ts index 285bd83d1..5299e1dd4 100644 --- a/src/agents/pi-embedded-subscribe.tools.ts +++ b/src/agents/pi-embedded-subscribe.tools.ts @@ -33,6 +33,24 @@ export function sanitizeToolResult(result: unknown): unknown { return { ...record, content: sanitized }; } +export function extractToolResultText(result: unknown): string | undefined { + if (!result || typeof result !== "object") return undefined; + const record = result as Record; + const content = Array.isArray(record.content) ? record.content : null; + if (!content) return undefined; + const texts = content + .map((item) => { + if (!item || typeof item !== "object") return undefined; + const entry = item as Record; + if (entry.type !== "text" || typeof entry.text !== "string") return undefined; + const trimmed = entry.text.trim(); + return trimmed ? trimmed : undefined; + }) + .filter((value): value is string => Boolean(value)); + if (texts.length === 0) return undefined; + return texts.join("\n"); +} + export function isToolResultError(result: unknown): boolean { if (!result || typeof result !== "object") return false; const record = result as { details?: unknown }; diff --git a/src/agents/pi-embedded-subscribe.ts b/src/agents/pi-embedded-subscribe.ts index 0de93e941..aefd84f35 100644 --- a/src/agents/pi-embedded-subscribe.ts +++ b/src/agents/pi-embedded-subscribe.ts @@ -172,7 +172,16 @@ export function subscribeEmbeddedPiSession(params: SubscribeEmbeddedPiSessionPar const shouldEmitToolResult = () => typeof params.shouldEmitToolResult === "function" ? params.shouldEmitToolResult() - : params.verboseLevel === "on"; + : params.verboseLevel === "on" || params.verboseLevel === "full"; + const shouldEmitToolOutput = () => + typeof params.shouldEmitToolOutput === "function" + ? params.shouldEmitToolOutput() + : params.verboseLevel === "full"; + const formatToolOutputBlock = (text: string) => { + const trimmed = text.trim(); + if (!trimmed) return "(no output)"; + return `\`\`\`txt\n${trimmed}\n\`\`\``; + }; const emitToolSummary = (toolName?: string, meta?: string) => { if (!params.onToolResult) return; const agg = formatToolAggregate(toolName, meta ? [meta] : undefined); @@ -187,6 +196,21 @@ export function subscribeEmbeddedPiSession(params: SubscribeEmbeddedPiSessionPar // ignore tool result delivery failures } }; + const emitToolOutput = (toolName?: string, meta?: string, output?: string) => { + if (!params.onToolResult || !output) return; + const agg = formatToolAggregate(toolName, meta ? [meta] : undefined); + const message = `${agg}\n${formatToolOutputBlock(output)}`; + const { text: cleanedText, mediaUrls } = parseReplyDirectives(message); + if (!cleanedText && (!mediaUrls || mediaUrls.length === 0)) return; + try { + void params.onToolResult({ + text: cleanedText, + mediaUrls: mediaUrls?.length ? mediaUrls : undefined, + }); + } catch { + // ignore tool result delivery failures + } + }; const stripBlockTags = ( text: string, @@ -363,7 +387,9 @@ export function subscribeEmbeddedPiSession(params: SubscribeEmbeddedPiSessionPar blockChunking, blockChunker, shouldEmitToolResult, + shouldEmitToolOutput, emitToolSummary, + emitToolOutput, stripBlockTags, emitBlockChunk, flushBlockReplyBuffer, diff --git a/src/agents/pi-embedded-subscribe.types.ts b/src/agents/pi-embedded-subscribe.types.ts index dbdaa8582..dec837863 100644 --- a/src/agents/pi-embedded-subscribe.types.ts +++ b/src/agents/pi-embedded-subscribe.types.ts @@ -1,14 +1,15 @@ import type { AgentSession } from "@mariozechner/pi-coding-agent"; -import type { ReasoningLevel } from "../auto-reply/thinking.js"; +import type { ReasoningLevel, VerboseLevel } from "../auto-reply/thinking.js"; import type { BlockReplyChunking } from "./pi-embedded-block-chunker.js"; export type SubscribeEmbeddedPiSessionParams = { session: AgentSession; runId: string; - verboseLevel?: "off" | "on"; + verboseLevel?: VerboseLevel; reasoningMode?: ReasoningLevel; shouldEmitToolResult?: () => boolean; + shouldEmitToolOutput?: () => boolean; onToolResult?: (payload: { text?: string; mediaUrls?: string[] }) => void | Promise; onReasoningStream?: (payload: { text?: string; mediaUrls?: string[] }) => void | Promise; onBlockReply?: (payload: { diff --git a/src/agents/tool-display.json b/src/agents/tool-display.json index da714a6dd..79358640e 100644 --- a/src/agents/tool-display.json +++ b/src/agents/tool-display.json @@ -30,6 +30,12 @@ "title": "Exec", "detailKeys": ["command"] }, + "bash": { + "emoji": "🛠️", + "title": "Exec", + "label": "exec", + "detailKeys": ["command"] + }, "process": { "emoji": "🧰", "title": "Process", diff --git a/src/auto-reply/reply.directive.directive-behavior.shows-current-verbose-level-verbose-has-no.test.ts b/src/auto-reply/reply.directive.directive-behavior.shows-current-verbose-level-verbose-has-no.test.ts index 189238dc6..b623837a1 100644 --- a/src/auto-reply/reply.directive.directive-behavior.shows-current-verbose-level-verbose-has-no.test.ts +++ b/src/auto-reply/reply.directive.directive-behavior.shows-current-verbose-level-verbose-has-no.test.ts @@ -81,7 +81,7 @@ describe("directive behavior", () => { const text = Array.isArray(res) ? res[0]?.text : res?.text; expect(text).toContain("Current verbose level: on"); - expect(text).toContain("Options: on, off."); + expect(text).toContain("Options: on, full, off."); expect(runEmbeddedPiAgent).not.toHaveBeenCalled(); }); }); diff --git a/src/auto-reply/reply/agent-runner-execution.ts b/src/auto-reply/reply/agent-runner-execution.ts index ba6665860..7a528faaa 100644 --- a/src/auto-reply/reply/agent-runner-execution.ts +++ b/src/auto-reply/reply/agent-runner-execution.ts @@ -61,6 +61,7 @@ export async function runAgentTurnWithFallback(params: { resolvedBlockStreamingBreak: "text_end" | "message_end"; applyReplyToMode: (payload: ReplyPayload) => ReplyPayload; shouldEmitToolResult: () => boolean; + shouldEmitToolOutput: () => boolean; pendingToolTasks: Set>; resetSessionAfterCompactionFailure: (reason: string) => Promise; resetSessionAfterRoleOrderingConflict: (reason: string) => Promise; @@ -335,6 +336,7 @@ export async function runAgentTurnWithFallback(params: { } : undefined, shouldEmitToolResult: params.shouldEmitToolResult, + shouldEmitToolOutput: params.shouldEmitToolOutput, onToolResult: onToolResult ? (payload) => { // `subscribeEmbeddedPiSession` may invoke tool callbacks without awaiting them. diff --git a/src/auto-reply/reply/agent-runner-helpers.ts b/src/auto-reply/reply/agent-runner-helpers.ts index f783442da..47bd79e80 100644 --- a/src/auto-reply/reply/agent-runner-helpers.ts +++ b/src/auto-reply/reply/agent-runner-helpers.ts @@ -18,17 +18,38 @@ export const createShouldEmitToolResult = (params: { }): (() => boolean) => { return () => { if (!params.sessionKey || !params.storePath) { - return params.resolvedVerboseLevel === "on"; + return params.resolvedVerboseLevel !== "off"; } try { const store = loadSessionStore(params.storePath); const entry = store[params.sessionKey]; const current = normalizeVerboseLevel(entry?.verboseLevel); - if (current) return current === "on"; + if (current) return current !== "off"; } catch { // ignore store read failures } - return params.resolvedVerboseLevel === "on"; + return params.resolvedVerboseLevel !== "off"; + }; +}; + +export const createShouldEmitToolOutput = (params: { + sessionKey?: string; + storePath?: string; + resolvedVerboseLevel: VerboseLevel; +}): (() => boolean) => { + return () => { + if (!params.sessionKey || !params.storePath) { + return params.resolvedVerboseLevel === "full"; + } + try { + const store = loadSessionStore(params.storePath); + const entry = store[params.sessionKey]; + const current = normalizeVerboseLevel(entry?.verboseLevel); + if (current) return current === "full"; + } catch { + // ignore store read failures + } + return params.resolvedVerboseLevel === "full"; }; }; diff --git a/src/auto-reply/reply/agent-runner.ts b/src/auto-reply/reply/agent-runner.ts index 84f17a1d9..5630586a9 100644 --- a/src/auto-reply/reply/agent-runner.ts +++ b/src/auto-reply/reply/agent-runner.ts @@ -24,6 +24,7 @@ import type { VerboseLevel } from "../thinking.js"; import type { GetReplyOptions, ReplyPayload } from "../types.js"; import { runAgentTurnWithFallback } from "./agent-runner-execution.js"; import { + createShouldEmitToolOutput, createShouldEmitToolResult, finalizeWithFollowup, isAudioPayload, @@ -116,6 +117,11 @@ export async function runReplyAgent(params: { storePath, resolvedVerboseLevel, }); + const shouldEmitToolOutput = createShouldEmitToolOutput({ + sessionKey, + storePath, + resolvedVerboseLevel, + }); const pendingToolTasks = new Set>(); const blockReplyTimeoutMs = opts?.blockReplyTimeoutMs ?? BLOCK_REPLY_SEND_TIMEOUT_MS; @@ -296,6 +302,7 @@ export async function runReplyAgent(params: { resolvedBlockStreamingBreak, applyReplyToMode, shouldEmitToolResult, + shouldEmitToolOutput, pendingToolTasks, resetSessionAfterCompactionFailure, resetSessionAfterRoleOrderingConflict, @@ -473,6 +480,7 @@ export async function runReplyAgent(params: { // If verbose is enabled and this is a new session, prepend a session hint. let finalPayloads = replyPayloads; + const verboseEnabled = resolvedVerboseLevel !== "off"; if (autoCompactionCompleted) { const count = await incrementCompactionCount({ sessionEntry: activeSessionEntry, @@ -480,12 +488,12 @@ export async function runReplyAgent(params: { sessionKey, storePath, }); - if (resolvedVerboseLevel === "on") { + if (verboseEnabled) { const suffix = typeof count === "number" ? ` (count ${count})` : ""; finalPayloads = [{ text: `🧹 Auto-compaction complete${suffix}.` }, ...finalPayloads]; } } - if (resolvedVerboseLevel === "on" && activeIsNewSession) { + if (verboseEnabled && activeIsNewSession) { finalPayloads = [{ text: `🧭 New session: ${followupRun.run.sessionId}` }, ...finalPayloads]; } if (responseUsageLine) { diff --git a/src/auto-reply/reply/directive-handling.impl.ts b/src/auto-reply/reply/directive-handling.impl.ts index 18eac8dec..872ab2617 100644 --- a/src/auto-reply/reply/directive-handling.impl.ts +++ b/src/auto-reply/reply/directive-handling.impl.ts @@ -137,11 +137,11 @@ export async function handleDirectiveOnly(params: { if (!directives.rawVerboseLevel) { const level = currentVerboseLevel ?? "off"; return { - text: withOptions(`Current verbose level: ${level}.`, "on, off"), + text: withOptions(`Current verbose level: ${level}.`, "on, full, off"), }; } return { - text: `Unrecognized verbose level "${directives.rawVerboseLevel}". Valid levels: off, on.`, + text: `Unrecognized verbose level "${directives.rawVerboseLevel}". Valid levels: off, on, full.`, }; } if (directives.hasReasoningDirective && !directives.reasoningLevel) { @@ -333,7 +333,9 @@ export async function handleDirectiveOnly(params: { parts.push( directives.verboseLevel === "off" ? formatDirectiveAck("Verbose logging disabled.") - : formatDirectiveAck("Verbose logging enabled."), + : directives.verboseLevel === "full" + ? formatDirectiveAck("Verbose logging set to full.") + : formatDirectiveAck("Verbose logging enabled."), ); } if (directives.hasReasoningDirective && directives.reasoningLevel) { diff --git a/src/auto-reply/reply/followup-runner.ts b/src/auto-reply/reply/followup-runner.ts index a8df22e5d..64e8ac91f 100644 --- a/src/auto-reply/reply/followup-runner.ts +++ b/src/auto-reply/reply/followup-runner.ts @@ -227,7 +227,7 @@ export function createFollowupRunner(params: { sessionKey, storePath, }); - if (queued.run.verboseLevel === "on") { + if (queued.run.verboseLevel && queued.run.verboseLevel !== "off") { const suffix = typeof count === "number" ? ` (count ${count})` : ""; finalPayloads.unshift({ text: `🧹 Auto-compaction complete${suffix}.`, diff --git a/src/auto-reply/status.ts b/src/auto-reply/status.ts index d54eff98f..b77280c30 100644 --- a/src/auto-reply/status.ts +++ b/src/auto-reply/status.ts @@ -271,7 +271,8 @@ export function buildStatusMessage(args: StatusArgs): string { const queueMode = args.queue?.mode ?? "unknown"; const queueDetails = formatQueueDetails(args.queue); - const verboseLabel = verboseLevel === "on" ? "verbose" : null; + const verboseLabel = + verboseLevel === "full" ? "verbose:full" : verboseLevel === "on" ? "verbose" : null; const elevatedLabel = elevatedLevel === "on" ? "elevated" : null; const optionParts = [ `Runtime: ${runtime.label}`, @@ -338,7 +339,7 @@ export function buildStatusMessage(args: StatusArgs): string { export function buildHelpMessage(cfg?: ClawdbotConfig): string { const options = [ "/think ", - "/verbose on|off", + "/verbose on|full|off", "/reasoning on|off", "/elevated on|off", "/model ", diff --git a/src/auto-reply/thinking.ts b/src/auto-reply/thinking.ts index 447f98c20..9abb8c0ca 100644 --- a/src/auto-reply/thinking.ts +++ b/src/auto-reply/thinking.ts @@ -1,5 +1,5 @@ export type ThinkLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh"; -export type VerboseLevel = "off" | "on"; +export type VerboseLevel = "off" | "on" | "full"; export type ElevatedLevel = "off" | "on"; export type ReasoningLevel = "off" | "on" | "stream"; export type UsageDisplayLevel = "off" | "on"; @@ -87,7 +87,8 @@ export function normalizeVerboseLevel(raw?: string | null): VerboseLevel | undef if (!raw) return undefined; const key = raw.toLowerCase(); if (["off", "false", "no", "0"].includes(key)) return "off"; - if (["on", "full", "true", "yes", "1"].includes(key)) return "on"; + if (["full", "all", "everything"].includes(key)) return "full"; + if (["on", "minimal", "true", "yes", "1"].includes(key)) return "on"; return undefined; } diff --git a/src/commands/agent.ts b/src/commands/agent.ts index 231bbdfe4..514234c37 100644 --- a/src/commands/agent.ts +++ b/src/commands/agent.ts @@ -93,7 +93,7 @@ export async function agentCommand( const verboseOverride = normalizeVerboseLevel(opts.verbose); if (opts.verbose && !verboseOverride) { - throw new Error('Invalid verbose level. Use "on" or "off".'); + throw new Error('Invalid verbose level. Use "on", "full", or "off".'); } const timeoutSecondsRaw = diff --git a/src/config/types.agent-defaults.ts b/src/config/types.agent-defaults.ts index 8a70ff464..aa4d48b3b 100644 --- a/src/config/types.agent-defaults.ts +++ b/src/config/types.agent-defaults.ts @@ -118,7 +118,7 @@ export type AgentDefaultsConfig = { /** Default thinking level when no /think directive is present. */ thinkingDefault?: "off" | "minimal" | "low" | "medium" | "high" | "xhigh"; /** Default verbose level when no /verbose directive is present. */ - verboseDefault?: "off" | "on"; + verboseDefault?: "off" | "on" | "full"; /** Default elevated level when no /elevated directive is present. */ elevatedDefault?: "off" | "on"; /** Default block streaming level when no override is present. */ diff --git a/src/config/zod-schema.agent-defaults.ts b/src/config/zod-schema.agent-defaults.ts index d868de080..d32a2cb45 100644 --- a/src/config/zod-schema.agent-defaults.ts +++ b/src/config/zod-schema.agent-defaults.ts @@ -99,7 +99,7 @@ export const AgentDefaultsSchema = z z.literal("xhigh"), ]) .optional(), - verboseDefault: z.union([z.literal("off"), z.literal("on")]).optional(), + verboseDefault: z.union([z.literal("off"), z.literal("on"), z.literal("full")]).optional(), elevatedDefault: z.union([z.literal("off"), z.literal("on")]).optional(), blockStreamingDefault: z.union([z.literal("off"), z.literal("on")]).optional(), blockStreamingBreak: z.union([z.literal("text_end"), z.literal("message_end")]).optional(), diff --git a/src/cron/isolated-agent/run.ts b/src/cron/isolated-agent/run.ts index f3ca6be11..44e2ac885 100644 --- a/src/cron/isolated-agent/run.ts +++ b/src/cron/isolated-agent/run.ts @@ -27,6 +27,7 @@ import { ensureAgentWorkspace } from "../../agents/workspace.js"; import { formatXHighModelHint, normalizeThinkLevel, + normalizeVerboseLevel, supportsXHighThinking, } from "../../auto-reply/thinking.js"; import { createOutboundSendDeps, type CliDeps } from "../../cli/outbound-send-deps.js"; @@ -245,8 +246,9 @@ export async function runCronIsolatedAgentTurn(params: { try { const sessionFile = resolveSessionTranscriptPath(cronSession.sessionEntry.sessionId, agentId); const resolvedVerboseLevel = - (cronSession.sessionEntry.verboseLevel as "on" | "off" | undefined) ?? - (agentCfg?.verboseDefault as "on" | "off" | undefined); + normalizeVerboseLevel(cronSession.sessionEntry.verboseLevel) ?? + normalizeVerboseLevel(agentCfg?.verboseDefault) ?? + "off"; registerAgentRunContext(cronSession.sessionEntry.sessionId, { sessionKey: agentSessionKey, verboseLevel: resolvedVerboseLevel, diff --git a/src/infra/agent-events.ts b/src/infra/agent-events.ts index abf51946e..08f44a09e 100644 --- a/src/infra/agent-events.ts +++ b/src/infra/agent-events.ts @@ -1,3 +1,5 @@ +import type { VerboseLevel } from "../auto-reply/thinking.js"; + export type AgentEventStream = "lifecycle" | "tool" | "assistant" | "error" | (string & {}); export type AgentEventPayload = { @@ -11,7 +13,7 @@ export type AgentEventPayload = { export type AgentRunContext = { sessionKey?: string; - verboseLevel?: "off" | "on"; + verboseLevel?: VerboseLevel; }; // Keep per-run counters so streams stay strictly monotonic per runId.