























@@ -279,8 +279,9 @@ describe("stripInvalidThinkingSignatures", () => {
279279expect(result).toBe(messages);
280280});
281281282-it("strips thinking blocks with missing, empty, or blank signatures", () => {
282+it("preserves invalid thinking signatures on the latest assistant message", () => {
283283const messages: AgentMessage[] = [
284+castAgentMessage({ role: "user", content: "hello" }),
284285castAgentMessage({
285286role: "assistant",
286287content: [
@@ -293,6 +294,58 @@ describe("stripInvalidThinkingSignatures", () => {
293294}),
294295];
295296297+const result = stripInvalidThinkingSignatures(messages);
298+const assistant = result[1] as Extract<AgentMessage, { role: "assistant" }>;
299+300+expect(result).toBe(messages);
301+expect(assistant.content).toEqual([
302+{ type: "thinking", thinking: "missing" },
303+{ type: "thinking", thinking: "empty", thinkingSignature: "" },
304+{ type: "thinking", thinking: "blank", thinkingSignature: " " },
305+{ type: "thinking", thinking: "signed", thinkingSignature: "sig" },
306+{ type: "text", text: "answer" },
307+]);
308+});
309+310+it("can strip invalid thinking signatures from the latest assistant message", () => {
311+const messages: AgentMessage[] = [
312+castAgentMessage({ role: "user", content: "hello" }),
313+castAgentMessage({
314+role: "assistant",
315+content: [
316+{ type: "thinking", thinking: "missing" },
317+{ type: "thinking", thinking: "signed", thinkingSignature: "sig" },
318+{ type: "text", text: "answer" },
319+],
320+}),
321+];
322+323+const result = stripInvalidThinkingSignatures(messages, { preserveLatestAssistant: false });
324+const assistant = result[1] as Extract<AgentMessage, { role: "assistant" }>;
325+326+expect(result).not.toBe(messages);
327+expect(assistant.content).toEqual([
328+{ type: "thinking", thinking: "signed", thinkingSignature: "sig" },
329+{ type: "text", text: "answer" },
330+]);
331+});
332+333+it("strips thinking blocks with missing, empty, or blank signatures from older assistant messages", () => {
334+const messages: AgentMessage[] = [
335+castAgentMessage({
336+role: "assistant",
337+content: [
338+{ type: "thinking", thinking: "missing" },
339+{ type: "thinking", thinking: "empty", thinkingSignature: "" },
340+{ type: "thinking", thinking: "blank", thinkingSignature: " " },
341+{ type: "thinking", thinking: "signed", thinkingSignature: "sig" },
342+{ type: "text", text: "answer" },
343+],
344+}),
345+castAgentMessage({ role: "user", content: "follow up" }),
346+castAgentMessage({ role: "assistant", content: [{ type: "text", text: "latest" }] }),
347+];
348+296349const result = stripInvalidThinkingSignatures(messages);
297350const assistant = result[0] as Extract<AgentMessage, { role: "assistant" }>;
298351@@ -309,6 +362,8 @@ describe("stripInvalidThinkingSignatures", () => {
309362role: "assistant",
310363content: [{ type: "thinking", thinking: "reasoning", thinkingSignature: "" }],
311364}),
365+castAgentMessage({ role: "user", content: "follow up" }),
366+castAgentMessage({ role: "assistant", content: [{ type: "text", text: "latest" }] }),
312367];
313368314369const result = stripInvalidThinkingSignatures(messages);
@@ -317,7 +372,7 @@ describe("stripInvalidThinkingSignatures", () => {
317372expect(assistant.content).toEqual([{ type: "text", text: OMITTED_ASSISTANT_REASONING_TEXT }]);
318373});
319374320-it("strips redacted thinking blocks with invalid opaque signatures", () => {
375+it("strips redacted thinking blocks with invalid opaque signatures from older assistant messages", () => {
321376const messages: AgentMessage[] = [
322377castAgentMessage({
323378role: "assistant",
@@ -328,6 +383,8 @@ describe("stripInvalidThinkingSignatures", () => {
328383{ type: "text", text: "answer" },
329384],
330385}),
386+castAgentMessage({ role: "user", content: "follow up" }),
387+castAgentMessage({ role: "assistant", content: [{ type: "text", text: "latest" }] }),
331388];
332389333390const result = stripInvalidThinkingSignatures(messages);
@@ -497,6 +554,36 @@ describe("wrapAnthropicStreamWithRecovery", () => {
497554expect(retryMessage.content).toEqual([{ type: "text", text: "visible answer" }]);
498555});
499556557+it("retries Bedrock-style invalid thinking signature errors", async () => {
558+let callCount = 0;
559+const bedrockThinkingError = new Error(
560+"ValidationException: invalid signature on thinking block in message history",
561+);
562+const wrapped = wrapAnthropicStreamWithRecovery(
563+(() => {
564+callCount += 1;
565+return Promise.reject(bedrockThinkingError);
566+}) as Parameters<typeof wrapAnthropicStreamWithRecovery>[0],
567+{ id: "test-session" },
568+);
569+570+await expect(
571+wrapped(
572+{} as never,
573+{
574+messages: castAgentMessages([
575+{
576+role: "assistant",
577+content: [{ type: "thinking", thinking: "secret", thinkingSignature: "" }],
578+},
579+]),
580+} as never,
581+{} as never,
582+),
583+).rejects.toBe(bedrockThinkingError);
584+expect(callCount).toBe(2);
585+});
586+500587it("does not retry when the stream fails after yielding a chunk", async () => {
501588let callCount = 0;
502589const wrapped = wrapAnthropicStreamWithRecovery(
@@ -540,6 +627,34 @@ describe("wrapAnthropicStreamWithRecovery", () => {
540627expect(callCount).toBe(1);
541628});
542629630+it("allows each provider call to recover once", async () => {
631+let callCount = 0;
632+const wrapped = wrapAnthropicStreamWithRecovery(
633+(() => {
634+callCount += 1;
635+return Promise.reject(anthropicThinkingError);
636+}) as Parameters<typeof wrapAnthropicStreamWithRecovery>[0],
637+{ id: "test-session" },
638+);
639+const context = {
640+messages: castAgentMessages([
641+{
642+role: "assistant",
643+content: [{ type: "thinking", thinking: "secret", thinkingSignature: "sig" }],
644+},
645+]),
646+};
647+648+await expect(wrapped({} as never, context as never, {} as never)).rejects.toBe(
649+anthropicThinkingError,
650+);
651+await expect(wrapped({} as never, context as never, {} as never)).rejects.toBe(
652+anthropicThinkingError,
653+);
654+655+expect(callCount).toBe(4);
656+});
657+543658it("preserves result() for synchronous event streams", async () => {
544659const finalMessage = castAgentMessage({
545660role: "assistant",
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。