diff --git a/CHANGELOG.md b/CHANGELOG.md index c23e73007..dd7d15e8f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -121,6 +121,7 @@ Docs: https://docs.openclaw.ai - Agents/Fallbacks: treat JSON payloads with `type: "api_error"` + `"Internal server error"` as transient failover errors so Anthropic 500-style failures trigger model fallback. (#23193) Thanks @jarvis-lane. - Agents/Google: sanitize non-base64 `thought_signature`/`thoughtSignature` values from assistant replay transcripts for native Google Gemini requests while preserving valid signatures and tool-call order. (#23457) Thanks @echoVic. - Agents/Transcripts: validate assistant tool-call names (syntax/length + registered tool allowlist) before transcript persistence and during replay sanitization so malformed failover tool names no longer poison sessions with repeated provider HTTP 400 errors. (#23324) Thanks @johnsantry. +- Agents/Mistral: sanitize tool-call IDs in the embedded agent loop and generate strict provider-safe pending tool-call IDs, preventing Mistral strict9 `HTTP 400` failures on tool continuations. (#23698) Thanks @echoVic. - Agents/Compaction: strip stale assistant usage snapshots from pre-compaction turns when replaying history after a compaction summary so context-token estimation no longer reuses pre-compaction totals and immediately re-triggers destructive follow-up compactions. (#19127) Thanks @tedwatson. - Agents/Replies: emit a default completion acknowledgement (`✅ Done.`) when runs execute tools successfully but return no final assistant text, preventing silent no-reply turns after tool-only completions. (#22834) Thanks @Oldshue. - Agents/Subagents: honor `tools.subagents.tools.alsoAllow` and explicit subagent `allow` entries when resolving built-in subagent deny defaults, so explicitly granted tools (for example `sessions_send`) are no longer blocked unless re-denied in `tools.subagents.tools.deny`. (#23359) Thanks @goren-beehero. diff --git a/src/agents/pi-embedded-helpers/errors.ts b/src/agents/pi-embedded-helpers/errors.ts index 9e0ceb050..68ee31f3f 100644 --- a/src/agents/pi-embedded-helpers/errors.ts +++ b/src/agents/pi-embedded-helpers/errors.ts @@ -614,6 +614,7 @@ const ERROR_PATTERNS = { "tool_use_id", "messages.1.content.1.tool_use.id", "invalid request format", + /tool call id was.*must be/i, ], } as const; diff --git a/src/agents/pi-embedded-runner/run.ts b/src/agents/pi-embedded-runner/run.ts index 0a810a38a..1347354e3 100644 --- a/src/agents/pi-embedded-runner/run.ts +++ b/src/agents/pi-embedded-runner/run.ts @@ -1,3 +1,4 @@ +import { randomBytes } from "node:crypto"; import fs from "node:fs/promises"; import type { ThinkLevel } from "../../auto-reply/thinking.js"; import { generateSecureToken } from "../../infra/secure-random.js"; @@ -1122,7 +1123,7 @@ export async function runEmbeddedPiAgent( pendingToolCalls: attempt.clientToolCall ? [ { - id: `call_${Date.now()}`, + id: randomBytes(5).toString("hex").slice(0, 9), name: attempt.clientToolCall.name, arguments: JSON.stringify(attempt.clientToolCall.params), }, diff --git a/src/agents/pi-embedded-runner/run/attempt.ts b/src/agents/pi-embedded-runner/run/attempt.ts index d8ba48e15..d0892148b 100644 --- a/src/agents/pi-embedded-runner/run/attempt.ts +++ b/src/agents/pi-embedded-runner/run/attempt.ts @@ -77,6 +77,7 @@ import { } from "../../skills.js"; import { buildSystemPromptParams } from "../../system-prompt-params.js"; import { buildSystemPromptReport } from "../../system-prompt-report.js"; +import { sanitizeToolCallIdsForCloudCodeAssist } from "../../tool-call-id.js"; import { resolveTranscriptPolicy } from "../../transcript-policy.js"; import { DEFAULT_BOOTSTRAP_FILENAME } from "../../workspace.js"; import { isRunnerAbortError } from "../abort.js"; @@ -771,6 +772,32 @@ export async function runEmbeddedAttempt( }; } + // Mistral (and other strict providers) reject tool call IDs that don't match their + // format requirements (e.g. [a-zA-Z0-9]{9}). sanitizeSessionHistory only processes + // historical messages at attempt start, but the agent loop's internal tool call → + // tool result cycles bypass that path. Wrap streamFn so every outbound request + // sees sanitized tool call IDs. + if (transcriptPolicy.sanitizeToolCallIds && transcriptPolicy.toolCallIdMode) { + const inner = activeSession.agent.streamFn; + const mode = transcriptPolicy.toolCallIdMode; + activeSession.agent.streamFn = (model, context, options) => { + const ctx = context as unknown as { messages?: unknown }; + const messages = ctx?.messages; + if (!Array.isArray(messages)) { + return inner(model, context, options); + } + const sanitized = sanitizeToolCallIdsForCloudCodeAssist(messages as AgentMessage[], mode); + if (sanitized === messages) { + return inner(model, context, options); + } + const nextContext = { + ...(context as unknown as Record), + messages: sanitized, + } as unknown; + return inner(model, nextContext as typeof context, options); + }; + } + if (anthropicPayloadLogger) { activeSession.agent.streamFn = anthropicPayloadLogger.wrapStreamFn( activeSession.agent.streamFn,