@@ -451,6 +451,141 @@ describe("handleMessageUpdate text signatures", () => {
451451 ] ) ;
452452 } ) ;
453453
454+ it ( "uses incremental deltas for same-item phased streams" , ( ) => {
455+ const onAgentEvent = vi . fn ( ) ;
456+ const context = createMessageUpdateContext ( { onAgentEvent } ) ;
457+ const signature = JSON . stringify ( { v : 1 , id : "item-final" , phase : "final_answer" } ) ;
458+ const partial = {
459+ role : "assistant" ,
460+ phase : "final_answer" ,
461+ content : [
462+ {
463+ type : "text" ,
464+ textSignature : signature ,
465+ get text ( ) {
466+ throw new Error ( "full partial text should not be read" ) ;
467+ } ,
468+ } ,
469+ ] ,
470+ } ;
471+
472+ const createPhasedDelta = ( delta : string ) =>
473+ ( {
474+ type : "message_update" ,
475+ message : { role : "assistant" , content : [ ] } ,
476+ assistantMessageEvent : {
477+ type : "text_delta" ,
478+ delta,
479+ partial,
480+ } ,
481+ } ) as never ;
482+
483+ handleMessageUpdate ( context , createPhasedDelta ( "Hello" ) ) ;
484+ handleMessageUpdate ( context , createPhasedDelta ( " world" ) ) ;
485+
486+ expect ( onAgentEvent . mock . calls . map ( ( [ event ] ) => event ) ) . toMatchObject ( [
487+ {
488+ stream : "assistant" ,
489+ data : { text : "Hello" , delta : "Hello" , phase : "final_answer" } ,
490+ } ,
491+ {
492+ stream : "assistant" ,
493+ data : { text : "Hello world" , delta : " world" , phase : "final_answer" } ,
494+ } ,
495+ ] ) ;
496+ } ) ;
497+
498+ it ( "keeps same-item phased stream deltas on the user-visible sanitizer path" , ( ) => {
499+ const onAgentEvent = vi . fn ( ) ;
500+ const context = createMessageUpdateContext ( { onAgentEvent } ) ;
501+ const signature = JSON . stringify ( { v : 1 , id : "item-final" , phase : "final_answer" } ) ;
502+ const partial = {
503+ role : "assistant" ,
504+ phase : "final_answer" ,
505+ content : [
506+ {
507+ type : "text" ,
508+ textSignature : signature ,
509+ get text ( ) {
510+ throw new Error ( "full partial text should not be read" ) ;
511+ } ,
512+ } ,
513+ ] ,
514+ } ;
515+
516+ const createPhasedDelta = ( delta : string ) =>
517+ ( {
518+ type : "message_update" ,
519+ message : { role : "assistant" , content : [ ] } ,
520+ assistantMessageEvent : {
521+ type : "text_delta" ,
522+ delta,
523+ partial,
524+ } ,
525+ } ) as never ;
526+
527+ handleMessageUpdate ( context , createPhasedDelta ( "Visible\n<tool_call>{" ) ) ;
528+ handleMessageUpdate (
529+ context ,
530+ createPhasedDelta ( '"name":"read","arguments":{"file_path":"secret.md"}}</tool_call>' ) ,
531+ ) ;
532+ handleMessageUpdate ( context , createPhasedDelta ( "\nDone." ) ) ;
533+
534+ expect ( onAgentEvent . mock . calls . map ( ( [ event ] ) => event ) ) . toMatchObject ( [
535+ {
536+ stream : "assistant" ,
537+ data : { text : "Visible" , delta : "Visible" , phase : "final_answer" } ,
538+ } ,
539+ {
540+ stream : "assistant" ,
541+ data : { text : "Visible\n\nDone." , delta : "\n\nDone." , phase : "final_answer" } ,
542+ } ,
543+ ] ) ;
544+ } ) ;
545+
546+ it ( "keeps sanitizer context when a same-item phased stream starts hidden" , ( ) => {
547+ const onAgentEvent = vi . fn ( ) ;
548+ const context = createMessageUpdateContext ( { onAgentEvent } ) ;
549+ const signature = JSON . stringify ( { v : 1 , id : "item-final" , phase : "final_answer" } ) ;
550+ const partial = {
551+ role : "assistant" ,
552+ phase : "final_answer" ,
553+ content : [
554+ {
555+ type : "text" ,
556+ textSignature : signature ,
557+ get text ( ) {
558+ throw new Error ( "full partial text should not be read" ) ;
559+ } ,
560+ } ,
561+ ] ,
562+ } ;
563+
564+ const createPhasedDelta = ( delta : string ) =>
565+ ( {
566+ type : "message_update" ,
567+ message : { role : "assistant" , content : [ ] } ,
568+ assistantMessageEvent : {
569+ type : "text_delta" ,
570+ delta,
571+ partial,
572+ } ,
573+ } ) as never ;
574+
575+ handleMessageUpdate ( context , createPhasedDelta ( "<tool_call>{" ) ) ;
576+ handleMessageUpdate (
577+ context ,
578+ createPhasedDelta ( '"name":"read","arguments":{"file_path":"secret.md"}}</tool_call>\nDone.' ) ,
579+ ) ;
580+
581+ expect ( onAgentEvent . mock . calls . map ( ( [ event ] ) => event ) ) . toMatchObject ( [
582+ {
583+ stream : "assistant" ,
584+ data : { text : "Done." , delta : "Done." , phase : "final_answer" } ,
585+ } ,
586+ ] ) ;
587+ } ) ;
588+
454589 it ( "treats phased textSignature item changes as assistant-message boundaries" , ( ) => {
455590 const flushBlockReplyBuffer = vi . fn ( ) ;
456591 const resetAssistantMessageState = vi . fn ( ) ;
0 commit comments