From 3df2dc0b151a318b92b557454d89364eeebdb57d Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 17 Jan 2026 18:14:43 +0000 Subject: [PATCH] fix: normalize exec tool alias naming --- CHANGELOG.md | 1 + .../pi-embedded-subscribe.handlers.tools.ts | 8 ++- src/agents/pi-tool-definition-adapter.test.ts | 21 ++++++ src/agents/pi-tool-definition-adapter.ts | 71 ++----------------- 4 files changed, 32 insertions(+), 69 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 49448c775..4e5699818 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -73,6 +73,7 @@ Docs: https://docs.clawd.bot - macOS: drain subprocess pipes before waiting to avoid deadlocks. (#1081) — thanks @thesash. - Verbose: wrap tool summaries/output in markdown only for markdown-capable channels. - Tools: include provider/session context in elevated exec denial errors. +- Tools: normalize exec tool alias naming in tool error logs. - Telegram: accept tg/group/telegram prefixes + topic targets for inline button validation. (#1072) — thanks @danielz1z. - Telegram: split long captions into follow-up messages. - Config: block startup on invalid config, preserve best-effort doctor config, and keep rolling config backups. (#1083) — thanks @mukhtharcm. diff --git a/src/agents/pi-embedded-subscribe.handlers.tools.ts b/src/agents/pi-embedded-subscribe.handlers.tools.ts index 5d1f235a6..30f6124b6 100644 --- a/src/agents/pi-embedded-subscribe.handlers.tools.ts +++ b/src/agents/pi-embedded-subscribe.handlers.tools.ts @@ -11,6 +11,7 @@ import { sanitizeToolResult, } from "./pi-embedded-subscribe.tools.js"; import { inferToolMetaFromArgs } from "./pi-embedded-utils.js"; +import { normalizeToolName } from "./tool-policy.js"; function extendExecMeta(toolName: string, args: unknown, meta?: string): string | undefined { const normalized = toolName.trim().toLowerCase(); @@ -35,7 +36,8 @@ export async function handleToolExecutionStart( void ctx.params.onBlockReplyFlush(); } - const toolName = String(evt.toolName); + const rawToolName = String(evt.toolName); + const toolName = normalizeToolName(rawToolName); const toolCallId = String(evt.toolCallId); const args = evt.args; @@ -109,7 +111,7 @@ export function handleToolExecutionUpdate( partialResult?: unknown; }, ) { - const toolName = String(evt.toolName); + const toolName = normalizeToolName(String(evt.toolName)); const toolCallId = String(evt.toolCallId); const partial = evt.partialResult; const sanitized = sanitizeToolResult(partial); @@ -142,7 +144,7 @@ export function handleToolExecutionEnd( result?: unknown; }, ) { - const toolName = String(evt.toolName); + const toolName = normalizeToolName(String(evt.toolName)); const toolCallId = String(evt.toolCallId); const isError = Boolean(evt.isError); const result = evt.result; diff --git a/src/agents/pi-tool-definition-adapter.test.ts b/src/agents/pi-tool-definition-adapter.test.ts index 48700ad28..e773a874c 100644 --- a/src/agents/pi-tool-definition-adapter.test.ts +++ b/src/agents/pi-tool-definition-adapter.test.ts @@ -25,4 +25,25 @@ describe("pi tool definition adapter", () => { expect(result.details).toMatchObject({ error: "nope" }); expect(JSON.stringify(result.details)).not.toContain("\n at "); }); + + it("normalizes exec tool aliases in error results", async () => { + const tool = { + name: "bash", + label: "Bash", + description: "throws", + parameters: {}, + execute: async () => { + throw new Error("nope"); + }, + } satisfies AgentTool; + + const defs = toToolDefinitions([tool]); + const result = await defs[0].execute("call2", {}, undefined, undefined); + + expect(result.details).toMatchObject({ + status: "error", + tool: "exec", + error: "nope", + }); + }); }); diff --git a/src/agents/pi-tool-definition-adapter.ts b/src/agents/pi-tool-definition-adapter.ts index 95a261d6c..0b9b5bbe1 100644 --- a/src/agents/pi-tool-definition-adapter.ts +++ b/src/agents/pi-tool-definition-adapter.ts @@ -5,6 +5,7 @@ import type { } from "@mariozechner/pi-agent-core"; import type { ToolDefinition } from "@mariozechner/pi-coding-agent"; import { logDebug, logError } from "../logger.js"; +import { normalizeToolName } from "./tool-policy.js"; import { jsonResult } from "./tools/common.js"; // biome-ignore lint/suspicious/noExplicitAny: TypeBox schema type from pi-agent-core uses a different module instance. @@ -21,70 +22,10 @@ function describeToolExecutionError(err: unknown): { return { message: String(err) }; } -function asScalar(value: unknown): string | undefined { - if (typeof value === "string") { - const trimmed = value.trim(); - return trimmed ? trimmed : undefined; - } - if (typeof value === "number" && Number.isFinite(value)) return String(value); - if (typeof value === "bigint") return String(value); - return undefined; -} - -function summarizeList(value: unknown): string | undefined { - if (!Array.isArray(value)) return undefined; - const items = value.map(asScalar).filter((entry): entry is string => Boolean(entry)); - if (items.length === 0) return undefined; - const sample = items.slice(0, 3).join(", "); - const suffix = items.length > 3 ? ` (+${items.length - 3})` : ""; - return `${sample}${suffix}`; -} - -function looksLikeMemberTarget(value: string): boolean { - return /^user:/i.test(value) || value.startsWith("@") || /^<@!?/.test(value); -} - -function describeMessageToolContext(params: Record): string | undefined { - const action = asScalar(params.action); - const channel = asScalar(params.channel); - const accountId = asScalar(params.accountId); - const guildId = asScalar(params.guildId); - const channelId = asScalar(params.channelId); - const threadId = asScalar(params.threadId); - const messageId = asScalar(params.messageId); - const userId = asScalar(params.userId) ?? asScalar(params.authorId) ?? asScalar(params.participant); - const target = - asScalar(params.target) ?? - asScalar(params.to) ?? - summarizeList(params.targets) ?? - summarizeList(params.target); - - const member = - userId ?? (target && looksLikeMemberTarget(target) ? target : undefined) ?? undefined; - const pairs: string[] = []; - if (action) pairs.push(`action=${action}`); - if (channel) pairs.push(`channel=${channel}`); - if (accountId) pairs.push(`accountId=${accountId}`); - if (member) { - pairs.push(`member=${member}`); - } else if (target) { - pairs.push(`target=${target}`); - } - if (guildId) pairs.push(`guildId=${guildId}`); - if (channelId) pairs.push(`channelId=${channelId}`); - if (threadId) pairs.push(`threadId=${threadId}`); - if (messageId) pairs.push(`messageId=${messageId}`); - return pairs.length > 0 ? pairs.join(" ") : undefined; -} - -function describeToolContext(toolName: string, params: Record): string | undefined { - if (toolName === "message") return describeMessageToolContext(params); - return undefined; -} - export function toToolDefinitions(tools: AnyAgentTool[]): ToolDefinition[] { return tools.map((tool) => { const name = tool.name || "tool"; + const normalizedName = normalizeToolName(name); return { name, label: tool.label ?? name, @@ -111,14 +52,12 @@ export function toToolDefinitions(tools: AnyAgentTool[]): ToolDefinition[] { if (name === "AbortError") throw err; const described = describeToolExecutionError(err); if (described.stack && described.stack !== described.message) { - logDebug(`tools: ${tool.name} failed stack:\n${described.stack}`); + logDebug(`tools: ${normalizedName} failed stack:\n${described.stack}`); } - const context = describeToolContext(tool.name, params); - const suffix = context ? ` (${context})` : ""; - logError(`tools: ${tool.name} failed: ${described.message}${suffix}`); + logError(`[tools] ${normalizedName} failed: ${described.message}`); return jsonResult({ status: "error", - tool: tool.name, + tool: normalizedName, error: described.message, }); }