fix(ai/providers): fixed orphaned tool result handling in message conversion

- Track skipped tool call IDs to prevent dangling tool results when assistant messages are omitted.
- Fixed indentation in Python tool output renderer.
This commit is contained in:
can1357
2026-01-23 12:48:06 +00:00
parent 0fe761dc4b
commit 0cab1fbce9
5 changed files with 56 additions and 19 deletions
+2 -2
View File
@@ -2,8 +2,8 @@
## [Unreleased]
### Changed
- Updated TypeScript configuration for better publish-time configuration handling with tsconfig.publish.json
### Fixed
- Fixed orphaned tool result handling by tracking skipped tool call IDs in message conversion to prevent dangling references
## [8.0.0] - 2026-01-23
### Fixed
+43 -12
View File
@@ -727,6 +727,8 @@ function convertMessages(
isOAuthToken: boolean,
): MessageParam[] {
const params: MessageParam[] = [];
// Track tool call IDs from skipped assistant messages to also skip their results
let skippedToolCallIds: string[] | null = null;
// Transform messages for cross-provider compatibility
const transformedMessages = transformMessages(messages, model);
@@ -779,6 +781,31 @@ function convertMessages(
// Skip messages with undefined/null content
if (!msg.content || !Array.isArray(msg.content)) continue;
// When interleaved thinking is enabled, Anthropic requires the last assistant
// message to start with a thinking block. If the first content block is a thinking
// block with a missing/invalid signature (e.g., from aborted stream), we must skip
// the entire message to avoid API rejection. Checking the first non-empty block.
const firstContentBlock = msg.content.find(
(b) =>
(b.type === "text" && b.text.trim().length > 0) ||
(b.type === "thinking" && b.thinking.trim().length > 0) ||
b.type === "toolCall",
);
if (
firstContentBlock?.type === "thinking" &&
(!firstContentBlock.thinkingSignature || firstContentBlock.thinkingSignature.trim().length === 0)
) {
// Skip this assistant message - it has corrupt thinking that would break the API.
// Also track any tool calls in this message so we can skip their results.
for (const block of msg.content) {
if (block.type === "toolCall") {
skippedToolCallIds ??= [];
skippedToolCallIds.push(block.id);
}
}
continue;
}
const blocks: Array<ContentBlockParam & CacheControlBlock> = [];
for (const block of msg.content) {
@@ -824,23 +851,27 @@ function convertMessages(
const toolResults: Array<ContentBlockParam & CacheControlBlock> = [];
// Add the current tool result
toolResults.push({
type: "tool_result",
tool_use_id: sanitizeToolCallId(msg.toolCallId),
content: convertContentBlocks(msg.content),
is_error: msg.isError,
});
if (!skippedToolCallIds?.includes(msg.toolCallId)) {
toolResults.push({
type: "tool_result",
tool_use_id: sanitizeToolCallId(msg.toolCallId),
content: convertContentBlocks(msg.content),
is_error: msg.isError,
});
}
// Look ahead for consecutive toolResult messages
let j = i + 1;
while (j < transformedMessages.length && transformedMessages[j].role === "toolResult") {
const nextMsg = transformedMessages[j] as ToolResultMessage; // We know it's a toolResult
toolResults.push({
type: "tool_result",
tool_use_id: sanitizeToolCallId(nextMsg.toolCallId),
content: convertContentBlocks(nextMsg.content),
is_error: nextMsg.isError,
});
if (!skippedToolCallIds?.includes(nextMsg.toolCallId)) {
toolResults.push({
type: "tool_result",
tool_use_id: sanitizeToolCallId(nextMsg.toolCallId),
content: convertContentBlocks(nextMsg.content),
is_error: nextMsg.isError,
});
}
j++;
}
+1
View File
@@ -106,6 +106,7 @@
- Handle undefined code parameter in code cell renderer
- Fixed indentation formatting in split-commit tool function signature
- Fixed changelog application to process proposals containing only deletion entries without additions
- Fixed indentation formatting in Python tool output renderer
## [8.0.0] - 2026-01-23
### Added
+1 -1
View File
@@ -900,7 +900,7 @@ export const pythonToolRenderer = {
spinnerFrame: options.spinnerFrame,
duration: cell.durationMs,
output: outputLines.length > 0 ? outputLines.join("\n") : undefined,
outputMaxLines: outputLines.length,
outputMaxLines: outputLines.length,
codeMaxLines: expanded ? Number.POSITIVE_INFINITY : PYTHON_DEFAULT_PREVIEW_LINES,
expanded,
width,
@@ -126,10 +126,8 @@ export function renderWebSearchResult(
{
icon: sourceCount > 0 ? "success" : "warning",
title: "Web Search",
description: queryPreview ?? providerLabel,
meta: queryPreview
? [providerLabel, formatCount("source", sourceCount)]
: [providerLabel, formatCount("source", sourceCount)],
description: providerLabel,
meta: [formatCount("source", sourceCount)],
},
theme,
);
@@ -241,6 +239,13 @@ export function renderWebSearchResult(
}
const sections = [
...(queryPreview
? [
{
lines: [`${theme.fg("muted", "Query:")} ${theme.fg("text", queryPreview)}`],
},
]
: []),
{ label: theme.fg("toolTitle", "Answer"), lines: answerTree },
{
label: theme.fg("toolTitle", "Sources"),