@jini-ai/chat 0.6.5 → 0.6.7
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 +12 -0
- package/dist/core/accepted-input.d.ts +54 -0
- package/dist/core/accepted-input.js +165 -0
- package/dist/core/agent-plugin-chat-state.d.ts +43 -0
- package/dist/core/agent-plugin-chat-state.js +48 -0
- package/dist/core/assistant-client/a2ui-action-poster.js +32 -28
- package/dist/core/assistant-client/messages.d.ts +1 -1
- package/dist/core/assistant-client/messages.js +2 -2
- package/dist/core/assistant-client/transport.d.ts +2 -2
- package/dist/core/assistant-client/transport.js +184 -97
- package/dist/core/assistant-content.d.ts +2 -1
- package/dist/core/assistant-content.js +9 -11
- package/dist/core/browser/page-actions.js +4 -2
- package/dist/core/composer-history.d.ts +16 -9
- package/dist/core/composer-history.js +28 -16
- package/dist/core/conversation-title.d.ts +3 -0
- package/dist/core/conversation-title.js +3 -0
- package/dist/core/durable-projection.js +20 -13
- package/dist/core/durable-runs/continuation.d.ts +4 -0
- package/dist/core/durable-runs/continuation.js +35 -6
- package/dist/core/durable-runs/ledger-contracts.d.ts +2 -1
- package/dist/core/durable-runs/policies.d.ts +1 -0
- package/dist/core/durable-runs/policies.js +1 -0
- package/dist/core/durable-runs/ports.d.ts +6 -2
- package/dist/core/index.d.ts +2 -0
- package/dist/core/index.js +2 -0
- package/dist/core/messages.d.ts +2 -0
- package/dist/core/partial-json.d.ts +6 -7
- package/dist/core/partial-json.js +81 -47
- package/dist/core/persistence/title.js +4 -2
- package/dist/core/question-answer.d.ts +23 -0
- package/dist/core/question-answer.js +66 -0
- package/dist/core/question-form/format.d.ts +6 -6
- package/dist/core/question-form/format.js +16 -17
- package/dist/core/question-form/normalize.d.ts +18 -0
- package/dist/core/question-form/normalize.js +112 -122
- package/dist/core/question-form/scan.js +141 -203
- package/dist/core/run-activity.js +62 -49
- package/dist/core/run-events/entry.d.ts +10 -1
- package/dist/core/run-events/entry.js +117 -87
- package/dist/core/todos.d.ts +1 -1
- package/dist/core/todos.js +60 -85
- package/dist/core/tool-events.js +14 -12
- package/dist/core/transcript.js +62 -16
- package/dist/core/turn-usage.d.ts +2 -1
- package/dist/core/turn-usage.js +56 -27
- package/dist/core/user-text-redaction.js +3 -0
- package/dist/core/util/manifest.js +77 -82
- package/dist/core/util/markdown-context.js +51 -53
- package/dist/core/util/parser.js +136 -144
- package/dist/core/util/pointer.js +16 -16
- package/dist/core/util/strip.js +57 -67
- package/dist/core/util/validate.js +15 -17
- package/dist/embed-widget/browser/session-store.js +21 -12
- package/dist/embed-widget/transports/fetch-sse.js +108 -95
- package/dist/embed-widget/vanilla/index.js +81 -70
- package/dist/react/agent-bridge/frontend-session-bridge.js +80 -52
- package/dist/react/components/AttachmentPreviewModal.js +25 -1
- package/dist/react/components/ChatSettingsDialog.d.ts +13 -0
- package/dist/react/components/ChatSettingsDialog.hooks.d.ts +115 -0
- package/dist/react/components/ChatSettingsDialog.hooks.js +40 -0
- package/dist/react/components/ChatSettingsDialog.js +72 -0
- package/dist/react/components/ChatSettingsSegmented.d.ts +18 -0
- package/dist/react/components/ChatSettingsSegmented.js +11 -0
- package/dist/react/components/Composer.d.ts +5 -1
- package/dist/react/components/Composer.js +126 -82
- package/dist/react/components/ConversationList.js +25 -39
- package/dist/react/components/ConversationTitleEditor.d.ts +27 -0
- package/dist/react/components/ConversationTitleEditor.js +54 -0
- package/dist/react/components/Icon.d.ts +1 -1
- package/dist/react/components/Icon.js +25 -45
- package/dist/react/components/Markdown.js +134 -123
- package/dist/react/components/McpUiSurfaceCard.d.ts +2 -2
- package/dist/react/components/McpUiSurfaceCard.js +30 -9
- package/dist/react/components/MessageList.d.ts +13 -13
- package/dist/react/components/MessageList.hooks.d.ts +6 -1
- package/dist/react/components/MessageList.hooks.js +19 -3
- package/dist/react/components/MessageList.js +34 -10
- package/dist/react/components/MessageRow.js +48 -32
- package/dist/react/components/MessageRow.usage.d.ts +5 -2
- package/dist/react/components/MessageRow.usage.js +20 -11
- package/dist/react/components/MidRunUserMessage.js +16 -3
- package/dist/react/components/QuestionForm.js +148 -76
- package/dist/react/components/QuestionsPanel.js +1 -1
- package/dist/react/components/TodoCard.js +12 -6
- package/dist/react/components/ToolCard.js +47 -40
- package/dist/react/components/composer-discovery.js +6 -1
- package/dist/react/components/mcp-ui-surface-state.d.ts +6 -11
- package/dist/react/components/mcp-ui-surface-state.js +23 -8
- package/dist/react/ext-event-renderer-registry.d.ts +8 -0
- package/dist/react/ext-event-renderer-registry.js +5 -2
- package/dist/react/features/chat-pane/components/AgentPluginUsage.d.ts +7 -0
- package/dist/react/features/chat-pane/components/AgentPluginUsage.js +45 -0
- package/dist/react/features/chat-pane/components/AgentRuntimePicker.js +55 -19
- package/dist/react/features/chat-pane/components/ChatPane.d.ts +1 -1
- package/dist/react/features/chat-pane/components/ChatPane.js +192 -74
- package/dist/react/features/chat-pane/components/RuntimeDropUp.d.ts +29 -0
- package/dist/react/features/chat-pane/components/RuntimeDropUp.js +45 -0
- package/dist/react/features/chat-pane/create-daemon-attachment-uploader.js +80 -47
- package/dist/react/features/chat-pane/create-mcp-ui-tool-caller.js +44 -40
- package/dist/react/features/chat-pane/create-mid-run-message-poster.d.ts +6 -6
- package/dist/react/features/chat-pane/create-mid-run-message-poster.js +8 -8
- package/dist/react/features/chat-pane/create-typed-answer-poster.d.ts +3 -3
- package/dist/react/features/chat-pane/create-typed-answer-poster.js +3 -3
- package/dist/react/features/chat-pane/hooks/useAgentRuntimePicker.hooks.d.ts +6 -1
- package/dist/react/features/chat-pane/hooks/useAgentRuntimePicker.hooks.js +11 -5
- package/dist/react/features/chat-pane/hooks/useChatPane.hooks.d.ts +12 -7
- package/dist/react/features/chat-pane/hooks/useChatPane.hooks.js +340 -149
- package/dist/react/features/chat-pane/hooks/useChatPaneControlsHeight.hooks.js +7 -6
- package/dist/react/features/chat-pane/hooks/useChatPaneNewTabLinks.hooks.js +28 -10
- package/dist/react/features/chat-pane/hooks/useChatPaneRuntimeInventory.hooks.d.ts +3 -3
- package/dist/react/features/chat-pane/hooks/useChatPaneRuntimeInventory.hooks.js +60 -12
- package/dist/react/features/chat-pane/hooks/useRuntimeDropUp.hooks.d.ts +49 -0
- package/dist/react/features/chat-pane/hooks/useRuntimeDropUp.hooks.js +143 -0
- package/dist/react/features/chat-pane/model-options.d.ts +18 -1
- package/dist/react/features/chat-pane/model-options.js +49 -20
- package/dist/react/features/chat-pane/rules.d.ts +8 -9
- package/dist/react/features/chat-pane/rules.js +84 -46
- package/dist/react/features/chat-pane/styles.d.ts +1 -1
- package/dist/react/features/chat-pane/styles.js +156 -37
- package/dist/react/features/chat-pane/types.d.ts +17 -8
- package/dist/react/hooks/assistant-chats.js +12 -5
- package/dist/react/hooks/composer-draft-cache.js +116 -118
- package/dist/react/hooks/composer-history-storage.js +5 -5
- package/dist/react/hooks/fab-position.js +7 -4
- package/dist/react/hooks/useAnsweredCards.d.ts +21 -4
- package/dist/react/hooks/useAnsweredCards.js +38 -11
- package/dist/react/hooks/useArtifactStream.js +28 -22
- package/dist/react/hooks/useComposer.d.ts +20 -0
- package/dist/react/hooks/useComposer.js +22 -9
- package/dist/react/hooks/useComposerHistory.hooks.d.ts +6 -1
- package/dist/react/hooks/useComposerHistory.hooks.js +39 -19
- package/dist/react/hooks/useComposerHistoryKeys.hooks.js +24 -14
- package/dist/react/hooks/useConversation.d.ts +7 -0
- package/dist/react/hooks/useConversation.js +23 -10
- package/dist/react/hooks/useExtEventGroups.js +39 -39
- package/dist/react/hooks/useQuestionForms.js +21 -26
- package/dist/react/hooks/useRunStream.js +10 -2
- package/dist/react/index.d.ts +7 -1
- package/dist/react/index.js +9 -0
- package/dist/react/message-blocks.d.ts +6 -4
- package/dist/react/message-blocks.js +94 -72
- package/dist/react/slots.d.ts +2 -2
- package/dist/react/tool-call-display.js +19 -17
- package/dist/server/durable-runs/recover.d.ts +1 -1
- package/dist/server/durable-runs/recover.js +139 -76
- package/dist/server/run-finalizer/entry.js +25 -15
- package/dist/store/legacy/sqlite/conversations.d.ts +11 -11
- package/dist/store/legacy/sqlite/conversations.js +69 -98
- package/dist/store/legacy/sqlite/messages.d.ts +10 -2
- package/dist/store/legacy/sqlite/messages.js +113 -71
- package/dist/store/runtime/chat-history-store.js +9 -3
- package/dist/store/runtime/durable-run-store.d.ts +2 -2
- package/dist/store/runtime/durable-run-store.js +165 -117
- package/dist/store/runtime/history-tools.js +118 -82
- package/dist/store/runtime/run-ledger.d.ts +4 -1
- package/dist/store/runtime/run-ledger.js +56 -16
- package/dist/store/sql/cursor.js +38 -18
- package/dist/store/sql/store.d.ts +18 -0
- package/dist/store/sql/store.js +46 -15
- package/package.json +12 -10
|
@@ -10,19 +10,34 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { parsePartialJson } from '../partial-json.js';
|
|
12
12
|
import { parseQuotedAttrs } from '../util/markup-attributes.js';
|
|
13
|
-
import { mapRawQuestion } from './normalize.js';
|
|
13
|
+
import { asRecord, mapRawQuestion, nonEmptyString, omitUndefined, stringOrUndefined } from './normalize.js';
|
|
14
14
|
// `question-form` is the canonical tag; `ask-question` is an alias the model
|
|
15
15
|
// occasionally drifts to. The close tag must match the open tag name, so each
|
|
16
16
|
// match captures the name and computes its own close-tag string. Matching is
|
|
17
17
|
// case-insensitive so `<Question-Form>` / `<ASK-QUESTION>` still parse.
|
|
18
18
|
const OPEN_RE = /<(question-form|ask-question)\b([^>]*)>/i;
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
19
|
+
/** The first question-form block opening at or after `from`, or `null` when there is none. */
|
|
20
|
+
function locateBlock({ input, from }) {
|
|
21
|
+
const m = OPEN_RE.exec(input.slice(from));
|
|
22
|
+
if (!m)
|
|
23
|
+
return null;
|
|
24
|
+
// Group 1 is a mandatory (non-optional) alternation and group 2 is an
|
|
25
|
+
// unconditional `*`-quantified capture, so both always participate whenever
|
|
26
|
+
// `m` itself is non-null — `noUncheckedIndexedAccess` just can't see that
|
|
27
|
+
// from the regex alone.
|
|
28
|
+
const closeTag = `</${m[1].toLowerCase()}>`;
|
|
29
|
+
const start = from + m.index;
|
|
30
|
+
const bodyStart = start + m[0].length;
|
|
31
|
+
const closeIdx = findCloseTag(input, bodyStart, closeTag);
|
|
32
|
+
const closed = closeIdx !== -1;
|
|
33
|
+
return {
|
|
34
|
+
start,
|
|
35
|
+
bodyStart,
|
|
36
|
+
bodyEnd: closed ? closeIdx : input.length,
|
|
37
|
+
end: closed ? closeIdx + closeTag.length : input.length,
|
|
38
|
+
closed,
|
|
39
|
+
rawAttrs: m[2],
|
|
40
|
+
};
|
|
26
41
|
}
|
|
27
42
|
/**
|
|
28
43
|
* Split `input` into ordered prose/form segments. Scans repeatedly for
|
|
@@ -37,37 +52,20 @@ export function splitOnQuestionForms({ input }) {
|
|
|
37
52
|
const out = [];
|
|
38
53
|
let cursor = 0;
|
|
39
54
|
while (cursor < input.length) {
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
out.push({ kind: 'text', text: slice });
|
|
55
|
+
const block = locateBlock({ input, from: cursor });
|
|
56
|
+
if (!block?.closed) {
|
|
57
|
+
// No block, or an unterminated one — leave the rest as prose so we don't swallow it.
|
|
58
|
+
out.push({ kind: 'text', text: input.slice(cursor) });
|
|
44
59
|
break;
|
|
45
60
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const openStart = cursor + m.index;
|
|
49
|
-
const openEnd = openStart + m[0].length;
|
|
50
|
-
const closeIdx = findCloseTag(input, openEnd, closeTag);
|
|
51
|
-
if (closeIdx === -1) {
|
|
52
|
-
// Unterminated — leave the rest as prose so we don't swallow it.
|
|
53
|
-
out.push({ kind: 'text', text: slice });
|
|
54
|
-
break;
|
|
55
|
-
}
|
|
56
|
-
if (openStart > cursor) {
|
|
57
|
-
out.push({ kind: 'text', text: input.slice(cursor, openStart) });
|
|
58
|
-
}
|
|
59
|
-
const body = input.slice(openEnd, closeIdx);
|
|
60
|
-
const attrs = parseQuotedAttrs({ raw: rawAttrs });
|
|
61
|
-
const form = tryParseForm(body, attrs);
|
|
62
|
-
const blockEnd = closeIdx + closeTag.length;
|
|
63
|
-
if (form) {
|
|
64
|
-
out.push({ kind: 'form', form, raw: input.slice(openStart, blockEnd) });
|
|
61
|
+
if (block.start > cursor) {
|
|
62
|
+
out.push({ kind: 'text', text: input.slice(cursor, block.start) });
|
|
65
63
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
}
|
|
70
|
-
cursor =
|
|
64
|
+
const raw = input.slice(block.start, block.end);
|
|
65
|
+
const form = tryParseForm(input.slice(block.bodyStart, block.bodyEnd), parseQuotedAttrs({ raw: block.rawAttrs }));
|
|
66
|
+
// Malformed — keep raw text so the user can still see it.
|
|
67
|
+
out.push(form ? { kind: 'form', form, raw } : { kind: 'text', text: raw });
|
|
68
|
+
cursor = block.end;
|
|
71
69
|
}
|
|
72
70
|
return out;
|
|
73
71
|
}
|
|
@@ -98,19 +96,12 @@ export function parseQuestionForm({ input }) {
|
|
|
98
96
|
export function stripTrailingOpenQuestionForm({ input }) {
|
|
99
97
|
let cursor = 0;
|
|
100
98
|
while (cursor < input.length) {
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
if (!m)
|
|
99
|
+
const block = locateBlock({ input, from: cursor });
|
|
100
|
+
if (!block)
|
|
104
101
|
break;
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
const openEnd = openStart + m[0].length;
|
|
109
|
-
const closeIdx = findCloseTag(input, openEnd, closeTag);
|
|
110
|
-
if (closeIdx === -1) {
|
|
111
|
-
return { text: input.slice(0, openStart), hadOpenForm: true };
|
|
112
|
-
}
|
|
113
|
-
cursor = closeIdx + closeTag.length;
|
|
102
|
+
if (!block.closed)
|
|
103
|
+
return { text: input.slice(0, block.start), hadOpenForm: true };
|
|
104
|
+
cursor = block.end;
|
|
114
105
|
}
|
|
115
106
|
return { text: input, hadOpenForm: false };
|
|
116
107
|
}
|
|
@@ -130,12 +121,10 @@ function findCloseTag(input, from, closeTag) {
|
|
|
130
121
|
return -1;
|
|
131
122
|
}
|
|
132
123
|
function tryParseForm(body, attrs) {
|
|
133
|
-
const trimmed = body.trim();
|
|
134
|
-
if (!trimmed)
|
|
135
|
-
return null;
|
|
136
124
|
// Allow the JSON to be wrapped in a fenced ```json block — common when the
|
|
137
125
|
// model echoes its own indented body.
|
|
138
|
-
const stripped =
|
|
126
|
+
const stripped = body
|
|
127
|
+
.trim()
|
|
139
128
|
.replace(/^```(?:json)?\s*/i, '')
|
|
140
129
|
.replace(/```\s*$/i, '')
|
|
141
130
|
.trim();
|
|
@@ -146,31 +135,24 @@ function tryParseForm(body, attrs) {
|
|
|
146
135
|
catch {
|
|
147
136
|
return null;
|
|
148
137
|
}
|
|
149
|
-
|
|
138
|
+
const obj = asRecord(data);
|
|
139
|
+
if (!obj || !Array.isArray(obj.questions))
|
|
150
140
|
return null;
|
|
151
|
-
const
|
|
152
|
-
const rawQuestions = Array.isArray(obj.questions) ? obj.questions : null;
|
|
153
|
-
if (!rawQuestions)
|
|
154
|
-
return null;
|
|
155
|
-
const questions = [];
|
|
156
|
-
rawQuestions.forEach((q, i) => {
|
|
157
|
-
const mapped = mapRawQuestion({ q: q, index: i });
|
|
158
|
-
if (mapped)
|
|
159
|
-
questions.push(mapped);
|
|
160
|
-
});
|
|
141
|
+
const questions = obj.questions.flatMap((q, index) => mapRawQuestion({ q, index }) ?? []);
|
|
161
142
|
if (questions.length === 0)
|
|
162
143
|
return null;
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
144
|
+
return assembleForm({ attrs, top: obj, bodyId: stringOrUndefined(obj.id), bodyTitle: stringOrUndefined(obj.title), questions });
|
|
145
|
+
}
|
|
146
|
+
// Form-level fields shared by the complete and streaming parsers: the open
|
|
147
|
+
// tag's attrs win over the body's own id/title, which win over the defaults.
|
|
148
|
+
function assembleForm({ attrs, top, bodyId, bodyTitle, questions }) {
|
|
149
|
+
return omitUndefined({
|
|
150
|
+
id: attrs.id ?? bodyId ?? 'discovery',
|
|
151
|
+
title: attrs.title ?? bodyTitle ?? 'A few quick questions',
|
|
170
152
|
questions,
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
};
|
|
153
|
+
description: nonEmptyString(top.description),
|
|
154
|
+
submitLabel: nonEmptyString(top.submitLabel),
|
|
155
|
+
});
|
|
174
156
|
}
|
|
175
157
|
/**
|
|
176
158
|
* Tolerant parser for a still-streaming `<question-form>` block. Unlike
|
|
@@ -185,15 +167,10 @@ function tryParseForm(body, attrs) {
|
|
|
185
167
|
* string-aware brace walk via {@link parsePartialJson}).
|
|
186
168
|
*/
|
|
187
169
|
export function parsePartialQuestionForm({ input }) {
|
|
188
|
-
const
|
|
189
|
-
if (!
|
|
170
|
+
const block = locateBlock({ input, from: 0 });
|
|
171
|
+
if (!block)
|
|
190
172
|
return null;
|
|
191
|
-
const
|
|
192
|
-
const closeTag = `</${tagName}>`;
|
|
193
|
-
const openEnd = m.index + m[0].length;
|
|
194
|
-
const attrs = parseQuotedAttrs({ raw: rawAttrs });
|
|
195
|
-
const closeIdx = findCloseTag(input, openEnd, closeTag);
|
|
196
|
-
const rawBody = closeIdx === -1 ? input.slice(openEnd) : input.slice(openEnd, closeIdx);
|
|
173
|
+
const rawBody = input.slice(block.bodyStart, block.bodyEnd);
|
|
197
174
|
// Strip the fenced ```json wrapper some models emit. The opening fence is
|
|
198
175
|
// removed always; the trailing fence is removed too once it streams in
|
|
199
176
|
// (possibly only a partial ``` so far) — otherwise leftover backticks make
|
|
@@ -203,24 +180,18 @@ export function parsePartialQuestionForm({ input }) {
|
|
|
203
180
|
// Derive form-level metadata from the *parsed top-level object*, not a
|
|
204
181
|
// whole-body regex scan: a nested question/option `id`/`title`/`description`
|
|
205
182
|
// must not masquerade as the form's own.
|
|
206
|
-
const
|
|
207
|
-
const
|
|
208
|
-
const topTitle = typeof top.title === 'string' && top.title.trim().length > 0 ? top.title : undefined;
|
|
183
|
+
const top = asRecord(parsePartialJson({ buf: body })) ?? {};
|
|
184
|
+
const title = stringOrUndefined(top.title);
|
|
209
185
|
// `id` keys a still-editable panel, so it must be stable for the whole
|
|
210
186
|
// stream — don't adopt it until its string literal is fully terminated,
|
|
211
187
|
// otherwise it would churn character-by-character and remount the panel.
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
title,
|
|
220
|
-
questions,
|
|
221
|
-
...(description ? { description } : {}),
|
|
222
|
-
...(submitLabel ? { submitLabel } : {}),
|
|
223
|
-
};
|
|
188
|
+
return assembleForm({
|
|
189
|
+
attrs: parseQuotedAttrs({ raw: block.rawAttrs }),
|
|
190
|
+
top,
|
|
191
|
+
bodyId: completeTopLevelString(body, 'id'),
|
|
192
|
+
bodyTitle: title?.trim() ? title : undefined,
|
|
193
|
+
questions: shapeStreamingQuestions(top.questions, countClosedQuestionObjects(body)),
|
|
194
|
+
});
|
|
224
195
|
}
|
|
225
196
|
// Strip a trailing ``` fence (possibly only partially streamed) from a form
|
|
226
197
|
// body — but only when those backticks are the closing wrapper, not content
|
|
@@ -235,104 +206,82 @@ function stripTrailingFence(body) {
|
|
|
235
206
|
return before;
|
|
236
207
|
}
|
|
237
208
|
function endsInsideJsonString(s) {
|
|
238
|
-
let
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
if (inStr) {
|
|
243
|
-
if (esc)
|
|
244
|
-
esc = false;
|
|
245
|
-
else if (c === '\\')
|
|
246
|
-
esc = true;
|
|
247
|
-
else if (c === '"')
|
|
248
|
-
inStr = false;
|
|
249
|
-
}
|
|
250
|
-
else if (c === '"') {
|
|
251
|
-
inStr = true;
|
|
252
|
-
}
|
|
209
|
+
for (let i = s.indexOf('"'); i !== -1; i = s.indexOf('"', i)) {
|
|
210
|
+
i = jsonStringEnd({ s, start: i });
|
|
211
|
+
if (i === -1)
|
|
212
|
+
return true;
|
|
253
213
|
}
|
|
254
|
-
return
|
|
214
|
+
return false;
|
|
255
215
|
}
|
|
216
|
+
// Index just past the closing quote of the JSON string literal whose opening
|
|
217
|
+
// quote is at `start`, or -1 when that literal hasn't finished streaming.
|
|
218
|
+
function jsonStringEnd({ s, start }) {
|
|
219
|
+
for (let i = start + 1; i < s.length; i++) {
|
|
220
|
+
if (s[i] === '\\')
|
|
221
|
+
i++;
|
|
222
|
+
else if (s[i] === '"')
|
|
223
|
+
return i + 1;
|
|
224
|
+
}
|
|
225
|
+
return -1;
|
|
226
|
+
}
|
|
227
|
+
const DEPTH_DELTA = { '{': 1, '[': 1, '}': -1, ']': -1 };
|
|
256
228
|
// Return a top-level (depth-1) string field's value ONLY if its string
|
|
257
229
|
// literal is fully terminated in the (possibly partial) body. Depth-aware so
|
|
258
230
|
// a nested question `id` can't be mistaken for the form's own.
|
|
259
231
|
function completeTopLevelString(body, field) {
|
|
260
232
|
const marker = `"${field}"`;
|
|
261
233
|
let depth = 0;
|
|
262
|
-
let inStr = false;
|
|
263
|
-
let esc = false;
|
|
264
234
|
for (let i = 0; i < body.length; i++) {
|
|
265
235
|
const c = body[i];
|
|
266
|
-
if (
|
|
267
|
-
|
|
268
|
-
esc = false;
|
|
269
|
-
else if (c === '\\')
|
|
270
|
-
esc = true;
|
|
271
|
-
else if (c === '"')
|
|
272
|
-
inStr = false;
|
|
273
|
-
continue;
|
|
274
|
-
}
|
|
275
|
-
if (c === '{' || c === '[') {
|
|
276
|
-
depth++;
|
|
277
|
-
continue;
|
|
278
|
-
}
|
|
279
|
-
if (c === '}' || c === ']') {
|
|
280
|
-
depth--;
|
|
236
|
+
if (c !== '"') {
|
|
237
|
+
depth += DEPTH_DELTA[c] ?? 0;
|
|
281
238
|
continue;
|
|
282
239
|
}
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
}
|
|
292
|
-
j++;
|
|
293
|
-
while (j < body.length && /\s/.test(body[j]))
|
|
294
|
-
j++;
|
|
295
|
-
if (body[j] !== '"')
|
|
296
|
-
return undefined; // value not a (started) string
|
|
297
|
-
let value = '';
|
|
298
|
-
let vesc = false;
|
|
299
|
-
for (let k = j + 1; k < body.length; k++) {
|
|
300
|
-
const vc = body[k];
|
|
301
|
-
if (vesc) {
|
|
302
|
-
value += vc;
|
|
303
|
-
vesc = false;
|
|
304
|
-
}
|
|
305
|
-
else if (vc === '\\') {
|
|
306
|
-
value += vc;
|
|
307
|
-
vesc = true;
|
|
308
|
-
}
|
|
309
|
-
else if (vc === '"') {
|
|
310
|
-
try {
|
|
311
|
-
return JSON.parse(`"${value}"`);
|
|
312
|
-
}
|
|
313
|
-
catch {
|
|
314
|
-
return value;
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
else {
|
|
318
|
-
value += vc;
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
return undefined; // closing quote hasn't streamed yet
|
|
322
|
-
}
|
|
323
|
-
inStr = true;
|
|
324
|
-
}
|
|
240
|
+
const valueStart = depth === 1 ? keyValueStart({ body, at: i, marker }) : -1;
|
|
241
|
+
if (valueStart !== -1)
|
|
242
|
+
return readStringLiteral({ body, start: valueStart });
|
|
243
|
+
// A string that isn't our key (any other key, or a value) — skip it.
|
|
244
|
+
const end = jsonStringEnd({ s: body, start: i });
|
|
245
|
+
if (end === -1)
|
|
246
|
+
return undefined;
|
|
247
|
+
i = end - 1;
|
|
325
248
|
}
|
|
326
249
|
return undefined;
|
|
327
250
|
}
|
|
251
|
+
const KEY_COLON_RE = /\s*:\s*/y;
|
|
252
|
+
// Where the value starts when the string literal at `at` is the `marker` key,
|
|
253
|
+
// or -1 when it isn't — including a value string that merely spells the key,
|
|
254
|
+
// which no `:` follows.
|
|
255
|
+
function keyValueStart({ body, at, marker }) {
|
|
256
|
+
if (!body.startsWith(marker, at))
|
|
257
|
+
return -1;
|
|
258
|
+
KEY_COLON_RE.lastIndex = at + marker.length;
|
|
259
|
+
return KEY_COLON_RE.test(body) ? KEY_COLON_RE.lastIndex : -1;
|
|
260
|
+
}
|
|
261
|
+
// The decoded string literal at `start`, or `undefined` when the value isn't
|
|
262
|
+
// a (started) string or its closing quote hasn't streamed yet.
|
|
263
|
+
function readStringLiteral({ body, start }) {
|
|
264
|
+
if (body[start] !== '"')
|
|
265
|
+
return undefined; // value not a (started) string
|
|
266
|
+
const end = jsonStringEnd({ s: body, start });
|
|
267
|
+
if (end === -1)
|
|
268
|
+
return undefined; // closing quote hasn't streamed yet
|
|
269
|
+
const value = body.slice(start + 1, end - 1);
|
|
270
|
+
try {
|
|
271
|
+
return JSON.parse(`"${value}"`);
|
|
272
|
+
}
|
|
273
|
+
catch {
|
|
274
|
+
return value;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
328
277
|
function shapeStreamingQuestions(rawQuestions, closedCount) {
|
|
329
278
|
if (!Array.isArray(rawQuestions))
|
|
330
279
|
return [];
|
|
331
280
|
const out = [];
|
|
332
281
|
rawQuestions.forEach((raw, index) => {
|
|
333
|
-
|
|
282
|
+
const q = asRecord(raw);
|
|
283
|
+
if (!q)
|
|
334
284
|
return;
|
|
335
|
-
const q = raw;
|
|
336
285
|
const label = q.label;
|
|
337
286
|
if (typeof label !== 'string' || label.trim().length === 0)
|
|
338
287
|
return;
|
|
@@ -357,48 +306,37 @@ function countClosedQuestionObjects(body) {
|
|
|
357
306
|
return 0;
|
|
358
307
|
let i = keyMatch.index + keyMatch[0].length;
|
|
359
308
|
let count = 0;
|
|
360
|
-
|
|
309
|
+
for (;;) {
|
|
361
310
|
while (i < body.length && /[\s,]/.test(body[i]))
|
|
362
311
|
i++;
|
|
363
|
-
|
|
364
|
-
break;
|
|
312
|
+
// Past the end, at `]`, or at a non-object element: nothing more to count.
|
|
365
313
|
if (body[i] !== '{')
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
if (
|
|
369
|
-
|
|
314
|
+
return count;
|
|
315
|
+
i = balancedObjectEnd({ s: body, start: i });
|
|
316
|
+
if (i === -1)
|
|
317
|
+
return count; // trailing object hasn't closed yet
|
|
370
318
|
count++;
|
|
371
|
-
i += obj.length;
|
|
372
319
|
}
|
|
373
|
-
return count;
|
|
374
320
|
}
|
|
375
|
-
//
|
|
376
|
-
//
|
|
377
|
-
function
|
|
321
|
+
// Index just past the balanced `{...}` object starting at `start`, or -1 if
|
|
322
|
+
// it never closes (string-aware so braces inside strings don't count).
|
|
323
|
+
function balancedObjectEnd({ s, start }) {
|
|
378
324
|
let depth = 0;
|
|
379
|
-
let inStr = false;
|
|
380
|
-
let esc = false;
|
|
381
325
|
for (let i = start; i < s.length; i++) {
|
|
382
326
|
const c = s[i];
|
|
383
|
-
if (
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
else if (c === '"')
|
|
389
|
-
inStr = false;
|
|
390
|
-
continue;
|
|
327
|
+
if (c === '"') {
|
|
328
|
+
const end = jsonStringEnd({ s, start: i });
|
|
329
|
+
if (end === -1)
|
|
330
|
+
return -1;
|
|
331
|
+
i = end - 1;
|
|
391
332
|
}
|
|
392
|
-
if (c === '
|
|
393
|
-
inStr = true;
|
|
394
|
-
else if (c === '{')
|
|
333
|
+
else if (c === '{') {
|
|
395
334
|
depth++;
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
return s.slice(start, i + 1);
|
|
335
|
+
}
|
|
336
|
+
else if (c === '}' && --depth === 0) {
|
|
337
|
+
return i + 1;
|
|
400
338
|
}
|
|
401
339
|
}
|
|
402
|
-
return
|
|
340
|
+
return -1;
|
|
403
341
|
}
|
|
404
342
|
//# sourceMappingURL=scan.js.map
|
|
@@ -77,6 +77,16 @@ function isAnswerCard(scan, name, data) {
|
|
|
77
77
|
const surfaceId = a2uiSurfaceIdOf({ message: data });
|
|
78
78
|
return surfaceId === undefined || !scan.displayOnlySurfaces.has(surfaceId);
|
|
79
79
|
}
|
|
80
|
+
function scanStatus({ scan, event, index }, _optional = {}) {
|
|
81
|
+
if (event.code === 'api_retry') {
|
|
82
|
+
scan.retry = { index, data: event.data };
|
|
83
|
+
}
|
|
84
|
+
else if (event.code === 'tool_progress') {
|
|
85
|
+
const seconds = numberField(event.data, 'elapsedSeconds');
|
|
86
|
+
if (seconds !== undefined)
|
|
87
|
+
scan.progress = { index, seconds };
|
|
88
|
+
}
|
|
89
|
+
}
|
|
80
90
|
function scanEvent(scan, event, index) {
|
|
81
91
|
if (isVisible(event)) {
|
|
82
92
|
scan.lastVisible = { index, kind: event.kind };
|
|
@@ -89,17 +99,60 @@ function scanEvent(scan, event, index) {
|
|
|
89
99
|
else if (event.kind === 'tool_result') {
|
|
90
100
|
scan.open = scan.open.filter((tool) => tool.id !== event.toolUseId);
|
|
91
101
|
}
|
|
92
|
-
else if (event.kind === 'ext'
|
|
93
|
-
scan.
|
|
102
|
+
else if (event.kind === 'ext') {
|
|
103
|
+
if (isAnswerCard(scan, event.name, event.data))
|
|
104
|
+
scan.lastCardIndex = index;
|
|
94
105
|
}
|
|
95
|
-
else if (event.kind === 'status'
|
|
96
|
-
|
|
106
|
+
else if (event.kind === 'status') {
|
|
107
|
+
scanStatus({ scan, event, index });
|
|
97
108
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
109
|
+
}
|
|
110
|
+
/** `{ a: 1, b: undefined }` → `{ a: 1 }`, so optional activity fields are absent rather than undefined. */
|
|
111
|
+
function definedFields({ fields }, _optional = {}) {
|
|
112
|
+
return Object.fromEntries(Object.entries(fields).filter(([, value]) => value !== undefined));
|
|
113
|
+
}
|
|
114
|
+
function retryingActivity({ data }, _optional = {}) {
|
|
115
|
+
return {
|
|
116
|
+
kind: 'retrying',
|
|
117
|
+
...definedFields({
|
|
118
|
+
fields: {
|
|
119
|
+
attempt: numberField(data, 'attempt'),
|
|
120
|
+
maxAttempts: numberField(data, 'maxAttempts'),
|
|
121
|
+
service: stringField(data, 'service'),
|
|
122
|
+
},
|
|
123
|
+
}),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
/** The signal while a tool call is open: a card waiting on the person, tool discovery, or the tool itself. */
|
|
127
|
+
function openToolSignal({ scan, newestOpen }, _optional = {}) {
|
|
128
|
+
if (scan.lastCardIndex > newestOpen.index) {
|
|
129
|
+
return { activity: { kind: 'awaiting-answer' }, key: `card:${scan.lastCardIndex}` };
|
|
102
130
|
}
|
|
131
|
+
const key = `tool:${newestOpen.id}`;
|
|
132
|
+
if (DISCOVERY_TOOL_NAMES.has(newestOpen.name))
|
|
133
|
+
return { activity: { kind: 'finding-tool' }, key };
|
|
134
|
+
const reported = scan.progress && scan.progress.index > newestOpen.index ? scan.progress.seconds : undefined;
|
|
135
|
+
return {
|
|
136
|
+
activity: {
|
|
137
|
+
kind: 'running-tool',
|
|
138
|
+
tool: humanizeToolName({ name: newestOpen.name }, { input: newestOpen.input }),
|
|
139
|
+
...definedFields({ fields: { reportedSeconds: reported } }),
|
|
140
|
+
},
|
|
141
|
+
key,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
function latestSignal({ scan }, _optional = {}) {
|
|
145
|
+
const lastVisibleIndex = scan.lastVisible?.index ?? -1;
|
|
146
|
+
if (scan.retry && scan.retry.index > lastVisibleIndex) {
|
|
147
|
+
return { activity: retryingActivity({ data: scan.retry.data }), key: `retry:${lastVisibleIndex}` };
|
|
148
|
+
}
|
|
149
|
+
const newestOpen = scan.open[scan.open.length - 1];
|
|
150
|
+
if (newestOpen)
|
|
151
|
+
return openToolSignal({ scan, newestOpen });
|
|
152
|
+
if (scan.lastVisible?.kind === 'text') {
|
|
153
|
+
return { activity: { kind: 'writing' }, key: `writing:${lastVisibleIndex}` };
|
|
154
|
+
}
|
|
155
|
+
return { activity: { kind: 'thinking' }, key: `thinking:${lastVisibleIndex}` };
|
|
103
156
|
}
|
|
104
157
|
/**
|
|
105
158
|
* The latest signal in a running turn's events.
|
|
@@ -114,47 +167,7 @@ function scanEvent(scan, event, index) {
|
|
|
114
167
|
export function deriveRunActivity({ events }, { isAnsweredCard } = {}) {
|
|
115
168
|
const scan = { open: [], lastVisible: null, lastCardIndex: -1, retry: null, progress: null, visibleCount: 0, displayOnlySurfaces: new Set(), isAnsweredCard };
|
|
116
169
|
(events ?? []).forEach((event, index) => scanEvent(scan, event, index));
|
|
117
|
-
|
|
118
|
-
const lastVisibleIndex = scan.lastVisible?.index ?? -1;
|
|
119
|
-
if (scan.retry && scan.retry.index > lastVisibleIndex) {
|
|
120
|
-
const data = scan.retry.data;
|
|
121
|
-
const attempt = numberField(data, 'attempt');
|
|
122
|
-
const maxAttempts = numberField(data, 'maxAttempts');
|
|
123
|
-
const service = stringField(data, 'service');
|
|
124
|
-
return {
|
|
125
|
-
activity: {
|
|
126
|
-
kind: 'retrying',
|
|
127
|
-
...(attempt !== undefined ? { attempt } : {}),
|
|
128
|
-
...(maxAttempts !== undefined ? { maxAttempts } : {}),
|
|
129
|
-
...(service !== undefined ? { service } : {}),
|
|
130
|
-
},
|
|
131
|
-
key: `retry:${lastVisibleIndex}`,
|
|
132
|
-
visibleCount,
|
|
133
|
-
};
|
|
134
|
-
}
|
|
135
|
-
const newestOpen = scan.open[scan.open.length - 1];
|
|
136
|
-
if (newestOpen) {
|
|
137
|
-
if (scan.lastCardIndex > newestOpen.index) {
|
|
138
|
-
return { activity: { kind: 'awaiting-answer' }, key: `card:${scan.lastCardIndex}`, visibleCount };
|
|
139
|
-
}
|
|
140
|
-
if (DISCOVERY_TOOL_NAMES.has(newestOpen.name)) {
|
|
141
|
-
return { activity: { kind: 'finding-tool' }, key: `tool:${newestOpen.id}`, visibleCount };
|
|
142
|
-
}
|
|
143
|
-
const reported = scan.progress && scan.progress.index > newestOpen.index ? scan.progress.seconds : undefined;
|
|
144
|
-
return {
|
|
145
|
-
activity: {
|
|
146
|
-
kind: 'running-tool',
|
|
147
|
-
tool: humanizeToolName({ name: newestOpen.name }, { input: newestOpen.input }),
|
|
148
|
-
...(reported !== undefined ? { reportedSeconds: reported } : {}),
|
|
149
|
-
},
|
|
150
|
-
key: `tool:${newestOpen.id}`,
|
|
151
|
-
visibleCount,
|
|
152
|
-
};
|
|
153
|
-
}
|
|
154
|
-
if (scan.lastVisible?.kind === 'text') {
|
|
155
|
-
return { activity: { kind: 'writing' }, key: `writing:${lastVisibleIndex}`, visibleCount };
|
|
156
|
-
}
|
|
157
|
-
return { activity: { kind: 'thinking' }, key: `thinking:${lastVisibleIndex}`, visibleCount };
|
|
170
|
+
return { ...latestSignal({ scan }), visibleCount: scan.visibleCount };
|
|
158
171
|
}
|
|
159
172
|
/**
|
|
160
173
|
* Whether the run is waiting on the person: a card (form, confirm, sign-in) arrived while its tool
|
|
@@ -83,6 +83,12 @@ export declare function parseUsageEvent({ payload }: {
|
|
|
83
83
|
* cannot fall through to `default` — see below). A lookup table would have to carry those as a
|
|
84
84
|
* parallel structure, one step removed from the code they explain. Left as a `switch`, documented
|
|
85
85
|
* here rather than only in this session's report.
|
|
86
|
+
*
|
|
87
|
+
* Superseded (2026-10-20, code-health wave 1; bar is ≤10/≤10): the flat switch is now TWO switches —
|
|
88
|
+
* the chat-core event types here, and the UI-envelope / dropped / `ext` types in
|
|
89
|
+
* {@link translateEnvelopePayload} below — so each scope stays under the cyclomatic bar while BOTH
|
|
90
|
+
* keep what the paragraph above argued for: every case is still a `case` with its own comment
|
|
91
|
+
* attached, not an entry in a parallel lookup table.
|
|
86
92
|
*/
|
|
87
93
|
/** Reduce one daemon payload; preserve UI envelopes and drop duplicate input fragments. */
|
|
88
94
|
export declare function translateRunAgentPayload({ payload }: {
|
|
@@ -280,7 +286,10 @@ export declare function runContentFromEvents({ events }: {
|
|
|
280
286
|
* onto the screen; saved, it only repeats the event wrapper per token. Used by every saver (the server
|
|
281
287
|
* finalizer and the chat PUT route the browser saves through), so both write the same shape.
|
|
282
288
|
*/
|
|
283
|
-
/** Compact adjacent text/thinking deltas
|
|
289
|
+
/** Compact adjacent text/thinking deltas and keep the first occurrence of each tool-call ID.
|
|
290
|
+
* Replayed tool calls must not become duplicate history rows. Other events retain their order.
|
|
291
|
+
* @complexity O(n + total text length) time and O(n) space for n events.
|
|
292
|
+
*/
|
|
284
293
|
export declare function runEventsForSave({ events }: {
|
|
285
294
|
events: readonly AgentEvent[];
|
|
286
295
|
}, _options?: Record<string, never>): AgentEvent[];
|