AIAgent: update anthropic thinking mode

This commit is contained in:
Timofey
2025-12-29 17:31:00 +08:00
parent 9bf99405ca
commit 676723b0ff
2 changed files with 35 additions and 56 deletions

View File

@ -1,11 +1,16 @@
import type Anthropic from "@anthropic-ai/sdk";
import type {
ReasoningMessagePart,
TextMessagePart,
ThreadMessageLike,
ToolCallMessagePart,
} from "@assistant-ui/react";
type ContentArray = (TextMessagePart | ToolCallMessagePart)[];
type ContentArray = (
| TextMessagePart
| ToolCallMessagePart
| ReasoningMessagePart
)[];
// ============================================================================
// Helpers
@ -51,7 +56,10 @@ const handleThinkingBlockStart = (
block: { thinking: string },
content: ContentArray
): void => {
content.push(createTextPart(`<think>\n${block.thinking}`));
content.push({
type: "reasoning",
text: block.thinking,
});
};
const handleToolUseBlockStart = (
@ -80,9 +88,12 @@ const handleThinkingDelta = (
content: ContentArray
): void => {
const last = getLastContent(content);
if (last?.type !== "text") return;
if (last?.type !== "reasoning") return;
content[content.length - 1] = createTextPart(last.text + delta.thinking);
content[content.length - 1] = {
...last,
text: last.text + delta.thinking,
};
};
const handleSignatureDelta = (
@ -90,14 +101,13 @@ const handleSignatureDelta = (
content: ContentArray
): void => {
const last = getLastContent(content);
if (last?.type !== "text" || !last.text.startsWith("<think>")) return;
console.log(delta);
if (last?.type !== "reasoning") return;
// Append signature to the thinking block (will be closed later)
content[content.length - 1] = createTextPart(
`${last.text}<!--sig:${delta.signature}-->`
);
content[content.length - 1] = {
...last,
parentId: delta.signature,
};
};
const tryParseJson = (text: string): ToolCallMessagePart["args"] => {

View File

@ -136,50 +136,16 @@ const convertSystemMessage = (message: ThreadMessageLike): MessageParam[] => {
return [{ role: "user", content }];
};
/**
* Strips <think> tags from text and returns the thinking content, signature, and non-thinking text.
* Format: <think>content<!--sig:SIGNATURE--></think>
*/
const stripThinkingFromText = (
text: string
): { thinkBlock: string; signature: string; text: string } => {
const thinkMatch = text.match(/<think>([\s\S]*?)<\/think>/);
let thinkBlock = "";
let signature = "";
if (thinkMatch) {
const thinkContent = thinkMatch[1];
const sigMatch = thinkContent.match(/<!--sig:([\s\S]*?)-->/);
if (sigMatch) {
signature = sigMatch[1];
thinkBlock = thinkContent.replace(/<!--sig:[\s\S]*?-->/, "").trim();
} else {
thinkBlock = thinkContent.trim();
}
}
const strippedText = text.replace(/<think>[\s\S]*?<\/think>\n*/g, "").trim();
return { thinkBlock, signature, text: strippedText };
};
const convertAssistantMessage = (
message: ThreadMessageLike
): MessageParam[] => {
const result: MessageParam[] = [];
if (typeof message.content === "string") {
const { text, thinkBlock, signature } = stripThinkingFromText(
message.content
);
const content: ContentBlockParam[] = [
{ type: "text", text: message.content },
];
const content: ContentBlockParam[] = [];
if (thinkBlock && signature) {
content.push({ type: "thinking", thinking: thinkBlock, signature });
}
if (text) {
content.push({ type: "text", text });
}
return [{ role: "assistant", content }];
}
@ -187,16 +153,19 @@ const convertAssistantMessage = (
let toolResults: ToolResultBlockParam[] = [];
for (const part of message.content) {
if (part.type === "text") {
// Strip thinking blocks - they require signature to be sent back
const { text, thinkBlock, signature } = stripThinkingFromText(part.text);
if (part.type === "reasoning") {
content.push({
type: "thinking",
thinking: part.text,
signature: part.parentId ?? "",
});
continue;
}
if (part.type === "text") {
content.push({ type: "text", text: part.text });
if (thinkBlock && signature) {
content.push({ type: "thinking", thinking: thinkBlock, signature });
}
if (text) {
content.push({ type: "text", text });
}
continue;
}