@jini-ai/chat 0.6.6 → 0.6.10
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 +129 -1
- 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 +187 -98
- 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 +31 -6
- package/dist/core/durable-runs/ledger-contracts.d.ts +2 -1
- 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.js +54 -32
- 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.d.ts +1 -1
- package/dist/react/components/MessageRow.js +75 -47
- package/dist/react/components/MessageRow.usage.js +16 -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/ToolCallGroup.d.ts +4 -3
- package/dist/react/components/ToolCallGroup.js +7 -5
- 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 +14 -0
- package/dist/react/features/chat-pane/model-options.js +45 -25
- 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 +152 -36
- package/dist/react/features/chat-pane/types.d.ts +19 -9
- 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.d.ts +12 -6
- package/dist/react/tool-call-display.js +53 -24
- package/dist/server/durable-runs/recover.js +132 -80
- 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 +18 -16
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
* compaction and paragraph boundaries so live and persisted turns agree. Notice strings, custom
|
|
4
4
|
* event names and excluded run-ID prefixes are explicit host inputs, never product wire defaults.
|
|
5
5
|
* Authenticated routing, ownership, boot repair and storage transactions remain host responsibilities. */
|
|
6
|
-
import { decodeSseFrames, parseSseRecord } from "@jini-ai/
|
|
6
|
+
import { decodeSseFrames, parseSseRecord } from "@jini-ai/core/primitives";
|
|
7
7
|
import { assistantContentFromEvents } from "../assistant-content.js";
|
|
8
8
|
import { mergeAdjacentTextEvents } from "../compact-events.js";
|
|
9
|
+
import { dedupeToolUsesById } from "../tool-events.js";
|
|
9
10
|
function wireString(v) {
|
|
10
11
|
return typeof v === "string" ? v : v == null ? "" : JSON.stringify(v);
|
|
11
12
|
}
|
|
@@ -63,38 +64,13 @@ export function parseUsageEvent({ payload }, _options = {}) {
|
|
|
63
64
|
* cannot fall through to `default` — see below). A lookup table would have to carry those as a
|
|
64
65
|
* parallel structure, one step removed from the code they explain. Left as a `switch`, documented
|
|
65
66
|
* here rather than only in this session's report.
|
|
67
|
+
*
|
|
68
|
+
* Superseded (2026-10-20, code-health wave 1; bar is ≤10/≤10): the flat switch is now TWO switches —
|
|
69
|
+
* the chat-core event types here, and the UI-envelope / dropped / `ext` types in
|
|
70
|
+
* {@link translateEnvelopePayload} below — so each scope stays under the cyclomatic bar while BOTH
|
|
71
|
+
* keep what the paragraph above argued for: every case is still a `case` with its own comment
|
|
72
|
+
* attached, not an entry in a parallel lookup table.
|
|
66
73
|
*/
|
|
67
|
-
// Typed media (currently just images) the daemon attached alongside the flattened `content`
|
|
68
|
-
// string — see `@jini-ai/protocol`'s `events.ts` doc on why the wire field is `unknown`
|
|
69
|
-
// rather than a checked type here: the real shape is validated where `ToolCard` renders it.
|
|
70
|
-
// Forwarded verbatim rather than re-validated a second time in this reducer — an
|
|
71
|
-
// `Array.isArray` guard rather than a deep shape check, since a malformed entry inside it
|
|
72
|
-
// is a rendering concern (`ToolCard`'s own `ToolResultMedia` already ignores anything that
|
|
73
|
-
// isn't a recognized block), not a transport one.
|
|
74
|
-
// An MCP content block the daemon withheld from the tool result because it is for the HUMAN,
|
|
75
|
-
// not the model (`@jini-ai/daemon`'s `delegated-tool-bridge.ts` → `tool-result-surfaces.ts`).
|
|
76
|
-
// Explicit rather than left to `default` below because the shapes do not line up: the default
|
|
77
|
-
// passes the WHOLE wire payload as `data`, but `@jini-ai/chat`'s `McpUiSurfaceCard` runs
|
|
78
|
-
// `parseUIResource` over each event's `data` and that requires the bare `EmbeddedResource`
|
|
79
|
-
// (`{type:'resource', resource:{uri,mimeType,text}}`). Handing it the envelope instead fails
|
|
80
|
-
// the `type !== 'resource'` check and renders an empty frame — a silent no-op, which is the
|
|
81
|
-
// worst possible failure for a confirmation dialog. Unwrapping here is what makes the two ends
|
|
82
|
-
// meet. `name` must stay `"mcp-ui"` to match `MCP_UI_EXT_EVENT_NAME`.
|
|
83
|
-
// A2UI's own agent->renderer envelope (`@jini-ai/core`'s `SurfaceEmission` with
|
|
84
|
-
// `channel: "a2ui"`, injected by `@jini-ai/daemon`'s `delegated-tool-bridge.ts` as
|
|
85
|
-
// `{type: "a2ui", message: <AgentToRendererMessage>}`). Unwrapped to the bare `.message` here,
|
|
86
|
-
// not left to the `default` branch below, for the same reason `mcp-ui` above is explicit:
|
|
87
|
-
// `@jini-ai/chat/react`'s `A2uiSurfaceCard` (registered against `'a2ui'` in
|
|
88
|
-
// `AssistantDock.tsx`) runs `extractSurfaceId`/`interpreter.applyAgentMessage` over each event's
|
|
89
|
-
// `data` directly, and both require a bare, spec-shaped envelope — not the `{type, message}`
|
|
90
|
-
// wire wrapper. Mirrors Jini's own reference host's identical `case "a2ui"` in
|
|
91
|
-
// `examples/reference-web/src/daemon-transport.ts`.
|
|
92
|
-
// thinking_start/stage_start/stage_end/surface_request/surface_response: no dedicated
|
|
93
|
-
// chat-core variant. Routed through the `ext` escape hatch rather than dropped, so a future
|
|
94
|
-
// renderer can opt in without a transport change.
|
|
95
|
-
// Streamed tool input (with `--include-partial-messages`): the finished `tool_use` carries the
|
|
96
|
-
// same input whole, and nothing renders the fragments. Kept, they became one no-op `ext` event
|
|
97
|
-
// per chunk in the saved row.
|
|
98
74
|
/** Reduce one daemon payload; preserve UI envelopes and drop duplicate input fragments. */
|
|
99
75
|
export function translateRunAgentPayload({ payload }, _options = {}) {
|
|
100
76
|
switch (payload.type) {
|
|
@@ -107,25 +83,66 @@ export function translateRunAgentPayload({ payload }, _options = {}) {
|
|
|
107
83
|
case "tool_use":
|
|
108
84
|
return { kind: "tool_use", id: wireString(payload.id), name: wireString(payload.name), input: payload.input };
|
|
109
85
|
case "tool_result":
|
|
110
|
-
return
|
|
111
|
-
kind: "tool_result",
|
|
112
|
-
toolUseId: wireString(payload.toolUseId),
|
|
113
|
-
content: wireString(payload.content),
|
|
114
|
-
isError: Boolean(payload.isError),
|
|
115
|
-
...(Array.isArray(payload.media) ? { media: payload.media } : {}),
|
|
116
|
-
};
|
|
86
|
+
return toolResultEvent(payload);
|
|
117
87
|
case "usage":
|
|
118
88
|
return parseUsageEvent({ payload });
|
|
119
89
|
case "raw":
|
|
120
90
|
return { kind: "raw", line: wireString(payload.line) };
|
|
91
|
+
default:
|
|
92
|
+
return translateEnvelopePayload(payload);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
function toolResultEvent(payload) {
|
|
96
|
+
return {
|
|
97
|
+
kind: "tool_result",
|
|
98
|
+
toolUseId: wireString(payload.toolUseId),
|
|
99
|
+
content: wireString(payload.content),
|
|
100
|
+
isError: Boolean(payload.isError),
|
|
101
|
+
// Typed media (currently just images) the daemon attached alongside the flattened `content`
|
|
102
|
+
// string — see `@jini-ai/protocol`'s `events.ts` doc on why the wire field is `unknown`
|
|
103
|
+
// rather than a checked type here: the real shape is validated where `ToolCard` renders it.
|
|
104
|
+
// Forwarded verbatim rather than re-validated a second time in this reducer — an
|
|
105
|
+
// `Array.isArray` guard rather than a deep shape check, since a malformed entry inside it
|
|
106
|
+
// is a rendering concern (`ToolCard`'s own `ToolResultMedia` already ignores anything that
|
|
107
|
+
// isn't a recognized block), not a transport one.
|
|
108
|
+
...(Array.isArray(payload.media) ? { media: payload.media } : {}),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
/** The payload types with no chat-core event of their own: UI envelopes, dropped types, and `ext`. */
|
|
112
|
+
function translateEnvelopePayload(payload) {
|
|
113
|
+
switch (payload.type) {
|
|
114
|
+
// An MCP content block the daemon withheld from the tool result because it is for the HUMAN,
|
|
115
|
+
// not the model (`@jini-ai/daemon`'s `delegated-tool-bridge.ts` → `tool-result-surfaces.ts`).
|
|
116
|
+
// Explicit rather than left to `default` below because the shapes do not line up: the default
|
|
117
|
+
// passes the WHOLE wire payload as `data`, but `@jini-ai/chat`'s `McpUiSurfaceCard` runs
|
|
118
|
+
// `parseUIResource` over each event's `data` and that requires the bare `EmbeddedResource`
|
|
119
|
+
// (`{type:'resource', resource:{uri,mimeType,text}}`). Handing it the envelope instead fails
|
|
120
|
+
// the `type !== 'resource'` check and renders an empty frame — a silent no-op, which is the
|
|
121
|
+
// worst possible failure for a confirmation dialog. Unwrapping here is what makes the two ends
|
|
122
|
+
// meet. `name` must stay `"mcp-ui"` to match `MCP_UI_EXT_EVENT_NAME`.
|
|
121
123
|
case "mcp-ui":
|
|
122
124
|
return { kind: "ext", name: "mcp-ui", data: payload.resource };
|
|
125
|
+
// A2UI's own agent->renderer envelope (`@jini-ai/core`'s `SurfaceEmission` with
|
|
126
|
+
// `channel: "a2ui"`, injected by `@jini-ai/daemon`'s `delegated-tool-bridge.ts` as
|
|
127
|
+
// `{type: "a2ui", message: <AgentToRendererMessage>}`). Unwrapped to the bare `.message` here,
|
|
128
|
+
// not left to the `default` branch below, for the same reason `mcp-ui` above is explicit:
|
|
129
|
+
// `@jini-ai/chat/react`'s `A2uiSurfaceCard` (registered against `'a2ui'` in
|
|
130
|
+
// `AssistantDock.tsx`) runs `extractSurfaceId`/`interpreter.applyAgentMessage` over each event's
|
|
131
|
+
// `data` directly, and both require a bare, spec-shaped envelope — not the `{type, message}`
|
|
132
|
+
// wire wrapper. Mirrors Jini's own reference host's identical `case "a2ui"` in
|
|
133
|
+
// `examples/reference-web/src/daemon-transport.ts`.
|
|
123
134
|
case "a2ui":
|
|
124
135
|
return { kind: "ext", name: "a2ui", data: payload.message };
|
|
125
136
|
case "thinking_start":
|
|
126
137
|
return null;
|
|
138
|
+
// Streamed tool input (with `--include-partial-messages`): the finished `tool_use` carries the
|
|
139
|
+
// same input whole, and nothing renders the fragments. Kept, they became one no-op `ext` event
|
|
140
|
+
// per chunk in the saved row.
|
|
127
141
|
case "tool_input_delta":
|
|
128
142
|
return null;
|
|
143
|
+
// thinking_start/stage_start/stage_end/surface_request/surface_response: no dedicated
|
|
144
|
+
// chat-core variant. Routed through the `ext` escape hatch rather than dropped, so a future
|
|
145
|
+
// renderer can opt in without a transport change.
|
|
129
146
|
default:
|
|
130
147
|
return { kind: "ext", name: payload.type, data: payload };
|
|
131
148
|
}
|
|
@@ -175,18 +192,9 @@ function neverStarted(outcome) {
|
|
|
175
192
|
}
|
|
176
193
|
/** Read non-successful daemon classification without throwing on malformed frames. */
|
|
177
194
|
export function readTerminalOutcome({ raw }, _options = {}) {
|
|
178
|
-
|
|
195
|
+
const payload = parseWirePayload(raw);
|
|
196
|
+
if (!payload)
|
|
179
197
|
return null;
|
|
180
|
-
let payload;
|
|
181
|
-
try {
|
|
182
|
-
const frame = JSON.parse(raw);
|
|
183
|
-
if (!isRecord(frame) || !isRecord(frame.payload))
|
|
184
|
-
return null;
|
|
185
|
-
payload = frame.payload;
|
|
186
|
-
}
|
|
187
|
-
catch {
|
|
188
|
-
return null;
|
|
189
|
-
}
|
|
190
198
|
const status = payload.status;
|
|
191
199
|
if (status !== "failed" && status !== "canceled")
|
|
192
200
|
return null;
|
|
@@ -375,9 +383,12 @@ export function runContentFromEvents({ events }, _options = {}) {
|
|
|
375
383
|
* onto the screen; saved, it only repeats the event wrapper per token. Used by every saver (the server
|
|
376
384
|
* finalizer and the chat PUT route the browser saves through), so both write the same shape.
|
|
377
385
|
*/
|
|
378
|
-
/** Compact adjacent text/thinking deltas
|
|
386
|
+
/** Compact adjacent text/thinking deltas and keep the first occurrence of each tool-call ID.
|
|
387
|
+
* Replayed tool calls must not become duplicate history rows. Other events retain their order.
|
|
388
|
+
* @complexity O(n + total text length) time and O(n) space for n events.
|
|
389
|
+
*/
|
|
379
390
|
export function runEventsForSave({ events }, _options = {}) {
|
|
380
|
-
return mergeAdjacentTextEvents({ events });
|
|
391
|
+
return mergeAdjacentTextEvents({ events: dedupeToolUsesById({ events: [...events] }) });
|
|
381
392
|
}
|
|
382
393
|
function parseWire(raw) {
|
|
383
394
|
if (!raw)
|
|
@@ -390,12 +401,18 @@ function parseWire(raw) {
|
|
|
390
401
|
return null;
|
|
391
402
|
}
|
|
392
403
|
}
|
|
404
|
+
/** A wire frame's `payload` when it is a plain object; `null` for an absent, malformed or bare body. */
|
|
405
|
+
function parseWirePayload(raw) {
|
|
406
|
+
const payload = parseWire(raw)?.payload;
|
|
407
|
+
return isRecord(payload) ? payload : null;
|
|
408
|
+
}
|
|
393
409
|
function parseJsonLine(line) {
|
|
394
410
|
if (!line.startsWith("{"))
|
|
395
411
|
return null;
|
|
396
412
|
try {
|
|
397
|
-
|
|
398
|
-
|
|
413
|
+
// A line starting with `{` either parses to a plain object or throws; it cannot parse to an
|
|
414
|
+
// array, a scalar or `null`, so no shape check is needed after the parse.
|
|
415
|
+
return JSON.parse(line);
|
|
399
416
|
}
|
|
400
417
|
catch {
|
|
401
418
|
return null;
|
|
@@ -451,32 +468,41 @@ function chunkEvent(raw, channel, carry) {
|
|
|
451
468
|
return [];
|
|
452
469
|
const chunk = wireString(frame.payload?.chunk);
|
|
453
470
|
const open = carry ? (carry.openEchoChannels ??= new Set()) : undefined;
|
|
471
|
+
const { kept, statuses, droppedAny } = splitChunk({ chunk, channel }, { open });
|
|
472
|
+
const line = droppedAny ? kept : chunk;
|
|
473
|
+
return line.length > 0 ? [{ kind: "raw", line }, ...statuses] : statuses;
|
|
474
|
+
}
|
|
475
|
+
/** A chunk's non-echo text and heartbeat statuses, and whether any echo piece was dropped. */
|
|
476
|
+
function splitChunk({ chunk, channel }, { open } = {}) {
|
|
454
477
|
const statuses = [];
|
|
455
478
|
let kept = "";
|
|
456
479
|
let droppedAny = false;
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
// Only a chunk's first piece can continue the previous chunk's line; splitting on "\n" starts
|
|
460
|
-
// every later piece on a fresh line. Channels are tracked apart: stderr can interleave mid-line.
|
|
461
|
-
const echo = (first && open?.has(channel) === true) || ECHO_LINE.test(piece.trimStart());
|
|
462
|
-
first = false;
|
|
463
|
-
if (echo && !piece.endsWith("\n"))
|
|
464
|
-
open?.add(channel);
|
|
465
|
-
else
|
|
466
|
-
open?.delete(channel);
|
|
467
|
-
if (echo) {
|
|
480
|
+
chunk.split(/(?<=\n)/).forEach((piece, index) => {
|
|
481
|
+
if (isEchoPiece({ piece, channel, first: index === 0 }, { open })) {
|
|
468
482
|
droppedAny = true;
|
|
469
|
-
|
|
483
|
+
return;
|
|
470
484
|
}
|
|
471
|
-
const
|
|
472
|
-
const status = line ? heartbeatStatus(line) : null;
|
|
485
|
+
const status = pieceStatus(piece);
|
|
473
486
|
if (status)
|
|
474
487
|
statuses.push(status);
|
|
475
488
|
kept += piece;
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
489
|
+
});
|
|
490
|
+
return { kept, statuses, droppedAny };
|
|
491
|
+
}
|
|
492
|
+
/** Whether a piece belongs to an echo line, updating which channels are mid-way through one. */
|
|
493
|
+
function isEchoPiece({ piece, channel, first }, { open } = {}) {
|
|
494
|
+
// Only a chunk's first piece can continue the previous chunk's line; splitting on "\n" starts
|
|
495
|
+
// every later piece on a fresh line. Channels are tracked apart: stderr can interleave mid-line.
|
|
496
|
+
const echo = (first && open?.has(channel) === true) || ECHO_LINE.test(piece.trimStart());
|
|
497
|
+
if (echo && !piece.endsWith("\n"))
|
|
498
|
+
open?.add(channel);
|
|
499
|
+
else
|
|
500
|
+
open?.delete(channel);
|
|
501
|
+
return echo;
|
|
502
|
+
}
|
|
503
|
+
function pieceStatus(piece) {
|
|
504
|
+
const line = parseJsonLine(piece.trim());
|
|
505
|
+
return line ? heartbeatStatus(line) : null;
|
|
480
506
|
}
|
|
481
507
|
// Order matters and matches the browser's `end` listener: the `max_tool_turns` notice first, then
|
|
482
508
|
// the failed/canceled notice, then the failure itself.
|
|
@@ -504,33 +530,37 @@ function endOutcome(raw, notices) {
|
|
|
504
530
|
*
|
|
505
531
|
* @complexity O(size of the frame).
|
|
506
532
|
*/
|
|
507
|
-
// The agent CLI's own output. `stderr` is where a dying CLI says WHY it is dying.
|
|
508
|
-
// Shown AND saved as a status line, not only reported: the error is live-only state, so without
|
|
509
|
-
// the event the saved turn (and a reload) showed a bare "failed" with the reason lost. This is
|
|
510
|
-
// where the daemon's plain pre-spawn reason ("The assistant could not start: ...") and the
|
|
511
|
-
// host's start refusals reach the user.
|
|
512
533
|
/** Fold one named daemon frame into events, a failure and an optional terminal status. */
|
|
513
534
|
export function translateRunFrame({ kind, raw, notices }, { carry } = {}) {
|
|
514
535
|
switch (kind) {
|
|
515
|
-
case "agent":
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
return { events: translated ? [translated] : [] };
|
|
519
|
-
}
|
|
536
|
+
case "agent":
|
|
537
|
+
return { events: agentFrameEvents(raw) };
|
|
538
|
+
// The agent CLI's own output. `stderr` is where a dying CLI says WHY it is dying.
|
|
520
539
|
case "stdout":
|
|
521
540
|
case "stderr":
|
|
522
541
|
return { events: chunkEvent(raw, kind, carry) };
|
|
523
|
-
case "error":
|
|
524
|
-
|
|
525
|
-
const message = wireString(frame?.payload?.message) || "agent run failed";
|
|
526
|
-
return { events: [{ kind: "status", label: message }], error: new Error(message) };
|
|
527
|
-
}
|
|
542
|
+
case "error":
|
|
543
|
+
return errorFrameOutcome(raw);
|
|
528
544
|
case "end":
|
|
529
545
|
return endOutcome(raw, notices);
|
|
530
546
|
default:
|
|
531
547
|
return { events: [] };
|
|
532
548
|
}
|
|
533
549
|
}
|
|
550
|
+
function agentFrameEvents(raw) {
|
|
551
|
+
const payload = parseWirePayload(raw);
|
|
552
|
+
const translated = typeof payload?.type === "string" ? translateRunAgentPayload({ payload: payload }) : null;
|
|
553
|
+
return translated ? [translated] : [];
|
|
554
|
+
}
|
|
555
|
+
// Shown AND saved as a status line, not only reported: the error is live-only state, so without
|
|
556
|
+
// the event the saved turn (and a reload) showed a bare "failed" with the reason lost. This is
|
|
557
|
+
// where the daemon's plain pre-spawn reason ("The assistant could not start: ...") and the
|
|
558
|
+
// host's start refusals reach the user.
|
|
559
|
+
function errorFrameOutcome(raw) {
|
|
560
|
+
const frame = parseWire(raw);
|
|
561
|
+
const message = wireString(frame?.payload?.message) || "agent run failed";
|
|
562
|
+
return { events: [{ kind: "status", label: message }], error: new Error(message) };
|
|
563
|
+
}
|
|
534
564
|
function isRecord(value) {
|
|
535
565
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
536
566
|
}
|
package/dist/core/todos.d.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* no framework.
|
|
8
8
|
*/
|
|
9
9
|
import type { AgentEvent } from './events.js';
|
|
10
|
-
import type
|
|
10
|
+
import { type ChatRunStatus } from './messages.js';
|
|
11
11
|
export type TodoStatus = 'pending' | 'in_progress' | 'completed' | 'stopped';
|
|
12
12
|
export interface TodoItem {
|
|
13
13
|
content: string;
|
package/dist/core/todos.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isTerminalRunStatus } from './messages.js';
|
|
1
2
|
const TODO_WRITE_TOOL_NAMES = new Set([
|
|
2
3
|
'TodoWrite',
|
|
3
4
|
'todowrite',
|
|
@@ -28,39 +29,43 @@ function normalizeTodoStatus(status) {
|
|
|
28
29
|
* @complexity O(n) in the number of raw plan entries.
|
|
29
30
|
*/
|
|
30
31
|
export function parseTodoWriteInput({ input }) {
|
|
32
|
+
const list = todoListOf({ input });
|
|
33
|
+
return list ? list.items.map((todo) => parseTodoItem({ todo })).filter((todo) => todo !== null) : [];
|
|
34
|
+
}
|
|
35
|
+
/** The plan array a tool input carries (`todos` wins over `plan`), with the key it sits under. */
|
|
36
|
+
function todoListOf({ input }, _optional = {}) {
|
|
31
37
|
if (!input || typeof input !== 'object')
|
|
32
|
-
return
|
|
38
|
+
return null;
|
|
33
39
|
const obj = input;
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
return
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
.filter((todo) => todo !== null);
|
|
40
|
+
if (Array.isArray(obj.todos))
|
|
41
|
+
return { key: 'todos', items: obj.todos };
|
|
42
|
+
if (Array.isArray(obj.plan))
|
|
43
|
+
return { key: 'plan', items: obj.plan };
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
/** The first of `keys` whose value is a string (even an empty one), else undefined. */
|
|
47
|
+
function firstStringField({ record, keys }, _optional = {}) {
|
|
48
|
+
for (const key of keys) {
|
|
49
|
+
const value = record[key];
|
|
50
|
+
if (typeof value === 'string')
|
|
51
|
+
return value;
|
|
52
|
+
}
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
const TODO_BODY_KEYS = ['content', 'step', 'description', 'label', 'text'];
|
|
56
|
+
const TODO_ACTIVE_FORM_KEYS = ['activeForm', 'active_form'];
|
|
57
|
+
function parseTodoItem({ todo }, _optional = {}) {
|
|
58
|
+
if (!todo || typeof todo !== 'object')
|
|
59
|
+
return null;
|
|
60
|
+
const record = todo;
|
|
61
|
+
const content = firstStringField({ record, keys: TODO_BODY_KEYS });
|
|
62
|
+
if (!content)
|
|
63
|
+
return null;
|
|
64
|
+
return {
|
|
65
|
+
content,
|
|
66
|
+
status: normalizeTodoStatus(record.status),
|
|
67
|
+
activeForm: firstStringField({ record, keys: TODO_ACTIVE_FORM_KEYS }),
|
|
68
|
+
};
|
|
64
69
|
}
|
|
65
70
|
/**
|
|
66
71
|
* Find the most recent plan-tool `tool_use` in a single turn's events and
|
|
@@ -68,15 +73,22 @@ export function parseTodoWriteInput({ input }) {
|
|
|
68
73
|
* @complexity O(n), scanning from the end.
|
|
69
74
|
*/
|
|
70
75
|
export function latestTodosFromEvents({ events }) {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
76
|
+
const event = latestTodoWriteEvent({ events });
|
|
77
|
+
return event ? parseTodoWriteInput({ input: event.input }) : [];
|
|
78
|
+
}
|
|
79
|
+
/** The newest plan-tool `tool_use` in one turn's events, scanning from the end. */
|
|
80
|
+
function latestTodoWriteEvent({ events }, _optional = {}) {
|
|
81
|
+
return events?.findLast((event) => event.kind === 'tool_use' && isTodoWriteToolName({ name: event.name }));
|
|
82
|
+
}
|
|
83
|
+
/** The newest message holding a plan-tool call, with that call; messages are scanned newest first. */
|
|
84
|
+
function latestTodoWriteInMessages({ messages }, _optional = {}) {
|
|
85
|
+
for (let mi = (messages?.length ?? 0) - 1; mi >= 0; mi -= 1) {
|
|
86
|
+
const message = messages[mi];
|
|
87
|
+
const event = latestTodoWriteEvent({ events: message.events });
|
|
88
|
+
if (event)
|
|
89
|
+
return { message, event };
|
|
78
90
|
}
|
|
79
|
-
return
|
|
91
|
+
return null;
|
|
80
92
|
}
|
|
81
93
|
/** The subset of {@link latestTodosFromEvents} not yet marked `completed`. */
|
|
82
94
|
export function unfinishedTodosFromEvents({ events }) {
|
|
@@ -95,42 +107,20 @@ export function unfinishedTodosFromEvents({ events }) {
|
|
|
95
107
|
* update) is far cheaper.
|
|
96
108
|
*/
|
|
97
109
|
export function latestTodoWriteInputFromMessages({ messages }) {
|
|
98
|
-
|
|
99
|
-
return null;
|
|
100
|
-
for (let mi = messages.length - 1; mi >= 0; mi -= 1) {
|
|
101
|
-
const events = messages[mi]?.events;
|
|
102
|
-
if (!events || events.length === 0)
|
|
103
|
-
continue;
|
|
104
|
-
for (let ei = events.length - 1; ei >= 0; ei -= 1) {
|
|
105
|
-
const event = events[ei];
|
|
106
|
-
if (event?.kind !== 'tool_use')
|
|
107
|
-
continue;
|
|
108
|
-
if (!isTodoWriteToolName({ name: event.name }))
|
|
109
|
-
continue;
|
|
110
|
-
return event.input;
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
return null;
|
|
110
|
+
return latestTodoWriteInMessages({ messages })?.event.input ?? null;
|
|
114
111
|
}
|
|
115
112
|
/** Convenience alias for {@link latestTodoWriteInputFromMessages} matching the `latestTodoWriteInput()` name in the target API. */
|
|
116
113
|
export const latestTodoWriteInput = latestTodoWriteInputFromMessages;
|
|
117
114
|
function hasTerminalRunEnded(runStatus, endedAt) {
|
|
118
|
-
return (runStatus ===
|
|
119
|
-
runStatus === 'failed' ||
|
|
120
|
-
runStatus === 'canceled' ||
|
|
121
|
-
(runStatus === undefined && endedAt !== undefined));
|
|
115
|
+
return isTerminalRunStatus({ status: runStatus }) || (runStatus === undefined && endedAt !== undefined);
|
|
122
116
|
}
|
|
123
117
|
function stoppedTodoWriteInput(input) {
|
|
124
|
-
|
|
118
|
+
const list = todoListOf({ input });
|
|
119
|
+
if (!list)
|
|
125
120
|
return input;
|
|
126
|
-
const obj = input;
|
|
127
|
-
const key = Array.isArray(obj.todos) ? 'todos' : Array.isArray(obj.plan) ? 'plan' : null;
|
|
128
|
-
if (!key)
|
|
129
|
-
return input;
|
|
130
|
-
const items = obj[key];
|
|
131
121
|
return {
|
|
132
122
|
...input,
|
|
133
|
-
[key]: items.map((todo) => {
|
|
123
|
+
[list.key]: list.items.map((todo) => {
|
|
134
124
|
if (!todo || typeof todo !== 'object')
|
|
135
125
|
return todo;
|
|
136
126
|
const record = todo;
|
|
@@ -151,25 +141,10 @@ function stoppedTodoWriteInput(input) {
|
|
|
151
141
|
* remap of the winning turn's items when the run has ended.
|
|
152
142
|
*/
|
|
153
143
|
export function latestTodoWriteInputForPinnedCard({ messages }) {
|
|
154
|
-
|
|
144
|
+
const found = latestTodoWriteInMessages({ messages });
|
|
145
|
+
if (!found)
|
|
155
146
|
return null;
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
const events = message?.events;
|
|
159
|
-
if (!events || events.length === 0)
|
|
160
|
-
continue;
|
|
161
|
-
for (let ei = events.length - 1; ei >= 0; ei -= 1) {
|
|
162
|
-
const event = events[ei];
|
|
163
|
-
if (event?.kind !== 'tool_use')
|
|
164
|
-
continue;
|
|
165
|
-
if (!isTodoWriteToolName({ name: event.name }))
|
|
166
|
-
continue;
|
|
167
|
-
if (!hasTerminalRunEnded(message.runStatus, message.endedAt)) {
|
|
168
|
-
return event.input;
|
|
169
|
-
}
|
|
170
|
-
return stoppedTodoWriteInput(event.input);
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
return null;
|
|
147
|
+
const { message, event } = found;
|
|
148
|
+
return hasTerminalRunEnded(message.runStatus, message.endedAt) ? stoppedTodoWriteInput(event.input) : event.input;
|
|
174
149
|
}
|
|
175
150
|
//# sourceMappingURL=todos.js.map
|
package/dist/core/tool-events.js
CHANGED
|
@@ -20,16 +20,16 @@ export function dedupeToolUsesById({ events }) {
|
|
|
20
20
|
let deduped = null;
|
|
21
21
|
for (let i = 0; i < events.length; i += 1) {
|
|
22
22
|
const event = events[i];
|
|
23
|
-
if (event.kind
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
23
|
+
if (event.kind !== 'tool_use') {
|
|
24
|
+
deduped?.push(event);
|
|
25
|
+
}
|
|
26
|
+
else if (seen.has(event.id)) {
|
|
27
|
+
deduped ??= events.slice(0, i);
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
29
30
|
seen.add(event.id);
|
|
31
|
+
deduped?.push(event);
|
|
30
32
|
}
|
|
31
|
-
if (deduped)
|
|
32
|
-
deduped.push(event);
|
|
33
33
|
}
|
|
34
34
|
return deduped ?? events;
|
|
35
35
|
}
|
|
@@ -46,8 +46,8 @@ const DELEGATED_WRAPPER_PATTERN = /^(?:mcp__.+__)?execute_(?:readonly_)?delegate
|
|
|
46
46
|
export function isDelegatedWrapperToolName({ name }) {
|
|
47
47
|
return DELEGATED_WRAPPER_PATTERN.test(name);
|
|
48
48
|
}
|
|
49
|
-
function wrapperToolId(event) {
|
|
50
|
-
if (
|
|
49
|
+
function wrapperToolId({ event }) {
|
|
50
|
+
if (!isDelegatedWrapperToolName({ name: event.name }))
|
|
51
51
|
return undefined;
|
|
52
52
|
const input = event.input;
|
|
53
53
|
const toolId = typeof input === 'object' && input !== null ? input.toolId : undefined;
|
|
@@ -79,9 +79,11 @@ export function foldDelegatedWrapperCalls({ events }) {
|
|
|
79
79
|
const claimed = new Set();
|
|
80
80
|
const folded = new Set();
|
|
81
81
|
events.forEach((event, i) => {
|
|
82
|
-
|
|
82
|
+
if (event.kind !== 'tool_use')
|
|
83
|
+
return;
|
|
84
|
+
const toolId = wrapperToolId({ event });
|
|
83
85
|
// A replayed wrapper (same id) must not claim a second canonical row.
|
|
84
|
-
if (toolId === undefined ||
|
|
86
|
+
if (toolId === undefined || folded.has(event.id))
|
|
85
87
|
return;
|
|
86
88
|
const match = events.findIndex((other, j) => j > i && !claimed.has(j) && other.kind === 'tool_use' && other.name === toolId);
|
|
87
89
|
if (match === -1)
|
package/dist/core/transcript.js
CHANGED
|
@@ -45,18 +45,7 @@ function escapeTranscriptRoleDelimiters(content) {
|
|
|
45
45
|
* @complexity O(n) across all messages' events.
|
|
46
46
|
*/
|
|
47
47
|
function buildPriorRunContextWarning(history, largeToolResultChars, highInputTokenWarningThreshold) {
|
|
48
|
-
|
|
49
|
-
let largeToolResults = 0;
|
|
50
|
-
for (const message of history) {
|
|
51
|
-
for (const event of message.events ?? []) {
|
|
52
|
-
if (event.kind === 'usage' && typeof event.inputTokens === 'number') {
|
|
53
|
-
highestInputTokens = Math.max(highestInputTokens, event.inputTokens);
|
|
54
|
-
}
|
|
55
|
-
if (event.kind === 'tool_result' && event.content.length > largeToolResultChars) {
|
|
56
|
-
largeToolResults += 1;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
}
|
|
48
|
+
const { highestInputTokens, largeToolResults } = scanPriorRunTelemetry({ history, largeToolResultChars });
|
|
60
49
|
const notes = [];
|
|
61
50
|
if (highestInputTokens >= highInputTokenWarningThreshold) {
|
|
62
51
|
notes.push(`a previous run reported ${highestInputTokens} input tokens`);
|
|
@@ -68,6 +57,20 @@ function buildPriorRunContextWarning(history, largeToolResultChars, highInputTok
|
|
|
68
57
|
return null;
|
|
69
58
|
return ['## context warning', `Detected ${notes.join(', ')}.`, 'Keep this turn compact: summarize prior tool output, read large references from temp files, and quote only task-relevant lines.'].join('\n');
|
|
70
59
|
}
|
|
60
|
+
/** The highest reported input-token count and the number of oversized tool results across all events. */
|
|
61
|
+
function scanPriorRunTelemetry({ history, largeToolResultChars }, _optional = {}) {
|
|
62
|
+
let highestInputTokens = 0;
|
|
63
|
+
let largeToolResults = 0;
|
|
64
|
+
for (const event of history.flatMap((message) => message.events ?? [])) {
|
|
65
|
+
if (event.kind === 'usage' && typeof event.inputTokens === 'number') {
|
|
66
|
+
highestInputTokens = Math.max(highestInputTokens, event.inputTokens);
|
|
67
|
+
}
|
|
68
|
+
if (event.kind === 'tool_result' && event.content.length > largeToolResultChars) {
|
|
69
|
+
largeToolResults += 1;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return { highestInputTokens, largeToolResults };
|
|
73
|
+
}
|
|
71
74
|
function scopeHistoryToAgent(history, targetAgentId, isSameAgentFamily) {
|
|
72
75
|
if (!targetAgentId)
|
|
73
76
|
return history;
|
|
@@ -79,6 +82,43 @@ function scopeHistoryToAgent(history, targetAgentId, isSameAgentFamily) {
|
|
|
79
82
|
}
|
|
80
83
|
return history;
|
|
81
84
|
}
|
|
85
|
+
/** Scan tag boundaries once, including malformed openers, without retrying an unterminated tag. */
|
|
86
|
+
function questionFormTags({ content }) {
|
|
87
|
+
const matcher = /<(question-form|ask-question)\b|<\/(question-form|ask-question)>/g;
|
|
88
|
+
const tags = [];
|
|
89
|
+
let match;
|
|
90
|
+
while ((match = matcher.exec(content))) {
|
|
91
|
+
const closing = match[2] !== undefined;
|
|
92
|
+
const end = closing ? matcher.lastIndex : content.indexOf('>', matcher.lastIndex) + 1;
|
|
93
|
+
if (!end)
|
|
94
|
+
break;
|
|
95
|
+
tags.push({ name: (match[1] ?? match[2]), start: match.index, end, closing });
|
|
96
|
+
matcher.lastIndex = end;
|
|
97
|
+
}
|
|
98
|
+
return tags;
|
|
99
|
+
}
|
|
100
|
+
/** Find matched form ranges in reverse source order without retrying unmatched openers.
|
|
101
|
+
* Keep the first following close of the same name, matching the former non-greedy replacement.
|
|
102
|
+
* Matching by name (formerly a `\1` backreference) means a `<question-form>…</ask-question>`
|
|
103
|
+
* mismatch is never spliced.
|
|
104
|
+
* @complexity O(n) time and space in content length; each tag and source character is scanned once.
|
|
105
|
+
*/
|
|
106
|
+
function questionFormRanges({ content }) {
|
|
107
|
+
const tags = questionFormTags({ content });
|
|
108
|
+
const nextClose = new Map();
|
|
109
|
+
const ranges = [];
|
|
110
|
+
for (let i = tags.length - 1; i >= 0; i--) {
|
|
111
|
+
const tag = tags[i];
|
|
112
|
+
if (tag.closing)
|
|
113
|
+
nextClose.set(tag.name, tag.end);
|
|
114
|
+
else {
|
|
115
|
+
const end = nextClose.get(tag.name);
|
|
116
|
+
if (end !== undefined)
|
|
117
|
+
ranges.push({ start: tag.start, end });
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return ranges;
|
|
121
|
+
}
|
|
82
122
|
/**
|
|
83
123
|
* Strip transcript-echo hazards from a prior assistant turn before it goes
|
|
84
124
|
* back to the agent as history: an already-answered `<question-form>` /
|
|
@@ -99,10 +139,16 @@ function scopeHistoryToAgent(history, targetAgentId, isSameAgentFamily) {
|
|
|
99
139
|
* passes) plus {@link summarizeArtifactsForTranscript}'s cost.
|
|
100
140
|
*/
|
|
101
141
|
export function sanitizePriorAssistantTurn({ content }, { persistedArtifactFiles = [] } = {}) {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
142
|
+
const chunks = [];
|
|
143
|
+
let copiedTo = 0;
|
|
144
|
+
for (const range of questionFormRanges({ content }).reverse()) {
|
|
145
|
+
if (range.start < copiedTo)
|
|
146
|
+
continue;
|
|
147
|
+
chunks.push(content.slice(copiedTo, range.start), '[question-form was emitted here on a prior turn; the user already answered, see their reply below.]');
|
|
148
|
+
copiedTo = range.end;
|
|
149
|
+
}
|
|
150
|
+
chunks.push(content.slice(copiedTo));
|
|
151
|
+
let sanitized = chunks.join('');
|
|
106
152
|
// Strip ```json (or plain ```) fenced blocks whose body matches the form
|
|
107
153
|
// schema shape — `"questions": [` is the strongest tell. A generic JSON
|
|
108
154
|
// snippet without that key is left intact.
|