diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 464a3361e..082a380e3 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -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 diff --git a/packages/ai/src/providers/anthropic.ts b/packages/ai/src/providers/anthropic.ts index dbdad8890..332f3395f 100644 --- a/packages/ai/src/providers/anthropic.ts +++ b/packages/ai/src/providers/anthropic.ts @@ -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 = []; for (const block of msg.content) { @@ -824,23 +851,27 @@ function convertMessages( const toolResults: Array = []; // 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++; } diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 2e65edf6a..ae1393230 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -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 diff --git a/packages/coding-agent/src/tools/python.ts b/packages/coding-agent/src/tools/python.ts index 81aeb2f25..e79c4495f 100644 --- a/packages/coding-agent/src/tools/python.ts +++ b/packages/coding-agent/src/tools/python.ts @@ -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, diff --git a/packages/coding-agent/src/web/search/render.ts b/packages/coding-agent/src/web/search/render.ts index 64a4f245c..deb63e90c 100644 --- a/packages/coding-agent/src/web/search/render.ts +++ b/packages/coding-agent/src/web/search/render.ts @@ -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"),