diff --git a/CHANGELOG.md b/CHANGELOG.md index adb990f01..765889304 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ - ClawdbotKit: fix SwiftPM resource bundling path for `tool-display.json`. Thanks @fcatuhe for PR #398. - Tools: add Telegram/WhatsApp reaction tools (with per-provider gating). Thanks @zats for PR #353. - Tools: flatten literal-union schemas for Claude on Vertex AI. Thanks @carlulsoe for PR #409. +- Tools: keep tool failure logs concise (no stack traces); full stack only in debug logs. - Tools: unify reaction removal semantics across Discord/Slack/Telegram/WhatsApp and allow WhatsApp reaction routing across accounts. - Android: fix APK output filename renaming after AGP updates. Thanks @Syhids for PR #410. - Android: rotate camera photos by EXIF orientation. Thanks @fcatuhe for PR #403. @@ -40,6 +41,7 @@ - Agent: protect bootstrap prefix from context pruning. Thanks @maxsumrall for PR #381. - Agent: deliver final replies for non-streaming models when block chunking is enabled. Thank you @mneves75 for PR #369! - Agent: trim bootstrap context injections and keep group guidance concise (emoji reactions allowed). Thanks @tobiasbischoff for PR #370. +- Agent: return a friendly context overflow response (413/request_too_large). Thanks @alejandroOPI for PR #395. - Sub-agents: allow `sessions_spawn` model overrides and error on invalid models. Thanks @azade-c for PR #298. - Sub-agents: skip invalid model overrides with a warning and keep the run alive; tool exceptions now return tool errors instead of crashing the agent. - Sessions: forward explicit sessionKey through gateway/chat/node bridge to avoid sub-agent sessionId mixups. diff --git a/README.md b/README.md index a6623a632..eb491440b 100644 --- a/README.md +++ b/README.md @@ -454,5 +454,5 @@ Thanks to all clawtributors: adamgall jalehman jarvis-medmatic mneves75 regenrek tobiasbischoff MSch obviyus dbhurley Asleep123 Iamadig imfing kitze nachoiacovino VACInc cash-echo-bot claude kiranjd pcty-nextgen-service-account minghinmatthewlam ngutman onutc oswalpalash snopoke ManuelHettich loukotal hugobarauna AbhisekBasu1 emanuelst dantelex erikpr1994 antons RandyVentures - reeltimeapps fcatuhe maxsumrall carlulsoe + reeltimeapps fcatuhe maxsumrall carlulsoe alejandroOPI

