* fix: sanitize tool call IDs in agent loop for Mistral strict9 format (#23595) Mistral requires tool call IDs to be exactly 9 alphanumeric characters ([a-zA-Z0-9]{9}). The existing sanitizeToolCallIdsForCloudCodeAssist mechanism only ran on historical messages at attempt start via sanitizeSessionHistory, but the pi-agent-core agent loop's internal tool call → tool result cycles bypassed that path entirely. Changes: - Wrap streamFn (like dropThinkingBlocks) so every outbound request sees sanitized tool call IDs when the transcript policy requires it - Replace call_${Date.now()} in pendingToolCalls with a 9-char hex ID generated from crypto.randomBytes - Add Mistral tool call ID error pattern to ERROR_PATTERNS.format so the error is correctly classified for retry/rotation * Changelog: document Mistral strict9 tool-call ID fix --------- Co-authored-by: echoVic <AkiraVic@outlook.com> Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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),
|
||||
},
|
||||
|
||||
@@ -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<string, unknown>),
|
||||
messages: sanitized,
|
||||
} as unknown;
|
||||
return inner(model, nextContext as typeof context, options);
|
||||
};
|
||||
}
|
||||
|
||||
if (anthropicPayloadLogger) {
|
||||
activeSession.agent.streamFn = anthropicPayloadLogger.wrapStreamFn(
|
||||
activeSession.agent.streamFn,
|
||||
|
||||
Reference in New Issue
Block a user