From 527bed2b537fb8b155444b0cb498cfc1228d2064 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 3 Dec 2025 09:57:41 +0000 Subject: [PATCH] Verbose: include tool arg metadata in prefixes --- CHANGELOG.md | 2 +- README.md | 2 +- docs/thinking.md | 2 +- src/agents/agents.test.ts | 9 ++++++-- src/agents/pi.ts | 25 +++++++++++++++++++++- src/agents/types.ts | 1 + src/auto-reply/command-reply.ts | 38 ++++++++++++++++++++++++++++----- src/index.core.test.ts | 4 ++-- 8 files changed, 70 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dad0d1935..15c5b91c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ ### Highlights - **Thinking directives & state:** `/t|/think|/thinking ` (aliases off|minimal|low|medium|high|max/highest). Inline applies to that message; directive-only message pins the level for the session; `/think:off` clears. Resolution: inline > session override > `inbound.reply.thinkingDefault` > off. Pi/Tau get `--thinking ` (except off); other agents append cue words (`think` โ†’ `think hard` โ†’ `think harder` โ†’ `ultrathink`). Heartbeat probe uses `HEARTBEAT /think:high`. -- **Verbose directives + session hints:** `/v|/verbose on|full|off` mirrors thinking: inline > session > config default. Directive-only replies with an acknowledgement; invalid levels return a hint. When enabled, tool results from JSON-emitting agents (Pi/Tau, etc.) are forwarded as metadata-only `[๐Ÿ› ๏ธ ]` messages (now streamed as they happen), and new sessions surface a `๐Ÿงญ New session: ` hint. +- **Verbose directives + session hints:** `/v|/verbose on|full|off` mirrors thinking: inline > session > config default. Directive-only replies with an acknowledgement; invalid levels return a hint. When enabled, tool results from JSON-emitting agents (Pi/Tau, etc.) are forwarded as metadata-only `[๐Ÿ› ๏ธ ]` messages (now streamed as they happen), and new sessions surface a `๐Ÿงญ New session: ` hint. - **Directive confirmations:** Directive-only messages now reply with an acknowledgement (`Thinking level set to high.` / `Thinking disabled.`) and reject unknown levels with a helpful hint (state is unchanged). - **Pi/Tau stability:** RPC replies buffered until the assistant turn finishes; parsers return consistent `texts[]`; web auto-replies keep a warm Tau RPC process to avoid cold starts. - **Claude prompt flow:** One-time `sessionIntro` with per-message `/think:high` bodyPrefix; system prompt always sent on first turn even with `sendSystemOnce`. diff --git a/README.md b/README.md index b5f3d86dc..baea54ef6 100644 --- a/README.md +++ b/README.md @@ -166,7 +166,7 @@ warelay supports running on the same phone number you message fromโ€”you chat wi - Levels: `on|full` (same) or `off` (default). Use `/v on`, `/verbose:full`, `/v off`, etc.; colon optional. - Directive-only message sets a session-level verbose flag (`Verbose logging enabled./disabled.`); invalid levels reply with a hint and donโ€™t change state. - Inline directive applies only to that message; resolution: inline > session default > `inbound.reply.verboseDefault` (config) > off. -- When verbose is on **and the agent emits structured tool results (Pi/Tau and other JSON-emitting agents)**, only tool metadata is forwarded: each tool result becomes `[๐Ÿ› ๏ธ ]` (output/body is not inlined). +- When verbose is on **and the agent emits structured tool results (Pi/Tau and other JSON-emitting agents)**, only tool metadata is forwarded: each tool result becomes `[๐Ÿ› ๏ธ ]` when available (e.g., read path or bash command); output/body is not inlined. - Starting a new session while verbose is on adds a first reply like `๐Ÿงญ New session: ` so you can correlate runs. ### Logging (optional) diff --git a/docs/thinking.md b/docs/thinking.md index fbe0d06aa..cc0d0fbec 100644 --- a/docs/thinking.md +++ b/docs/thinking.md @@ -28,7 +28,7 @@ - Levels: `on|full` or `off` (default). - Directive-only message toggles session verbose and replies `Verbose logging enabled.` / `Verbose logging disabled.`; invalid levels return a hint without changing state. - Inline directive affects only that message; session/global defaults apply otherwise. -- When verbose is on, agents that emit structured tool results (Pi/Tau, other JSON agents) send each tool result back as its own metadata-only message, prefixed with `[๐Ÿ› ๏ธ ]` (the tool output itself is not forwarded). +- When verbose is on, agents that emit structured tool results (Pi/Tau, 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. ## Heartbeats - Heartbeat probe body is `HEARTBEAT /think:high`, so it always asks for max thinking on the probe. Inline directive wins; session/global defaults are used only when no directive is present. diff --git a/src/agents/agents.test.ts b/src/agents/agents.test.ts index 9a6be1e74..1d55da210 100644 --- a/src/agents/agents.test.ts +++ b/src/agents/agents.test.ts @@ -69,11 +69,16 @@ describe("agent buildArgs + parseOutput helpers", () => { it("piSpec carries tool names when present", () => { const stdout = - '{"type":"message_end","message":{"role":"tool_result","name":"bash","content":[{"type":"text","text":"ls output"}]}}'; + '{"type":"message_end","message":{"role":"tool_result","name":"bash","details":{"command":"ls -la"},"content":[{"type":"text","text":"ls output"}]}}'; const parsed = piSpec.parseOutput(stdout); - const tool = parsed.toolResults?.[0] as { text?: string; toolName?: string }; + const tool = parsed.toolResults?.[0] as { + text?: string; + toolName?: string; + meta?: string; + }; expect(tool?.text).toBe("ls output"); expect(tool?.toolName).toBe("bash"); + expect(tool?.meta).toBe("ls -la"); }); it("codexSpec parses agent_message and aggregates usage", () => { diff --git a/src/agents/pi.ts b/src/agents/pi.ts index 2687b1c2d..4b35965da 100644 --- a/src/agents/pi.ts +++ b/src/agents/pi.ts @@ -18,6 +18,8 @@ type PiAssistantMessage = { toolName?: string; tool_call_id?: string; toolCallId?: string; + details?: Record; + arguments?: Record; }; function inferToolName(msg: PiAssistantMessage): string | undefined { @@ -39,6 +41,23 @@ function inferToolName(msg: PiAssistantMessage): string | undefined { return undefined; } +function deriveToolMeta(msg: PiAssistantMessage): string | undefined { + const details = msg.details ?? msg.arguments; + const pathVal = details && typeof details.path === "string" ? details.path : undefined; + const offset = details && typeof details.offset === "number" ? details.offset : undefined; + const limit = details && typeof details.limit === "number" ? details.limit : undefined; + const command = details && typeof details.command === "string" ? details.command : undefined; + + if (pathVal) { + if (offset !== undefined && limit !== undefined) { + return `${pathVal}:${offset}-${offset + limit}`; + } + return pathVal; + } + if (command) return command; + return undefined; +} + function parsePiJson(raw: string): AgentParseResult { const lines = raw.split(/\n+/).filter((l) => l.trim().startsWith("{")); @@ -87,7 +106,11 @@ function parsePiJson(raw: string): AgentParseResult { .join("\n") .trim(); if (toolText) { - toolResults.push({ text: toolText, toolName: inferToolName(msg) }); + toolResults.push({ + text: toolText, + toolName: inferToolName(msg), + meta: deriveToolMeta(msg), + }); } } } catch { diff --git a/src/agents/types.ts b/src/agents/types.ts index f826188d6..ccfb9e22e 100644 --- a/src/agents/types.ts +++ b/src/agents/types.ts @@ -18,6 +18,7 @@ export type AgentMeta = { export type AgentToolResult = { text: string; toolName?: string; + meta?: string; }; export type AgentParseResult = { diff --git a/src/auto-reply/command-reply.ts b/src/auto-reply/command-reply.ts index b40315599..919d73fc6 100644 --- a/src/auto-reply/command-reply.ts +++ b/src/auto-reply/command-reply.ts @@ -59,6 +59,8 @@ type ToolMessageLike = { tool_call_id?: string; toolCallId?: string; role?: string; + details?: Record; + arguments?: Record; }; function inferToolName(message?: ToolMessageLike): string | undefined { @@ -80,6 +82,24 @@ function inferToolName(message?: ToolMessageLike): string | undefined { return undefined; } +function inferToolMeta(message?: ToolMessageLike): string | undefined { + if (!message) return undefined; + const details = message.details ?? message.arguments; + const pathVal = details && typeof details.path === "string" ? details.path : undefined; + const offset = details && typeof details.offset === "number" ? details.offset : undefined; + const limit = details && typeof details.limit === "number" ? details.limit : undefined; + const command = details && typeof details.command === "string" ? details.command : undefined; + + if (pathVal) { + if (offset !== undefined && limit !== undefined) { + return `${pathVal}:${offset}-${offset + limit}`; + } + return pathVal; + } + if (command) return command; + return undefined; +} + function normalizeToolResults( toolResults?: Array, ): AgentToolResult[] { @@ -89,13 +109,15 @@ function normalizeToolResults( .map((tr) => ({ text: (tr.text ?? "").trim(), toolName: tr.toolName?.trim() || undefined, + meta: tr.meta?.trim() || undefined, })) .filter((tr) => tr.text.length > 0); } -function formatToolPrefix(toolName?: string) { +function formatToolPrefix(toolName?: string, meta?: string) { const label = toolName?.trim() || "tool"; - return `[๐Ÿ› ๏ธ ${label}]`; + const extra = meta?.trim(); + return extra ? `[๐Ÿ› ๏ธ ${label} ${extra}]` : `[๐Ÿ› ๏ธ ${label}]`; } export function summarizeClaudeMetadata(payload: unknown): string | undefined { @@ -327,7 +349,12 @@ export async function runCommandReply( try { const ev = JSON.parse(line) as { type?: string; - message?: { role?: string; content?: unknown[] }; + message?: { + role?: string; + content?: unknown[]; + details?: Record; + arguments?: Record; + }; }; if ( (ev.type === "message" || ev.type === "message_end") && @@ -335,7 +362,8 @@ export async function runCommandReply( Array.isArray(ev.message.content) ) { const toolName = inferToolName(ev.message); - const prefix = formatToolPrefix(toolName); + const meta = inferToolMeta(ev.message); + const prefix = formatToolPrefix(toolName, meta); const { text: cleanedText, mediaUrls: mediaFound } = splitMediaFromOutput(prefix); void onPartialReply({ @@ -387,7 +415,7 @@ export async function runCommandReply( if (includeToolResultsInline) { for (const tr of parsedToolResults) { - const prefixed = formatToolPrefix(tr.toolName); + const prefixed = formatToolPrefix(tr.toolName, tr.meta); const { text: cleanedText, mediaUrls: mediaFound } = splitMediaFromOutput(prefixed); replyItems.push({ diff --git a/src/index.core.test.ts b/src/index.core.test.ts index 78743c4a7..5683eff1e 100644 --- a/src/index.core.test.ts +++ b/src/index.core.test.ts @@ -719,7 +719,7 @@ describe("config and templating", () => { const rpcSpy = vi.spyOn(tauRpc, "runPiRpc").mockResolvedValue({ stdout: '{"type":"message","message":{"role":"assistant","content":[{"type":"text","text":"summary"}]}}\n' + - '{"type":"message_end","message":{"role":"tool_result","name":"bash","content":[{"type":"text","text":"ls output"}]}}', + '{"type":"message_end","message":{"role":"tool_result","name":"bash","details":{"command":"ls"},"content":[{"type":"text","text":"ls output"}]}}', stderr: "", code: 0, signal: null, @@ -744,7 +744,7 @@ describe("config and templating", () => { expect(rpcSpy).toHaveBeenCalled(); const payloads = Array.isArray(res) ? res : res ? [res] : []; expect(payloads.length).toBeGreaterThanOrEqual(2); - expect(payloads[0]?.text).toBe("[๐Ÿ› ๏ธ bash]"); + expect(payloads[0]?.text).toBe("[๐Ÿ› ๏ธ bash ls]"); expect(payloads[1]?.text).toContain("summary"); });