@jini-ai/chat 0.6.15-rc.2 → 0.6.15
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/core/accepted-input.js +20 -11
- package/dist/core/ag-ui/entry.js +3 -1
- package/dist/core/agent-plugin-chat-state.js +3 -1
- package/dist/core/assistant-client/ag-ui-transport.js +1 -1
- package/dist/core/assistant-client/attachment-liveness.js +1 -1
- package/dist/core/assistant-client/durable-run-subscription.js +2 -1
- package/dist/core/assistant-client/transport.js +13 -7
- package/dist/core/assistant-content.js +2 -1
- package/dist/core/composer-history.js +2 -1
- package/dist/core/durable-projection.js +2 -1
- package/dist/core/durable-runs/continuation.js +2 -1
- package/dist/core/messages.d.ts +5 -1
- package/dist/core/partial-json.js +18 -9
- package/dist/core/persistence/title.d.ts +1 -1
- package/dist/core/persistence/title.js +25 -7
- package/dist/core/question-form/normalize.js +1 -1
- package/dist/core/question-form/scan.js +25 -15
- package/dist/core/question-form/secret-answers.js +2 -1
- package/dist/core/run-activity.js +8 -4
- package/dist/core/run-events/entry.js +6 -2
- package/dist/core/transcript.js +13 -6
- package/dist/core/turn-usage.js +7 -2
- package/dist/core/user-text-redaction.d.ts +2 -5
- package/dist/core/user-text-redaction.js +3 -3
- package/dist/core/util/manifest.js +13 -11
- package/dist/core/util/markdown-context.d.ts +14 -0
- package/dist/core/util/markdown-context.js +42 -4
- package/dist/core/util/markup-attributes.js +1 -1
- package/dist/core/util/parser.js +7 -7
- package/dist/core/util/pointer.js +22 -6
- package/dist/core/util/recover.js +5 -8
- package/dist/core/util/strip.js +12 -15
- package/dist/core/util/validate.js +30 -18
- package/dist/embed-widget/browser/lazy-loader.js +16 -12
- package/dist/embed-widget/react/dialog.hooks.js +2 -2
- package/dist/embed-widget/react/embed.js +1 -1
- package/dist/embed-widget/react/header.hooks.js +1 -1
- package/dist/embed-widget/transports/fetch-sse.js +2 -1
- package/dist/embed-widget/vanilla/index.js +19 -12
- package/dist/embed-widget/vanilla/state.js +4 -2
- package/dist/react/components/AttachmentPreviewModal.hooks.js +2 -1
- package/dist/react/components/ChatSettingsDialog.js +18 -9
- package/dist/react/components/Composer.js +5 -4
- package/dist/react/components/ConversationList.js +5 -4
- package/dist/react/components/Markdown.js +29 -26
- package/dist/react/components/McpUiSurfaceCard.js +2 -1
- package/dist/react/components/MessageList.hooks.js +3 -2
- package/dist/react/components/MessageRow.js +23 -7
- package/dist/react/components/MessageRow.usage.js +3 -1
- package/dist/react/components/QuestionForm.js +4 -5
- package/dist/react/components/SentAttachment.js +4 -1
- package/dist/react/components/TodoCard.js +9 -1
- package/dist/react/components/ToolCard.js +8 -7
- package/dist/react/components/composer-discovery.js +8 -8
- package/dist/react/features/chat-pane/components/AgentPluginUsage.js +1 -2
- package/dist/react/features/chat-pane/components/AgentRuntimePicker.d.ts +1 -1
- package/dist/react/features/chat-pane/components/AgentRuntimePicker.js +15 -5
- package/dist/react/features/chat-pane/components/ChatPane.js +4 -2
- package/dist/react/features/chat-pane/components/RuntimeDropUp.js +1 -1
- package/dist/react/features/chat-pane/create-daemon-attachment-uploader.js +1 -1
- package/dist/react/features/chat-pane/hooks/useAgentRuntimePicker.hooks.js +2 -2
- package/dist/react/features/chat-pane/hooks/useChatPane.hooks.js +4 -2
- package/dist/react/features/chat-pane/hooks/useChatPaneAgentControl.hooks.js +2 -2
- package/dist/react/features/chat-pane/hooks/useChatPaneControlsHeight.hooks.js +1 -1
- package/dist/react/features/chat-pane/hooks/useChatPaneRuntimeInventory.hooks.js +2 -2
- package/dist/react/features/chat-pane/hooks/useChatPaneWorkingDirectory.hooks.js +2 -2
- package/dist/react/features/chat-pane/hooks/useRuntimeDropUp.hooks.js +2 -2
- package/dist/react/features/chat-pane/model-options.js +4 -2
- package/dist/react/features/chat-pane/styles.d.ts +1 -1
- package/dist/react/features/chat-pane/styles.js +26 -3
- package/dist/react/features/chat-pane/types.d.ts +8 -0
- package/dist/react/hooks/assistant-chats.js +22 -8
- package/dist/react/hooks/useArtifactStream.js +2 -1
- package/dist/react/hooks/useComposer.js +2 -2
- package/dist/react/hooks/useComposerHistoryKeys.hooks.js +2 -2
- package/dist/react/hooks/useConversation.js +9 -4
- package/dist/react/hooks/useExtEventGroups.js +4 -2
- package/dist/react/hooks/useLatestOperation.js +2 -1
- package/dist/react/hooks/useQuestionForms.js +32 -22
- package/dist/react/hooks/useRunStream.js +4 -2
- package/dist/react/message-blocks.js +4 -2
- package/dist/react/styles/reference.css +9 -0
- package/dist/server/durable-runs/recover.js +13 -5
- package/dist/server/run-finalizer/entry.js +1 -0
- package/dist/store/runtime/chat-history-store.js +9 -3
- package/dist/store/runtime/durable-run-store.js +34 -24
- package/dist/store/runtime/run-ledger.js +7 -4
- package/dist/store/sql/boundary.js +3 -1
- package/dist/store/sql/cursor.js +8 -3
- package/dist/store/sql/store.js +36 -28
- package/package.json +15 -15
|
@@ -15,7 +15,9 @@ export function projectAcceptedInputMessages({ messages }, _options = {}) {
|
|
|
15
15
|
});
|
|
16
16
|
if (inputs.length === 0)
|
|
17
17
|
return [message];
|
|
18
|
-
const
|
|
18
|
+
const turn = { ...message };
|
|
19
|
+
delete turn.attachments;
|
|
20
|
+
delete turn.secretRedaction;
|
|
19
21
|
// MOVE: Older receipts have no per-input files; retain those legacy files on the final bubble.
|
|
20
22
|
return inputs.map((input, index) => ({ ...turn, id: input.id, content: input.text,
|
|
21
23
|
attachments: input.attachments ?? (index === inputs.length - 1 ? message.attachments ?? [] : []), ...(input.secretRedaction ? { secretRedaction: input.secretRedaction } : {}) }));
|
|
@@ -86,7 +88,8 @@ function expandQuestionAnswers({ events }, { answerContexts } = {}) {
|
|
|
86
88
|
const questions = new Map(events.flatMap(event => event.kind === 'tool_use' ? [[event.id, event.input]] : []));
|
|
87
89
|
const answers = new Map(events.flatMap(event => {
|
|
88
90
|
const context = event.kind === 'tool_result' ? answerContexts?.get(event.toolUseId) : undefined;
|
|
89
|
-
const
|
|
91
|
+
const originalInput = event.kind === 'tool_result' ? questions.get(event.toolUseId) : undefined;
|
|
92
|
+
const input = context ? context.input : originalInput;
|
|
90
93
|
const answer = questionAnswerEvent({ event, input }, { includeTitle: context?.includeTitle === true });
|
|
91
94
|
return answer ? [[answer.data.toolUseId, answer]] : [];
|
|
92
95
|
}));
|
|
@@ -121,13 +124,17 @@ function matchOptimisticInput({ input, optimistic, seen }, _options = {}) {
|
|
|
121
124
|
const next = candidates?.events[candidates.index];
|
|
122
125
|
const follows = next?.data.afterInputId === undefined || seen.has(next.data.afterInputId);
|
|
123
126
|
// Keep predecessor testing before the caller adds this receipt's ids to seen.
|
|
124
|
-
|
|
127
|
+
if (input.data.toolUseId)
|
|
128
|
+
return optimistic.byQuestion.get(input.data.toolUseId);
|
|
129
|
+
return follows ? next : undefined;
|
|
125
130
|
}
|
|
126
131
|
/** Apply authoritative receipt data while retaining local identity and queued constituents. */
|
|
127
132
|
function mergeAcceptedReceipt({ input, pending }, _options = {}) {
|
|
128
133
|
// Replayed queue acceptance cannot undo confirmed consumption.
|
|
129
134
|
const deliveryState = pending.data.deliveryState === 'consumed' ? 'consumed' : input.data.deliveryState ?? pending.data.deliveryState;
|
|
130
|
-
const
|
|
135
|
+
const pendingData = { ...pending.data };
|
|
136
|
+
delete pendingData.optimistic;
|
|
137
|
+
delete pendingData.afterInputId;
|
|
131
138
|
const aliases = [...new Set([...(input.data.aliases ?? []), input.data.id, ...(pending.data.aliases ?? [])])].filter(id => id !== pending.data.id);
|
|
132
139
|
return { ...input, data: { ...pendingData, ...input.data, id: pending.data.id,
|
|
133
140
|
// Legacy acceptance receipts omit state; HTTP acceptance still means queued until consumption.
|
|
@@ -199,8 +206,10 @@ export function projectAcceptedInputEvents({ events }, options) {
|
|
|
199
206
|
return projectEvents({ events }, options);
|
|
200
207
|
// Event arrays are replaced, never mutated, so a transcript update re-projects only changed messages.
|
|
201
208
|
let projected = defaultProjections.get(events);
|
|
202
|
-
if (!projected)
|
|
203
|
-
|
|
209
|
+
if (!projected) {
|
|
210
|
+
projected = projectEvents({ events }, {});
|
|
211
|
+
defaultProjections.set(events, projected);
|
|
212
|
+
}
|
|
204
213
|
return projected;
|
|
205
214
|
}
|
|
206
215
|
const defaultProjections = new WeakMap();
|
|
@@ -218,11 +227,11 @@ function projectEvents({ events }, options) {
|
|
|
218
227
|
continue;
|
|
219
228
|
const next = replacements.get(event) ?? event;
|
|
220
229
|
const input = acceptedInputEvent({ event: next });
|
|
221
|
-
if (input
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
230
|
+
if (!input || !emitted.has(input.data.id)) {
|
|
231
|
+
if (input)
|
|
232
|
+
emitted.add(input.data.id);
|
|
233
|
+
projected.push(next);
|
|
234
|
+
}
|
|
226
235
|
}
|
|
227
236
|
return projected;
|
|
228
237
|
}
|
package/dist/core/ag-ui/entry.js
CHANGED
|
@@ -6,7 +6,9 @@
|
|
|
6
6
|
* The host emits RUN_STARTED and terminal RUN_FINISHED/RUN_ERROR outside this pure projection. */
|
|
7
7
|
import { EventType } from '@ag-ui/core';
|
|
8
8
|
function asString(v) {
|
|
9
|
-
|
|
9
|
+
if (typeof v === "string")
|
|
10
|
+
return v;
|
|
11
|
+
return v == null ? "" : JSON.stringify(v);
|
|
10
12
|
}
|
|
11
13
|
function reduceUsagePayload(payload) {
|
|
12
14
|
const usage = (payload.usage ?? {});
|
|
@@ -13,7 +13,9 @@ function isStringList({ value }) {
|
|
|
13
13
|
}
|
|
14
14
|
/** A stored row is usable only when its exclusions and (null or listed) decision are all string ids. */
|
|
15
15
|
function usableChatState({ value }) {
|
|
16
|
-
|
|
16
|
+
if (!value || !isStringList({ value: value.excluded }))
|
|
17
|
+
return null;
|
|
18
|
+
return value.decision === null || isStringList({ value: value.decision?.selected }) ? value : null;
|
|
17
19
|
}
|
|
18
20
|
/** Corrupt/blocked storage permits a fresh question, as clearing localStorage does. */
|
|
19
21
|
export function readAgentPluginChatState({ conversationId }, { storage = browserStorage() } = {}) {
|
|
@@ -66,7 +66,7 @@ export function createAgUiRunClient({ ports }, _options = {}) {
|
|
|
66
66
|
context: [],
|
|
67
67
|
forwardedProps: {
|
|
68
68
|
...(input.agentId ? { agentId: input.agentId } : {}),
|
|
69
|
-
...(input.history.
|
|
69
|
+
...(input.history.findLast(message => message.role === "user")?.secretRedaction?.secretRedacted ? { secretRedacted: true } : {}),
|
|
70
70
|
},
|
|
71
71
|
};
|
|
72
72
|
const collected = [];
|
|
@@ -47,7 +47,7 @@ async function isAttachmentStillServed(ref, options) {
|
|
|
47
47
|
* @complexity O(n) requests, at most `concurrency` concurrent; O(n) space for the verdicts.
|
|
48
48
|
*/
|
|
49
49
|
async function probeAll(refs, concurrency, probe) {
|
|
50
|
-
const verdicts =
|
|
50
|
+
const verdicts = Array.from({ length: refs.length }, () => false);
|
|
51
51
|
const entries = refs.entries();
|
|
52
52
|
async function worker() {
|
|
53
53
|
for (const [index, ref] of entries) {
|
|
@@ -29,7 +29,8 @@ export function followDurableRun({ runId: initialRunId, handlers, signal, bindin
|
|
|
29
29
|
handlers.onDone(message.events ?? []);
|
|
30
30
|
}
|
|
31
31
|
function open() {
|
|
32
|
-
|
|
32
|
+
streamEpoch += 1;
|
|
33
|
+
const epoch = streamEpoch;
|
|
33
34
|
dropped = false;
|
|
34
35
|
stopStream?.();
|
|
35
36
|
stopStream = ports.stream({ runId, cursor,
|
|
@@ -293,7 +293,9 @@ export function createAssistantTransportClient({ ports }, _options = {}) {
|
|
|
293
293
|
const payload = JSON.parse(frame.data);
|
|
294
294
|
ctx.handlers.onError(new Error(asString(payload.message) || "BYOK turn failed"));
|
|
295
295
|
}
|
|
296
|
-
else
|
|
296
|
+
else {
|
|
297
|
+
if (frame.event !== "end")
|
|
298
|
+
return;
|
|
297
299
|
const notice = terminalReasonNotice(readTerminalReason(frame.data, false));
|
|
298
300
|
if (notice) {
|
|
299
301
|
ctx.collected.push(notice);
|
|
@@ -391,7 +393,8 @@ export function createAssistantTransportClient({ ports }, _options = {}) {
|
|
|
391
393
|
// A response whose body stream errors out (e.g. a dropped connection) must not turn "the server
|
|
392
394
|
// rejected the request" into "the client crashed reading the rejection".
|
|
393
395
|
const detail = await response.text().catch(() => "");
|
|
394
|
-
|
|
396
|
+
const suffix = detail ? ": " + detail.slice(0, 300) : "";
|
|
397
|
+
return new Error(`${action} (${response.status})${suffix}`);
|
|
395
398
|
}
|
|
396
399
|
/**
|
|
397
400
|
* The BYOK run path (2026-08-04) — one held-open `POST` to `assistant-byok.ts`, no separate
|
|
@@ -573,8 +576,9 @@ export function createAssistantTransportClient({ ports }, _options = {}) {
|
|
|
573
576
|
prompt: ports.projectPrompt({ prompt, context: input.context }, {}),
|
|
574
577
|
// Select one complete transcript after binding; concatenating it with the tail repeats turns.
|
|
575
578
|
coldStartContext: ports.projectPrompt({ prompt: runPrompt(input.history), context: input.context }, {}),
|
|
576
|
-
// The transcript is already sanitized; retain
|
|
577
|
-
|
|
579
|
+
// The transcript is already sanitized; retain only the CURRENT user's signal across the
|
|
580
|
+
// daemon JSON boundary. Replaying an old signal reports another paste on every later turn.
|
|
581
|
+
...(newestUserMessage({ history: input.history }, {})?.secretRedaction?.secretRedacted ? { secretRedacted: true } : {}),
|
|
578
582
|
assistantMessageId: input.assistantMessageId,
|
|
579
583
|
...frontendBindTokenField(input),
|
|
580
584
|
...modelField(input),
|
|
@@ -766,7 +770,7 @@ export function createAssistantTransportClient({ ports }, _options = {}) {
|
|
|
766
770
|
// can never arrive.
|
|
767
771
|
if (runId.startsWith(BYOK_RUN_ID_PREFIX)) {
|
|
768
772
|
handlers.onDone([]);
|
|
769
|
-
return;
|
|
773
|
+
return undefined;
|
|
770
774
|
}
|
|
771
775
|
// Same "no server-side record to resume" reasoning as the BYOK branch above — see
|
|
772
776
|
// `assistant-transport-ag-ui.ts`'s own doc for why this path has no reattach story yet.
|
|
@@ -774,6 +778,7 @@ export function createAssistantTransportClient({ ports }, _options = {}) {
|
|
|
774
778
|
return ports.agUi.reattachAgUiRun({ handlers }, {});
|
|
775
779
|
}
|
|
776
780
|
subscribeToRun(runId, handlers, options?.signal);
|
|
781
|
+
return undefined;
|
|
777
782
|
},
|
|
778
783
|
async fetchRunStatus(runId) {
|
|
779
784
|
// Same reasoning as `reattachRun` above — no server-side run record exists for a BYOK run id,
|
|
@@ -794,7 +799,7 @@ export function createAssistantTransportClient({ ports }, _options = {}) {
|
|
|
794
799
|
// this exact id — there is no server-side run to `POST .../cancel` against.
|
|
795
800
|
if (runId.startsWith(BYOK_RUN_ID_PREFIX)) {
|
|
796
801
|
byokAbortControllers.get(runId)?.abort();
|
|
797
|
-
return;
|
|
802
|
+
return undefined;
|
|
798
803
|
}
|
|
799
804
|
if (ports.agUi.isAgUiRunId({ runId }, {})) {
|
|
800
805
|
return ports.agUi.stopAgUiRun({ runId }, {});
|
|
@@ -807,11 +812,12 @@ export function createAssistantTransportClient({ ports }, _options = {}) {
|
|
|
807
812
|
});
|
|
808
813
|
// 404: the daemon no longer knows this run (it restarted), so nothing is left running to stop.
|
|
809
814
|
if (response.status === 404)
|
|
810
|
-
return;
|
|
815
|
+
return undefined;
|
|
811
816
|
// `fetch` resolves on any status: without this a refused cancel (401/403/500) resolved exactly
|
|
812
817
|
// like a successful one while the run kept executing. `useRunStream` logs a rejection here.
|
|
813
818
|
if (!response.ok)
|
|
814
819
|
throw await refusalError({ action: "cancelling agent run failed", response }, {});
|
|
820
|
+
return undefined;
|
|
815
821
|
},
|
|
816
822
|
};
|
|
817
823
|
}
|
|
@@ -32,8 +32,9 @@ export function assistantContentFromEvents({ events }, _options = {}) {
|
|
|
32
32
|
for (const ev of dedupeToolUsesById({ events: events ? [...events] : undefined })) {
|
|
33
33
|
if (isAssistantStepBoundary({ event: ev })) {
|
|
34
34
|
stepBoundary = true;
|
|
35
|
+
continue;
|
|
35
36
|
}
|
|
36
|
-
|
|
37
|
+
if (ev.kind === 'text' && ev.text.length > 0) {
|
|
37
38
|
out += (stepBoundary ? separatorBefore({ out, text: ev.text }) : '') + ev.text;
|
|
38
39
|
stepBoundary = false;
|
|
39
40
|
}
|
|
@@ -21,7 +21,8 @@ export function createComposerHistoryState(_required, _optional = {}) {
|
|
|
21
21
|
return { entries: [], index: null, draft: null };
|
|
22
22
|
}
|
|
23
23
|
function hasModifierOrPopup({ input }, _optional = {}) {
|
|
24
|
-
|
|
24
|
+
const modified = input.shiftKey || input.ctrlKey || input.altKey || input.metaKey;
|
|
25
|
+
return Boolean(modified || input.isComposing || input.popupOpen);
|
|
25
26
|
}
|
|
26
27
|
/**
|
|
27
28
|
* Whether the selection sits on the first/last line. A selection spanning a newline is on
|
|
@@ -52,8 +52,9 @@ export function mergeRunEvents({ saved, incoming }, _optional = {}) {
|
|
|
52
52
|
skip = Math.max(0, skip - event.text.length);
|
|
53
53
|
if (text)
|
|
54
54
|
result.push({ ...event, text });
|
|
55
|
+
continue;
|
|
55
56
|
}
|
|
56
|
-
|
|
57
|
+
if (claimNewKey({ key: identity(event, incomingUsage), keys })) {
|
|
57
58
|
result.push(event);
|
|
58
59
|
}
|
|
59
60
|
}
|
|
@@ -6,7 +6,8 @@ export function canonicalToolInput({ value }, _optional = {}) {
|
|
|
6
6
|
return `[${value.map((item) => canonicalToolInput({ value: item }, {})).join(",")}]`;
|
|
7
7
|
if (value !== null && typeof value === "object") {
|
|
8
8
|
const object = value;
|
|
9
|
-
|
|
9
|
+
const fields = Object.keys(object).sort().map((key) => `${JSON.stringify(key)}:${canonicalToolInput({ value: object[key] }, {})}`);
|
|
10
|
+
return `{${fields.join(",")}}`;
|
|
10
11
|
}
|
|
11
12
|
return JSON.stringify(value) ?? "null";
|
|
12
13
|
}
|
package/dist/core/messages.d.ts
CHANGED
|
@@ -25,6 +25,10 @@ export interface ChatAttachment {
|
|
|
25
25
|
/** User-visible attachment order for this turn; older items may omit it. */
|
|
26
26
|
order?: number;
|
|
27
27
|
}
|
|
28
|
+
export interface SecretRedactionSignal {
|
|
29
|
+
readonly secretRedacted: true;
|
|
30
|
+
readonly count: number;
|
|
31
|
+
}
|
|
28
32
|
/**
|
|
29
33
|
* A single turn in a conversation. This is the generic subset of OD's
|
|
30
34
|
* `ChatMessage` (`packages/contracts/src/api/chat.ts`): product-shaped
|
|
@@ -36,7 +40,7 @@ export interface ChatMessage {
|
|
|
36
40
|
/** A normal send stays outside recall until the transport accepts it. */
|
|
37
41
|
inputPending?: boolean;
|
|
38
42
|
/** Transient safe receipt; storage requires no new column. */
|
|
39
|
-
secretRedaction?:
|
|
43
|
+
secretRedaction?: SecretRedactionSignal;
|
|
40
44
|
id: string;
|
|
41
45
|
role: ChatRole;
|
|
42
46
|
content: string;
|
|
@@ -38,14 +38,22 @@ function scanJsonPrefix({ buf }, _optional = {}) {
|
|
|
38
38
|
({ inStr, esc } = advanceJsonString({ c, esc }));
|
|
39
39
|
continue;
|
|
40
40
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
41
|
+
switch (c) {
|
|
42
|
+
case '"':
|
|
43
|
+
inStr = true;
|
|
44
|
+
break;
|
|
45
|
+
case '{':
|
|
46
|
+
stack.push('}');
|
|
47
|
+
break;
|
|
48
|
+
case '[':
|
|
49
|
+
stack.push(']');
|
|
50
|
+
break;
|
|
51
|
+
case '}':
|
|
52
|
+
case ']':
|
|
53
|
+
stack.pop();
|
|
54
|
+
break;
|
|
55
|
+
default: break;
|
|
56
|
+
}
|
|
49
57
|
}
|
|
50
58
|
return { stack, inStr };
|
|
51
59
|
}
|
|
@@ -76,7 +84,8 @@ function trimIncompleteTail({ out }, _optional = {}) {
|
|
|
76
84
|
let prev;
|
|
77
85
|
let passes = 0;
|
|
78
86
|
do {
|
|
79
|
-
|
|
87
|
+
passes += 1;
|
|
88
|
+
if (passes > MAX_TAIL_CLEANUP_PASSES)
|
|
80
89
|
return null;
|
|
81
90
|
prev = out;
|
|
82
91
|
out = trimTrailingSeparators({ text: out });
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* Ported from Open Design's `summarizeProjectNameFromPrompt` (`apps/web/src/utils/projectName.ts`).
|
|
14
14
|
* The Latin path has since diverged: it keeps the prompt's words, names and dotted domains (the
|
|
15
15
|
* original's stop-word removal and title casing turned "port example.com to jini" into "Port
|
|
16
|
-
* Example Com Jini"), and `
|
|
16
|
+
* Example Com Jini"), and `stripLeadingLatinFiller` fixes the original's article bug, documented at
|
|
17
17
|
* its declaration. The CJK path keeps the original's filler and punctuation rules, which is not
|
|
18
18
|
* decoration: they differ enough from the Latin ones that a shared path produced worse titles in both.
|
|
19
19
|
*/
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* Ported from Open Design's `summarizeProjectNameFromPrompt` (`apps/web/src/utils/projectName.ts`).
|
|
14
14
|
* The Latin path has since diverged: it keeps the prompt's words, names and dotted domains (the
|
|
15
15
|
* original's stop-word removal and title casing turned "port example.com to jini" into "Port
|
|
16
|
-
* Example Com Jini"), and `
|
|
16
|
+
* Example Com Jini"), and `stripLeadingLatinFiller` fixes the original's article bug, documented at
|
|
17
17
|
* its declaration. The CJK path keeps the original's filler and punctuation rules, which is not
|
|
18
18
|
* decoration: they differ enough from the Latin ones that a shared path produced worse titles in both.
|
|
19
19
|
*/
|
|
@@ -38,7 +38,16 @@ const CJK_PATTERN = /[㐀-鿿]/;
|
|
|
38
38
|
* with `a` after a verb silently lost its first character. Requiring the separator as part of the
|
|
39
39
|
* article makes an article an article rather than a prefix.
|
|
40
40
|
*/
|
|
41
|
-
|
|
41
|
+
function stripLeadingLatinFiller(input) {
|
|
42
|
+
const polite = input.replace(/^please\s+/i, '');
|
|
43
|
+
const request = polite.replace(/^(?:can\s+you|could\s+you|help\s+me|i\s+want\s+to|i\s+need\s+to)\s+/i, '');
|
|
44
|
+
const verb = /^(?:create|build|make|design|implement|add|fix|update|improve|optimize|generate|write|search|find|show|list|turn)\s+/i.exec(request);
|
|
45
|
+
if (!verb)
|
|
46
|
+
return input;
|
|
47
|
+
return request.slice(verb[0].length)
|
|
48
|
+
.replace(/^(?:this|that)\s+into\s+/i, '')
|
|
49
|
+
.replace(/^(?:an|a|the|this|that|my|me)\s+/i, '');
|
|
50
|
+
}
|
|
42
51
|
const LEADING_CJK_FILLER = [
|
|
43
52
|
/^先?(帮我|帮忙|麻烦|请|可以|能不能|能否|给我|我想要|我要)/,
|
|
44
53
|
/^(先)?(实现|做|做一下|创建|生成|设计|开发|新增|添加|优化|修复|改|更改|调整)(一下|一个|一版|下)?/,
|
|
@@ -72,11 +81,11 @@ export function stripConversationTitleMarkdown({ text }, _options = {}) {
|
|
|
72
81
|
.replace(/^\s{0,3}\[[^\]\n]+\]:[^\n]*(?:\n|$)/gm, ' ')
|
|
73
82
|
.replace(/!?\[([^\[\]\n]+)\]\((?:[^()[\]\n]|\([^()[\]\n]*\))*(?:\)|$)/g, '$1')
|
|
74
83
|
.replace(/\[([^\[\]\n]+)\]\[[^\[\]\n]*(?:\]|$)/g, '$1')
|
|
75
|
-
.replace(/^\s{0,3}#{1,6}\s+|[ \t]+#+[ \t]*$/gm, '')
|
|
84
|
+
.replace(/^\s{0,3}#{1,6}\s+|(?<![ \t])[ \t]+#+[ \t]*$/gm, '')
|
|
76
85
|
.replace(/(?<![\w\\*])(\*\*|\*)(?=[^\s*])([^*\n]*?[^\s*\\])\1(?![\w*])/g, '$2')
|
|
77
|
-
.replace(/(?<![\w\\
|
|
86
|
+
.replace(/(?<![\w\\])(__|_)(?=[^\s_])([^\n]*?[^\s\\])\1(?!\w)/g, '$2')
|
|
78
87
|
.replace(/`|~~/g, '')
|
|
79
|
-
.
|
|
88
|
+
.split('\n').map(stripUnclosedTitleLink).join('\n')
|
|
80
89
|
// Unescape after markup removal so an escaped literal marker cannot become emphasis.
|
|
81
90
|
.replace(/\\([\\`*_[\]~])/g, '$1')
|
|
82
91
|
.replace(/\s+/g, ' ')
|
|
@@ -97,7 +106,7 @@ function truncateTitle(title) {
|
|
|
97
106
|
return (lastSpace > 0 ? prefix.slice(0, lastSpace) : prefix).trimEnd();
|
|
98
107
|
}
|
|
99
108
|
function trimLatinTitle(input) {
|
|
100
|
-
const words = input
|
|
109
|
+
const words = stripLeadingLatinFiller(input).match(/[\p{L}\p{N}]+(?:[._-][\p{L}\p{N}]+)*/gu) ?? [];
|
|
101
110
|
const title = words.join(' ');
|
|
102
111
|
// Domain and filename tokens retain their original case, even at the start of a sentence.
|
|
103
112
|
return words[0]?.includes('.') ? title : title.charAt(0).toUpperCase() + title.slice(1);
|
|
@@ -107,7 +116,7 @@ function trimCjkTitle(input) {
|
|
|
107
116
|
for (const pattern of LEADING_CJK_FILLER)
|
|
108
117
|
title = title.replace(pattern, '').trim();
|
|
109
118
|
// Cut at the first sentence-ending punctuation, then drop spacing — CJK titles do not use it.
|
|
110
|
-
title = title.
|
|
119
|
+
title = title.split(/[,。!?;:,.!?;:]/, 1)[0].replace(/\s+/g, '');
|
|
111
120
|
return title.slice(0, MAX_CJK_LENGTH);
|
|
112
121
|
}
|
|
113
122
|
/**
|
|
@@ -144,4 +153,13 @@ export function conversationTitleForDisplay({ title, titleSource, firstPrompt },
|
|
|
144
153
|
}
|
|
145
154
|
return truncateTitle(stripConversationTitleMarkdown({ text: title })) || null;
|
|
146
155
|
}
|
|
156
|
+
/** The first open label after the last closing bracket is the only possible trailing cut link.
|
|
157
|
+
* Retain any later '[' characters inside its label, as the original tolerant pattern does. */
|
|
158
|
+
function stripUnclosedTitleLink(line) {
|
|
159
|
+
const open = line.indexOf('[', line.lastIndexOf(']') + 1);
|
|
160
|
+
if (open === -1)
|
|
161
|
+
return line;
|
|
162
|
+
const start = line[open - 1] === '!' ? open - 1 : open;
|
|
163
|
+
return line.slice(0, start) + line.slice(open + 1);
|
|
164
|
+
}
|
|
147
165
|
//# sourceMappingURL=title.js.map
|
|
@@ -135,7 +135,7 @@ const COLOR_KEYWORD = /^[a-z]+$/i;
|
|
|
135
135
|
* A colour function. The argument charset deliberately excludes `(`, so no nested function —
|
|
136
136
|
* `var()`, `url()`, `image-set()`, `attr()` — can appear inside one of these either.
|
|
137
137
|
*/
|
|
138
|
-
const COLOR_FUNCTION = /^(?:rgb|rgba|hsl|hsla|hwb|lab|lch|oklab|oklch|color)\(
|
|
138
|
+
const COLOR_FUNCTION = /^(?:rgb|rgba|hsl|hsla|hwb|lab|lch|oklab|oklch|color)\([0-9a-z%.,/\s+-]*\)$/i;
|
|
139
139
|
/**
|
|
140
140
|
* Whether a model-supplied palette entry may be written into a CSS value.
|
|
141
141
|
*
|
|
@@ -197,7 +197,7 @@ export function parsePartialQuestionForm({ input }) {
|
|
|
197
197
|
// body — but only when those backticks are the closing wrapper, not content
|
|
198
198
|
// of a JSON string value still being typed.
|
|
199
199
|
function stripTrailingFence(body) {
|
|
200
|
-
const m =
|
|
200
|
+
const m = /(?<!\s)\s*`{1,3}\s*$/.exec(body);
|
|
201
201
|
if (!m)
|
|
202
202
|
return body;
|
|
203
203
|
const before = body.slice(0, m.index);
|
|
@@ -206,10 +206,12 @@ function stripTrailingFence(body) {
|
|
|
206
206
|
return before;
|
|
207
207
|
}
|
|
208
208
|
function endsInsideJsonString(s) {
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
209
|
+
let i = s.indexOf('"');
|
|
210
|
+
while (i !== -1) {
|
|
211
|
+
const end = jsonStringEnd({ s, start: i });
|
|
212
|
+
if (end === -1)
|
|
212
213
|
return true;
|
|
214
|
+
i = s.indexOf('"', end);
|
|
213
215
|
}
|
|
214
216
|
return false;
|
|
215
217
|
}
|
|
@@ -217,9 +219,11 @@ function endsInsideJsonString(s) {
|
|
|
217
219
|
// quote is at `start`, or -1 when that literal hasn't finished streaming.
|
|
218
220
|
function jsonStringEnd({ s, start }) {
|
|
219
221
|
for (let i = start + 1; i < s.length; i++) {
|
|
220
|
-
if (s[i] === '\\')
|
|
222
|
+
if (s[i] === '\\') {
|
|
221
223
|
i++;
|
|
222
|
-
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
if (s[i] === '"')
|
|
223
227
|
return i + 1;
|
|
224
228
|
}
|
|
225
229
|
return -1;
|
|
@@ -231,10 +235,12 @@ const DEPTH_DELTA = { '{': 1, '[': 1, '}': -1, ']': -1 };
|
|
|
231
235
|
function completeTopLevelString(body, field) {
|
|
232
236
|
const marker = `"${field}"`;
|
|
233
237
|
let depth = 0;
|
|
234
|
-
|
|
238
|
+
let i = 0;
|
|
239
|
+
while (i < body.length) {
|
|
235
240
|
const c = body[i];
|
|
236
241
|
if (c !== '"') {
|
|
237
242
|
depth += DEPTH_DELTA[c] ?? 0;
|
|
243
|
+
i += 1;
|
|
238
244
|
continue;
|
|
239
245
|
}
|
|
240
246
|
const valueStart = depth === 1 ? keyValueStart({ body, at: i, marker }) : -1;
|
|
@@ -244,7 +250,7 @@ function completeTopLevelString(body, field) {
|
|
|
244
250
|
const end = jsonStringEnd({ s: body, start: i });
|
|
245
251
|
if (end === -1)
|
|
246
252
|
return undefined;
|
|
247
|
-
i = end
|
|
253
|
+
i = end;
|
|
248
254
|
}
|
|
249
255
|
return undefined;
|
|
250
256
|
}
|
|
@@ -322,20 +328,24 @@ function countClosedQuestionObjects(body) {
|
|
|
322
328
|
// it never closes (string-aware so braces inside strings don't count).
|
|
323
329
|
function balancedObjectEnd({ s, start }) {
|
|
324
330
|
let depth = 0;
|
|
325
|
-
|
|
331
|
+
let i = start;
|
|
332
|
+
while (i < s.length) {
|
|
326
333
|
const c = s[i];
|
|
327
334
|
if (c === '"') {
|
|
328
335
|
const end = jsonStringEnd({ s, start: i });
|
|
329
336
|
if (end === -1)
|
|
330
337
|
return -1;
|
|
331
|
-
i = end
|
|
332
|
-
|
|
333
|
-
else if (c === '{') {
|
|
334
|
-
depth++;
|
|
338
|
+
i = end;
|
|
339
|
+
continue;
|
|
335
340
|
}
|
|
336
|
-
|
|
337
|
-
|
|
341
|
+
if (c === '{')
|
|
342
|
+
depth += 1;
|
|
343
|
+
if (c === '}') {
|
|
344
|
+
depth -= 1;
|
|
345
|
+
if (depth === 0)
|
|
346
|
+
return i + 1;
|
|
338
347
|
}
|
|
348
|
+
i += 1;
|
|
339
349
|
}
|
|
340
350
|
return -1;
|
|
341
351
|
}
|
|
@@ -31,7 +31,8 @@ export function withoutSecretQuestionDefaults({ form }) {
|
|
|
31
31
|
return { ...form, questions: form.questions.map(question => {
|
|
32
32
|
if (!isSecretQuestion({ question }))
|
|
33
33
|
return question;
|
|
34
|
-
const
|
|
34
|
+
const safe = { ...question };
|
|
35
|
+
delete safe.defaultValue;
|
|
35
36
|
return safe;
|
|
36
37
|
}) };
|
|
37
38
|
}
|
|
@@ -85,8 +85,9 @@ function isAnswerCard(scan, name, data) {
|
|
|
85
85
|
function scanStatus({ scan, event, index }, _optional = {}) {
|
|
86
86
|
if (event.code === 'api_retry') {
|
|
87
87
|
scan.retry = { index, data: event.data };
|
|
88
|
+
return;
|
|
88
89
|
}
|
|
89
|
-
|
|
90
|
+
if (event.code === 'tool_progress') {
|
|
90
91
|
const seconds = numberField(event.data, 'elapsedSeconds');
|
|
91
92
|
if (seconds !== undefined)
|
|
92
93
|
scan.progress = { index, seconds };
|
|
@@ -108,15 +109,18 @@ function scanEvent(scan, event, index) {
|
|
|
108
109
|
scan.pendingQuestionIndex = -1;
|
|
109
110
|
scan.open = scan.open.filter((tool) => tool.id !== event.id);
|
|
110
111
|
scan.open.push({ id: event.id, name: event.name, input: event.input, index });
|
|
112
|
+
return;
|
|
111
113
|
}
|
|
112
|
-
|
|
114
|
+
if (event.kind === 'tool_result') {
|
|
113
115
|
// Completion may be unrelated background work or the ask tool returning before human input.
|
|
114
116
|
scan.open = scan.open.filter((tool) => tool.id !== event.toolUseId);
|
|
117
|
+
return;
|
|
115
118
|
}
|
|
116
|
-
|
|
119
|
+
if (event.kind === 'ext') {
|
|
117
120
|
scanCard({ scan, event, index });
|
|
121
|
+
return;
|
|
118
122
|
}
|
|
119
|
-
|
|
123
|
+
if (event.kind === 'status') {
|
|
120
124
|
scanStatus({ scan, event, index });
|
|
121
125
|
}
|
|
122
126
|
}
|
|
@@ -8,7 +8,9 @@ import { assistantContentFromEvents } from "../assistant-content.js";
|
|
|
8
8
|
import { mergeAdjacentTextEvents } from "../compact-events.js";
|
|
9
9
|
import { dedupeToolUsesById } from "../tool-events.js";
|
|
10
10
|
function wireString(v) {
|
|
11
|
-
|
|
11
|
+
if (typeof v === "string")
|
|
12
|
+
return v;
|
|
13
|
+
return v == null ? "" : JSON.stringify(v);
|
|
12
14
|
}
|
|
13
15
|
/** Display a wire scalar or JSON value using the existing empty-null convention. */
|
|
14
16
|
export function asString({ value }, _options = {}) {
|
|
@@ -470,7 +472,9 @@ function chunkEvent(raw, channel, carry) {
|
|
|
470
472
|
if (!frame)
|
|
471
473
|
return [];
|
|
472
474
|
const chunk = wireString(frame.payload?.chunk);
|
|
473
|
-
|
|
475
|
+
if (carry)
|
|
476
|
+
carry.openEchoChannels ??= new Set();
|
|
477
|
+
const open = carry?.openEchoChannels;
|
|
474
478
|
const { kept, statuses, droppedAny } = splitChunk({ chunk, channel }, { open });
|
|
475
479
|
const line = droppedAny ? kept : chunk;
|
|
476
480
|
return line.length > 0 ? [{ kind: "raw", line }, ...statuses] : statuses;
|
package/dist/core/transcript.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { fencedCodeRanges } from './util/markdown-context.js';
|
|
1
2
|
/**
|
|
2
3
|
* @module transcript
|
|
3
4
|
*
|
|
@@ -152,12 +153,7 @@ export function sanitizePriorAssistantTurn({ content }, { persistedArtifactFiles
|
|
|
152
153
|
// Strip ```json (or plain ```) fenced blocks whose body matches the form
|
|
153
154
|
// schema shape — `"questions": [` is the strongest tell. A generic JSON
|
|
154
155
|
// snippet without that key is left intact.
|
|
155
|
-
sanitized =
|
|
156
|
-
if (/"questions"\s*:\s*\[/.test(body)) {
|
|
157
|
-
return '[form schema was echoed here on a prior turn; stripped to avoid a loop.]';
|
|
158
|
-
}
|
|
159
|
-
return match;
|
|
160
|
-
});
|
|
156
|
+
sanitized = stripEchoedFormFences({ content: sanitized });
|
|
161
157
|
// Replace prior-turn `<artifact>` HTML with a one-line summary for
|
|
162
158
|
// artifacts CONFIRMED persisted (see summarizeArtifactsForTranscript's own
|
|
163
159
|
// doc for why an unconfirmed save is left verbatim).
|
|
@@ -192,4 +188,15 @@ export function buildTranscript({ history }, options = {}) {
|
|
|
192
188
|
const warning = buildPriorRunContextWarning(scopedHistory, largeToolResultChars, highInputTokenWarningThreshold);
|
|
193
189
|
return warning ? `${warning}\n\n${transcript}` : transcript;
|
|
194
190
|
}
|
|
191
|
+
function stripEchoedFormFences({ content }) {
|
|
192
|
+
const chunks = [];
|
|
193
|
+
let copied = 0;
|
|
194
|
+
for (const range of fencedCodeRanges({ content, opener: /```(?:json)?/g })) {
|
|
195
|
+
if (!/"questions"\s*:\s*\[/.test(range.body))
|
|
196
|
+
continue;
|
|
197
|
+
chunks.push(content.slice(copied, range.start), '[form schema was echoed here on a prior turn; stripped to avoid a loop.]');
|
|
198
|
+
copied = range.end;
|
|
199
|
+
}
|
|
200
|
+
return chunks.join('') + content.slice(copied);
|
|
201
|
+
}
|
|
195
202
|
//# sourceMappingURL=transcript.js.map
|
package/dist/core/turn-usage.js
CHANGED
|
@@ -32,6 +32,9 @@ export function replySegments({ events }, _optional = {}) {
|
|
|
32
32
|
started = false;
|
|
33
33
|
continue;
|
|
34
34
|
}
|
|
35
|
+
// Repeated delivery receipts are bookkeeping, never content of the reply they already split.
|
|
36
|
+
if (isReplyBoundary({ event }))
|
|
37
|
+
continue;
|
|
35
38
|
const lateResult = event.kind === 'usage' && !started && segments.length > 1;
|
|
36
39
|
started ||= producesReplyOutput({ event });
|
|
37
40
|
segments[segments.length - (lateResult ? 2 : 1)].push(event);
|
|
@@ -52,8 +55,10 @@ function usageCost({ event, agentId, sessions }, _optional = {}) {
|
|
|
52
55
|
const key = JSON.stringify([agentId, sessionId]);
|
|
53
56
|
const chargedUsd = sessions.get(key)?.chargedUsd ?? 0;
|
|
54
57
|
const cumulative = validCost(event.cumulativeCostUsd);
|
|
55
|
-
|
|
56
|
-
|
|
58
|
+
let snapshotCost;
|
|
59
|
+
if (cumulative !== undefined)
|
|
60
|
+
snapshotCost = cumulative < chargedUsd ? cumulative : cumulative - chargedUsd;
|
|
61
|
+
const cost = cumulative === undefined ? validCost(event.costUsd) : snapshotCost;
|
|
57
62
|
// Interim interrupted-segment charges are already paid; later snapshots subtract them.
|
|
58
63
|
// After a counter reset the new snapshot, rather than the old epoch's total, is the baseline.
|
|
59
64
|
sessions.set(key, { sessionId, chargedUsd: cumulative ?? chargedUsd + (cost ?? 0) });
|
|
@@ -1,8 +1,5 @@
|
|
|
1
|
-
import type { ChatMessage } from './messages.js';
|
|
2
|
-
export
|
|
3
|
-
readonly secretRedacted: true;
|
|
4
|
-
readonly count: number;
|
|
5
|
-
}
|
|
1
|
+
import type { ChatMessage, SecretRedactionSignal } from './messages.js';
|
|
2
|
+
export type { SecretRedactionSignal } from './messages.js';
|
|
6
3
|
export interface UserTextRedaction {
|
|
7
4
|
readonly text: string;
|
|
8
5
|
readonly secretRedacted: boolean;
|