@jini-ai/chat 0.6.12 → 0.6.15-rc.2
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/CHANGELOG.md +30 -0
- package/dist/contracts/chat-bubble-style.d.ts +3 -2
- package/dist/contracts/chat-bubble-style.js +3 -2
- package/dist/core/accepted-input.d.ts +20 -1
- package/dist/core/accepted-input.js +73 -19
- package/dist/core/ag-ui/entry.d.ts +2 -4
- package/dist/core/ag-ui/entry.js +11 -5
- package/dist/core/assistant-client/ag-ui-transport.js +6 -1
- package/dist/core/assistant-client/durable-run-subscription.d.ts +2 -1
- package/dist/core/assistant-client/durable-run-subscription.js +3 -1
- package/dist/core/assistant-client/transport.js +5 -1
- package/dist/core/assistant-content.d.ts +8 -1
- package/dist/core/assistant-content.js +17 -4
- package/dist/core/durable-runs/continuation.js +6 -2
- package/dist/core/events.d.ts +3 -0
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.js +1 -0
- package/dist/core/question-answer.d.ts +8 -0
- package/dist/core/question-answer.js +24 -7
- package/dist/core/run-activity.d.ts +1 -1
- package/dist/core/run-activity.js +46 -4
- package/dist/core/run-events/entry.js +5 -2
- package/dist/core/turn-usage.js +5 -2
- package/dist/embed-widget/vanilla/style.css +1 -1
- package/dist/react/components/CopyMessageButton.d.ts +18 -0
- package/dist/react/components/CopyMessageButton.js +22 -0
- package/dist/react/components/Markdown.js +24 -5
- package/dist/react/components/McpUiSurfaceCard.js +50 -17
- package/dist/react/components/MessageList.hooks.d.ts +2 -1
- package/dist/react/components/MessageList.hooks.js +14 -4
- package/dist/react/components/MessageList.js +5 -3
- package/dist/react/components/MessageRow.d.ts +3 -1
- package/dist/react/components/MessageRow.js +66 -38
- package/dist/react/components/MessageRow.usage.js +4 -1
- package/dist/react/components/MidRunUserMessage.d.ts +1 -1
- package/dist/react/components/MidRunUserMessage.js +11 -7
- package/dist/react/components/QuestionForm.js +22 -2
- package/dist/react/components/ToolCard.js +21 -8
- package/dist/react/components/mcp-ui-question-input.d.ts +20 -0
- package/dist/react/components/mcp-ui-question-input.js +72 -0
- package/dist/react/ext-event-renderer-registry.d.ts +2 -0
- package/dist/react/features/chat-pane/components/ChatPane.js +10 -8
- package/dist/react/features/chat-pane/hooks/useChatPane.hooks.d.ts +6 -0
- package/dist/react/features/chat-pane/hooks/useChatPane.hooks.js +78 -20
- package/dist/react/features/chat-pane/hooks/useChatPaneAgentControl.hooks.js +16 -20
- package/dist/react/hooks/useAnsweredCards.js +36 -20
- package/dist/react/hooks/useConversation.d.ts +6 -0
- package/dist/react/hooks/useConversation.js +10 -3
- package/dist/react/index.d.ts +7 -0
- package/dist/react/index.js +4 -0
- package/dist/react/message-blocks.d.ts +6 -0
- package/dist/react/message-blocks.js +27 -1
- package/dist/react/tool-call-display.js +1 -2
- package/dist/server/durable-runs/accept.d.ts +4 -0
- package/dist/server/durable-runs/accept.js +8 -1
- package/dist/server/durable-runs/recover.js +5 -4
- package/package.json +15 -15
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,33 @@
|
|
|
1
|
+
## 0.6.13
|
|
2
|
+
|
|
3
|
+
### Patch Changes
|
|
4
|
+
|
|
5
|
+
- 3feabff: Keep standalone MCP-UI questions answered across history replay through resource-bound accepted-input receipts. Preserve readable question and option labels, accessible frame titles, and waiting activity until an answer is recorded.
|
|
6
|
+
- e5f4969: Chat transcript fixes: answered approval/option cards show "<title>: <option label>" instead of raw values; mid-run follow-ups read "Queued — read after the current step" until consumed, render as ordinary transcript rows at the point they were sent; standalone approval forms show "Waiting for your answer above"; text from separate assistant steps is joined with a paragraph break.
|
|
7
|
+
- f1b2266: Keep collapsed tool details compact, bound and redact error previews and tooltips, and show complete stored tool text on expansion. Add long-chat DOM and page-reader regression coverage.
|
|
8
|
+
- 0728914: Engine guard clean-up (`pnpm guard` 106 -> 0): layer, subpath-isolation and module-state findings fixed by moving
|
|
9
|
+
shared contracts to their owners instead of reaching across domains. Additive public API: `@jini-ai/core/filesystem`
|
|
10
|
+
(guarded-reader contracts), `useFetchQueryScope` from `@jini-ai/ui` fetch-query (per-provider query scope),
|
|
11
|
+
`@jini-ai/chat/react` re-exports of the accepted-input, run-state and agent-plugin chat-state helpers. The Claude
|
|
12
|
+
filesystem probe cache in `@jini-ai/agent-runtime` is instance-owned. No behaviour change intended; host-facing
|
|
13
|
+
breaks are listed in `owner-store-to-server` and `guard-host-ports`.
|
|
14
|
+
- 0a2b903: Share accessible eye toggles across React credential fields, including multiline question answers,
|
|
15
|
+
CLI overrides, provider keys, login, new users, password resets, and source configuration. Preserve
|
|
16
|
+
autofill protection, translated toggle names, agent handles, and secret-answer redaction. Hidden
|
|
17
|
+
source environment blocks keep names visible and stored values masked until explicitly revealed.
|
|
18
|
+
|
|
19
|
+
Chat and user-management require `@jini-ai/ui ^0.5.7` for the new `SecretInput` and `SecretTextarea`
|
|
20
|
+
exports. Release UI 0.5.7 (or adjust those peer floors to the actual release containing these exports)
|
|
21
|
+
with these consumers; older UI versions cannot satisfy their imports. Hosts supply `.jini-secret`,
|
|
22
|
+
`.jini-secret-toggle`, and `.jini-secret--multiline` visual rules.
|
|
23
|
+
|
|
24
|
+
- Updated dependencies [7479159]
|
|
25
|
+
- Updated dependencies [c31264d]
|
|
26
|
+
- Updated dependencies [0728914]
|
|
27
|
+
- Updated dependencies [6ecbdb2]
|
|
28
|
+
- Updated dependencies [63bacc5]
|
|
29
|
+
- @jini-ai/core@0.6.13
|
|
30
|
+
|
|
1
31
|
## 0.6.12
|
|
2
32
|
|
|
3
33
|
### Patch Changes
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
-
/** Shared bubble styles for server placeholders and mounted DOM widgets; no runtime dependencies.
|
|
2
|
-
|
|
1
|
+
/** Shared bubble styles for server placeholders and mounted DOM widgets; no runtime dependencies.
|
|
2
|
+
* Consent can suspend floating UI without moving a tall open panel beyond the viewport. */
|
|
3
|
+
export declare const CHAT_BUBBLE_STYLE = ".jini-chat-vanilla{display:var(--jini-floating-ui-display,block)}.jini-chat-bubble{position:fixed;right:var(--jini-embed-right,24px);bottom:24px;width:56px;height:56px;border:0;border-radius:50%;background:var(--jini-chat-accent,var(--color-primary,#2563eb));color:var(--jini-chat-accent-text,#fff);box-shadow:0 4px 16px #0003;cursor:pointer;z-index:var(--jini-embed-z-index,1000);display:var(--jini-floating-ui-display,grid);place-items:center}\n.jini-chat-bubble[hidden]{display:none!important}\n.jini-chat-bubble svg{width:24px;height:24px}\n.jini-chat-bubble:focus-visible{outline:3px solid var(--jini-chat-accent,#2563eb);outline-offset:3px}\n@media(max-width:480px){.jini-chat-bubble{right:16px;bottom:16px}}\n.jini-chat-bubble[aria-busy=\"true\"]{cursor:progress;opacity:.65}\n.jini-chat-bubble[data-load-failed=\"true\"]{outline:2px solid var(--jini-chat-error,#b91c1c);outline-offset:3px}";
|
|
3
4
|
//# sourceMappingURL=chat-bubble-style.d.ts.map
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
-
/** Shared bubble styles for server placeholders and mounted DOM widgets; no runtime dependencies.
|
|
2
|
-
|
|
1
|
+
/** Shared bubble styles for server placeholders and mounted DOM widgets; no runtime dependencies.
|
|
2
|
+
* Consent can suspend floating UI without moving a tall open panel beyond the viewport. */
|
|
3
|
+
export const CHAT_BUBBLE_STYLE = '.jini-chat-vanilla{display:var(--jini-floating-ui-display,block)}.jini-chat-bubble{position:fixed;right:var(--jini-embed-right,24px);bottom:24px;width:56px;height:56px;border:0;border-radius:50%;background:var(--jini-chat-accent,var(--color-primary,#2563eb));color:var(--jini-chat-accent-text,#fff);box-shadow:0 4px 16px #0003;cursor:pointer;z-index:var(--jini-embed-z-index,1000);display:var(--jini-floating-ui-display,grid);place-items:center}\n.jini-chat-bubble[hidden]{display:none!important}\n.jini-chat-bubble svg{width:24px;height:24px}\n.jini-chat-bubble:focus-visible{outline:3px solid var(--jini-chat-accent,#2563eb);outline-offset:3px}\n@media(max-width:480px){.jini-chat-bubble{right:16px;bottom:16px}}\n.jini-chat-bubble[aria-busy="true"]{cursor:progress;opacity:.65}\n.jini-chat-bubble[data-load-failed="true"]{outline:2px solid var(--jini-chat-error,#b91c1c);outline-offset:3px}';
|
|
3
4
|
//# sourceMappingURL=chat-bubble-style.js.map
|
|
@@ -3,6 +3,10 @@ import type { ChatAttachment, ChatMessage } from './messages.js';
|
|
|
3
3
|
export interface AcceptedChatInput {
|
|
4
4
|
id: string;
|
|
5
5
|
text: string;
|
|
6
|
+
/** Standalone questions have no owning tool id; a receipt answers the preceding document at its URI. */
|
|
7
|
+
surfaceUri?: string;
|
|
8
|
+
/** The card's answer without its title; text retains the complete user bubble. */
|
|
9
|
+
answerText?: string;
|
|
6
10
|
/** Absent for legacy/ordinary inputs; consumed only after a CLI UUID receipt. */
|
|
7
11
|
deliveryState?: 'queued' | 'consumed';
|
|
8
12
|
attachments?: ChatAttachment[];
|
|
@@ -27,10 +31,25 @@ type UserMessageEvent = Extract<AgentEvent, {
|
|
|
27
31
|
inputs?: AcceptedChatInput[];
|
|
28
32
|
};
|
|
29
33
|
};
|
|
34
|
+
/** Hosts can recover standalone form labels without adding a DOM dependency to core. */
|
|
35
|
+
export interface AcceptedInputProjectionOptions {
|
|
36
|
+
answerContexts?: ReadonlyMap<string, {
|
|
37
|
+
input: unknown;
|
|
38
|
+
includeTitle?: boolean;
|
|
39
|
+
}>;
|
|
40
|
+
}
|
|
30
41
|
/** Reads the existing event-backed user bubble without depending on a host renderer. */
|
|
31
42
|
export declare function acceptedInputEvent({ event }: {
|
|
32
43
|
event: AgentEvent;
|
|
33
44
|
}, _options?: {}): UserMessageEvent | null;
|
|
45
|
+
/** Bind each unique receipt to the preceding document, never a later outcome or retry at its URI.
|
|
46
|
+
* Both transcript closure and activity use this projection; resource identity alone is not an answer.
|
|
47
|
+
* @returns Answer labels keyed by the original ext-event data; no caller mutation or I/O.
|
|
48
|
+
* @complexity O(e) time/space for events, including replay deduplication by accepted-input id.
|
|
49
|
+
*/
|
|
50
|
+
export declare function recordedSurfaceAnswers({ events }: {
|
|
51
|
+
events: readonly AgentEvent[];
|
|
52
|
+
}, _options?: {}): ReadonlyMap<unknown, string>;
|
|
34
53
|
/** Builds the same bubble for delivered answers and steering; queues use the existing pending row. */
|
|
35
54
|
export declare function acceptedInputUserEvent({ input, inputs, toolUseId, afterInputId }: {
|
|
36
55
|
input: AcceptedChatInput;
|
|
@@ -47,7 +66,7 @@ export declare function acceptedInputUserEvent({ input, inputs, toolUseId, after
|
|
|
47
66
|
*/
|
|
48
67
|
export declare function projectAcceptedInputEvents({ events }: {
|
|
49
68
|
events: readonly AgentEvent[];
|
|
50
|
-
},
|
|
69
|
+
}, options?: AcceptedInputProjectionOptions): AgentEvent[];
|
|
51
70
|
/** The sole transcript-to-recall projection. A merged queued turn retains its original inputs. */
|
|
52
71
|
export declare function acceptedInputsFromMessages({ messages }: {
|
|
53
72
|
messages: readonly ChatMessage[];
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Accepted composer input has one identity across optimistic display, replay and recall. */
|
|
2
|
-
import { questionAnswerFromResult } from './question-answer.js';
|
|
2
|
+
import { questionAnswerFromResult, questionAnswerLabel } from './question-answer.js';
|
|
3
3
|
import { MID_RUN_USER_MESSAGE_EVENT_NAME } from './turn-usage.js';
|
|
4
4
|
/** Display queue constituents using their accepted identities; storage and delivery retain one turn.
|
|
5
5
|
* MOVE: Receipts travel in events_json, so separate bubbles survive a history reload without another store.
|
|
@@ -28,22 +28,56 @@ export function acceptedInputEvent({ event }, _options = {}) {
|
|
|
28
28
|
const data = event.data;
|
|
29
29
|
return data && typeof data.id === 'string' && typeof data.text === 'string' ? event : null;
|
|
30
30
|
}
|
|
31
|
+
/** Read resource identity without importing the browser/UI parser into core. */
|
|
32
|
+
function surfaceUriOf({ event }, _options = {}) {
|
|
33
|
+
if (event.kind !== 'ext' || event.name !== 'mcp-ui')
|
|
34
|
+
return undefined;
|
|
35
|
+
const data = event.data;
|
|
36
|
+
return typeof data?.resource?.uri === 'string' ? data.resource.uri : undefined;
|
|
37
|
+
}
|
|
38
|
+
/** Bind each unique receipt to the preceding document, never a later outcome or retry at its URI.
|
|
39
|
+
* Both transcript closure and activity use this projection; resource identity alone is not an answer.
|
|
40
|
+
* @returns Answer labels keyed by the original ext-event data; no caller mutation or I/O.
|
|
41
|
+
* @complexity O(e) time/space for events, including replay deduplication by accepted-input id.
|
|
42
|
+
*/
|
|
43
|
+
export function recordedSurfaceAnswers({ events }, _options = {}) {
|
|
44
|
+
const latest = new Map();
|
|
45
|
+
const answers = new Map();
|
|
46
|
+
const seen = new Set();
|
|
47
|
+
for (const event of events) {
|
|
48
|
+
const uri = surfaceUriOf({ event });
|
|
49
|
+
if (uri !== undefined && event.kind === 'ext')
|
|
50
|
+
latest.set(uri, event.data);
|
|
51
|
+
const receipt = acceptedInputEvent({ event })?.data;
|
|
52
|
+
if (typeof receipt?.surfaceUri !== 'string' || seen.has(receipt.id))
|
|
53
|
+
continue;
|
|
54
|
+
seen.add(receipt.id);
|
|
55
|
+
const document = latest.get(receipt.surfaceUri);
|
|
56
|
+
if (document !== undefined)
|
|
57
|
+
answers.set(document, surfaceAnswerText({ receipt }));
|
|
58
|
+
}
|
|
59
|
+
return answers;
|
|
60
|
+
}
|
|
61
|
+
function surfaceAnswerText({ receipt }, _options = {}) {
|
|
62
|
+
return typeof receipt.answerText === 'string' ? receipt.answerText : receipt.text;
|
|
63
|
+
}
|
|
31
64
|
/** Project a persisted answer through the same receipt owner as composer input. */
|
|
32
|
-
function questionAnswerEvent({ event, input },
|
|
65
|
+
function questionAnswerEvent({ event, input }, { includeTitle = false } = {}) {
|
|
33
66
|
if (event.kind !== 'tool_result')
|
|
34
67
|
return null;
|
|
35
68
|
const text = questionAnswerFromResult({ result: event, input });
|
|
36
69
|
if (text === undefined)
|
|
37
70
|
return null;
|
|
71
|
+
// A standalone form's bubble reads like its closed card: "<title>: <answer>".
|
|
38
72
|
return { kind: 'ext', name: MID_RUN_USER_MESSAGE_EVENT_NAME,
|
|
39
|
-
data: { id: `answer:${event.toolUseId}`, text, toolUseId: event.toolUseId } };
|
|
73
|
+
data: { id: `answer:${event.toolUseId}`, text: includeTitle ? questionAnswerLabel({ input, text }) : text, toolUseId: event.toolUseId } };
|
|
40
74
|
}
|
|
41
75
|
/** Builds the same bubble for delivered answers and steering; queues use the existing pending row. */
|
|
42
76
|
export function acceptedInputUserEvent({ input, inputs, toolUseId, afterInputId }, { receipt = 'optimistic' } = {}) {
|
|
43
77
|
return { kind: 'ext', name: MID_RUN_USER_MESSAGE_EVENT_NAME,
|
|
44
78
|
data: { ...input, ...(receipt === 'optimistic' ? { optimistic: true } : {}), ...(inputs === undefined ? {} : { inputs }), ...(afterInputId === undefined ? {} : { afterInputId }), ...(toolUseId === undefined ? {} : { toolUseId }) } };
|
|
45
79
|
}
|
|
46
|
-
function expandQuestionAnswers({ events },
|
|
80
|
+
function expandQuestionAnswers({ events }, { answerContexts } = {}) {
|
|
47
81
|
const savedAnswers = new Set(events.flatMap(event => {
|
|
48
82
|
const input = acceptedInputEvent({ event });
|
|
49
83
|
return input && !input.data.optimistic && input.data.toolUseId ? [input.data.toolUseId] : [];
|
|
@@ -51,7 +85,9 @@ function expandQuestionAnswers({ events }, _options = {}) {
|
|
|
51
85
|
// MOVE: Persisted picks carry values; the original tool input retains their human-readable labels.
|
|
52
86
|
const questions = new Map(events.flatMap(event => event.kind === 'tool_use' ? [[event.id, event.input]] : []));
|
|
53
87
|
const answers = new Map(events.flatMap(event => {
|
|
54
|
-
const
|
|
88
|
+
const context = event.kind === 'tool_result' ? answerContexts?.get(event.toolUseId) : undefined;
|
|
89
|
+
const input = context ? context.input : event.kind === 'tool_result' ? questions.get(event.toolUseId) : undefined;
|
|
90
|
+
const answer = questionAnswerEvent({ event, input }, { includeTitle: context?.includeTitle === true });
|
|
55
91
|
return answer ? [[answer.data.toolUseId, answer]] : [];
|
|
56
92
|
}));
|
|
57
93
|
return events.flatMap(event => {
|
|
@@ -87,10 +123,17 @@ function matchOptimisticInput({ input, optimistic, seen }, _options = {}) {
|
|
|
87
123
|
// Keep predecessor testing before the caller adds this receipt's ids to seen.
|
|
88
124
|
return input.data.toolUseId ? optimistic.byQuestion.get(input.data.toolUseId) : follows ? next : undefined;
|
|
89
125
|
}
|
|
126
|
+
/** Apply authoritative receipt data while retaining local identity and queued constituents. */
|
|
90
127
|
function mergeAcceptedReceipt({ input, pending }, _options = {}) {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
128
|
+
// Replayed queue acceptance cannot undo confirmed consumption.
|
|
129
|
+
const deliveryState = pending.data.deliveryState === 'consumed' ? 'consumed' : input.data.deliveryState ?? pending.data.deliveryState;
|
|
130
|
+
const { optimistic: _optimistic, afterInputId: _afterInputId, ...pendingData } = pending.data;
|
|
131
|
+
const aliases = [...new Set([...(input.data.aliases ?? []), input.data.id, ...(pending.data.aliases ?? [])])].filter(id => id !== pending.data.id);
|
|
132
|
+
return { ...input, data: { ...pendingData, ...input.data, id: pending.data.id,
|
|
133
|
+
// Legacy acceptance receipts omit state; HTTP acceptance still means queued until consumption.
|
|
134
|
+
...(deliveryState === undefined ? {} : { deliveryState }),
|
|
135
|
+
...(pending.data.inputs ? { inputs: pending.data.inputs.map(item => ({ ...item, ...(deliveryState ? { deliveryState } : {}) })) } : {}),
|
|
136
|
+
...(aliases.length > 0 ? { aliases } : {}) } };
|
|
94
137
|
}
|
|
95
138
|
function acceptedInputIds({ input }, _options = {}) {
|
|
96
139
|
return [input.data.id, ...(input.data.toolUseId ? [`answer:${input.data.toolUseId}`] : []), ...(input.data.aliases ?? [])];
|
|
@@ -114,8 +157,9 @@ function reconcileAcceptedInput({ event, optimistic, state }, _options = {}) {
|
|
|
114
157
|
for (const id of [pending.data.id, ...(pending.data.aliases ?? [])])
|
|
115
158
|
seen.add(id);
|
|
116
159
|
optimistic.byQuestion.delete(input.data.toolUseId ?? '');
|
|
117
|
-
|
|
118
|
-
|
|
160
|
+
// Acceptance can arrive after the reply it triggered. Keep the position where the person sent it.
|
|
161
|
+
replaced.add(event);
|
|
162
|
+
replacements.set(pending, mergeAcceptedReceipt({ input, pending }));
|
|
119
163
|
}
|
|
120
164
|
function advanceOptimisticInput({ input, optimistic }, _options = {}) {
|
|
121
165
|
const candidates = optimistic.byText.get(input.data.text);
|
|
@@ -130,28 +174,38 @@ function collapseDeliveryReceipts({ events }, _options = {}) {
|
|
|
130
174
|
for (const event of events) {
|
|
131
175
|
const input = acceptedInputEvent({ event });
|
|
132
176
|
const position = input ? positions.get(input.data.id) : undefined;
|
|
133
|
-
if (input && position !== undefined && input.data.
|
|
177
|
+
if (input && position !== undefined && !input.data.optimistic) {
|
|
134
178
|
const original = acceptedInputEvent({ event: result[position] });
|
|
135
|
-
|
|
136
|
-
const deliveryState = original.data.deliveryState === 'consumed' ? 'consumed' : input.data.deliveryState;
|
|
137
|
-
result[position] = { ...original, data: { ...original.data, deliveryState,
|
|
138
|
-
...(original.data.inputs ? { inputs: original.data.inputs.map(input => ({ ...input, deliveryState })) } : {}) } };
|
|
179
|
+
result[position] = mergeAcceptedReceipt({ input, pending: original });
|
|
139
180
|
continue;
|
|
140
181
|
}
|
|
141
182
|
if (input)
|
|
142
|
-
|
|
143
|
-
positions.set(id, result.length);
|
|
183
|
+
recordInputPosition({ input, positions, position: result.length });
|
|
144
184
|
result.push(event);
|
|
145
185
|
}
|
|
146
186
|
return result;
|
|
147
187
|
}
|
|
188
|
+
function recordInputPosition({ input, positions, position }, _options = {}) {
|
|
189
|
+
for (const id of acceptedInputIds({ input }))
|
|
190
|
+
positions.set(id, position);
|
|
191
|
+
}
|
|
148
192
|
/** Projects saved answers and reconciles optimistic bubbles with wire events, occurrence for
|
|
149
193
|
* occurrence. Legacy steering receipts have no client id: only unmatched optimistic input may
|
|
150
194
|
* match by text; two real events with equal text always remain distinct.
|
|
151
195
|
* @complexity O(e + u + a) time, O(e + u) space for e events, u user inputs and total answer-label lookup cost a.
|
|
152
196
|
*/
|
|
153
|
-
export function projectAcceptedInputEvents({ events },
|
|
154
|
-
|
|
197
|
+
export function projectAcceptedInputEvents({ events }, options) {
|
|
198
|
+
if (options !== undefined)
|
|
199
|
+
return projectEvents({ events }, options);
|
|
200
|
+
// Event arrays are replaced, never mutated, so a transcript update re-projects only changed messages.
|
|
201
|
+
let projected = defaultProjections.get(events);
|
|
202
|
+
if (!projected)
|
|
203
|
+
defaultProjections.set(events, projected = projectEvents({ events }, {}));
|
|
204
|
+
return projected;
|
|
205
|
+
}
|
|
206
|
+
const defaultProjections = new WeakMap();
|
|
207
|
+
function projectEvents({ events }, options) {
|
|
208
|
+
const expanded = expandQuestionAnswers({ events: collapseDeliveryReceipts({ events }) }, options);
|
|
155
209
|
const optimistic = collectOptimisticInputs({ events: expanded });
|
|
156
210
|
const state = { replacements: new Map(), replaced: new Set(), seen: new Set() };
|
|
157
211
|
for (const event of expanded)
|
|
@@ -72,10 +72,8 @@ export declare function createAgUiToAgentTranslationState({ customEventNames }:
|
|
|
72
72
|
* Translates one AG-UI event into zero or more chat-core `AgentEvent`s, threading `state` across
|
|
73
73
|
* calls within a single run for tool-call argument accumulation.
|
|
74
74
|
*
|
|
75
|
-
* `TEXT_MESSAGE_START
|
|
76
|
-
*
|
|
77
|
-
* the same asymmetry `translateRunAgentPayload` already has reducing the OTHER leg of this round
|
|
78
|
-
* trip (daemon wire -> `AgentEvent`) — this function is that reduction's mirror image. The two cases
|
|
75
|
+
* `TEXT_MESSAGE_START` preserves an explicit step boundary so two assistant messages cannot fuse
|
|
76
|
+
* into one word. End/reasoning markers carry no text. The two cases
|
|
79
77
|
* with real internal branching (`TOOL_CALL_END`, `CUSTOM`) and the one with a guard (`TOOL_CALL_ARGS`)
|
|
80
78
|
* are pulled into their own named functions above so this switch's own cost is just its case count.
|
|
81
79
|
*/
|
package/dist/core/ag-ui/entry.js
CHANGED
|
@@ -145,6 +145,14 @@ function accumulateToolCallArgs(toolCallId, delta, state) {
|
|
|
145
145
|
if (call)
|
|
146
146
|
call.argsJson += delta;
|
|
147
147
|
}
|
|
148
|
+
/** `TEXT_MESSAGE_START` keeps an explicit step boundary so two assistant messages cannot fuse into
|
|
149
|
+
* one word; every other unmapped event translates to nothing. Split out so this `if` is scored in
|
|
150
|
+
* its own complexity budget rather than the switch's. */
|
|
151
|
+
function translateAgUiStepStart({ event }, _options = {}) {
|
|
152
|
+
if (event.type !== EventType.TEXT_MESSAGE_START)
|
|
153
|
+
return [];
|
|
154
|
+
return [{ kind: 'status', code: 'step_start', label: '', data: { messageId: event.messageId } }];
|
|
155
|
+
}
|
|
148
156
|
/** The `TOOL_CALL_END` case of {@link translateAgUiEventToAgentEvent} — parses the accumulated
|
|
149
157
|
* args buffer (already removed from `state` by the caller) into one `tool_use` event, or `[]` for
|
|
150
158
|
* an id with no matching `TOOL_CALL_START`. */
|
|
@@ -177,10 +185,8 @@ function translateAgUiCustomEvent(event, names) {
|
|
|
177
185
|
* Translates one AG-UI event into zero or more chat-core `AgentEvent`s, threading `state` across
|
|
178
186
|
* calls within a single run for tool-call argument accumulation.
|
|
179
187
|
*
|
|
180
|
-
* `TEXT_MESSAGE_START
|
|
181
|
-
*
|
|
182
|
-
* the same asymmetry `translateRunAgentPayload` already has reducing the OTHER leg of this round
|
|
183
|
-
* trip (daemon wire -> `AgentEvent`) — this function is that reduction's mirror image. The two cases
|
|
188
|
+
* `TEXT_MESSAGE_START` preserves an explicit step boundary so two assistant messages cannot fuse
|
|
189
|
+
* into one word. End/reasoning markers carry no text. The two cases
|
|
184
190
|
* with real internal branching (`TOOL_CALL_END`, `CUSTOM`) and the one with a guard (`TOOL_CALL_ARGS`)
|
|
185
191
|
* are pulled into their own named functions above so this switch's own cost is just its case count.
|
|
186
192
|
*/
|
|
@@ -211,7 +217,7 @@ export function translateAgUiEventToAgentEvent({ event, state }, _options = {})
|
|
|
211
217
|
case EventType.CUSTOM:
|
|
212
218
|
return translateAgUiCustomEvent(event, state.customEventNames);
|
|
213
219
|
default:
|
|
214
|
-
return
|
|
220
|
+
return translateAgUiStepStart({ event });
|
|
215
221
|
}
|
|
216
222
|
}
|
|
217
223
|
//# sourceMappingURL=entry.js.map
|
|
@@ -79,7 +79,7 @@ export function createAgUiRunClient({ ports }, _options = {}) {
|
|
|
79
79
|
handlers.onDone(collected);
|
|
80
80
|
};
|
|
81
81
|
const state = createAgUiToAgentTranslationState({ customEventNames: ports.customEventNames }, {});
|
|
82
|
-
// Deliberately racing the Observable's first `next`/`error` against this Promise rather than
|
|
82
|
+
// Deliberately racing the Observable's first `next`/`error`/`complete` against this Promise rather than
|
|
83
83
|
// awaiting it directly — `HttpAgent.run()` returns a cold Observable, never a Promise (module
|
|
84
84
|
// doc). The subscription itself keeps running after this Promise settles, driving `handlers`
|
|
85
85
|
// through to `finish()` in the background — matching `startByokRun`'s "resolves once STARTED, not
|
|
@@ -115,6 +115,11 @@ export function createAgUiRunClient({ ports }, _options = {}) {
|
|
|
115
115
|
finish();
|
|
116
116
|
},
|
|
117
117
|
complete: () => {
|
|
118
|
+
// An empty stream has no first notification to settle the start promise.
|
|
119
|
+
if (!startSettled) {
|
|
120
|
+
startSettled = true;
|
|
121
|
+
resolve({ runId });
|
|
122
|
+
}
|
|
118
123
|
finish();
|
|
119
124
|
},
|
|
120
125
|
});
|
|
@@ -28,7 +28,8 @@ type CheckpointHandlers = RunHandlers & {
|
|
|
28
28
|
onCheckpoint?: (message: ChatMessage) => void;
|
|
29
29
|
};
|
|
30
30
|
/** SSE is a view of one attempt. The saved message owns terminal state and can point at a new
|
|
31
|
-
* attempt after recovery; neither a browser 404 nor a closed stream may fail the logical run.
|
|
31
|
+
* attempt after recovery; neither a browser 404 nor a closed stream may fail the logical run.
|
|
32
|
+
* An explicit "gone" result ends observation and releases the caller's streaming state. */
|
|
32
33
|
export declare function followDurableRun({ runId: initialRunId, handlers, signal, binding, ports, notices, bindings }: {
|
|
33
34
|
runId: string;
|
|
34
35
|
handlers: CheckpointHandlers;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { translateRunFrame } from "../run-events/entry.js";
|
|
2
2
|
/** SSE is a view of one attempt. The saved message owns terminal state and can point at a new
|
|
3
|
-
* attempt after recovery; neither a browser 404 nor a closed stream may fail the logical run.
|
|
3
|
+
* attempt after recovery; neither a browser 404 nor a closed stream may fail the logical run.
|
|
4
|
+
* An explicit "gone" result ends observation and releases the caller's streaming state. */
|
|
4
5
|
export function followDurableRun({ runId: initialRunId, handlers, signal, binding, ports, notices, bindings }, { logger = console } = {}) {
|
|
5
6
|
let streamEpoch = 0;
|
|
6
7
|
let runId = initialRunId;
|
|
@@ -93,6 +94,7 @@ export function followDurableRun({ runId: initialRunId, handlers, signal, bindin
|
|
|
93
94
|
return;
|
|
94
95
|
if (message === "gone") {
|
|
95
96
|
stop();
|
|
97
|
+
handlers.onDone([]);
|
|
96
98
|
return;
|
|
97
99
|
}
|
|
98
100
|
applySavedMessage(message);
|
|
@@ -406,7 +406,11 @@ export function createAssistantTransportClient({ ports }, _options = {}) {
|
|
|
406
406
|
const runId = mintByokRunId();
|
|
407
407
|
const controller = new AbortController();
|
|
408
408
|
byokAbortControllers.set(runId, controller);
|
|
409
|
-
|
|
409
|
+
// Conversation adoption may have yielded while the subscription was torn down.
|
|
410
|
+
if (input.signal?.aborted)
|
|
411
|
+
controller.abort();
|
|
412
|
+
else
|
|
413
|
+
input.signal?.addEventListener("abort", () => controller.abort());
|
|
410
414
|
let response;
|
|
411
415
|
try {
|
|
412
416
|
response = await ports.fetch(ports.byokTurnUrl, {
|
|
@@ -17,6 +17,13 @@
|
|
|
17
17
|
import type { AgentEvent } from './events.js';
|
|
18
18
|
/** What separates two text runs that a tool call or card sat between. */
|
|
19
19
|
export declare const ASSISTANT_STEP_SEPARATOR = "\n\n";
|
|
20
|
+
/** Only explicit step transitions separate text. Thinking-token heartbeats and raw diagnostics
|
|
21
|
+
* may occur inside a streamed step and must never split its deltas.
|
|
22
|
+
* @complexity O(1); no mutation or I/O.
|
|
23
|
+
*/
|
|
24
|
+
export declare function isAssistantStepBoundary({ event }: {
|
|
25
|
+
event: AgentEvent;
|
|
26
|
+
}, _options?: {}): boolean;
|
|
20
27
|
/**
|
|
21
28
|
* A turn's text, with a paragraph break between runs separated by a tool call or card.
|
|
22
29
|
* A replay of an already-seen tool ID is not a new step, matching the saved-event projection.
|
|
@@ -26,7 +33,7 @@ export declare const ASSISTANT_STEP_SEPARATOR = "\n\n";
|
|
|
26
33
|
*/
|
|
27
34
|
export declare function assistantContentFromEvents({ events }: {
|
|
28
35
|
events: readonly AgentEvent[] | undefined;
|
|
29
|
-
}): string;
|
|
36
|
+
}, _options?: {}): string;
|
|
30
37
|
/** The pre-2026-09-27 rule (plain concatenation), kept only so older saved rows still match. */
|
|
31
38
|
export declare function legacyAssistantContentFromEvents({ events }: {
|
|
32
39
|
events: readonly AgentEvent[] | undefined;
|
|
@@ -1,11 +1,24 @@
|
|
|
1
1
|
import { dedupeToolUsesById } from './tool-events.js';
|
|
2
2
|
/** What separates two text runs that a tool call or card sat between. */
|
|
3
3
|
export const ASSISTANT_STEP_SEPARATOR = '\n\n';
|
|
4
|
-
function separatorBefore(out, text) {
|
|
4
|
+
function separatorBefore({ out, text }, _options = {}) {
|
|
5
5
|
if (out.length === 0 || out.endsWith(ASSISTANT_STEP_SEPARATOR) || text.startsWith('\n'))
|
|
6
6
|
return '';
|
|
7
7
|
return out.endsWith('\n') ? '\n' : ASSISTANT_STEP_SEPARATOR;
|
|
8
8
|
}
|
|
9
|
+
/** Only explicit step transitions separate text. Thinking-token heartbeats and raw diagnostics
|
|
10
|
+
* may occur inside a streamed step and must never split its deltas.
|
|
11
|
+
* @complexity O(1); no mutation or I/O.
|
|
12
|
+
*/
|
|
13
|
+
export function isAssistantStepBoundary({ event }, _options = {}) {
|
|
14
|
+
if (event.kind === 'tool_use' || event.kind === 'ext' || event.kind === 'usage')
|
|
15
|
+
return true;
|
|
16
|
+
if (event.kind !== 'status')
|
|
17
|
+
return false;
|
|
18
|
+
if (event.code !== undefined)
|
|
19
|
+
return event.code === 'step_start';
|
|
20
|
+
return event.label === 'thinking' || event.label === 'running';
|
|
21
|
+
}
|
|
9
22
|
/**
|
|
10
23
|
* A turn's text, with a paragraph break between runs separated by a tool call or card.
|
|
11
24
|
* A replay of an already-seen tool ID is not a new step, matching the saved-event projection.
|
|
@@ -13,15 +26,15 @@ function separatorBefore(out, text) {
|
|
|
13
26
|
* @param events - The turn's events, in arrival order.
|
|
14
27
|
* @complexity O(n + total text length) time and O(n) space for n events.
|
|
15
28
|
*/
|
|
16
|
-
export function assistantContentFromEvents({ events }) {
|
|
29
|
+
export function assistantContentFromEvents({ events }, _options = {}) {
|
|
17
30
|
let out = '';
|
|
18
31
|
let stepBoundary = false;
|
|
19
32
|
for (const ev of dedupeToolUsesById({ events: events ? [...events] : undefined })) {
|
|
20
|
-
if (
|
|
33
|
+
if (isAssistantStepBoundary({ event: ev })) {
|
|
21
34
|
stepBoundary = true;
|
|
22
35
|
}
|
|
23
36
|
else if (ev.kind === 'text' && ev.text.length > 0) {
|
|
24
|
-
out += (stepBoundary ? separatorBefore(out, ev.text) : '') + ev.text;
|
|
37
|
+
out += (stepBoundary ? separatorBefore({ out, text: ev.text }) : '') + ev.text;
|
|
25
38
|
stepBoundary = false;
|
|
26
39
|
}
|
|
27
40
|
}
|
|
@@ -16,8 +16,12 @@ function delegatedCall(event) {
|
|
|
16
16
|
return { toolId: input.toolId, input: input.input };
|
|
17
17
|
return { toolId: event.name, input: event.input };
|
|
18
18
|
}
|
|
19
|
+
/** Results that settle their call. An outcome-unknown result does not: the call may have run. */
|
|
20
|
+
function settledResults({ events }, _optional = {}) {
|
|
21
|
+
return new Map(events.flatMap((event) => event.kind === "tool_result" && event.outcome !== "unknown" ? [[event.toolUseId, event]] : []));
|
|
22
|
+
}
|
|
19
23
|
export function hasUnknownToolCall({ events, toolId, input }, _optional = {}) {
|
|
20
|
-
const completed =
|
|
24
|
+
const completed = settledResults({ events }, {});
|
|
21
25
|
const canonical = canonicalToolInput({ value: input }, {});
|
|
22
26
|
return events.some((event) => {
|
|
23
27
|
if (event.kind !== "tool_use" || completed.has(event.id))
|
|
@@ -30,7 +34,7 @@ export function hasUnknownToolCall({ events, toolId, input }, _optional = {}) {
|
|
|
30
34
|
});
|
|
31
35
|
}
|
|
32
36
|
function toolHistory(events) {
|
|
33
|
-
const results =
|
|
37
|
+
const results = settledResults({ events }, {});
|
|
34
38
|
return events.filter((event) => event.kind === "tool_use").map((event) => {
|
|
35
39
|
const result = results.get(event.id);
|
|
36
40
|
return JSON.stringify({ call: delegatedCall(event), result: result ?? "OUTCOME UNKNOWN: verify state before repeating" });
|
package/dist/core/events.d.ts
CHANGED
|
@@ -60,6 +60,9 @@ export type AgentEvent = {
|
|
|
60
60
|
/** Typed media (currently just images) a renderer may show directly. Absent for the
|
|
61
61
|
* overwhelming majority of tool results, which carry none. */
|
|
62
62
|
media?: readonly ToolResultMediaBlock[];
|
|
63
|
+
/** Set when the call may or may not have run (its tool RPC connection broke after sending).
|
|
64
|
+
* Such a result does not complete the call: a repeat must verify state first. */
|
|
65
|
+
outcome?: 'unknown';
|
|
63
66
|
} | {
|
|
64
67
|
kind: 'usage';
|
|
65
68
|
inputTokens?: number;
|
package/dist/core/index.d.ts
CHANGED
|
@@ -38,4 +38,5 @@ export * from './history-tool-metadata.js';
|
|
|
38
38
|
export { turnUsage, conversationUsage, replySegments, isReplyBoundary, MID_RUN_USER_MESSAGE_EVENT_NAME, type ChatUsageSummary } from './turn-usage.js';
|
|
39
39
|
export { formatUsageCost } from './usage-format.js';
|
|
40
40
|
export * from './accepted-input.js';
|
|
41
|
+
export * from './question-answer.js';
|
|
41
42
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/core/index.js
CHANGED
|
@@ -37,4 +37,5 @@ export * from './history-tool-metadata.js';
|
|
|
37
37
|
export { turnUsage, conversationUsage, replySegments, isReplyBoundary, MID_RUN_USER_MESSAGE_EVENT_NAME } from './turn-usage.js';
|
|
38
38
|
export { formatUsageCost } from './usage-format.js';
|
|
39
39
|
export * from './accepted-input.js';
|
|
40
|
+
export * from './question-answer.js';
|
|
40
41
|
//# sourceMappingURL=index.js.map
|
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
/** @module question-answer — plain transcript text from the ask-choice result contract. */
|
|
2
|
+
/** Optional question context on an MCP-UI resource, before HTML presentation flattens its labels. */
|
|
3
|
+
export declare const SURFACE_QUESTION_META_KEY = "x-jini-chat/question";
|
|
4
|
+
/** The shared title/answer format for closed cards and standalone form receipts. */
|
|
5
|
+
export declare function questionAnswerLabel({ input, text }: {
|
|
6
|
+
input: unknown;
|
|
7
|
+
text: string;
|
|
8
|
+
}, _options?: {}): string;
|
|
1
9
|
/** Read a classified result or delivered callback as plain user text. Decline wins over other fields;
|
|
2
10
|
* expiry, invalid answers and unrecognized results produce no user message.
|
|
3
11
|
* @returns Answer text, or undefined when no supported human answer is present; never throws.
|
|
@@ -1,13 +1,25 @@
|
|
|
1
1
|
/** @module question-answer — plain transcript text from the ask-choice result contract. */
|
|
2
|
+
/** Optional question context on an MCP-UI resource, before HTML presentation flattens its labels. */
|
|
3
|
+
export const SURFACE_QUESTION_META_KEY = 'x-jini-chat/question';
|
|
2
4
|
function record(value) {
|
|
3
5
|
return typeof value === 'object' && value !== null ? value : {};
|
|
4
6
|
}
|
|
7
|
+
/** The shared title/answer format for closed cards and standalone form receipts. */
|
|
8
|
+
export function questionAnswerLabel({ input, text }, _options = {}) {
|
|
9
|
+
const question = record(input);
|
|
10
|
+
const title = [question.title, question.question, question.prompt]
|
|
11
|
+
.find(value => typeof value === 'string' && value.trim() !== '');
|
|
12
|
+
return typeof title === 'string' ? `${title}: ${text}` : text;
|
|
13
|
+
}
|
|
5
14
|
/** Resolve a stored option value through the question that offered it. Unknown values stay readable. */
|
|
6
|
-
function optionLabel(group, value) {
|
|
15
|
+
function optionLabel({ group, value }, _options = {}) {
|
|
7
16
|
const options = record(group).options;
|
|
8
17
|
if (!Array.isArray(options))
|
|
9
18
|
return value;
|
|
10
|
-
const option = options.find(candidate =>
|
|
19
|
+
const option = options.find(candidate => {
|
|
20
|
+
const offered = record(candidate);
|
|
21
|
+
return (offered.value ?? offered.id ?? offered.label) === value;
|
|
22
|
+
});
|
|
11
23
|
const label = record(option).label;
|
|
12
24
|
return typeof label === 'string' ? label : value;
|
|
13
25
|
}
|
|
@@ -15,17 +27,22 @@ function selectedAnswerText({ result, input }, _options = {}) {
|
|
|
15
27
|
const question = record(input);
|
|
16
28
|
const labels = [];
|
|
17
29
|
if (typeof result.choice === 'string')
|
|
18
|
-
labels.push(optionLabel(question.singleSelect ?? question, result.choice));
|
|
30
|
+
labels.push(optionLabel({ group: question.singleSelect ?? question, value: result.choice }));
|
|
19
31
|
if (Array.isArray(result.selections)) {
|
|
20
|
-
|
|
21
|
-
if (typeof selection === 'string')
|
|
22
|
-
labels.push(optionLabel(question.multiSelect ?? question, selection));
|
|
23
|
-
}
|
|
32
|
+
labels.push(...selectionLabels({ selections: result.selections, question }));
|
|
24
33
|
}
|
|
25
34
|
if (labels.length > 0)
|
|
26
35
|
return labels.join(', ');
|
|
27
36
|
return Array.isArray(result.selections) && result.selections.length === 0 ? 'None selected' : undefined;
|
|
28
37
|
}
|
|
38
|
+
function selectionLabels({ selections, question }, _options = {}) {
|
|
39
|
+
const labels = [];
|
|
40
|
+
for (const selection of selections) {
|
|
41
|
+
if (typeof selection === 'string')
|
|
42
|
+
labels.push(optionLabel({ group: question.multiSelect ?? question, value: selection }));
|
|
43
|
+
}
|
|
44
|
+
return labels;
|
|
45
|
+
}
|
|
29
46
|
/** Read a classified result or delivered callback as plain user text. Decline wins over other fields;
|
|
30
47
|
* expiry, invalid answers and unrecognized results produce no user message.
|
|
31
48
|
* @returns Answer text, or undefined when no supported human answer is present; never throws.
|
|
@@ -55,7 +55,7 @@ export interface RunActivityOptions {
|
|
|
55
55
|
* card); the newest open tool call; else
|
|
56
56
|
* writing (last visible event is text) or thinking.
|
|
57
57
|
*
|
|
58
|
-
* @complexity O(n) in events, O(open tool calls) extra space.
|
|
58
|
+
* @complexity O(n) time in events, O(open tool calls + answered surfaces) extra space.
|
|
59
59
|
*/
|
|
60
60
|
export declare function deriveRunActivity({ events }: {
|
|
61
61
|
events: readonly AgentEvent[] | undefined;
|