@@ -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 ,
67 assessLastAssistantMessage ,
78 dropThinkingBlocks ,
89 isAssistantMessageWithContent ,
@@ -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} ) ;
107158
108159describe ( "sanitizeThinkingForRecovery" , ( ) => {
@@ -191,11 +242,13 @@ describe("wrapAnthropicStreamWithRecovery", () => {
191242 "thinking or redacted_thinking blocks in the latest assistant message cannot be modified" ,
192243 ) ;
193244
194- 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 ( ) => {
195246 let callCount = 0 ;
247+ const contexts : Array < { messages ?: AgentMessage [ ] } > = [ ] ;
196248 const wrapped = wrapAnthropicStreamWithRecovery (
197- ( ( ) => {
249+ ( ( _model , context ) => {
198250 callCount += 1 ;
251+ contexts . push ( context as { messages ?: AgentMessage [ ] } ) ;
199252 return Promise . reject ( anthropicThinkingError ) ;
200253 } ) as Parameters < typeof wrapAnthropicStreamWithRecovery > [ 0 ] ,
201254 { id : "test-session" } ,
@@ -216,6 +269,44 @@ describe("wrapAnthropicStreamWithRecovery", () => {
216269 ) ,
217270 ) . rejects . toBe ( anthropicThinkingError ) ;
218271 expect ( 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 } ) ;
220311
221312 it ( "does not retry when the stream fails after yielding a chunk" , async ( ) => {
0 commit comments