diff --git a/src/agents/pi-embedded-helpers.test.ts b/src/agents/pi-embedded-helpers.test.ts index c36664dba..69a93430a 100644 --- a/src/agents/pi-embedded-helpers.test.ts +++ b/src/agents/pi-embedded-helpers.test.ts @@ -1,6 +1,11 @@ +import type { AssistantMessage } from "@mariozechner/pi-ai"; import { describe, expect, it } from "vitest"; -import { buildBootstrapContextFiles } from "./pi-embedded-helpers.js"; +import { + buildBootstrapContextFiles, + formatAssistantErrorText, + isContextOverflowError, +} from "./pi-embedded-helpers.js"; import { DEFAULT_AGENTS_FILENAME, type WorkspaceBootstrapFile, @@ -46,3 +51,35 @@ describe("buildBootstrapContextFiles", () => { expect(result?.content.endsWith(long.slice(-120))).toBe(true); }); }); + +describe("isContextOverflowError", () => { + it("matches known overflow hints", () => { + const samples = [ + "request_too_large", + "Request exceeds the maximum size", + "context length exceeded", + "Maximum context length", + "413 Request Entity Too Large", + ]; + for (const sample of samples) { + expect(isContextOverflowError(sample)).toBe(true); + } + }); + + it("ignores unrelated errors", () => { + expect(isContextOverflowError("rate limit exceeded")).toBe(false); + }); +}); + +describe("formatAssistantErrorText", () => { + const makeAssistantError = (errorMessage: string): AssistantMessage => + ({ + stopReason: "error", + errorMessage, + }) as AssistantMessage; + + it("returns a friendly message for context overflow", () => { + const msg = makeAssistantError("request_too_large"); + expect(formatAssistantErrorText(msg)).toContain("Context overflow"); + }); +}); diff --git a/src/agents/pi-embedded-helpers.ts b/src/agents/pi-embedded-helpers.ts index 750c9504b..0b0eaec19 100644 --- a/src/agents/pi-embedded-helpers.ts +++ b/src/agents/pi-embedded-helpers.ts @@ -126,6 +126,18 @@ export function buildBootstrapContextFiles( return result; } +export function isContextOverflowError(errorMessage?: string): boolean { + if (!errorMessage) return false; + const lower = errorMessage.toLowerCase(); + return ( + lower.includes("request_too_large") || + lower.includes("request exceeds the maximum size") || + lower.includes("context length exceeded") || + lower.includes("maximum context length") || + (lower.includes("413") && lower.includes("too large")) + ); +} + export function formatAssistantErrorText( msg: AssistantMessage, ): string | undefined { @@ -133,6 +145,14 @@ export function formatAssistantErrorText( const raw = (msg.errorMessage ?? "").trim(); if (!raw) return "LLM request failed with an unknown error."; + // Check for context overflow (413) errors + if (isContextOverflowError(raw)) { + return ( + "Context overflow: the conversation history is too large. " + + "Use /new or /reset to start a fresh session." + ); + } + const invalidRequest = raw.match( /"type":"invalid_request_error".*?"message":"([^"]+)"/, ); diff --git a/src/agents/pi-embedded-runner.ts b/src/agents/pi-embedded-runner.ts index 86b790a44..e5a75a473 100644 --- a/src/agents/pi-embedded-runner.ts +++ b/src/agents/pi-embedded-runner.ts @@ -59,6 +59,7 @@ import { formatAssistantErrorText, isAuthAssistantError, isAuthErrorMessage, + isContextOverflowError, isRateLimitAssistantError, isRateLimitErrorMessage, pickFallbackThinkingLevel, @@ -1153,6 +1154,26 @@ export async function runEmbeddedPiAgent(params: { } if (promptError && !aborted) { const errorText = describeUnknownError(promptError); + if (isContextOverflowError(errorText)) { + return { + payloads: [ + { + text: + "Context overflow: the conversation history is too large for the model. " + + "Use /new or /reset to start a fresh session, or try a model with a larger context window.", + isError: true, + }, + ], + meta: { + durationMs: Date.now() - started, + agentMeta: { + sessionId: sessionIdUsed, + provider, + model: model.id, + }, + }, + }; + } if ( (isAuthErrorMessage(errorText) || isRateLimitErrorMessage(errorText)) && diff --git a/src/agents/pi-tool-definition-adapter.test.ts b/src/agents/pi-tool-definition-adapter.test.ts index 27a101002..48700ad28 100644 --- a/src/agents/pi-tool-definition-adapter.test.ts +++ b/src/agents/pi-tool-definition-adapter.test.ts @@ -22,6 +22,7 @@ describe("pi tool definition adapter", () => { status: "error", tool: "boom", }); - expect(JSON.stringify(result.details)).toContain("nope"); + expect(result.details).toMatchObject({ error: "nope" }); + expect(JSON.stringify(result.details)).not.toContain("\n at "); }); }); diff --git a/src/agents/pi-tool-definition-adapter.ts b/src/agents/pi-tool-definition-adapter.ts index df8b64d8d..9f4451625 100644 --- a/src/agents/pi-tool-definition-adapter.ts +++ b/src/agents/pi-tool-definition-adapter.ts @@ -4,12 +4,23 @@ import type { AgentToolUpdateCallback, } from "@mariozechner/pi-agent-core"; import type { ToolDefinition } from "@mariozechner/pi-coding-agent"; -import { logError } from "../logger.js"; +import { logDebug, logError } from "../logger.js"; import { jsonResult } from "./tools/common.js"; // biome-ignore lint/suspicious/noExplicitAny: TypeBox schema type from pi-agent-core uses a different module instance. type AnyAgentTool = AgentTool; +function describeToolExecutionError(err: unknown): { + message: string; + stack?: string; +} { + if (err instanceof Error) { + const message = err.message?.trim() ? err.message : String(err); + return { message, stack: err.stack }; + } + return { message: String(err) }; +} + export function toToolDefinitions(tools: AnyAgentTool[]): ToolDefinition[] { return tools.map((tool) => { const name = tool.name || "tool"; @@ -37,13 +48,15 @@ export function toToolDefinitions(tools: AnyAgentTool[]): ToolDefinition[] { ? String((err as { name?: unknown }).name) : ""; if (name === "AbortError") throw err; - const message = - err instanceof Error ? (err.stack ?? err.message) : String(err); - logError(`[tools] ${tool.name} failed: ${message}`); + const described = describeToolExecutionError(err); + if (described.stack && described.stack !== described.message) { + logDebug(`tools: ${tool.name} failed stack:\n${described.stack}`); + } + logError(`[tools] ${tool.name} failed: ${described.message}`); return jsonResult({ status: "error", tool: tool.name, - error: message, + error: described.message, }); } },