@flamingo-stack/openframe-frontend-core 0.0.532 → 0.0.533
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/confirm-tool.d.ts +50 -0
- package/dist/chat-protocol/confirm-tool.d.ts.map +1 -0
- package/dist/chat-protocol/decode.d.ts +1 -9
- package/dist/chat-protocol/decode.d.ts.map +1 -1
- package/dist/chat-protocol/events.d.ts +35 -0
- package/dist/chat-protocol/events.d.ts.map +1 -1
- package/dist/chat-protocol/index.cjs +72 -3
- package/dist/chat-protocol/index.cjs.map +1 -1
- package/dist/chat-protocol/index.d.ts +1 -0
- package/dist/chat-protocol/index.d.ts.map +1 -1
- package/dist/chat-protocol/index.js +67 -4
- package/dist/chat-protocol/index.js.map +1 -1
- package/dist/chat-protocol/leading-frames.d.ts +38 -0
- package/dist/chat-protocol/leading-frames.d.ts.map +1 -0
- package/dist/chat-protocol/nats-decoder.d.ts +41 -0
- package/dist/chat-protocol/nats-decoder.d.ts.map +1 -1
- package/dist/{chunk-QEMI34VA.cjs → chunk-2MYDGLJS.cjs} +87 -87
- package/dist/{chunk-QEMI34VA.cjs.map → chunk-2MYDGLJS.cjs.map} +1 -1
- package/dist/{chunk-TQ7EPKKO.js → chunk-2TUOBFPK.js} +2 -2
- package/dist/{chunk-XBYEPSNZ.js → chunk-2XGHEJZK.js} +4 -4
- package/dist/{chunk-OAEJWP7H.js → chunk-46FKXLNE.js} +2 -2
- package/dist/{chunk-ECR37JHH.js → chunk-4QKXNWH5.js} +2 -2
- package/dist/{chunk-GWFC3NE5.cjs → chunk-4ZRHVOEB.cjs} +31 -31
- package/dist/{chunk-GWFC3NE5.cjs.map → chunk-4ZRHVOEB.cjs.map} +1 -1
- package/dist/{chunk-AZBS4PZG.js → chunk-5U67WTAS.js} +2 -2
- package/dist/{chunk-3G7FDE5E.js → chunk-6SMLYZLH.js} +4 -4
- package/dist/{chunk-7SGBF2I2.js → chunk-6W3ULZ4X.js} +6 -6
- package/dist/{chunk-ALXLWF75.js → chunk-6YXN5RHW.js} +2 -2
- package/dist/{chunk-2QP67D2L.cjs → chunk-DFQLUK75.cjs} +3 -3
- package/dist/{chunk-2QP67D2L.cjs.map → chunk-DFQLUK75.cjs.map} +1 -1
- package/dist/{chunk-ETF2QXXU.cjs → chunk-ENPGPZIB.cjs} +61 -61
- package/dist/{chunk-ETF2QXXU.cjs.map → chunk-ENPGPZIB.cjs.map} +1 -1
- package/dist/{chunk-HJDCDOJM.cjs → chunk-GNOK5J34.cjs} +37 -37
- package/dist/{chunk-HJDCDOJM.cjs.map → chunk-GNOK5J34.cjs.map} +1 -1
- package/dist/{chunk-4GL2XJ5C.cjs → chunk-J4BE4FIR.cjs} +4 -4
- package/dist/{chunk-4GL2XJ5C.cjs.map → chunk-J4BE4FIR.cjs.map} +1 -1
- package/dist/{chunk-HXHA3DCI.cjs → chunk-JDW2IL7A.cjs} +11 -11
- package/dist/{chunk-HXHA3DCI.cjs.map → chunk-JDW2IL7A.cjs.map} +1 -1
- package/dist/{chunk-TMZS63G2.cjs → chunk-KMB66NYX.cjs} +7 -7
- package/dist/{chunk-TMZS63G2.cjs.map → chunk-KMB66NYX.cjs.map} +1 -1
- package/dist/{chunk-5W3RCZIK.js → chunk-LE3BJ5H2.js} +2 -2
- package/dist/{chunk-WINT757E.js → chunk-LIUARTOZ.js} +206 -64
- package/dist/chunk-LIUARTOZ.js.map +1 -0
- package/dist/{chunk-65NR333L.js → chunk-MVGL5NNJ.js} +2 -2
- package/dist/{chunk-LYWSHNK4.cjs → chunk-NAV4FRNL.cjs} +26 -26
- package/dist/{chunk-LYWSHNK4.cjs.map → chunk-NAV4FRNL.cjs.map} +1 -1
- package/dist/{chunk-2MBS3YEY.cjs → chunk-OLOKQQAP.cjs} +5 -5
- package/dist/{chunk-2MBS3YEY.cjs.map → chunk-OLOKQQAP.cjs.map} +1 -1
- package/dist/{chunk-QYNLVIGQ.cjs → chunk-QZ2NAPLF.cjs} +9 -9
- package/dist/{chunk-QYNLVIGQ.cjs.map → chunk-QZ2NAPLF.cjs.map} +1 -1
- package/dist/{chunk-L6EKEYF7.cjs → chunk-RKITTO5V.cjs} +14 -14
- package/dist/{chunk-L6EKEYF7.cjs.map → chunk-RKITTO5V.cjs.map} +1 -1
- package/dist/{chunk-UP4RA54U.js → chunk-U555TMPC.js} +5 -5
- package/dist/{chunk-GVLA2BPW.cjs → chunk-UPHN2VRZ.cjs} +259 -117
- package/dist/chunk-UPHN2VRZ.cjs.map +1 -0
- package/dist/{chunk-IZOGFZ7N.js → chunk-XN4JKIJY.js} +2 -2
- package/dist/{chunk-KYQZBQYV.js → chunk-YLB6XXO6.js} +2 -2
- package/dist/{chunk-JZ5XDZYD.cjs → chunk-Z3NOI7X7.cjs} +26 -26
- package/dist/{chunk-JZ5XDZYD.cjs.map → chunk-Z3NOI7X7.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/embeddable-chat.d.ts +13 -0
- package/dist/components/chat/embeddable-chat.d.ts.map +1 -1
- 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.js +9 -1
- package/dist/components/chat/stream/chat-stream-reducer.d.ts +9 -0
- package/dist/components/chat/stream/chat-stream-reducer.d.ts.map +1 -1
- package/dist/components/chat/types/message.types.d.ts +15 -0
- package/dist/components/chat/types/message.types.d.ts.map +1 -1
- package/dist/components/chat/types/network.types.d.ts +4 -0
- package/dist/components/chat/types/network.types.d.ts.map +1 -1
- package/dist/components/chat/types/processing.types.d.ts +7 -1
- package/dist/components/chat/types/processing.types.d.ts.map +1 -1
- package/dist/components/chat/utils/approval-display.d.ts +49 -0
- package/dist/components/chat/utils/approval-display.d.ts.map +1 -0
- package/dist/components/chat/utils/index.d.ts +1 -0
- package/dist/components/chat/utils/index.d.ts.map +1 -1
- package/dist/components/chat/utils/message-segment-accumulator.d.ts +25 -3
- package/dist/components/chat/utils/message-segment-accumulator.d.ts.map +1 -1
- 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 +140 -132
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.js +19 -11
- 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/related-content/index.cjs +3 -3
- package/dist/components/related-content/index.js +2 -2
- package/dist/components/tickets/index.cjs +6 -6
- package/dist/components/tickets/index.js +5 -5
- package/dist/components/ui/index.cjs +10 -2
- package/dist/components/ui/index.cjs.map +1 -1
- package/dist/components/ui/index.js +9 -1
- package/dist/index.cjs +10 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +9 -1
- package/dist/utils/index.cjs.map +1 -1
- package/dist/utils/index.js.map +1 -1
- package/package.json +1 -1
- package/src/chat-protocol/__tests__/__snapshots__/nats-decoder-golden.test.ts.snap +70 -0
- package/src/chat-protocol/__tests__/nats-decoder-golden.test.ts +108 -1
- package/src/chat-protocol/confirm-tool.ts +67 -0
- package/src/chat-protocol/decode.ts +7 -153
- package/src/chat-protocol/events.ts +40 -0
- package/src/chat-protocol/index.ts +3 -0
- package/src/chat-protocol/leading-frames.ts +175 -0
- package/src/chat-protocol/nats-decoder.ts +111 -3
- package/src/components/chat/embeddable-chat.tsx +18 -0
- package/src/components/chat/hooks/use-sse-chat-adapter.ts +7 -16
- package/src/components/chat/stream/chat-stream-reducer.ts +49 -3
- package/src/components/chat/types/message.types.ts +15 -0
- package/src/components/chat/types/network.types.ts +4 -0
- package/src/components/chat/types/processing.types.ts +12 -1
- package/src/components/chat/utils/__tests__/process-historical-messages-approvals.test.ts +92 -0
- package/src/components/chat/utils/approval-display.ts +70 -0
- package/src/components/chat/utils/index.ts +9 -0
- package/src/components/chat/utils/message-segment-accumulator.ts +62 -32
- package/src/components/chat/utils/process-historical-messages.ts +40 -6
- package/dist/chunk-GVLA2BPW.cjs.map +0 -1
- package/dist/chunk-WINT757E.js.map +0 -1
- /package/dist/{chunk-TQ7EPKKO.js.map → chunk-2TUOBFPK.js.map} +0 -0
- /package/dist/{chunk-XBYEPSNZ.js.map → chunk-2XGHEJZK.js.map} +0 -0
- /package/dist/{chunk-OAEJWP7H.js.map → chunk-46FKXLNE.js.map} +0 -0
- /package/dist/{chunk-ECR37JHH.js.map → chunk-4QKXNWH5.js.map} +0 -0
- /package/dist/{chunk-AZBS4PZG.js.map → chunk-5U67WTAS.js.map} +0 -0
- /package/dist/{chunk-3G7FDE5E.js.map → chunk-6SMLYZLH.js.map} +0 -0
- /package/dist/{chunk-7SGBF2I2.js.map → chunk-6W3ULZ4X.js.map} +0 -0
- /package/dist/{chunk-ALXLWF75.js.map → chunk-6YXN5RHW.js.map} +0 -0
- /package/dist/{chunk-5W3RCZIK.js.map → chunk-LE3BJ5H2.js.map} +0 -0
- /package/dist/{chunk-65NR333L.js.map → chunk-MVGL5NNJ.js.map} +0 -0
- /package/dist/{chunk-UP4RA54U.js.map → chunk-U555TMPC.js.map} +0 -0
- /package/dist/{chunk-IZOGFZ7N.js.map → chunk-XN4JKIJY.js.map} +0 -0
- /package/dist/{chunk-KYQZBQYV.js.map → chunk-YLB6XXO6.js.map} +0 -0
package/package.json
CHANGED
|
@@ -263,6 +263,76 @@ exports[`decodeNatsChunk — golden corpus > maps the full recorded corpus to no
|
|
|
263
263
|
"text": "",
|
|
264
264
|
"type": "guide-delta",
|
|
265
265
|
},
|
|
266
|
+
"guide_frame_approval_batch": {
|
|
267
|
+
"approvalType": "chat",
|
|
268
|
+
"origin": "guide",
|
|
269
|
+
"requestId": "batch:prop-1",
|
|
270
|
+
"status": "pending",
|
|
271
|
+
"toolCalls": [
|
|
272
|
+
{
|
|
273
|
+
"requiresApproval": true,
|
|
274
|
+
"toolCallArguments": null,
|
|
275
|
+
"toolExecutionRequestId": "prop-1",
|
|
276
|
+
"toolName": "create_ticket",
|
|
277
|
+
"toolTitle": "Open a ticket",
|
|
278
|
+
},
|
|
279
|
+
],
|
|
280
|
+
"type": "approval-request",
|
|
281
|
+
},
|
|
282
|
+
"guide_frame_approval_request": {
|
|
283
|
+
"approvalType": "create_ticket",
|
|
284
|
+
"command": "Open a support ticket",
|
|
285
|
+
"fields": [
|
|
286
|
+
{
|
|
287
|
+
"label": "Subject",
|
|
288
|
+
"value": "Agent will not enroll",
|
|
289
|
+
},
|
|
290
|
+
],
|
|
291
|
+
"origin": "guide",
|
|
292
|
+
"requestId": "prop-1",
|
|
293
|
+
"status": "pending",
|
|
294
|
+
"type": "approval-request",
|
|
295
|
+
},
|
|
296
|
+
"guide_frame_decision_resolved": {
|
|
297
|
+
"ok": true,
|
|
298
|
+
"origin": "guide",
|
|
299
|
+
"requestId": "prop-1",
|
|
300
|
+
"status": "approved",
|
|
301
|
+
"type": "approval-resolved",
|
|
302
|
+
"willAutoContinue": false,
|
|
303
|
+
},
|
|
304
|
+
"guide_frame_metadata": {
|
|
305
|
+
"conversationId": "conv-1",
|
|
306
|
+
"origin": "guide",
|
|
307
|
+
"type": "metadata",
|
|
308
|
+
},
|
|
309
|
+
"guide_frame_payload_array": null,
|
|
310
|
+
"guide_frame_payload_empty": null,
|
|
311
|
+
"guide_frame_payload_not_object": null,
|
|
312
|
+
"guide_frame_routing": null,
|
|
313
|
+
"guide_frame_status_thinking": null,
|
|
314
|
+
"guide_frame_text_leading": {
|
|
315
|
+
"text": "I'll look that up in the docs.",
|
|
316
|
+
"type": "guide-delta",
|
|
317
|
+
},
|
|
318
|
+
"guide_frame_text_wins": {
|
|
319
|
+
"text": "body",
|
|
320
|
+
"type": "guide-delta",
|
|
321
|
+
},
|
|
322
|
+
"guide_frame_thinking": {
|
|
323
|
+
"text": "The user asked about <policies> setup",
|
|
324
|
+
"type": "thinking-delta",
|
|
325
|
+
},
|
|
326
|
+
"guide_frame_tool_error": {
|
|
327
|
+
"title": "Could not reach HubSpot",
|
|
328
|
+
"type": "error",
|
|
329
|
+
},
|
|
330
|
+
"guide_frame_usage_start": null,
|
|
331
|
+
"guide_frame_with_seq": {
|
|
332
|
+
"seq": 77,
|
|
333
|
+
"text": "Checking the docs…",
|
|
334
|
+
"type": "guide-delta",
|
|
335
|
+
},
|
|
266
336
|
"guide_missing_text": null,
|
|
267
337
|
"message_end": {
|
|
268
338
|
"type": "turn-end",
|
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { describe, it, expect } from 'vitest'
|
|
14
|
-
import {
|
|
14
|
+
import type { ChatStreamEvent } from '../events'
|
|
15
|
+
import { decodeNatsChunk, guideEventForNats } from '../nats-decoder'
|
|
15
16
|
|
|
16
17
|
/** Recorded corpus of realistic NATS chunk shapes, keyed by scenario name. */
|
|
17
18
|
const CORPUS: Record<string, unknown> = {
|
|
@@ -29,6 +30,69 @@ const CORPUS: Record<string, unknown> = {
|
|
|
29
30
|
guide_empty_string: { type: 'GUIDE', text: '' },
|
|
30
31
|
guide_missing_text: { type: 'GUIDE' },
|
|
31
32
|
|
|
33
|
+
// GUIDE frames — hub frames the agent re-streams verbatim inside the same
|
|
34
|
+
// chunk type, under `payload`. They cross over by DEFAULT; only the dialog's
|
|
35
|
+
// own accounting and phase stop at the seam. See `guideEventForNats`.
|
|
36
|
+
guide_frame_text_leading: {
|
|
37
|
+
type: 'GUIDE',
|
|
38
|
+
payload: { kind: 'text-leading', text: "I'll look that up in the docs." },
|
|
39
|
+
},
|
|
40
|
+
guide_frame_thinking: {
|
|
41
|
+
type: 'GUIDE',
|
|
42
|
+
payload: { kind: 'thinking-delta', text: 'The user asked about <policies> setup' },
|
|
43
|
+
},
|
|
44
|
+
guide_frame_tool_error: {
|
|
45
|
+
type: 'GUIDE',
|
|
46
|
+
payload: { kind: 'tool_error', message: 'Could not reach HubSpot' },
|
|
47
|
+
},
|
|
48
|
+
// Dropped: would blank the model badge / relabel the turn with the hub's model.
|
|
49
|
+
guide_frame_routing: {
|
|
50
|
+
type: 'GUIDE',
|
|
51
|
+
payload: { kind: 'routing', routedComplexity: 'deep', routedModel: 'claude-x', routedThinkingBudget: 8000 },
|
|
52
|
+
},
|
|
53
|
+
guide_frame_metadata: {
|
|
54
|
+
type: 'GUIDE',
|
|
55
|
+
payload: { model: 'claude-x', modelLabel: 'Guide model', conversationId: 'conv-1' },
|
|
56
|
+
},
|
|
57
|
+
// Dropped: the dialog owns its own phase and token accounting.
|
|
58
|
+
guide_frame_status_thinking: { type: 'GUIDE', payload: { status: 'thinking' } },
|
|
59
|
+
guide_frame_usage_start: { type: 'GUIDE', payload: { kind: 'usage', stage: 'start', input_tokens: 12 } },
|
|
60
|
+
// Crosses over stamped `origin: 'guide'` — the marker that keeps the card
|
|
61
|
+
// inline and sends its buttons to the hub's confirm route.
|
|
62
|
+
guide_frame_approval_request: {
|
|
63
|
+
type: 'GUIDE',
|
|
64
|
+
payload: {
|
|
65
|
+
kind: 'approval_request',
|
|
66
|
+
proposalId: 'prop-1',
|
|
67
|
+
toolName: 'create_ticket',
|
|
68
|
+
title: 'Open a support ticket',
|
|
69
|
+
fields: [{ label: 'Subject', value: 'Agent will not enroll' }],
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
guide_frame_approval_batch: {
|
|
73
|
+
type: 'GUIDE',
|
|
74
|
+
payload: {
|
|
75
|
+
kind: 'approval_batch',
|
|
76
|
+
batchId: 'batch:prop-1',
|
|
77
|
+
proposals: [{ proposalId: 'prop-1', toolName: 'create_ticket', title: 'Open a ticket' }],
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
guide_frame_decision_resolved: {
|
|
81
|
+
type: 'GUIDE',
|
|
82
|
+
payload: { kind: 'decision_resolved', proposalId: 'prop-1', ok: true, action: 'approved', willAutoContinue: false },
|
|
83
|
+
},
|
|
84
|
+
// Malformed payloads degrade to a no-op, like every other chunk shape.
|
|
85
|
+
guide_frame_payload_not_object: { type: 'GUIDE', payload: 'text-leading' },
|
|
86
|
+
guide_frame_payload_array: { type: 'GUIDE', payload: [{ kind: 'text-leading', text: 'hi' }] },
|
|
87
|
+
guide_frame_payload_empty: { type: 'GUIDE', payload: {} },
|
|
88
|
+
// `text` wins when both are present — the body is what the agent persists.
|
|
89
|
+
guide_frame_text_wins: { type: 'GUIDE', text: 'body', payload: { kind: 'thinking-delta', text: 'ignored' } },
|
|
90
|
+
guide_frame_with_seq: {
|
|
91
|
+
type: 'GUIDE',
|
|
92
|
+
streamSeq: 77,
|
|
93
|
+
payload: { kind: 'text-leading', text: 'Checking the docs…' },
|
|
94
|
+
},
|
|
95
|
+
|
|
32
96
|
// ASK — the guide-routing clarification card. `text` is the intro sentence
|
|
33
97
|
// riding the same chunk; a card without a question or without usable options
|
|
34
98
|
// is dropped (nothing the user could answer).
|
|
@@ -280,6 +344,49 @@ describe('decodeNatsChunk — golden corpus', () => {
|
|
|
280
344
|
})
|
|
281
345
|
})
|
|
282
346
|
|
|
347
|
+
describe('guideEventForNats — the two kernels reconciled', () => {
|
|
348
|
+
// The property that matters for maintenance: `leading-frames` is the ONE
|
|
349
|
+
// place a hub frame kind is taught to the client, so an event kind that
|
|
350
|
+
// reaches this adapter must cross over WITHOUT an edit here. These use event
|
|
351
|
+
// types the frame table cannot produce today on purpose — they stand in for
|
|
352
|
+
// whatever the hub ships next.
|
|
353
|
+
it('passes an unfamiliar event through unchanged', () => {
|
|
354
|
+
const ask: ChatStreamEvent = {
|
|
355
|
+
type: 'ask',
|
|
356
|
+
question: 'Which workspace?',
|
|
357
|
+
options: [{ label: 'Acme' }],
|
|
358
|
+
}
|
|
359
|
+
expect(guideEventForNats(ask)).toEqual(ask)
|
|
360
|
+
})
|
|
361
|
+
|
|
362
|
+
it('stamps origin on the events that carry it, so the card routes to the hub', () => {
|
|
363
|
+
expect(
|
|
364
|
+
guideEventForNats({ type: 'approval-request', requestId: 'prop-1', approvalType: 'create_ticket' }),
|
|
365
|
+
).toMatchObject({ approvalType: 'create_ticket', origin: 'guide' })
|
|
366
|
+
expect(guideEventForNats({ type: 'approval-resolved', requestId: 'prop-1', status: 'approved' })).toMatchObject({
|
|
367
|
+
origin: 'guide',
|
|
368
|
+
})
|
|
369
|
+
})
|
|
370
|
+
|
|
371
|
+
it('stops the events the agent owns for this dialog', () => {
|
|
372
|
+
expect(guideEventForNats({ type: 'usage', stage: 'start', input_tokens: 10 })).toBeNull()
|
|
373
|
+
expect(guideEventForNats({ type: 'status', phase: 'thinking' })).toBeNull()
|
|
374
|
+
expect(
|
|
375
|
+
guideEventForNats({ type: 'token-usage', inputTokensSize: 1, outputTokensSize: 2, totalTokensSize: 3, contextSize: 4 }),
|
|
376
|
+
).toBeNull()
|
|
377
|
+
expect(guideEventForNats({ type: 'dialog-closed' })).toBeNull()
|
|
378
|
+
})
|
|
379
|
+
|
|
380
|
+
it('keeps metadata only for the hub conversation id every confirm must quote back', () => {
|
|
381
|
+
expect(guideEventForNats({ type: 'metadata', conversationId: 'conv-1', modelName: 'hub-model' })).toEqual({
|
|
382
|
+
type: 'metadata',
|
|
383
|
+
conversationId: 'conv-1',
|
|
384
|
+
origin: 'guide',
|
|
385
|
+
})
|
|
386
|
+
expect(guideEventForNats({ type: 'metadata', modelName: 'hub-model' })).toBeNull()
|
|
387
|
+
})
|
|
388
|
+
})
|
|
389
|
+
|
|
283
390
|
describe('decodeNatsChunk — seq envelope', () => {
|
|
284
391
|
it('lifts a numeric JetStream `streamSeq` into `seq`, and omits it otherwise (snapshot)', () => {
|
|
285
392
|
expect({
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The hub's confirm-tool contract — request shape and error envelope.
|
|
3
|
+
*
|
|
4
|
+
* Resolving a Product Guide proposal is a POST to `endpoints.approvalToolUrl`
|
|
5
|
+
* whose response is an ordinary guide stream (a `decision_resolved` frame, then
|
|
6
|
+
* the hub's auto-continuation). Two callers make that call today: the SSE chat
|
|
7
|
+
* adapter, and any host replaying the same route into a different transport's
|
|
8
|
+
* conversation (the product app resolves guide cards inside a NATS Mingo
|
|
9
|
+
* dialog). They MUST send byte-identical bodies — a proposal is single-use, so a
|
|
10
|
+
* caller that drifts on a field name does not degrade, it fails the write — and
|
|
11
|
+
* they must surface the same server copy, because the hub's errors are written
|
|
12
|
+
* for the end user ("This approval expired — ask again to get a fresh one").
|
|
13
|
+
*
|
|
14
|
+
* Hence both live here rather than at either call site.
|
|
15
|
+
*
|
|
16
|
+
* Server-safe: no React, no browser APIs.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/** What the user did with the card. */
|
|
20
|
+
export type ApprovalToolAction = 'approve' | 'reject'
|
|
21
|
+
|
|
22
|
+
/** Client-side shape of a confirm; `buildConfirmToolBody` renames it to the
|
|
23
|
+
* wire's own casing so no caller hand-writes `proposal_id`. */
|
|
24
|
+
export interface ConfirmToolRequest {
|
|
25
|
+
/** The hub-minted proposal id carried by the card. */
|
|
26
|
+
proposalId: string
|
|
27
|
+
action: ApprovalToolAction
|
|
28
|
+
/**
|
|
29
|
+
* The hub's conversation id, quoted back verbatim. The hub rejects a confirm
|
|
30
|
+
* without it: a proposal only means something inside the conversation that
|
|
31
|
+
* minted it.
|
|
32
|
+
*/
|
|
33
|
+
conversationId: string | null | undefined
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Build the confirm-tool request body. The ONE place that knows the wire
|
|
37
|
+
* spells the id `proposal_id` while everything else is camelCase. */
|
|
38
|
+
export function buildConfirmToolBody(request: ConfirmToolRequest): Record<string, unknown> {
|
|
39
|
+
return {
|
|
40
|
+
proposal_id: request.proposalId,
|
|
41
|
+
action: request.action,
|
|
42
|
+
conversationId: request.conversationId,
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Pull the user-facing copy out of a failed chat/confirm response.
|
|
48
|
+
*
|
|
49
|
+
* The route-base envelope is `{error, code}`; `error` is written for the end
|
|
50
|
+
* user, so it beats any status-code copy the client could invent. Returns null
|
|
51
|
+
* when the body is not that envelope (HTML error page, empty body, gateway
|
|
52
|
+
* response) — the caller then supplies its own generic line.
|
|
53
|
+
*
|
|
54
|
+
* Consumes the body: call it once, on a response you have already decided is a
|
|
55
|
+
* failure.
|
|
56
|
+
*/
|
|
57
|
+
export async function readServerErrorMessage(response: {
|
|
58
|
+
json: () => Promise<unknown>
|
|
59
|
+
}): Promise<string | null> {
|
|
60
|
+
try {
|
|
61
|
+
const body = (await response.json()) as { error?: unknown } | null
|
|
62
|
+
if (typeof body?.error === 'string' && body.error.length > 0) return body.error
|
|
63
|
+
} catch {
|
|
64
|
+
/* non-JSON error body — the caller falls through to its generic copy */
|
|
65
|
+
}
|
|
66
|
+
return null
|
|
67
|
+
}
|
|
@@ -35,6 +35,9 @@
|
|
|
35
35
|
|
|
36
36
|
import { FRAME_TERMINATOR, END_OF_LEADING, TRAILER_SENTINEL } from './frames'
|
|
37
37
|
import type { ChatStreamEvent } from './events'
|
|
38
|
+
// The frame table is SHARED with the NATS decoder — guide answers re-stream the
|
|
39
|
+
// hub's frames verbatim inside `GUIDE` chunks — so it lives in its own module.
|
|
40
|
+
import { mapLeadingFrame } from './leading-frames'
|
|
38
41
|
|
|
39
42
|
export interface SseFrameDecoder {
|
|
40
43
|
/** Feed raw response bytes; returns the events they produced. */
|
|
@@ -49,159 +52,10 @@ export interface SseFrameDecoder {
|
|
|
49
52
|
end(): ChatStreamEvent[]
|
|
50
53
|
}
|
|
51
54
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
* blockquote `>` markers. Per-character, so it distributes over
|
|
57
|
-
* concatenation: escape(a + b) === escape(a) + escape(b) — callers may
|
|
58
|
-
* apply it per-delta or on the accumulated string interchangeably.
|
|
59
|
-
*/
|
|
60
|
-
export function escapeThinkingTags(text: string): string {
|
|
61
|
-
return text.replace(/</g, '<')
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Map one parsed leading frame to normalized events, replicating the
|
|
66
|
-
* legacy parser's else-if chain ORDER and its exact truthiness/typeof
|
|
67
|
-
* gates. Frames that matched a branch but failed its inner validation
|
|
68
|
-
* (e.g. `routing` without a string `routedComplexity`) produce NO event,
|
|
69
|
-
* exactly like the legacy no-op.
|
|
70
|
-
*/
|
|
71
|
-
function mapLeadingFrame(meta: any, out: ChatStreamEvent[]): void {
|
|
72
|
-
if (meta.status === 'thinking') {
|
|
73
|
-
out.push({ type: 'status', phase: 'thinking' })
|
|
74
|
-
} else if (meta.kind === 'thinking-delta' && typeof meta.text === 'string') {
|
|
75
|
-
// Wire is ALREADY delta — emit verbatim, append-only contract.
|
|
76
|
-
out.push({ type: 'thinking-delta', text: meta.text })
|
|
77
|
-
} else if (meta.kind === 'usage' && meta.stage === 'start') {
|
|
78
|
-
out.push({
|
|
79
|
-
type: 'usage',
|
|
80
|
-
stage: 'start',
|
|
81
|
-
input_tokens: meta.input_tokens,
|
|
82
|
-
cache_read_input_tokens: meta.cache_read_input_tokens,
|
|
83
|
-
cache_creation_input_tokens: meta.cache_creation_input_tokens,
|
|
84
|
-
})
|
|
85
|
-
} else if (meta.kind === 'decision_resolved' && typeof meta.action === 'string') {
|
|
86
|
-
const status = meta.action === 'rejected' ? 'rejected' : 'approved'
|
|
87
|
-
const toolName = typeof meta.tool_name === 'string' ? meta.tool_name : undefined
|
|
88
|
-
const result = meta.result ?? null
|
|
89
|
-
const card = meta.card ?? null
|
|
90
|
-
out.push({
|
|
91
|
-
type: 'approval-resolved',
|
|
92
|
-
status,
|
|
93
|
-
ok: meta.ok === true,
|
|
94
|
-
willAutoContinue: meta.willAutoContinue === true,
|
|
95
|
-
...(toolName ? { toolName } : {}),
|
|
96
|
-
...(result ? { result } : {}),
|
|
97
|
-
...(card?.marker ? { marker: card.marker } : {}),
|
|
98
|
-
...(card?.ref ? { cardRef: card.ref } : {}),
|
|
99
|
-
...(card?.type ? { cardType: card.type } : {}),
|
|
100
|
-
...(typeof meta.receiptText === 'string' ? { receiptText: meta.receiptText } : {}),
|
|
101
|
-
requestId: typeof meta.proposalId === 'string' ? meta.proposalId : undefined,
|
|
102
|
-
})
|
|
103
|
-
} else if (meta.kind === 'approval_batch' && meta.batchId && Array.isArray(meta.proposals)) {
|
|
104
|
-
// Server-grouped multi-proposal turn → ONE batch event carrying a
|
|
105
|
-
// tool-call row per proposal. `toolExecutionRequestId` is the row's
|
|
106
|
-
// PROPOSAL id (each row resolves through its own per-proposal
|
|
107
|
-
// confirm); the batch's `requestId` is the stable anchor the shell
|
|
108
|
-
// uses for status flips. Field rows ride as the row's expandable
|
|
109
|
-
// args so per-proposal detail stays reachable inside the batch.
|
|
110
|
-
const toolCalls = (meta.proposals as Array<Record<string, any>>)
|
|
111
|
-
.filter((p) => p && typeof p.proposalId === 'string')
|
|
112
|
-
.map((p) => {
|
|
113
|
-
const rawFields = Array.isArray(p.fields)
|
|
114
|
-
? (p.fields as Array<{ label?: string; value?: string }>).filter(
|
|
115
|
-
(f) => f && f.label && f.value,
|
|
116
|
-
)
|
|
117
|
-
: []
|
|
118
|
-
// Prefer human-readable identity labels for the row's
|
|
119
|
-
// disambiguator; opaque-id labels ("Task", "Ticket") only as
|
|
120
|
-
// the last-resort first field.
|
|
121
|
-
const detail =
|
|
122
|
-
rawFields.find((f) => /^(title|subject|name)$/i.test(String(f.label))) ??
|
|
123
|
-
rawFields[0]
|
|
124
|
-
const base =
|
|
125
|
-
typeof p.title === 'string' && p.title.length > 0 ? p.title : String(p.toolName ?? 'Tool call')
|
|
126
|
-
return {
|
|
127
|
-
toolExecutionRequestId: String(p.proposalId),
|
|
128
|
-
toolName: String(p.toolName ?? 'tool'),
|
|
129
|
-
toolTitle: detail ? `${base} — ${detail.value}` : base,
|
|
130
|
-
requiresApproval: true,
|
|
131
|
-
toolCallArguments:
|
|
132
|
-
rawFields.length > 0
|
|
133
|
-
? Object.fromEntries(rawFields.map((f) => [String(f.label), String(f.value)]))
|
|
134
|
-
: null,
|
|
135
|
-
}
|
|
136
|
-
})
|
|
137
|
-
if (toolCalls.length > 0) {
|
|
138
|
-
out.push({
|
|
139
|
-
type: 'approval-request',
|
|
140
|
-
requestId: String(meta.batchId),
|
|
141
|
-
approvalType: 'chat',
|
|
142
|
-
toolCalls,
|
|
143
|
-
status: 'pending',
|
|
144
|
-
})
|
|
145
|
-
}
|
|
146
|
-
} else if (meta.kind === 'approval_request' && meta.proposalId) {
|
|
147
|
-
const proposalId = String(meta.proposalId)
|
|
148
|
-
const toolName = String(meta.toolName ?? 'tool')
|
|
149
|
-
const headline =
|
|
150
|
-
typeof meta.title === 'string' && meta.title.length > 0 ? meta.title : toolName
|
|
151
|
-
const rawFields = Array.isArray(meta.fields)
|
|
152
|
-
? (meta.fields as Array<{ label?: string; value?: string }>)
|
|
153
|
-
: []
|
|
154
|
-
const fields: Array<{ label: string; value: string }> = []
|
|
155
|
-
for (const f of rawFields) {
|
|
156
|
-
if (!f || !f.label || !f.value) continue
|
|
157
|
-
fields.push({ label: f.label, value: f.value })
|
|
158
|
-
}
|
|
159
|
-
out.push({
|
|
160
|
-
type: 'approval-request',
|
|
161
|
-
requestId: proposalId,
|
|
162
|
-
approvalType: toolName,
|
|
163
|
-
command: headline,
|
|
164
|
-
fields,
|
|
165
|
-
status: 'pending',
|
|
166
|
-
})
|
|
167
|
-
} else if (meta.kind === 'text-leading' && typeof meta.text === 'string') {
|
|
168
|
-
out.push({ type: 'text-delta', text: meta.text, leading: true })
|
|
169
|
-
} else if (meta.kind === 'tool_error') {
|
|
170
|
-
const msg =
|
|
171
|
-
typeof meta.message === 'string' && meta.message.length > 0
|
|
172
|
-
? meta.message
|
|
173
|
-
: 'Could not complete the requested action right now.'
|
|
174
|
-
out.push({ type: 'error', title: msg })
|
|
175
|
-
} else if (meta.kind === 'routing') {
|
|
176
|
-
if (typeof meta.routedComplexity === 'string') {
|
|
177
|
-
out.push({
|
|
178
|
-
type: 'metadata',
|
|
179
|
-
routing: {
|
|
180
|
-
routedComplexity: meta.routedComplexity,
|
|
181
|
-
...(typeof meta.routedModel === 'string' ? { routedModel: meta.routedModel } : {}),
|
|
182
|
-
routedThinkingBudget:
|
|
183
|
-
typeof meta.routedThinkingBudget === 'number' ? meta.routedThinkingBudget : null,
|
|
184
|
-
},
|
|
185
|
-
})
|
|
186
|
-
}
|
|
187
|
-
} else {
|
|
188
|
-
// Catch-all metadata-ish frame. Raw values pass through UNVALIDATED
|
|
189
|
-
// (possibly undefined) so the consumer can replicate the legacy
|
|
190
|
-
// presence/truthiness gates exactly — including the `model`-presence
|
|
191
|
-
// trigger whose value is never stored.
|
|
192
|
-
out.push({
|
|
193
|
-
type: 'metadata',
|
|
194
|
-
sources: meta.sources,
|
|
195
|
-
refs: meta.refs,
|
|
196
|
-
provider: meta.provider,
|
|
197
|
-
modelLabel: meta.modelLabel,
|
|
198
|
-
modelName: meta.model,
|
|
199
|
-
contextWindowMaxTokens: meta.contextWindowMaxTokens,
|
|
200
|
-
scrollAnchor: meta.scrollAnchor,
|
|
201
|
-
conversationId: meta.conversationId,
|
|
202
|
-
})
|
|
203
|
-
}
|
|
204
|
-
}
|
|
55
|
+
// `escapeThinkingTags` moved to `./leading-frames` alongside the frame table,
|
|
56
|
+
// and is re-exported here: consumers have imported it from this module since it
|
|
57
|
+
// was written, and the `chat-protocol` barrel re-exports it from here.
|
|
58
|
+
export { escapeThinkingTags } from './leading-frames'
|
|
205
59
|
|
|
206
60
|
export function createSseFrameDecoder(): SseFrameDecoder {
|
|
207
61
|
const textDecoder = new TextDecoder()
|
|
@@ -138,6 +138,37 @@ export interface ApprovalRequestEvent extends ChatStreamEventBase {
|
|
|
138
138
|
fields?: ApprovalRequestField[]
|
|
139
139
|
toolCalls?: ApprovalToolCall[]
|
|
140
140
|
status?: 'pending'
|
|
141
|
+
/** Set when the card came from a Product Guide frame — see {@link GuideOrigin}. */
|
|
142
|
+
origin?: GuideOrigin
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Marks an event whose payload is a Product Guide frame, whatever transport
|
|
147
|
+
* carried it. It exists because ONE stream can now mix both worlds: the agent
|
|
148
|
+
* re-streams the hub's frames into a NATS dialog, so a card typed the hub's way
|
|
149
|
+
* (`approvalType` = the tool name, resolved through the hub's confirm route)
|
|
150
|
+
* travels beside cards typed the agent's way (`approvalType` = an approval TIER
|
|
151
|
+
* routed to human escalation).
|
|
152
|
+
*
|
|
153
|
+
* Consumers read it to keep the guide half behaving exactly as it does in the
|
|
154
|
+
* hub's own chat — NOT to give it special treatment. Without it the NATS kernel
|
|
155
|
+
* would have to guess from `approvalType`, and every tool the hub adds would
|
|
156
|
+
* silently fall into the escalation path.
|
|
157
|
+
*/
|
|
158
|
+
export type GuideOrigin = 'guide'
|
|
159
|
+
|
|
160
|
+
/** The only value of {@link GuideOrigin}. Lives beside the type, and beside the
|
|
161
|
+
* predicate below, because both decoders and every consumer that branches on
|
|
162
|
+
* provenance must compare against the same token — a bare `'guide'` literal
|
|
163
|
+
* typo silently disables the branch instead of failing to compile. */
|
|
164
|
+
export const GUIDE_ORIGIN: GuideOrigin = 'guide'
|
|
165
|
+
|
|
166
|
+
/** True for anything stamped as coming from the Product Guide — a stream event
|
|
167
|
+
* or the `data` of a segment built from one. */
|
|
168
|
+
export function isGuideOrigin(
|
|
169
|
+
source: { origin?: GuideOrigin | string } | null | undefined,
|
|
170
|
+
): boolean {
|
|
171
|
+
return source?.origin === GUIDE_ORIGIN
|
|
141
172
|
}
|
|
142
173
|
|
|
143
174
|
/** An approval request was resolved (SSE `decision_resolved` frame /
|
|
@@ -158,6 +189,8 @@ export interface ApprovalResolvedEvent extends ChatStreamEventBase {
|
|
|
158
189
|
marker?: string
|
|
159
190
|
result?: DecisionResolvedFrame['result']
|
|
160
191
|
willAutoContinue?: boolean
|
|
192
|
+
/** Set when the resolution came from a Product Guide frame — see {@link GuideOrigin}. */
|
|
193
|
+
origin?: GuideOrigin
|
|
161
194
|
}
|
|
162
195
|
|
|
163
196
|
/** The client is offered a handoff of this ticket to a human technician
|
|
@@ -245,6 +278,13 @@ export interface ChatMetadataEvent extends ChatStreamEventBase {
|
|
|
245
278
|
routedModel?: string
|
|
246
279
|
routedThinkingBudget: number | null
|
|
247
280
|
}
|
|
281
|
+
/**
|
|
282
|
+
* Set when the metadata came from a Product Guide frame — see
|
|
283
|
+
* {@link GuideOrigin}. Such an event carries ONLY `conversationId`: it exists
|
|
284
|
+
* to record the hub's conversation id (every confirm-tool call must quote it
|
|
285
|
+
* back), NOT to describe the dialog's model, which stays the agent's.
|
|
286
|
+
*/
|
|
287
|
+
origin?: GuideOrigin
|
|
248
288
|
}
|
|
249
289
|
|
|
250
290
|
/** SSE usage frames — raw wire keys (snake_case) preserved. */
|
|
@@ -14,6 +14,9 @@ export * from './events'
|
|
|
14
14
|
export * from './encode'
|
|
15
15
|
export * from './decode'
|
|
16
16
|
export * from './nats-decoder'
|
|
17
|
+
// The hub's confirm-tool request/error contract — shared by every transport
|
|
18
|
+
// that resolves a Product Guide proposal.
|
|
19
|
+
export * from './confirm-tool'
|
|
17
20
|
// Cross-repo IP bucket-key normalizer (producer app + consumer hub share it).
|
|
18
21
|
export * from './ip-normalize'
|
|
19
22
|
// Cross-repo env-flag predicate — the trust assertion that gates the IP
|