@flamingo-stack/openframe-frontend-core 0.0.508 → 0.0.509
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chat-protocol/decode.d.ts +58 -0
- package/dist/chat-protocol/decode.d.ts.map +1 -0
- package/dist/chat-protocol/encode.d.ts +44 -0
- package/dist/chat-protocol/encode.d.ts.map +1 -0
- package/dist/chat-protocol/env-flag.d.ts +35 -0
- package/dist/chat-protocol/env-flag.d.ts.map +1 -0
- package/dist/chat-protocol/events.d.ts +216 -0
- package/dist/chat-protocol/events.d.ts.map +1 -0
- package/dist/chat-protocol/frames.d.ts +213 -0
- package/dist/chat-protocol/frames.d.ts.map +1 -0
- package/dist/chat-protocol/index.cjs +506 -0
- package/dist/chat-protocol/index.cjs.map +1 -0
- package/dist/chat-protocol/index.d.ts +18 -0
- package/dist/chat-protocol/index.d.ts.map +1 -0
- package/dist/chat-protocol/index.js +490 -0
- package/dist/chat-protocol/index.js.map +1 -0
- package/dist/chat-protocol/ip-normalize.d.ts +44 -0
- package/dist/chat-protocol/ip-normalize.d.ts.map +1 -0
- package/dist/chat-protocol/nats-decoder.d.ts +26 -0
- package/dist/chat-protocol/nats-decoder.d.ts.map +1 -0
- package/dist/{chunk-3SQ5KXHQ.cjs → chunk-2T4GTV25.cjs} +9 -9
- package/dist/{chunk-3SQ5KXHQ.cjs.map → chunk-2T4GTV25.cjs.map} +1 -1
- package/dist/{chunk-CWAOV2FK.cjs → chunk-3CGZPAGR.cjs} +3 -3
- package/dist/{chunk-CWAOV2FK.cjs.map → chunk-3CGZPAGR.cjs.map} +1 -1
- package/dist/{chunk-TLMJHMXJ.js → chunk-3QSR22IC.js} +12 -12
- package/dist/chunk-3QSR22IC.js.map +1 -0
- package/dist/{chunk-TWYNR4TZ.cjs → chunk-4G5TLHJD.cjs} +7141 -4799
- package/dist/chunk-4G5TLHJD.cjs.map +1 -0
- package/dist/{chunk-ADPMHWOE.js → chunk-72XAID7Y.js} +4 -4
- package/dist/{chunk-PAGKRNWK.js → chunk-7WZHBQ4J.js} +2 -2
- package/dist/{chunk-OCEKO5CW.js → chunk-AEELJC4N.js} +12106 -9764
- package/dist/chunk-AEELJC4N.js.map +1 -0
- package/dist/{chunk-Z42BGM6Q.cjs → chunk-BT2WWDFP.cjs} +5 -5
- package/dist/{chunk-Z42BGM6Q.cjs.map → chunk-BT2WWDFP.cjs.map} +1 -1
- package/dist/{chunk-7LEL3RIX.cjs → chunk-C7OY3PQP.cjs} +11 -11
- package/dist/{chunk-7LEL3RIX.cjs.map → chunk-C7OY3PQP.cjs.map} +1 -1
- package/dist/{chunk-RHGGEKPQ.cjs → chunk-CIOLOQKQ.cjs} +4 -4
- package/dist/{chunk-RHGGEKPQ.cjs.map → chunk-CIOLOQKQ.cjs.map} +1 -1
- package/dist/{chunk-TSZHM74B.js → chunk-DC2TKS7C.js} +2 -2
- package/dist/{chunk-M2GOR3XQ.cjs → chunk-DYPYR7FR.cjs} +87 -87
- package/dist/{chunk-M2GOR3XQ.cjs.map → chunk-DYPYR7FR.cjs.map} +1 -1
- package/dist/{chunk-I64ABCDX.js → chunk-I2B6X77L.js} +2 -2
- package/dist/{chunk-NIZDKTGL.cjs → chunk-K5MSCMDK.cjs} +37 -37
- package/dist/{chunk-NIZDKTGL.cjs.map → chunk-K5MSCMDK.cjs.map} +1 -1
- package/dist/{chunk-DTYRYB2N.js → chunk-K6QTZ7CZ.js} +2 -2
- package/dist/{chunk-4LTMXDUS.js → chunk-KBN7PMFT.js} +2 -2
- package/dist/{chunk-TBMV7I5N.js → chunk-KXZO2WZX.js} +2 -2
- package/dist/{chunk-QTYZMP6D.cjs → chunk-LBAKLT6T.cjs} +62 -62
- package/dist/chunk-LBAKLT6T.cjs.map +1 -0
- package/dist/{chunk-KEBYLU3U.js → chunk-LFUPI7VX.js} +2 -2
- package/dist/{chunk-LDTR4IVZ.cjs → chunk-M37PBG4N.cjs} +31 -31
- package/dist/{chunk-LDTR4IVZ.cjs.map → chunk-M37PBG4N.cjs.map} +1 -1
- package/dist/{chunk-WA7RR64F.js → chunk-PO5O654H.js} +6 -5
- package/dist/chunk-PO5O654H.js.map +1 -0
- package/dist/{chunk-WK4N5VBX.cjs → chunk-QJJISLPA.cjs} +28 -27
- package/dist/chunk-QJJISLPA.cjs.map +1 -0
- package/dist/{chunk-2F44PLTV.cjs → chunk-T7MCSA55.cjs} +26 -26
- package/dist/{chunk-2F44PLTV.cjs.map → chunk-T7MCSA55.cjs.map} +1 -1
- package/dist/{chunk-WJQHJD7J.js → chunk-TWSKTJHW.js} +2 -2
- package/dist/{chunk-ZSQHZYCO.cjs → chunk-UQDMJ6DH.cjs} +13 -13
- package/dist/{chunk-ZSQHZYCO.cjs.map → chunk-UQDMJ6DH.cjs.map} +1 -1
- package/dist/{chunk-TZRUCD56.js → chunk-V3ZAAFE3.js} +5 -5
- package/dist/{chunk-LRGHJPET.js → chunk-XGIFOHKE.js} +2 -2
- package/dist/{chunk-POOMO3PA.cjs → chunk-YQEPRYT5.cjs} +7 -7
- package/dist/{chunk-POOMO3PA.cjs.map → chunk-YQEPRYT5.cjs.map} +1 -1
- package/dist/components/case-studies/index.cjs +8 -8
- package/dist/components/case-studies/index.js +2 -2
- package/dist/components/chat/chat-message-enhanced.d.ts.map +1 -1
- package/dist/components/chat/chat-message-list.d.ts.map +1 -1
- package/dist/components/chat/hooks/index.d.ts +0 -1
- package/dist/components/chat/hooks/index.d.ts.map +1 -1
- package/dist/components/chat/hooks/use-chat.d.ts.map +1 -1
- package/dist/components/chat/hooks/use-chunk-catchup.d.ts.map +1 -1
- package/dist/components/chat/hooks/use-nats-chat-adapter.d.ts +15 -48
- package/dist/components/chat/hooks/use-nats-chat-adapter.d.ts.map +1 -1
- package/dist/components/chat/hooks/use-sse-chat-adapter.d.ts +10 -57
- package/dist/components/chat/hooks/use-sse-chat-adapter.d.ts.map +1 -1
- package/dist/components/chat/index.cjs +10 -2
- package/dist/components/chat/index.cjs.map +1 -1
- package/dist/components/chat/index.d.ts +1 -0
- package/dist/components/chat/index.d.ts.map +1 -1
- package/dist/components/chat/index.js +21 -13
- package/dist/components/chat/stream/chat-dialog-store.d.ts +184 -0
- package/dist/components/chat/stream/chat-dialog-store.d.ts.map +1 -0
- package/dist/components/chat/stream/chat-stream-reducer.d.ts +380 -0
- package/dist/components/chat/stream/chat-stream-reducer.d.ts.map +1 -0
- package/dist/components/chat/stream/delta-batcher.d.ts +51 -0
- package/dist/components/chat/stream/delta-batcher.d.ts.map +1 -0
- package/dist/components/chat/stream/index.d.ts +17 -0
- package/dist/components/chat/stream/index.d.ts.map +1 -0
- package/dist/components/chat/stream/message-mutations.d.ts +77 -0
- package/dist/components/chat/stream/message-mutations.d.ts.map +1 -0
- package/dist/components/chat/stream/use-chat-stream-reducer.d.ts +22 -0
- package/dist/components/chat/stream/use-chat-stream-reducer.d.ts.map +1 -0
- package/dist/components/chat/types/api.types.d.ts +1 -81
- package/dist/components/chat/types/api.types.d.ts.map +1 -1
- package/dist/components/chat/types/processing.types.d.ts +2 -93
- package/dist/components/chat/types/processing.types.d.ts.map +1 -1
- package/dist/components/chat/types/unified-chat-state.types.d.ts +15 -0
- package/dist/components/chat/types/unified-chat-state.types.d.ts.map +1 -1
- package/dist/components/chat/utils/extract-incomplete-message-state.d.ts +34 -6
- package/dist/components/chat/utils/extract-incomplete-message-state.d.ts.map +1 -1
- package/dist/components/chat/utils/history-merge.d.ts.map +1 -1
- package/dist/components/chat/utils/index.d.ts +2 -3
- package/dist/components/chat/utils/index.d.ts.map +1 -1
- package/dist/components/chat/utils/process-historical-messages.d.ts +59 -11
- package/dist/components/chat/utils/process-historical-messages.d.ts.map +1 -1
- package/dist/components/contact/index.cjs +3 -3
- package/dist/components/contact/index.js +2 -2
- package/dist/components/docs/index.cjs +5 -5
- package/dist/components/docs/index.js +4 -4
- package/dist/components/embeds/index.cjs +3 -3
- package/dist/components/embeds/index.js +2 -2
- package/dist/components/faq/index.cjs +3 -3
- package/dist/components/faq/index.js +2 -2
- package/dist/components/features/index.cjs +2 -2
- package/dist/components/features/index.js +1 -1
- package/dist/components/help-center-pages/index.cjs +22 -22
- package/dist/components/help-center-pages/index.js +13 -13
- package/dist/components/index.cjs +174 -132
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.js +65 -23
- package/dist/components/index.js.map +1 -1
- package/dist/components/meeting-scheduler/index.cjs +34 -34
- package/dist/components/meeting-scheduler/index.js +3 -3
- package/dist/components/navigation/index.cjs +2 -2
- package/dist/components/navigation/index.js +1 -1
- package/dist/components/onboarding-guides/index.cjs +5 -5
- package/dist/components/onboarding-guides/index.js +4 -4
- package/dist/components/onboarding-guides/onboarding-guide-detail-view.d.ts.map +1 -1
- package/dist/components/related-content/index.cjs +3 -3
- package/dist/components/related-content/index.js +2 -2
- package/dist/components/shared/product-release/release-detail-page.d.ts.map +1 -1
- package/dist/components/tickets/index.cjs +6 -6
- package/dist/components/tickets/index.js +5 -5
- package/dist/components/ui/index.cjs +44 -2
- package/dist/components/ui/index.cjs.map +1 -1
- package/dist/components/ui/index.d.ts +1 -2
- package/dist/components/ui/index.d.ts.map +1 -1
- package/dist/components/ui/index.js +55 -13
- package/dist/components/ui/markdown/base-components.d.ts +51 -0
- package/dist/components/ui/markdown/base-components.d.ts.map +1 -0
- package/dist/components/ui/markdown/engine.d.ts +99 -0
- package/dist/components/ui/markdown/engine.d.ts.map +1 -0
- package/dist/components/ui/markdown/heading-ids.d.ts +65 -0
- package/dist/components/ui/markdown/heading-ids.d.ts.map +1 -0
- package/dist/components/ui/markdown/index.d.ts +18 -0
- package/dist/components/ui/markdown/index.d.ts.map +1 -0
- package/dist/components/ui/markdown/mermaid-diagram.d.ts +79 -0
- package/dist/components/ui/markdown/mermaid-diagram.d.ts.map +1 -0
- package/dist/components/ui/markdown/rich/embed-overrides.d.ts +14 -0
- package/dist/components/ui/markdown/rich/embed-overrides.d.ts.map +1 -0
- package/dist/components/ui/markdown/rich/rich-markdown-renderer.d.ts +40 -0
- package/dist/components/ui/markdown/rich/rich-markdown-renderer.d.ts.map +1 -0
- package/dist/components/ui/markdown/rich/shortcodes.d.ts +8 -0
- package/dist/components/ui/markdown/rich/shortcodes.d.ts.map +1 -0
- package/dist/components/ui/markdown/sanitize.d.ts +153 -0
- package/dist/components/ui/markdown/sanitize.d.ts.map +1 -0
- package/dist/components/ui/markdown/simple-markdown-renderer.d.ts +23 -0
- package/dist/components/ui/markdown/simple-markdown-renderer.d.ts.map +1 -0
- package/dist/components/ui/markdown/streaming.d.ts +78 -0
- package/dist/components/ui/markdown/streaming.d.ts.map +1 -0
- package/dist/components/ui/markdown/text-size.d.ts +34 -0
- package/dist/components/ui/markdown/text-size.d.ts.map +1 -0
- package/dist/index.cjs +60 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +71 -13
- package/dist/utils/index.cjs +169 -40
- package/dist/utils/index.cjs.map +1 -1
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +162 -41
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/markdown-fences.d.ts +42 -0
- package/dist/utils/markdown-fences.d.ts.map +1 -0
- package/dist/utils/markdown-heading-id.d.ts +85 -0
- package/dist/utils/markdown-heading-id.d.ts.map +1 -0
- package/dist/utils/markdown-section-extractor.d.ts.map +1 -1
- package/package.json +7 -1
- package/src/chat-protocol/__tests__/__snapshots__/nats-decoder-golden.test.ts.snap +319 -0
- package/src/chat-protocol/__tests__/chat-protocol.test.ts +516 -0
- package/src/chat-protocol/__tests__/env-flag.test.ts +52 -0
- package/src/chat-protocol/__tests__/ip-normalize.test.ts +136 -0
- package/src/chat-protocol/__tests__/nats-decoder-golden.test.ts +237 -0
- package/src/chat-protocol/decode.ts +278 -0
- package/src/chat-protocol/encode.ts +71 -0
- package/src/chat-protocol/env-flag.ts +39 -0
- package/src/chat-protocol/events.ts +254 -0
- package/src/chat-protocol/frames.ts +245 -0
- package/src/chat-protocol/index.ts +21 -0
- package/src/chat-protocol/ip-normalize.ts +146 -0
- package/src/chat-protocol/nats-decoder.ts +264 -0
- package/src/components/chat/.chat-message-list.md +10 -6
- package/src/components/chat/__tests__/chat-message-list.test.tsx +450 -9
- package/src/components/chat/__tests__/chat-message-streaming-memo.test.tsx +207 -0
- package/src/components/chat/__tests__/chat-pending-turn.test.tsx +250 -0
- package/src/components/chat/chat-message-enhanced.tsx +122 -20
- package/src/components/chat/chat-message-list.tsx +515 -133
- package/src/components/chat/embeddable-chat.tsx +6 -0
- package/src/components/chat/hooks/.index.md +30 -33
- package/src/components/chat/hooks/.use-nats-chat-adapter.md +36 -55
- package/src/components/chat/hooks/__tests__/__snapshots__/conversation-id-persistence-golden.test.ts.snap +37 -0
- package/src/components/chat/hooks/__tests__/__snapshots__/sse-stream-golden.test.ts.snap +0 -0
- package/src/components/chat/hooks/__tests__/chunk-catchup-dialog-staleness.test.ts +92 -0
- package/src/components/chat/hooks/__tests__/conversation-id-persistence-golden.test.ts +438 -0
- package/src/components/chat/hooks/__tests__/sse-stream-golden.test.ts +318 -0
- package/src/components/chat/hooks/index.ts +0 -1
- package/src/components/chat/hooks/use-chat.ts +8 -0
- package/src/components/chat/hooks/use-chunk-catchup.ts +26 -1
- package/src/components/chat/hooks/use-nats-chat-adapter.ts +254 -825
- package/src/components/chat/hooks/use-sse-chat-adapter.ts +380 -684
- package/src/components/chat/index.ts +5 -0
- package/src/components/chat/stream/__tests__/__snapshots__/chat-stream-reducer-golden.test.ts.snap +944 -0
- package/src/components/chat/stream/__tests__/chat-dialog-store.test.ts +806 -0
- package/src/components/chat/stream/__tests__/chat-stream-reducer-golden.test.ts +467 -0
- package/src/components/chat/stream/__tests__/chat-stream-reducer.test.ts +1330 -0
- package/src/components/chat/stream/__tests__/delta-batcher.test.ts +255 -0
- package/src/components/chat/stream/__tests__/use-chat-stream-reducer.test.ts +105 -0
- package/src/components/chat/stream/chat-dialog-store.ts +536 -0
- package/src/components/chat/stream/chat-stream-reducer.ts +1928 -0
- package/src/components/chat/stream/delta-batcher.ts +168 -0
- package/src/components/chat/stream/index.ts +55 -0
- package/src/components/chat/stream/message-mutations.ts +340 -0
- package/src/components/chat/stream/use-chat-stream-reducer.ts +126 -0
- package/src/components/chat/thinking-display.tsx +1 -1
- package/src/components/chat/types/.api.types.md +39 -49
- package/src/components/chat/types/.processing.types.md +29 -58
- package/src/components/chat/types/.unified-chat-state.types.md +2 -2
- package/src/components/chat/types/api.types.ts +6 -73
- package/src/components/chat/types/processing.types.ts +11 -53
- package/src/components/chat/types/unified-chat-state.types.ts +16 -0
- package/src/components/chat/utils/.extract-incomplete-message-state.md +21 -3
- package/src/components/chat/utils/.history-merge.md +44 -3
- package/src/components/chat/utils/.index.md +31 -40
- package/src/components/chat/utils/__tests__/__snapshots__/process-historical-messages-golden.test.ts.snap +420 -0
- package/src/components/chat/utils/__tests__/__snapshots__/segment-accumulator-golden.test.ts.snap +605 -0
- package/src/components/chat/utils/__tests__/extract-incomplete-message-state.test.ts +106 -0
- package/src/components/chat/utils/__tests__/history-merge.test.ts +160 -0
- package/src/components/chat/utils/__tests__/process-historical-messages-approvals.test.ts +61 -0
- package/src/components/chat/utils/__tests__/process-historical-messages-golden.test.ts +394 -0
- package/src/components/chat/utils/__tests__/segment-accumulator-golden.test.ts +270 -0
- package/src/components/chat/utils/extract-incomplete-message-state.ts +93 -6
- package/src/components/chat/utils/history-merge.ts +85 -5
- package/src/components/chat/utils/index.ts +5 -8
- package/src/components/chat/utils/process-historical-messages.ts +361 -385
- package/src/components/onboarding-guides/onboarding-guide-detail-view.tsx +22 -4
- package/src/components/shared/legal-document/legal-document-page.tsx +1 -1
- package/src/components/shared/product-release/release-detail-page.tsx +19 -4
- package/src/components/ui/__tests__/__snapshots__/markdown-parity.test.tsx.snap +2420 -0
- package/src/components/ui/__tests__/markdown-parity.test.tsx +2540 -0
- package/src/components/ui/index.ts +1 -2
- package/src/components/ui/markdown/__tests__/mermaid-security.test.ts +176 -0
- package/src/components/ui/markdown/__tests__/mermaid-stale-render.test.tsx +151 -0
- package/src/components/ui/markdown/__tests__/sanitize-invariant.test.ts +354 -0
- package/src/components/ui/markdown/__tests__/sanitize-render.test.tsx +118 -0
- package/src/components/ui/markdown/__tests__/streaming.test.tsx +436 -0
- package/src/components/ui/markdown/base-components.tsx +360 -0
- package/src/components/ui/markdown/engine.tsx +315 -0
- package/src/components/ui/markdown/heading-ids.ts +239 -0
- package/src/components/ui/markdown/index.ts +49 -0
- package/src/components/ui/markdown/mermaid-diagram.tsx +323 -0
- package/src/components/ui/markdown/rich/embed-overrides.tsx +199 -0
- package/src/components/ui/markdown/rich/rich-markdown-renderer.tsx +184 -0
- package/src/components/ui/markdown/rich/shortcodes.ts +170 -0
- package/src/components/ui/markdown/sanitize.ts +3584 -0
- package/src/components/ui/markdown/simple-markdown-renderer.tsx +28 -0
- package/src/components/ui/markdown/streaming.ts +390 -0
- package/src/components/ui/markdown/text-size.ts +106 -0
- package/src/components/ui/release-changelog-section.tsx +1 -1
- package/src/components/ui/ticket-info-section.tsx +1 -1
- package/src/utils/index.ts +1 -0
- package/src/utils/markdown-fences.ts +127 -0
- package/src/utils/markdown-heading-id.ts +348 -0
- package/src/utils/markdown-section-extractor.ts +41 -56
- package/dist/chunk-OCEKO5CW.js.map +0 -1
- package/dist/chunk-QTYZMP6D.cjs.map +0 -1
- package/dist/chunk-TLMJHMXJ.js.map +0 -1
- package/dist/chunk-TWYNR4TZ.cjs.map +0 -1
- package/dist/chunk-WA7RR64F.js.map +0 -1
- package/dist/chunk-WK4N5VBX.cjs.map +0 -1
- package/dist/components/chat/hooks/use-realtime-chunk-processor.d.ts +0 -6
- package/dist/components/chat/hooks/use-realtime-chunk-processor.d.ts.map +0 -1
- package/dist/components/chat/utils/chunk-parser.d.ts +0 -25
- package/dist/components/chat/utils/chunk-parser.d.ts.map +0 -1
- package/dist/components/ui/rich-markdown-renderer.d.ts +0 -34
- package/dist/components/ui/rich-markdown-renderer.d.ts.map +0 -1
- package/dist/components/ui/simple-markdown-renderer.d.ts +0 -73
- package/dist/components/ui/simple-markdown-renderer.d.ts.map +0 -1
- package/src/components/chat/hooks/.use-realtime-chunk-processor.md +0 -77
- package/src/components/chat/hooks/use-realtime-chunk-processor.ts +0 -489
- package/src/components/chat/utils/.chunk-parser.md +0 -62
- package/src/components/chat/utils/chunk-parser.ts +0 -262
- package/src/components/ui/.simple-markdown-renderer.md +0 -52
- package/src/components/ui/rich-markdown-renderer.tsx +0 -1223
- package/src/components/ui/simple-markdown-renderer.tsx +0 -964
- /package/dist/{chunk-ADPMHWOE.js.map → chunk-72XAID7Y.js.map} +0 -0
- /package/dist/{chunk-PAGKRNWK.js.map → chunk-7WZHBQ4J.js.map} +0 -0
- /package/dist/{chunk-TSZHM74B.js.map → chunk-DC2TKS7C.js.map} +0 -0
- /package/dist/{chunk-I64ABCDX.js.map → chunk-I2B6X77L.js.map} +0 -0
- /package/dist/{chunk-DTYRYB2N.js.map → chunk-K6QTZ7CZ.js.map} +0 -0
- /package/dist/{chunk-4LTMXDUS.js.map → chunk-KBN7PMFT.js.map} +0 -0
- /package/dist/{chunk-TBMV7I5N.js.map → chunk-KXZO2WZX.js.map} +0 -0
- /package/dist/{chunk-KEBYLU3U.js.map → chunk-LFUPI7VX.js.map} +0 -0
- /package/dist/{chunk-WJQHJD7J.js.map → chunk-TWSKTJHW.js.map} +0 -0
- /package/dist/{chunk-TZRUCD56.js.map → chunk-V3ZAAFE3.js.map} +0 -0
- /package/dist/{chunk-LRGHJPET.js.map → chunk-XGIFOHKE.js.map} +0 -0
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase-0 GOLDEN CHARACTERIZATION TESTS — `MessageSegmentAccumulator`.
|
|
3
|
+
*
|
|
4
|
+
* Pins the CURRENT segment-accumulation semantics before the stream-reader
|
|
5
|
+
* unification. Each test drives the accumulator through a recorded event
|
|
6
|
+
* sequence and snapshots the final segment array. Behaviors captured here
|
|
7
|
+
* (including the implicit approval-flip on tool execution and the batch
|
|
8
|
+
* no-downgrade rule) are the recorded baseline the future reducer must match.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, it, expect } from 'vitest'
|
|
12
|
+
import { createMessageSegmentAccumulator } from '../message-segment-accumulator'
|
|
13
|
+
import type { ToolExecutionSegment, PendingToolCallData } from '../../types'
|
|
14
|
+
|
|
15
|
+
const executing = (
|
|
16
|
+
execId: string | undefined,
|
|
17
|
+
overrides: Partial<ToolExecutionSegment['data']> = {},
|
|
18
|
+
): ToolExecutionSegment => ({
|
|
19
|
+
type: 'tool_execution',
|
|
20
|
+
data: {
|
|
21
|
+
type: 'EXECUTING_TOOL',
|
|
22
|
+
integratedToolType: 'TACTICAL_RMM',
|
|
23
|
+
toolFunction: 'run_script',
|
|
24
|
+
toolTitle: 'Run cleanup script',
|
|
25
|
+
parameters: { script: 'cleanup.sh' },
|
|
26
|
+
...(execId ? { toolExecutionRequestId: execId } : {}),
|
|
27
|
+
...overrides,
|
|
28
|
+
},
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
const executed = (
|
|
32
|
+
execId: string | undefined,
|
|
33
|
+
overrides: Partial<ToolExecutionSegment['data']> = {},
|
|
34
|
+
): ToolExecutionSegment => ({
|
|
35
|
+
type: 'tool_execution',
|
|
36
|
+
data: {
|
|
37
|
+
type: 'EXECUTED_TOOL',
|
|
38
|
+
integratedToolType: 'TACTICAL_RMM',
|
|
39
|
+
toolFunction: 'run_script',
|
|
40
|
+
// Backend omits toolTitle on EXECUTED_TOOL — the accumulator restores it.
|
|
41
|
+
result: 'Freed 2.3 GB',
|
|
42
|
+
success: true,
|
|
43
|
+
...(execId ? { toolExecutionRequestId: execId } : {}),
|
|
44
|
+
...overrides,
|
|
45
|
+
},
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
const batchToolCalls: PendingToolCallData[] = [
|
|
49
|
+
{
|
|
50
|
+
toolExecutionRequestId: 'exec-a',
|
|
51
|
+
toolName: 'create_ticket',
|
|
52
|
+
toolTitle: 'Create ticket',
|
|
53
|
+
toolExplanation: 'Opens a support ticket',
|
|
54
|
+
requiresApproval: true,
|
|
55
|
+
approvalType: 'CLIENT',
|
|
56
|
+
toolCallArguments: { subject: 'Printer down' },
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
toolExecutionRequestId: 'exec-b',
|
|
60
|
+
toolName: 'read_kb',
|
|
61
|
+
requiresApproval: false,
|
|
62
|
+
approvalType: null,
|
|
63
|
+
toolCallArguments: null,
|
|
64
|
+
},
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
describe('MessageSegmentAccumulator — golden sequences', () => {
|
|
68
|
+
it('coalesces consecutive text deltas into one text segment', () => {
|
|
69
|
+
const acc = createMessageSegmentAccumulator()
|
|
70
|
+
acc.appendText('Hello')
|
|
71
|
+
acc.appendText(', ')
|
|
72
|
+
acc.appendText('world')
|
|
73
|
+
expect(acc.getSegments()).toMatchSnapshot()
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('coalesces thinking deltas, and text after thinking starts a NEW segment', () => {
|
|
77
|
+
const acc = createMessageSegmentAccumulator()
|
|
78
|
+
acc.appendThinking('Let me think')
|
|
79
|
+
acc.appendThinking('… ok.')
|
|
80
|
+
acc.appendText('Answer: ')
|
|
81
|
+
acc.appendText('42')
|
|
82
|
+
// A late thinking delta after text starts ANOTHER thinking segment
|
|
83
|
+
// (append-to-last only merges into a trailing segment of the same type).
|
|
84
|
+
acc.appendThinking('post-answer thought')
|
|
85
|
+
expect(acc.getSegments()).toMatchSnapshot()
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('merges EXECUTING_TOOL → EXECUTED_TOOL in place by toolExecutionRequestId (title restored)', () => {
|
|
89
|
+
const acc = createMessageSegmentAccumulator()
|
|
90
|
+
acc.appendText('Running a cleanup now.')
|
|
91
|
+
acc.addToolExecution(executing('exec-1'))
|
|
92
|
+
acc.addToolExecution(executed('exec-1'))
|
|
93
|
+
expect(acc.getSegments()).toMatchSnapshot()
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it('pairs by (integratedToolType, toolFunction) when no exec id is present (older backends)', () => {
|
|
97
|
+
const acc = createMessageSegmentAccumulator()
|
|
98
|
+
acc.addToolExecution(executing(undefined))
|
|
99
|
+
acc.addToolExecution(executed(undefined))
|
|
100
|
+
expect(acc.getSegments()).toMatchSnapshot()
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('EXECUTED with no prior EXECUTING lands as a standalone completed segment', () => {
|
|
104
|
+
const acc = createMessageSegmentAccumulator()
|
|
105
|
+
acc.addToolExecution(executed('exec-orphan'))
|
|
106
|
+
expect(acc.getSegments()).toMatchSnapshot()
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it('CHARACTERIZATION: a LATE standalone EXECUTING after its EXECUTED pushes a NEW executing segment (no in-place downgrade, but a duplicate card appears)', () => {
|
|
110
|
+
// The no-downgrade rule is enforced INSIDE approval batches
|
|
111
|
+
// (applyExecutionToBatch); for standalone segments a redelivered
|
|
112
|
+
// EXECUTING after EXECUTED does NOT downgrade the merged segment —
|
|
113
|
+
// it appends a fresh EXECUTING segment. Recording as-is, see
|
|
114
|
+
// unification plan.
|
|
115
|
+
const acc = createMessageSegmentAccumulator()
|
|
116
|
+
acc.addToolExecution(executing('exec-1'))
|
|
117
|
+
acc.addToolExecution(executed('exec-1'))
|
|
118
|
+
acc.addToolExecution(executing('exec-1'))
|
|
119
|
+
expect(acc.getSegments()).toMatchSnapshot()
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
it('single approval request → updateApprovalStatus flips status and stamps resolvedByName only on batches', () => {
|
|
123
|
+
const acc = createMessageSegmentAccumulator({
|
|
124
|
+
onApprove: async () => {},
|
|
125
|
+
onReject: async () => {},
|
|
126
|
+
})
|
|
127
|
+
acc.addApprovalRequest('req-1', 'systemctl restart nats', 'Restart broker', 'CLIENT')
|
|
128
|
+
acc.updateApprovalStatus('req-1', 'approved', 'Jane Admin')
|
|
129
|
+
expect(acc.getSegments()).toMatchSnapshot()
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
it('tracked (pending) approval + processApprovalResult appends a resolved segment', () => {
|
|
133
|
+
const acc = createMessageSegmentAccumulator()
|
|
134
|
+
acc.trackApprovalRequest('req-2', {
|
|
135
|
+
command: 'rm -rf /tmp/cache',
|
|
136
|
+
explanation: 'Clear caches',
|
|
137
|
+
approvalType: 'CLIENT',
|
|
138
|
+
})
|
|
139
|
+
const result = acc.processApprovalResult('req-2', false, 'CLIENT')
|
|
140
|
+
expect({
|
|
141
|
+
segments: acc.getSegments(),
|
|
142
|
+
pendingData: result?.pendingData ?? null,
|
|
143
|
+
hasPendingAfter: acc.hasPendingApprovals(),
|
|
144
|
+
}).toMatchSnapshot()
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
it('batch approval with per-tool executions merged into the batch (no standalone segments)', () => {
|
|
148
|
+
const acc = createMessageSegmentAccumulator()
|
|
149
|
+
acc.addApprovalBatch('batch-1', 'CLIENT', batchToolCalls)
|
|
150
|
+
acc.updateApprovalStatus('batch-1', 'approved', 'Jane Admin')
|
|
151
|
+
// Executions matching the batch's toolExecutionRequestIds merge into
|
|
152
|
+
// `executions` instead of pushing standalone tool_execution segments.
|
|
153
|
+
acc.addToolExecution(executing('exec-a', { toolFunction: 'create_ticket' }))
|
|
154
|
+
acc.addToolExecution(
|
|
155
|
+
executed('exec-a', { toolFunction: 'create_ticket', result: 'Ticket #77 created' }),
|
|
156
|
+
)
|
|
157
|
+
acc.addToolExecution(executing('exec-b', { toolFunction: 'read_kb' }))
|
|
158
|
+
expect(acc.getSegments()).toMatchSnapshot()
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
it('batch no-downgrade rule: a redelivered EXECUTING after EXECUTED keeps the done slot', () => {
|
|
162
|
+
const acc = createMessageSegmentAccumulator()
|
|
163
|
+
acc.addApprovalBatch('batch-1', 'CLIENT', batchToolCalls, 'approved')
|
|
164
|
+
acc.addToolExecution(executed('exec-a', { result: 'done!' }))
|
|
165
|
+
// Late/redelivered EXECUTING for the same exec id — must NOT flip
|
|
166
|
+
// the slot back to 'executing'.
|
|
167
|
+
acc.addToolExecution(executing('exec-a'))
|
|
168
|
+
expect(acc.getSegments()).toMatchSnapshot()
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
it('addApprovalBatch upserts by approvalRequestId (replay path produces ONE batch segment)', () => {
|
|
172
|
+
const acc = createMessageSegmentAccumulator()
|
|
173
|
+
acc.addApprovalBatch('batch-1', 'CLIENT', batchToolCalls, 'pending')
|
|
174
|
+
acc.addApprovalBatch('batch-1', 'CLIENT', batchToolCalls, 'approved', {
|
|
175
|
+
'exec-a': { status: 'done', result: 'ok', success: true },
|
|
176
|
+
}, 'Jane Admin')
|
|
177
|
+
expect(acc.getSegments()).toMatchSnapshot()
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
it('implicit resolvePendingApprovalForExecution: a tool execution flips the LATEST pending single approval (NATS-observer behavior)', () => {
|
|
181
|
+
// An observer (technician mirroring a client chat) may never receive
|
|
182
|
+
// APPROVAL_RESULT — only the tool's EXECUTING/EXECUTED events. The
|
|
183
|
+
// accumulator treats a tool execution as implicit approval of the most
|
|
184
|
+
// recent still-pending single approval gate. Pinning explicitly.
|
|
185
|
+
const acc = createMessageSegmentAccumulator()
|
|
186
|
+
acc.addApprovalRequest('req-old', 'echo old', undefined, 'CLIENT', 'rejected')
|
|
187
|
+
acc.addApprovalRequest('req-1', 'systemctl restart nats', undefined, 'CLIENT', 'pending')
|
|
188
|
+
acc.addToolExecution(executing('exec-9'))
|
|
189
|
+
expect(acc.getSegments()).toMatchSnapshot()
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
it('implicit approval flips ONLY the latest pending gate, earlier pending gates stay pending', () => {
|
|
193
|
+
const acc = createMessageSegmentAccumulator()
|
|
194
|
+
acc.addApprovalRequest('req-1', 'first', undefined, 'CLIENT', 'pending')
|
|
195
|
+
acc.addApprovalRequest('req-2', 'second', undefined, 'CLIENT', 'pending')
|
|
196
|
+
acc.addToolExecution(executing('exec-1'))
|
|
197
|
+
expect(acc.getSegments()).toMatchSnapshot()
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
it('implicit approval does NOT touch approval_batch segments (handled by applyExecutionToBatch only)', () => {
|
|
201
|
+
const acc = createMessageSegmentAccumulator()
|
|
202
|
+
acc.addApprovalBatch('batch-1', 'CLIENT', batchToolCalls, 'pending')
|
|
203
|
+
// exec id NOT in the batch → standalone segment path; batch stays pending.
|
|
204
|
+
acc.addToolExecution(executing('exec-unrelated'))
|
|
205
|
+
expect(acc.getSegments()).toMatchSnapshot()
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
it('context compaction start → complete replaces the started segment in place', () => {
|
|
209
|
+
const acc = createMessageSegmentAccumulator()
|
|
210
|
+
acc.appendText('Before compaction')
|
|
211
|
+
acc.addContextCompaction()
|
|
212
|
+
acc.completeContextCompaction('Summarized 34 messages')
|
|
213
|
+
acc.appendText('After compaction')
|
|
214
|
+
expect(acc.getSegments()).toMatchSnapshot()
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
it('completeContextCompaction with no started segment pushes a completed one', () => {
|
|
218
|
+
const acc = createMessageSegmentAccumulator()
|
|
219
|
+
acc.completeContextCompaction(undefined)
|
|
220
|
+
expect(acc.getSegments()).toMatchSnapshot()
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
it('error segments append verbatim', () => {
|
|
224
|
+
const acc = createMessageSegmentAccumulator()
|
|
225
|
+
acc.appendText('partial answer')
|
|
226
|
+
acc.addError('Agent crashed', 'boom')
|
|
227
|
+
expect(acc.getSegments()).toMatchSnapshot()
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
it('flushPendingApprovals emits pending cards for every tracked-but-unresolved approval', () => {
|
|
231
|
+
const acc = createMessageSegmentAccumulator()
|
|
232
|
+
acc.trackApprovalRequest('req-1', { command: 'cmd-1', approvalType: 'CLIENT' })
|
|
233
|
+
acc.trackApprovalRequest('req-2', { command: 'cmd-2', explanation: 'why', approvalType: 'ADMIN' })
|
|
234
|
+
expect(acc.flushPendingApprovals()).toMatchSnapshot()
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
it('replaySegments reproduces a mixed recorded segment array (idempotent replay)', () => {
|
|
238
|
+
const acc = createMessageSegmentAccumulator()
|
|
239
|
+
acc.appendThinking('hmm')
|
|
240
|
+
acc.appendText('Sure — ')
|
|
241
|
+
acc.addToolExecution(executing('exec-1'))
|
|
242
|
+
acc.addToolExecution(executed('exec-1'))
|
|
243
|
+
acc.addApprovalBatch('batch-1', 'CLIENT', batchToolCalls, 'approved', {
|
|
244
|
+
'exec-a': { status: 'done', result: 'ok', success: true },
|
|
245
|
+
})
|
|
246
|
+
acc.appendText('done.')
|
|
247
|
+
const recorded = acc.getSegments()
|
|
248
|
+
|
|
249
|
+
const replayed = createMessageSegmentAccumulator().replaySegments(recorded)
|
|
250
|
+
expect(replayed).toMatchSnapshot()
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
it('initializeWithState continues an incomplete message across reconnect', () => {
|
|
254
|
+
const first = createMessageSegmentAccumulator()
|
|
255
|
+
first.appendText('partial ')
|
|
256
|
+
first.addToolExecution(executing('exec-1'))
|
|
257
|
+
const state = first.getState()
|
|
258
|
+
|
|
259
|
+
const resumed = createMessageSegmentAccumulator()
|
|
260
|
+
resumed.initializeWithState({
|
|
261
|
+
existingSegments: state.segments,
|
|
262
|
+
pendingApprovals: state.pendingApprovals,
|
|
263
|
+
executingTools: state.executingTools,
|
|
264
|
+
})
|
|
265
|
+
// The EXECUTED for the pre-reconnect EXECUTING merges into it.
|
|
266
|
+
resumed.addToolExecution(executed('exec-1'))
|
|
267
|
+
resumed.appendText('and the rest.')
|
|
268
|
+
expect(resumed.getSegments()).toMatchSnapshot()
|
|
269
|
+
})
|
|
270
|
+
})
|
|
@@ -9,6 +9,18 @@ import type {
|
|
|
9
9
|
PendingApproval,
|
|
10
10
|
ExecutingToolState,
|
|
11
11
|
} from '../types'
|
|
12
|
+
import type { InitializeExtras } from '../stream/chat-stream-reducer'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* What the realtime accumulator needs to RESUME an unfinished turn.
|
|
16
|
+
*
|
|
17
|
+
* DERIVED, not restated: this is exactly the reducer's `InitializeExtras`
|
|
18
|
+
* minus `escalatedApprovals` (which only the live stream can produce — it has
|
|
19
|
+
* no historical representation to extract). Hosts pass the result straight
|
|
20
|
+
* into `reducer.initializeWithState(messages, extras)`, so the two must not
|
|
21
|
+
* drift; a structural coincidence is not a contract.
|
|
22
|
+
*/
|
|
23
|
+
export type IncompleteMessageState = Omit<InitializeExtras, 'escalatedApprovals'>
|
|
12
24
|
|
|
13
25
|
/**
|
|
14
26
|
* Extract incomplete message state from the last historical assistant message
|
|
@@ -16,11 +28,7 @@ import type {
|
|
|
16
28
|
*/
|
|
17
29
|
export function extractIncompleteMessageState(
|
|
18
30
|
lastMessage: ProcessedMessage | undefined
|
|
19
|
-
): {
|
|
20
|
-
existingSegments?: MessageSegment[]
|
|
21
|
-
pendingApprovals?: Map<string, PendingApproval>
|
|
22
|
-
executingTools?: Map<string, ExecutingToolState>
|
|
23
|
-
} | undefined {
|
|
31
|
+
): IncompleteMessageState | undefined {
|
|
24
32
|
if (!lastMessage || lastMessage.role !== 'assistant' || typeof lastMessage.content === 'string') {
|
|
25
33
|
return undefined
|
|
26
34
|
}
|
|
@@ -29,8 +37,15 @@ export function extractIncompleteMessageState(
|
|
|
29
37
|
const pendingApprovals = new Map<string, PendingApproval>()
|
|
30
38
|
const executingTools = new Map<string, ExecutingToolState>()
|
|
31
39
|
let hasIncompleteState = false
|
|
40
|
+
// "Incomplete" and "the agent is working" are DIFFERENT questions, and only
|
|
41
|
+
// the second one drives the activity indicator. A pending approval is the
|
|
42
|
+
// clearest case of incomplete-but-idle: the turn is unfinished precisely
|
|
43
|
+
// BECAUSE the agent is blocked on the user, and spinning there claims work
|
|
44
|
+
// that is not happening. Everything below is the complement of that.
|
|
45
|
+
let agentBusy = false
|
|
32
46
|
|
|
33
|
-
segments.forEach(segment => {
|
|
47
|
+
segments.forEach((segment, index) => {
|
|
48
|
+
const isLast = index === segments.length - 1
|
|
34
49
|
switch (segment.type) {
|
|
35
50
|
case 'tool_execution':
|
|
36
51
|
if (segment.data.type === 'EXECUTING_TOOL') {
|
|
@@ -45,6 +60,8 @@ export function extractIncompleteMessageState(
|
|
|
45
60
|
parameters: segment.data.parameters,
|
|
46
61
|
})
|
|
47
62
|
hasIncompleteState = true
|
|
63
|
+
// An EXECUTING with no result: the tool is running right now.
|
|
64
|
+
agentBusy = true
|
|
48
65
|
}
|
|
49
66
|
break
|
|
50
67
|
|
|
@@ -56,6 +73,17 @@ export function extractIncompleteMessageState(
|
|
|
56
73
|
approvalType: segment.data.approvalType || 'CLIENT',
|
|
57
74
|
})
|
|
58
75
|
hasIncompleteState = true
|
|
76
|
+
} else if (isLast && segment.status === 'approved') {
|
|
77
|
+
// The user said yes and the agent has produced NOTHING since — it is
|
|
78
|
+
// executing the command. Position is the only signal here: unlike a
|
|
79
|
+
// batch, a single request carries no execution record, so an approved
|
|
80
|
+
// request that already ran is recognised by the work that follows it.
|
|
81
|
+
//
|
|
82
|
+
// This also has to mark the tail incomplete, or the state never
|
|
83
|
+
// reaches the reducer at all — and the continuation, when it replays,
|
|
84
|
+
// must merge into THIS bubble rather than open a second one.
|
|
85
|
+
hasIncompleteState = true
|
|
86
|
+
agentBusy = true
|
|
59
87
|
}
|
|
60
88
|
break
|
|
61
89
|
|
|
@@ -72,12 +100,20 @@ export function extractIncompleteMessageState(
|
|
|
72
100
|
if (segment.status !== 'rejected' && !allDone) {
|
|
73
101
|
hasIncompleteState = true
|
|
74
102
|
}
|
|
103
|
+
// Approved with tool calls still outstanding — the agent is running
|
|
104
|
+
// them. The execution record makes this position-independent: once
|
|
105
|
+
// every call is `done` the batch stops claiming work.
|
|
106
|
+
if (segment.status === 'approved' && !allDone) {
|
|
107
|
+
agentBusy = true
|
|
108
|
+
}
|
|
75
109
|
break
|
|
76
110
|
}
|
|
77
111
|
|
|
78
112
|
case 'context_compaction':
|
|
79
113
|
if (segment.status === 'started') {
|
|
80
114
|
hasIncompleteState = true
|
|
115
|
+
// Compaction is the agent's own work, not a wait on the user.
|
|
116
|
+
agentBusy = true
|
|
81
117
|
}
|
|
82
118
|
break
|
|
83
119
|
}
|
|
@@ -91,5 +127,56 @@ export function extractIncompleteMessageState(
|
|
|
91
127
|
existingSegments: segments,
|
|
92
128
|
pendingApprovals: pendingApprovals.size > 0 ? pendingApprovals : undefined,
|
|
93
129
|
executingTools: executingTools.size > 0 ? executingTools : undefined,
|
|
130
|
+
...(agentBusy ? { agentBusy: true } : {}),
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* THREAD-level generalization of `extractIncompleteMessageState`: given a
|
|
136
|
+
* processed thread, look at the TRAILING RUN of consecutive assistant
|
|
137
|
+
* messages (not just the very last row), flatten it to one segment list, and
|
|
138
|
+
* ask the single-message extractor whether that tail is unfinished.
|
|
139
|
+
*
|
|
140
|
+
* Why the run and not the last row: the backend can split one logical turn
|
|
141
|
+
* across several assistant messages (a preamble bubble, then the approval
|
|
142
|
+
* card, then the continuation). The unfinished artifact — a pending approval,
|
|
143
|
+
* an EXECUTING_TOOL with no result, an open compaction — can therefore sit in
|
|
144
|
+
* an EARLIER bubble of the same trailing run, and a last-row-only check
|
|
145
|
+
* reports "complete" while the turn is very much still in flight.
|
|
146
|
+
*
|
|
147
|
+
* SSOT for every "is the tail incomplete?" question — the NATS adapter (which
|
|
148
|
+
* used to inline the single-row check) and hosts replaying a stored thread
|
|
149
|
+
* both call this instead of re-deriving the walk.
|
|
150
|
+
*
|
|
151
|
+
* A trailing non-assistant row (user / error) means the last turn closed →
|
|
152
|
+
* `undefined`.
|
|
153
|
+
*/
|
|
154
|
+
export function extractIncompleteTailState(
|
|
155
|
+
messages: readonly ProcessedMessage[] | undefined,
|
|
156
|
+
): IncompleteMessageState | undefined {
|
|
157
|
+
if (!messages || messages.length === 0) return undefined
|
|
158
|
+
|
|
159
|
+
// Walk back over the trailing consecutive assistant run.
|
|
160
|
+
let start = messages.length
|
|
161
|
+
while (start > 0 && messages[start - 1]?.role === 'assistant') start -= 1
|
|
162
|
+
if (start === messages.length) return undefined
|
|
163
|
+
|
|
164
|
+
const segments: MessageSegment[] = []
|
|
165
|
+
for (let i = start; i < messages.length; i += 1) {
|
|
166
|
+
const content = messages[i]?.content
|
|
167
|
+
// A string-content assistant row is a plain-text bubble with nothing to
|
|
168
|
+
// resume; skip it rather than aborting — a later row in the same run may
|
|
169
|
+
// still hold the unfinished segment.
|
|
170
|
+
if (Array.isArray(content)) segments.push(...(content as MessageSegment[]))
|
|
94
171
|
}
|
|
172
|
+
if (segments.length === 0) return undefined
|
|
173
|
+
|
|
174
|
+
// Reuse the single-message extractor verbatim by handing it a synthetic
|
|
175
|
+
// assistant row carrying the flattened run — one incompleteness rule set,
|
|
176
|
+
// not two.
|
|
177
|
+
return extractIncompleteMessageState({
|
|
178
|
+
...messages[messages.length - 1],
|
|
179
|
+
role: 'assistant',
|
|
180
|
+
content: segments,
|
|
181
|
+
} as ProcessedMessage)
|
|
95
182
|
}
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
ApprovalBatchSegment,
|
|
3
|
+
ApprovalRequestSegment,
|
|
4
|
+
MessageContent,
|
|
5
|
+
ToolExecutionSegment,
|
|
6
|
+
} from '../types'
|
|
2
7
|
|
|
3
8
|
/**
|
|
4
9
|
* Reconciliation between persisted dialog history (processed GraphQL pages)
|
|
@@ -77,6 +82,50 @@ function assistantAnswerText(content: MessageContent): string {
|
|
|
77
82
|
return parts.join('\n').trim()
|
|
78
83
|
}
|
|
79
84
|
|
|
85
|
+
/**
|
|
86
|
+
* Backend request ids carried by a turn's segments — tool-call ids, approval
|
|
87
|
+
* request ids, batch ids.
|
|
88
|
+
*
|
|
89
|
+
* These identify the TURN, which is what lets the merge recognise history's
|
|
90
|
+
* persisted row and a live synthetic as two copies of the same thing when
|
|
91
|
+
* their IDS DIFFER. Ids only converge through adoption, and adoption only
|
|
92
|
+
* happens on a replay (a reload re-streams the turn into the persisted row);
|
|
93
|
+
* a refetch that lands mid-stream has no replay, so the live copy keeps the
|
|
94
|
+
* synthetic id the reducer minted.
|
|
95
|
+
*
|
|
96
|
+
* Empty for a turn that has produced only text — callers must treat an empty
|
|
97
|
+
* set as "no signal", never as a match.
|
|
98
|
+
*/
|
|
99
|
+
function turnRequestKeys(content: MessageContent): Set<string> {
|
|
100
|
+
const keys = new Set<string>()
|
|
101
|
+
if (!Array.isArray(content)) return keys
|
|
102
|
+
for (const seg of content) {
|
|
103
|
+
if (seg.type === 'tool_execution') {
|
|
104
|
+
const id = (seg as ToolExecutionSegment).data?.toolExecutionRequestId
|
|
105
|
+
if (id) keys.add(id)
|
|
106
|
+
} else if (seg.type === 'approval_request') {
|
|
107
|
+
const id = (seg as ApprovalRequestSegment).data?.requestId
|
|
108
|
+
if (id) keys.add(id)
|
|
109
|
+
} else if (seg.type === 'approval_batch') {
|
|
110
|
+
const data = (seg as ApprovalBatchSegment).data
|
|
111
|
+
if (data?.approvalRequestId) keys.add(data.approvalRequestId)
|
|
112
|
+
for (const call of data?.toolCalls ?? []) {
|
|
113
|
+
if (call?.toolExecutionRequestId) keys.add(call.toolExecutionRequestId)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return keys
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Whether `content` carries any of `keys` — see `turnRequestKeys`. */
|
|
121
|
+
function sharesRequestKey(content: MessageContent, keys: ReadonlySet<string>): boolean {
|
|
122
|
+
if (keys.size === 0) return false
|
|
123
|
+
for (const key of turnRequestKeys(content)) {
|
|
124
|
+
if (keys.has(key)) return true
|
|
125
|
+
}
|
|
126
|
+
return false
|
|
127
|
+
}
|
|
128
|
+
|
|
80
129
|
/** Flattens DESC-sorted message pages (newest page first, newest message
|
|
81
130
|
* first within a page) into one chronological list. */
|
|
82
131
|
export function flattenMessagePagesChronological<T>(pages: readonly { messages: readonly T[] }[] | undefined): T[] {
|
|
@@ -250,13 +299,32 @@ export function mergeHistoryWithRealtime<M extends MergeableChatMessage>(input:
|
|
|
250
299
|
) {
|
|
251
300
|
const persistedSize = historyTrailingAssistant.content.length
|
|
252
301
|
const persistedSeq = historyTrailingAssistant.streamSeq ?? 0
|
|
302
|
+
// Turn identity for a twin that has NOT adopted the persisted id. Adoption
|
|
303
|
+
// only happens on a REPLAY (a reload re-streams the turn from its
|
|
304
|
+
// MESSAGE_START into the persisted row); a refetch that lands while the
|
|
305
|
+
// turn is still streaming has no replay, so the live copy keeps the
|
|
306
|
+
// synthetic id the reducer minted and the id test above can never match.
|
|
307
|
+
// Both copies do carry the backend's per-call request ids, which identify
|
|
308
|
+
// the TURN — the same trick the approval-batch branch plays with
|
|
309
|
+
// `approvalRequestId`, widened to tool calls.
|
|
310
|
+
const trailingKeys = turnRequestKeys(historyTrailingAssistant.content)
|
|
311
|
+
const isSameTurn = (m: M): boolean =>
|
|
312
|
+
m.id === historyTrailingAssistant.id ||
|
|
313
|
+
(trailingKeys.size > 0 && sharesRequestKey(m.content, trailingKeys))
|
|
253
314
|
const richerTwin = existingMessages.find(
|
|
254
315
|
(m) =>
|
|
255
316
|
m !== historyTrailingAssistant &&
|
|
256
|
-
m.id === historyTrailingAssistant.id &&
|
|
257
317
|
m.role === 'assistant' &&
|
|
258
318
|
Array.isArray(m.content) &&
|
|
259
|
-
(m
|
|
319
|
+
isSameTurn(m) &&
|
|
320
|
+
// A STILL-STREAMING twin wins outright: it is the copy the chunks are
|
|
321
|
+
// landing in, so "richer" is a property it is about to have anyway,
|
|
322
|
+
// and history's snapshot of a turn in flight is stale by construction.
|
|
323
|
+
// Without this the same turn rendered TWICE — history's partial copy
|
|
324
|
+
// beside the live one, their tool rows disagreeing (one pending, one
|
|
325
|
+
// failed) because each stopped at a different chunk.
|
|
326
|
+
(m.id === streamingMessageId ||
|
|
327
|
+
m.content.length > persistedSize ||
|
|
260
328
|
(m.content.length === persistedSize && typeof m.streamSeq === 'number' && m.streamSeq > persistedSeq)),
|
|
261
329
|
)
|
|
262
330
|
if (richerTwin) {
|
|
@@ -384,8 +452,20 @@ export function mergeHistoryWithRealtime<M extends MergeableChatMessage>(input:
|
|
|
384
452
|
if (typeof m.streamSeq === 'number' && anyHistoryRowSeq) {
|
|
385
453
|
const roleMax = historyMaxSeqByRole.get(m.role) ?? 0
|
|
386
454
|
covered = roleMax > 0 && roleMax >= m.streamSeq
|
|
387
|
-
} else if (typeof m.streamSeq === 'number'
|
|
388
|
-
|
|
455
|
+
} else if (typeof m.streamSeq === 'number') {
|
|
456
|
+
// This synthetic carries its own seq, but no history row does. Decide
|
|
457
|
+
// against the global when it exists — and when it does NOT
|
|
458
|
+
// (`historyMaxStreamSeq === 0`), that is not "unknown, guess by clock":
|
|
459
|
+
// it is positive evidence that nothing in the snapshot has reached any
|
|
460
|
+
// seq at all, so the snapshot cannot contain this turn. KEEP it.
|
|
461
|
+
//
|
|
462
|
+
// This is the mid-stream refetch: the only persisted row of a turn in
|
|
463
|
+
// flight is the user MESSAGE_REQUEST, which the backend does not stamp,
|
|
464
|
+
// so the max is 0 while live bubbles keep arriving. Falling through to
|
|
465
|
+
// wall-clock there judged every one of them "older than the fetch
|
|
466
|
+
// instant, therefore persisted" and dropped all but the single
|
|
467
|
+
// `streamingMessageId` — the thread collapsed to the user's prompt.
|
|
468
|
+
covered = historyMaxStreamSeq > 0 && historyMaxStreamSeq >= m.streamSeq
|
|
389
469
|
} else {
|
|
390
470
|
covered =
|
|
391
471
|
historyCoversRealtime !== null
|
|
@@ -3,14 +3,9 @@
|
|
|
3
3
|
* Export all utilities for processing chat messages
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
//
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
isControlChunk,
|
|
10
|
-
isErrorChunk,
|
|
11
|
-
isMetadataChunk,
|
|
12
|
-
extractTextFromChunk,
|
|
13
|
-
} from './chunk-parser'
|
|
6
|
+
// NOTE: NATS chunk parsing lives in `chat-protocol/nats-decoder`
|
|
7
|
+
// (`decodeNatsChunk`) — the legacy `chunk-parser` module was deleted once the
|
|
8
|
+
// reducer became the single reading path.
|
|
14
9
|
|
|
15
10
|
// Segment accumulator
|
|
16
11
|
export {
|
|
@@ -24,11 +19,13 @@ export {
|
|
|
24
19
|
processHistoricalMessages,
|
|
25
20
|
extractErrorMessages,
|
|
26
21
|
processHistoricalMessagesWithErrors,
|
|
22
|
+
decodeHistoricalMessageData,
|
|
27
23
|
} from './process-historical-messages'
|
|
28
24
|
|
|
29
25
|
// Incomplete message state extraction
|
|
30
26
|
export {
|
|
31
27
|
extractIncompleteMessageState,
|
|
28
|
+
extractIncompleteTailState,
|
|
32
29
|
} from './extract-incomplete-message-state'
|
|
33
30
|
|
|
34
31
|
// History <-> realtime reconciliation
|