

























@@ -3,6 +3,7 @@ import { createAssistantMessageEventStream } from "@mariozechner/pi-ai";
33import { describe, expect, it } from "vitest";
44import { castAgentMessage, castAgentMessages } from "../test-helpers/agent-message-fixtures.js";
55import {
6+OMITTED_ASSISTANT_REASONING_TEXT,
67assessLastAssistantMessage,
78dropThinkingBlocks,
89isAssistantMessageWithContent,
@@ -103,6 +104,56 @@ describe("dropThinkingBlocks", () => {
103104{ type: "text", text: "latest text" },
104105]);
105106});
107+108+it("uses non-empty omitted-reasoning text when an older assistant turn is thinking-only", () => {
109+const messages: AgentMessage[] = [
110+castAgentMessage({ role: "user", content: "first" }),
111+castAgentMessage({
112+role: "assistant",
113+content: [{ type: "thinking", thinking: "old", thinkingSignature: "sig_old" }],
114+}),
115+castAgentMessage({ role: "user", content: "second" }),
116+castAgentMessage({
117+role: "assistant",
118+content: [
119+{ type: "thinking", thinking: "latest", thinkingSignature: "sig_latest" },
120+{ type: "text", text: "latest text" },
121+],
122+}),
123+];
124+125+const result = dropThinkingBlocks(messages);
126+const oldAssistant = result[1] as Extract<AgentMessage, { role: "assistant" }>;
127+const latestAssistant = result[3] as Extract<AgentMessage, { role: "assistant" }>;
128+const originalLatestAssistant = messages[3] as Extract<AgentMessage, { role: "assistant" }>;
129+130+expect(oldAssistant.content).toEqual([
131+{ type: "text", text: OMITTED_ASSISTANT_REASONING_TEXT },
132+]);
133+expect(latestAssistant.content).toEqual(originalLatestAssistant.content);
134+});
135+136+it("uses non-empty omitted-reasoning text when an older assistant turn is redacted-thinking-only", () => {
137+const messages: AgentMessage[] = [
138+castAgentMessage({ role: "user", content: "first" }),
139+castAgentMessage({
140+role: "assistant",
141+content: [{ type: "redacted_thinking", data: "opaque" }],
142+}),
143+castAgentMessage({ role: "user", content: "second" }),
144+castAgentMessage({
145+role: "assistant",
146+content: [{ type: "text", text: "latest text" }],
147+}),
148+];
149+150+const result = dropThinkingBlocks(messages);
151+const oldAssistant = result[1] as Extract<AgentMessage, { role: "assistant" }>;
152+153+expect(oldAssistant.content).toEqual([
154+{ type: "text", text: OMITTED_ASSISTANT_REASONING_TEXT },
155+]);
156+});
106157});
107158108159describe("sanitizeThinkingForRecovery", () => {
@@ -191,11 +242,13 @@ describe("wrapAnthropicStreamWithRecovery", () => {
191242"thinking or redacted_thinking blocks in the latest assistant message cannot be modified",
192243);
193244194-it("retries once when the request is rejected before streaming", async () => {
245+it("retries once with omitted-reasoning text when the request is rejected before streaming", async () => {
195246let callCount = 0;
247+const contexts: Array<{ messages?: AgentMessage[] }> = [];
196248const wrapped = wrapAnthropicStreamWithRecovery(
197-(() => {
249+((_model, context) => {
198250callCount += 1;
251+contexts.push(context as { messages?: AgentMessage[] });
199252return Promise.reject(anthropicThinkingError);
200253}) as Parameters<typeof wrapAnthropicStreamWithRecovery>[0],
201254{ id: "test-session" },
@@ -216,6 +269,44 @@ describe("wrapAnthropicStreamWithRecovery", () => {
216269),
217270).rejects.toBe(anthropicThinkingError);
218271expect(callCount).toBe(2);
272+expect(contexts[1]?.messages?.[0]).toMatchObject({
273+role: "assistant",
274+content: [{ type: "text", text: OMITTED_ASSISTANT_REASONING_TEXT }],
275+});
276+});
277+278+it("retries with visible assistant text when stripping thinking leaves content", async () => {
279+const contexts: Array<{ messages?: AgentMessage[] }> = [];
280+const wrapped = wrapAnthropicStreamWithRecovery(
281+((_model, context) => {
282+contexts.push(context as { messages?: AgentMessage[] });
283+return Promise.reject(anthropicThinkingError);
284+}) as Parameters<typeof wrapAnthropicStreamWithRecovery>[0],
285+{ id: "test-session" },
286+);
287+288+await expect(
289+wrapped(
290+{} as never,
291+{
292+messages: castAgentMessages([
293+{
294+role: "assistant",
295+content: [
296+{ type: "thinking", thinking: "secret", thinkingSignature: "sig" },
297+{ type: "text", text: "visible answer" },
298+],
299+},
300+]),
301+} as never,
302+{} as never,
303+),
304+).rejects.toBe(anthropicThinkingError);
305+306+expect(contexts[1]?.messages?.[0]).toMatchObject({
307+role: "assistant",
308+content: [{ type: "text", text: "visible answer" }],
309+});
219310});
220311221312it("does not retry when the stream fails after yielding a chunk", async () => {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。