@jini-ai/chat 0.6.10 → 0.6.12
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 +17 -0
- package/dist/core/accepted-input.d.ts +2 -0
- package/dist/core/accepted-input.js +34 -3
- package/dist/core/assistant-client/durable-run-subscription.d.ts +4 -2
- package/dist/core/assistant-client/durable-run-subscription.js +36 -7
- package/dist/core/persistence/title.d.ts +8 -16
- package/dist/core/persistence/title.js +20 -63
- package/dist/core/question-answer.js +2 -2
- package/dist/core/run-activity.js +4 -0
- package/dist/core/run-events/entry.d.ts +1 -1
- package/dist/core/run-events/entry.js +2 -1
- package/dist/core/turn-usage.js +19 -2
- package/dist/react/components/Markdown.js +2 -2
- package/dist/react/components/McpUiSurfaceCard.js +10 -7
- package/dist/react/components/MessageList.hooks.js +5 -1
- package/dist/react/components/MessageRow.js +13 -8
- package/dist/react/components/MessageRow.usage.d.ts +9 -1
- package/dist/react/components/MessageRow.usage.js +22 -5
- package/dist/react/components/MidRunUserMessage.d.ts +1 -1
- package/dist/react/components/MidRunUserMessage.js +8 -5
- package/dist/react/components/ToolCallGroup.hooks.d.ts +3 -1
- package/dist/react/components/ToolCallGroup.hooks.js +2 -1
- package/dist/react/components/ToolCallGroup.js +1 -1
- package/dist/react/features/chat-pane/create-mid-run-message-poster.d.ts +3 -3
- package/dist/react/features/chat-pane/create-mid-run-message-poster.js +8 -5
- package/dist/react/features/chat-pane/hooks/useChatPane.hooks.js +88 -27
- package/dist/react/features/chat-pane/rules.d.ts +5 -2
- package/dist/react/features/chat-pane/rules.js +8 -2
- package/dist/react/features/chat-pane/types.d.ts +7 -2
- package/dist/server/durable-runs/recover.js +59 -23
- package/dist/store/sql/store.d.ts +2 -2
- package/dist/store/sql/store.js +11 -3
- package/package.json +9 -9
|
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
2
2
|
import { projectAcceptedInputEvents } from '../../core/accepted-input.js';
|
|
3
3
|
import { MidRunUserMessage, MID_RUN_USER_MESSAGE_EXT_EVENT_NAME } from './MidRunUserMessage.js';
|
|
4
4
|
import { terminalMessageNotice, continuingRunNotice } from '../../core/durable-projection.js';
|
|
5
|
-
import { usageLayout, usageLineMeetsActions, useMessageUsage, useUsageSummary } from './MessageRow.usage.js';
|
|
5
|
+
import { usageEndLabel, usageLayout, usageLineMeetsActions, useMessageUsage, useUsageSummary } from './MessageRow.usage.js';
|
|
6
6
|
/**
|
|
7
7
|
* @module MessageRow
|
|
8
8
|
*
|
|
@@ -111,12 +111,13 @@ function isRunInProgress(runStatus) {
|
|
|
111
111
|
/** One "Done · 6m 29s · $0.41 · total $2.50" line from reported usage, never estimated (the total is
|
|
112
112
|
* left off when `showTotal` is false). Partial usage stays partial; output tokens are available to
|
|
113
113
|
* assistive technology and in the tooltip. */
|
|
114
|
-
function UsageSummary({ usage, showTotal }) {
|
|
114
|
+
function UsageSummary({ usage, showTotal, message, awaitingAnswer }) {
|
|
115
115
|
const t = useT();
|
|
116
|
-
const {
|
|
116
|
+
const label = usageEndLabel({ usage, ...(message === undefined ? {} : { message }), ...(awaitingAnswer === undefined ? {} : { awaitingAnswer }) });
|
|
117
|
+
const { text, title, ariaLabel } = useUsageSummary({ usage }, { showTotal, label });
|
|
117
118
|
if (!text)
|
|
118
119
|
return null;
|
|
119
|
-
return (_jsxs("div", { className: "jini-message-usage", title: title, role: "note", "aria-label": ariaLabel, children: [_jsx("span", { className: "jini-message-usage-dot", "aria-hidden": true, children: "\u25CF" }), t(
|
|
120
|
+
return (_jsxs("div", { className: "jini-message-usage", title: title, role: "note", "aria-label": ariaLabel, children: [label === 'Done' ? _jsx("span", { className: "jini-message-usage-dot", "aria-hidden": true, children: "\u25CF" }) : null, t(label), " \u00B7 ", text] }));
|
|
120
121
|
}
|
|
121
122
|
function messageBlocks({ message, rows, registry, content = message.content }, { toolCallDisplay = 'show', includeThinking = false } = {}) {
|
|
122
123
|
const blocks = interleaveMessageBlocks({ events: message.events, content, rows }, { slotOf: (ev) => registry.slot({ name: ev.name, data: ev.data }), includeThinking });
|
|
@@ -150,7 +151,7 @@ function messageAwaitingAnswer({ runStreaming, runInProgress, events }, { isAnsw
|
|
|
150
151
|
}
|
|
151
152
|
function AssistantMessageFooter({ message, usageEvent, showUsageTotal, terminalNotice, pending, runInProgress, runStreaming, visibleContent, isAnsweredCard }, _optional = {}) {
|
|
152
153
|
const t = useT();
|
|
153
|
-
return _jsxs(_Fragment, { children: [usageEvent ? _jsx(UsageSummary, { usage: usageEvent, showTotal: showUsageTotal }) : null, terminalNotice ? _jsx("div", { className: "jini-message-error", role: "status", children: t(terminalNotice) }) : null, !pending && !runInProgress ? (_jsx("div", { className: "jini-message-actions jini-message-actions--assistant", children: _jsx(CopyMessageButton, { text: visibleContent, label: t('Copy message') }) })) : null, runInProgress ? _jsx(RunActivityLine, { events: message.events, active: pending || (runStreaming === true && runInProgress), isAnsweredCard: isAnsweredCard }) : null] });
|
|
154
|
+
return _jsxs(_Fragment, { children: [usageEvent ? _jsx(UsageSummary, { usage: usageEvent, showTotal: showUsageTotal, message: message, awaitingAnswer: isAwaitingAnswer({ events: message.events }, { isAnsweredCard }) }) : null, terminalNotice ? _jsx("div", { className: "jini-message-error", role: "status", children: t(terminalNotice) }) : null, !pending && !runInProgress ? (_jsx("div", { className: "jini-message-actions jini-message-actions--assistant", children: _jsx(CopyMessageButton, { text: visibleContent, label: t('Copy message') }) })) : null, runInProgress ? _jsx(RunActivityLine, { events: message.events, active: pending || (runStreaming === true && runInProgress), isAnsweredCard: isAnsweredCard }) : null] });
|
|
154
155
|
}
|
|
155
156
|
/**
|
|
156
157
|
* Quiet, icon-only copy affordance shared by the user- and assistant-message
|
|
@@ -207,8 +208,11 @@ function AssistantMessageRow({ message, usageSummary, showUsageTotal, showUsageL
|
|
|
207
208
|
const pending = isPendingWithNoContent(message, visibleContent, timeline.rows.length);
|
|
208
209
|
const runInProgress = isRunInProgress(message.runStatus);
|
|
209
210
|
const awaitingAnswer = messageAwaitingAnswer({ runStreaming, runInProgress, events: message.events }, { isAnsweredCard: answeredCards.isAnsweredCard });
|
|
210
|
-
const usage = usageLayout({ usage: messageUsage, interleaved: blocks !== null, runInProgress }, {
|
|
211
|
+
const usage = usageLayout({ usage: messageUsage, interleaved: blocks !== null, runInProgress }, {
|
|
212
|
+
runStreaming: runStreaming && !isTerminalRunStatus({ status: message.runStatus }),
|
|
213
|
+
});
|
|
211
214
|
const footerUsage = showUsageLine ? usage.footer : undefined;
|
|
215
|
+
const lastFinishedReply = !runInProgress && !usage.footer ? usage.replies.findLastIndex(value => value !== undefined) : -1;
|
|
212
216
|
const renderToolCard = (row) => (_jsx(ToolCard, { use: row.use, result: row.result, runStreaming: runStreaming, runSucceeded: runSucceeded, ...(projectFileNames !== undefined ? { projectFileNames } : {}), ...(onRequestOpenFile !== undefined ? { onRequestOpenFile } : {}) }, row.id));
|
|
213
217
|
// Hidden mode reuses the same disclosure and card renderer for unrecovered failures.
|
|
214
218
|
const renderTools = (rows, key) => (toolCallDisplay !== 'show'
|
|
@@ -218,7 +222,7 @@ function AssistantMessageRow({ message, usageSummary, showUsageTotal, showUsageL
|
|
|
218
222
|
const renderExtGroup = (group) => {
|
|
219
223
|
const renderer = registry.get({ name: group.name });
|
|
220
224
|
if (!renderer && group.name === MID_RUN_USER_MESSAGE_EXT_EVENT_NAME)
|
|
221
|
-
return _jsx(MidRunUserMessage, { events: group.events });
|
|
225
|
+
return _jsx(MidRunUserMessage, { events: group.events, runStreaming: answeredCards.runStreaming }, group.slot);
|
|
222
226
|
if (!renderer)
|
|
223
227
|
return null;
|
|
224
228
|
const answerText = answeredCards.answerText(group.slot);
|
|
@@ -251,7 +255,8 @@ function AssistantMessageRow({ message, usageSummary, showUsageTotal, showUsageL
|
|
|
251
255
|
}
|
|
252
256
|
if (block.kind === 'reply-end') {
|
|
253
257
|
const reply = usage.replies[block.index];
|
|
254
|
-
|
|
258
|
+
// A trailing input may have no reply: its predecessor's usage belongs above that input.
|
|
259
|
+
return reply && showUsageLine ? _jsx(UsageSummary, { usage: reply, showTotal: showUsageTotal, ...(block.index === lastFinishedReply ? { message } : {}) }, block.key) : null;
|
|
255
260
|
}
|
|
256
261
|
if (block.kind === 'tools')
|
|
257
262
|
return renderTools(block.rows, block.key);
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import type { ChatMessage } from '../../core/messages.js';
|
|
2
2
|
import { type ChatUsageSummary } from '../../core/turn-usage.js';
|
|
3
|
+
/** A run's terminal state wins over its last model response; interrupted usage never claims success.
|
|
4
|
+
* Missing lifecycle data keeps the legacy completed label. O(n) in terminal notice events. */
|
|
5
|
+
export declare function usageEndLabel({ usage, message, awaitingAnswer }: {
|
|
6
|
+
usage: ChatUsageSummary;
|
|
7
|
+
message?: ChatMessage;
|
|
8
|
+
awaitingAnswer?: boolean;
|
|
9
|
+
}, _options?: {}): string;
|
|
3
10
|
/** A standalone row has one turn; MessageList supplies the full conversation projection. */
|
|
4
11
|
export declare function useMessageUsage({ message, summary }: {
|
|
5
12
|
message: ChatMessage;
|
|
@@ -41,8 +48,9 @@ export declare function usageLineMeetsActions({ footer, terminalNotice, pending,
|
|
|
41
48
|
* the current turn's cost is unknown. `showTotal: false` turns the total off. */
|
|
42
49
|
export declare function useUsageSummary({ usage }: {
|
|
43
50
|
usage: ChatUsageSummary;
|
|
44
|
-
}, { showTotal }?: {
|
|
51
|
+
}, { showTotal, label }?: {
|
|
45
52
|
showTotal?: boolean;
|
|
53
|
+
label?: string;
|
|
46
54
|
}): {
|
|
47
55
|
text: string;
|
|
48
56
|
title: string | undefined;
|
|
@@ -1,7 +1,24 @@
|
|
|
1
1
|
import { useMemo } from 'react';
|
|
2
|
+
import { terminalMessageNotice } from '../../core/durable-projection.js';
|
|
2
3
|
import { conversationUsage } from '../../core/turn-usage.js';
|
|
3
4
|
import { formatUsageCost } from '../../core/usage-format.js';
|
|
4
|
-
import {
|
|
5
|
+
import { useI18n } from '../hooks/context.js';
|
|
6
|
+
/** A run's terminal state wins over its last model response; interrupted usage never claims success.
|
|
7
|
+
* Missing lifecycle data keeps the legacy completed label. O(n) in terminal notice events. */
|
|
8
|
+
export function usageEndLabel({ usage, message, awaitingAnswer = false }, _options = {}) {
|
|
9
|
+
const status = message?.runStatus;
|
|
10
|
+
if (status === 'failed')
|
|
11
|
+
return 'Failed';
|
|
12
|
+
if (status === 'canceled') {
|
|
13
|
+
return /stopped/i.test(terminalMessageNotice({ message: message }) ?? '') ? 'Stopped' : 'Canceled';
|
|
14
|
+
}
|
|
15
|
+
if (awaitingAnswer)
|
|
16
|
+
return 'Waiting for your answer';
|
|
17
|
+
const reason = usage.stopReason ?? '';
|
|
18
|
+
if (/error|fail/i.test(reason))
|
|
19
|
+
return 'Failed';
|
|
20
|
+
return /abort|interrupt|cancel|max_tokens|max_tool_turns|length|tool_use/i.test(reason) ? 'Interrupted' : 'Done';
|
|
21
|
+
}
|
|
5
22
|
/** A standalone row has one turn; MessageList supplies the full conversation projection. */
|
|
6
23
|
export function useMessageUsage({ message, summary }, _optional = {}) {
|
|
7
24
|
return useMemo(() => summary ?? conversationUsage({ messages: [message] }).get(message.id), [message, summary]);
|
|
@@ -31,13 +48,13 @@ export function usageLineMeetsActions({ footer, terminalNotice, pending, runInPr
|
|
|
31
48
|
* Each line carries the chat's running total as of that reply, "Done · 16s · $0.39 · total $3.95" (owner,
|
|
32
49
|
* 2026-10-10: keep the total, always shown, even on the first turn). Known totals remain visible when
|
|
33
50
|
* the current turn's cost is unknown. `showTotal: false` turns the total off. */
|
|
34
|
-
export function useUsageSummary({ usage }, { showTotal = true } = {}) {
|
|
35
|
-
const t =
|
|
51
|
+
export function useUsageSummary({ usage }, { showTotal = true, label = 'Done' } = {}) {
|
|
52
|
+
const { t, locale } = useI18n();
|
|
36
53
|
const parts = [usageDuration({ durationMs: usage.durationMs }), usageCost({ usage }, { showTotal })]
|
|
37
54
|
.filter((part) => part !== undefined);
|
|
38
55
|
const text = parts.join(' · ');
|
|
39
|
-
const title = usage.outputTokens === undefined ? undefined : t('{n}
|
|
40
|
-
return { text, title, ariaLabel: title === undefined ? undefined : `${t(
|
|
56
|
+
const title = usage.outputTokens === undefined ? undefined : t('{n} output tokens', { n: usage.outputTokens.toLocaleString(locale) });
|
|
57
|
+
return { text, title, ariaLabel: title === undefined ? undefined : `${t(label)} · ${text} · ${title}` };
|
|
41
58
|
}
|
|
42
59
|
function usageDuration({ durationMs }, _options = {}) {
|
|
43
60
|
if (durationMs === undefined)
|
|
@@ -7,7 +7,7 @@ export declare function midRunUserMessageSlotKey({ data }: {
|
|
|
7
7
|
data: unknown;
|
|
8
8
|
}): string | undefined;
|
|
9
9
|
/** The bubble for one mid-run message — the same classes as a sent user message, so it reads as one. */
|
|
10
|
-
export declare function MidRunUserMessage({ events }: Pick<ExtEventRenderProps, 'events'>): import("react").JSX.Element | null;
|
|
10
|
+
export declare function MidRunUserMessage({ events, runStreaming }: Pick<ExtEventRenderProps, 'events'> & Partial<Pick<ExtEventRenderProps, 'runStreaming'>>): import("react").JSX.Element | null;
|
|
11
11
|
/**
|
|
12
12
|
* Registers {@link MidRunUserMessage} against the ext-event registry.
|
|
13
13
|
*
|
|
@@ -14,6 +14,7 @@ import { acceptedInputEvent } from '../../core/accepted-input.js';
|
|
|
14
14
|
import { SentAttachmentButton } from './SentAttachment.js';
|
|
15
15
|
import { AttachmentPreviewModal } from './AttachmentPreviewModal.js';
|
|
16
16
|
import { useAttachmentGallery } from './AttachmentGallery.hooks.js';
|
|
17
|
+
import { useT } from '../hooks/context.js';
|
|
17
18
|
import { MID_RUN_USER_MESSAGE_EVENT_NAME } from '../../core/turn-usage.js';
|
|
18
19
|
import { registerExtEventRenderer } from '../ext-event-renderer-registry.js';
|
|
19
20
|
/** The ext-event name the daemon's mid-run `user_message` run event arrives under. One source with
|
|
@@ -28,7 +29,7 @@ export function midRunUserMessageSlotKey({ data }) {
|
|
|
28
29
|
return field(data, 'id');
|
|
29
30
|
}
|
|
30
31
|
/** The bubble for one mid-run message — the same classes as a sent user message, so it reads as one. */
|
|
31
|
-
export function MidRunUserMessage({ events }) {
|
|
32
|
+
export function MidRunUserMessage({ events, runStreaming = false }) {
|
|
32
33
|
const data = events.at(-1);
|
|
33
34
|
const text = field(data, 'text');
|
|
34
35
|
if (text === undefined)
|
|
@@ -36,12 +37,14 @@ export function MidRunUserMessage({ events }) {
|
|
|
36
37
|
const receipt = acceptedInputEvent({ event: { kind: 'ext', name: MID_RUN_USER_MESSAGE_EVENT_NAME, data } });
|
|
37
38
|
// MOVE: A single live delivery may contain several queued inputs; display their original receipts.
|
|
38
39
|
const inputs = receipt?.data.inputs ?? [{ id: field(data, 'id') ?? 'mid-run', text }];
|
|
39
|
-
|
|
40
|
+
// A run that ended without reading the message hands it to the next turn; it is no longer queued here.
|
|
41
|
+
return _jsx(_Fragment, { children: inputs.map(input => _jsx(MidRunInput, { input: input, queued: runStreaming && (input.deliveryState ?? receipt?.data.deliveryState) === 'queued' }, input.id)) });
|
|
40
42
|
}
|
|
41
|
-
function MidRunInput({ input }) {
|
|
43
|
+
function MidRunInput({ input, queued }) {
|
|
44
|
+
const t = useT();
|
|
42
45
|
const attachments = input.attachments ?? [];
|
|
43
46
|
const gallery = useAttachmentGallery({ attachments });
|
|
44
|
-
return (_jsxs("div", { className: "jini-message jini-message-user jini-message-user--mid-run", "data-agent-role": "region", "data-agent-label": "A message from the user, sent while the agent was working", children: [_jsx("div", { className: "jini-message-content", children: input.text }), attachments.length > 0 && _jsx("div", { className: "jini-message-attachments", children: attachments.map(attachment => _jsx(SentAttachmentButton, { attachment: attachment, onOpen: gallery.open }, attachment.path)) }), gallery.opened !== null && _jsx(AttachmentPreviewModal, { attachment: gallery.opened, onClose: gallery.close, navigation: gallery.navigation })] }));
|
|
47
|
+
return (_jsxs("div", { className: "jini-message jini-message-user jini-message-user--mid-run", "data-agent-role": "region", "data-agent-label": "A message from the user, sent while the agent was working", children: [queued && _jsx("div", { className: "jini-message-user__queued", role: "status", children: t('Queued — read after the current step') }), _jsx("div", { className: "jini-message-content", children: input.text }), attachments.length > 0 && _jsx("div", { className: "jini-message-attachments", children: attachments.map(attachment => _jsx(SentAttachmentButton, { attachment: attachment, onOpen: gallery.open }, attachment.path)) }), gallery.opened !== null && _jsx(AttachmentPreviewModal, { attachment: gallery.opened, onClose: gallery.close, navigation: gallery.navigation })] }));
|
|
45
48
|
}
|
|
46
49
|
/**
|
|
47
50
|
* Registers {@link MidRunUserMessage} against the ext-event registry.
|
|
@@ -49,6 +52,6 @@ function MidRunInput({ input }) {
|
|
|
49
52
|
* @returns An unregister handle, so a test or a hot reload can dispose cleanly.
|
|
50
53
|
*/
|
|
51
54
|
export function registerMidRunUserMessageRenderer() {
|
|
52
|
-
return registerExtEventRenderer({ name: MID_RUN_USER_MESSAGE_EXT_EVENT_NAME, renderer: (props) => _jsx(MidRunUserMessage, { events: props.events }) }, { slotKey: midRunUserMessageSlotKey });
|
|
55
|
+
return registerExtEventRenderer({ name: MID_RUN_USER_MESSAGE_EXT_EVENT_NAME, renderer: (props) => _jsx(MidRunUserMessage, { events: props.events, runStreaming: props.runStreaming }) }, { slotKey: midRunUserMessageSlotKey });
|
|
53
56
|
}
|
|
54
57
|
//# sourceMappingURL=MidRunUserMessage.js.map
|
|
@@ -3,8 +3,10 @@ export interface ToolCallGroupState {
|
|
|
3
3
|
/** Whether the turn's tool cards are showing under the line. Starts closed. */
|
|
4
4
|
open: boolean;
|
|
5
5
|
toggle: () => void;
|
|
6
|
-
/** "Used 3 tools",
|
|
6
|
+
/** "Used 3 tools", or "Running Bash…" while a tool runs. */
|
|
7
7
|
label: string;
|
|
8
|
+
/** The failure suffix, styled independently of the tool count. */
|
|
9
|
+
failureLabel: string | undefined;
|
|
8
10
|
running: boolean;
|
|
9
11
|
failed: boolean;
|
|
10
12
|
}
|
|
@@ -15,7 +15,8 @@ export function useToolCallGroup({ rows }, _optional = {}) {
|
|
|
15
15
|
return {
|
|
16
16
|
open,
|
|
17
17
|
toggle,
|
|
18
|
-
label: toolGroupLabel({ summary, t }),
|
|
18
|
+
label: toolGroupLabel({ summary: summary.kind === 'done' ? { ...summary, failed: 0 } : summary, t }),
|
|
19
|
+
failureLabel: summary.kind === 'done' && summary.failed > 0 ? t('{count} failed', { count: summary.failed }) : undefined,
|
|
19
20
|
running: summary.kind === 'running',
|
|
20
21
|
failed: summary.kind === 'done' && summary.failed > 0,
|
|
21
22
|
};
|
|
@@ -10,6 +10,6 @@ import { Icon } from './Icon.js';
|
|
|
10
10
|
export function ToolCallGroup({ rows, renderRow, failureOnly = false }) {
|
|
11
11
|
const group = useToolCallGroup({ rows });
|
|
12
12
|
const t = useT();
|
|
13
|
-
return (_jsxs("div", { className: "jini-tool-group", "data-failed": group.failed ? 'true' : undefined, children: [failureOnly ? _jsxs("span", { className: "jini-tool-group__label", children: [t('A step failed'), " "] }) : null, _jsxs("button", { type: "button", className: "jini-tool-group__toggle", "aria-expanded": group.open, onClick: group.toggle, children: [_jsx("span", { className: `jini-tool-group__label${group.running ? ' shimmer-text' : ''}`, children: failureOnly ? t('Show') : group.label }), _jsx(Icon, { name: group.open ? 'chevron-down' : 'chevron-right', size: 12 })] }), group.open ? _jsx("div", { className: "jini-message-tools", children: rows.map(renderRow) }) : null] }));
|
|
13
|
+
return (_jsxs("div", { className: "jini-tool-group", "data-failed": group.failed ? 'true' : undefined, children: [failureOnly ? _jsxs("span", { className: "jini-tool-group__label", children: [t('A step failed'), " "] }) : null, _jsxs("button", { type: "button", className: "jini-tool-group__toggle", "aria-expanded": group.open, onClick: group.toggle, children: [_jsx("span", { className: `jini-tool-group__label${group.running ? ' shimmer-text' : ''}`, children: failureOnly ? t('Show') : group.label }), !failureOnly && group.failureLabel ? _jsxs("span", { className: "jini-tool-group__failed", children: [" \u00B7 ", group.failureLabel] }) : null, _jsx(Icon, { name: group.open ? 'chevron-down' : 'chevron-right', size: 12 })] }), group.open ? _jsx("div", { className: "jini-message-tools", children: rows.map(renderRow) }) : null] }));
|
|
14
14
|
}
|
|
15
15
|
//# sourceMappingURL=ToolCallGroup.js.map
|
|
@@ -15,11 +15,11 @@
|
|
|
15
15
|
*
|
|
16
16
|
* ## Why it resolves an outcome instead of throwing
|
|
17
17
|
*
|
|
18
|
-
* The pane acts differently on each outcome and must never drop the text: `'unsupported'`
|
|
19
|
-
*
|
|
18
|
+
* The pane acts differently on each outcome and must never drop the text: `'unsupported'` queues
|
|
19
|
+
* the whole turn; `'not-running'` queues behind the
|
|
20
20
|
* ending run; `'failed'` retains the draft for retry. A 404 means
|
|
21
21
|
* the daemon never saw this run (a host-side run such as a BYOK API turn), so it is `'unsupported'`
|
|
22
|
-
* too: no live process to steer
|
|
22
|
+
* too: no live process to steer. Accepted inputs retain their fallback copy until consumed.
|
|
23
23
|
*/
|
|
24
24
|
import { type UserTextRedactionOptions } from '../../../core/user-text-redaction.js';
|
|
25
25
|
import type { DeliverMidRunMessage } from './types.js';
|
|
@@ -15,11 +15,11 @@
|
|
|
15
15
|
*
|
|
16
16
|
* ## Why it resolves an outcome instead of throwing
|
|
17
17
|
*
|
|
18
|
-
* The pane acts differently on each outcome and must never drop the text: `'unsupported'`
|
|
19
|
-
*
|
|
18
|
+
* The pane acts differently on each outcome and must never drop the text: `'unsupported'` queues
|
|
19
|
+
* the whole turn; `'not-running'` queues behind the
|
|
20
20
|
* ending run; `'failed'` retains the draft for retry. A 404 means
|
|
21
21
|
* the daemon never saw this run (a host-side run such as a BYOK API turn), so it is `'unsupported'`
|
|
22
|
-
* too: no live process to steer
|
|
22
|
+
* too: no live process to steer. Accepted inputs retain their fallback copy until consumed.
|
|
23
23
|
*/
|
|
24
24
|
import { redactUserText } from '../../../core/user-text-redaction.js';
|
|
25
25
|
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
@@ -31,8 +31,11 @@ async function conflictDelivery(response) {
|
|
|
31
31
|
}
|
|
32
32
|
/** Maps the route's response onto an outcome. Only 202 is a delivery. */
|
|
33
33
|
async function deliveryFor(response) {
|
|
34
|
-
if (response.status === 202)
|
|
35
|
-
|
|
34
|
+
if (response.status === 202) {
|
|
35
|
+
const body = await response.json().catch(() => null);
|
|
36
|
+
const id = body?.id;
|
|
37
|
+
return typeof id === 'string' && id.length > 0 ? { delivery: 'delivered', id } : 'delivered';
|
|
38
|
+
}
|
|
36
39
|
if (response.status === 409)
|
|
37
40
|
return conflictDelivery(response);
|
|
38
41
|
return response.status === 404 ? 'unsupported' : 'failed';
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { acceptedInputEvent, acceptedInputUserEvent, projectAcceptedInputEvents } from '../../../../core/accepted-input.js';
|
|
2
2
|
import { useUserTextGuard } from '../../../hooks/useUserTextGuard.js';
|
|
3
|
+
import { isAwaitingAnswer } from '../../../../core/run-activity.js';
|
|
3
4
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
4
5
|
import { appendQueuedTurn, describeChatPaneSendBlocker, findAwaitedTypedAnswerId, findChatPaneSendBlocker, findMidRunMessageRunId, isChatPaneQueueableBlocker, resolveChatPaneSelection, } from '../rules.js';
|
|
5
6
|
import { definedProps } from '../../../util/defined-props.js';
|
|
@@ -8,6 +9,35 @@ import { readCachedAttachmentBatchId, writeCachedAttachmentBatchId, } from '../.
|
|
|
8
9
|
import { useComposer } from '../../../hooks/useComposer.js';
|
|
9
10
|
import { useConversation, } from '../../../hooks/useConversation.js';
|
|
10
11
|
import { useChatPaneWorkingDirectory, } from './useChatPaneWorkingDirectory.hooks.js';
|
|
12
|
+
/** Filter during render so a consumed receipt followed by end in the same batch cannot flush twice.
|
|
13
|
+
* Legacy host receipts can replace optimistic IDs; the accepted-input owner retains their aliases.
|
|
14
|
+
* @complexity O(e + q) time and space for receipt events and locally queued inputs.
|
|
15
|
+
*/
|
|
16
|
+
function unconsumedQueuedInputs({ inputs, messages }) {
|
|
17
|
+
const consumed = new Set();
|
|
18
|
+
for (const message of messages) {
|
|
19
|
+
for (const event of projectAcceptedInputEvents({ events: message.events ?? [] })) {
|
|
20
|
+
const receipt = acceptedInputEvent({ event });
|
|
21
|
+
if (receipt?.data.deliveryState !== 'consumed')
|
|
22
|
+
continue;
|
|
23
|
+
for (const input of [receipt.data, ...(receipt.data.inputs ?? [])]) {
|
|
24
|
+
consumed.add(input.id);
|
|
25
|
+
for (const alias of input.aliases ?? [])
|
|
26
|
+
consumed.add(alias);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return inputs.filter(input => !consumed.has(input.liveMessageId ?? input.id));
|
|
31
|
+
}
|
|
32
|
+
/** The conversation copies run events onto its messages one effect after streaming stops, so a
|
|
33
|
+
* consumed receipt can still be missing from the transcript when the send blocker clears. A
|
|
34
|
+
* live-accepted copy therefore waits until the run's own message reaches a terminal status. */
|
|
35
|
+
function awaitingRunReceipts({ inputs, messages }) {
|
|
36
|
+
if (!inputs.some(input => input.liveMessageId !== undefined))
|
|
37
|
+
return false;
|
|
38
|
+
const status = messages.at(-1)?.runStatus;
|
|
39
|
+
return status === 'queued' || status === 'running';
|
|
40
|
+
}
|
|
11
41
|
/** The stand-in prompt for a turn that carries only attachments. */
|
|
12
42
|
const ATTACHMENTS_ONLY_PROMPT = 'Review the attached file(s).';
|
|
13
43
|
/**
|
|
@@ -79,11 +109,17 @@ function resolveChatPaneActivity(selectedAgent, conversation) {
|
|
|
79
109
|
return 'streaming';
|
|
80
110
|
return 'ready';
|
|
81
111
|
}
|
|
112
|
+
/** Hold new input until its uploads or human gate settle; the existing queue owns the retry.
|
|
113
|
+
* @complexity O(e) in the newest message's events, with no I/O.
|
|
114
|
+
*/
|
|
115
|
+
function waitingForLiveInput({ uploadPending, messages }, _options = {}) {
|
|
116
|
+
return uploadPending || isAwaitingAnswer({ events: messages.at(-1)?.events });
|
|
117
|
+
}
|
|
82
118
|
/**
|
|
83
119
|
* The host delivery and live run a send can go INTO instead of queueing behind, or `null` when the
|
|
84
120
|
* host offers no mid-run delivery or no run can take it ({@link findMidRunMessageRunId}).
|
|
85
121
|
*
|
|
86
|
-
* @complexity O(
|
|
122
|
+
* @complexity O(e) in the newest message's events.
|
|
87
123
|
*/
|
|
88
124
|
function findMidRunTarget({ deliver, blocker, attachmentCount, messages }) {
|
|
89
125
|
if (deliver === undefined)
|
|
@@ -149,15 +185,11 @@ export function useChatPane(options) {
|
|
|
149
185
|
const [activeUploadCount, setActiveUploadCount] = useState(0);
|
|
150
186
|
const [attachmentError, setAttachmentError] = useState(null);
|
|
151
187
|
// MOVE: Keep the signal with each sanitized input; re-redaction cannot detect the original paste.
|
|
152
|
-
const [
|
|
153
|
-
const queuedTurn = useMemo(() => queuedInputs.reduce((queued, input) => appendQueuedTurn({
|
|
154
|
-
queued, next: { text: input.text, secretRedacted: input.secretRedaction?.secretRedacted ?? false,
|
|
155
|
-
count: input.secretRedaction?.count ?? 0 },
|
|
156
|
-
}), null), [queuedInputs]);
|
|
188
|
+
const [storedQueuedInputs, setQueuedInputs] = useState([]);
|
|
157
189
|
// MOVE: Uploads belong to the input that submitted them, even if a later message is queued first.
|
|
158
190
|
const queuedUploadOwnersRef = useRef(new Map());
|
|
159
|
-
// A send
|
|
160
|
-
const
|
|
191
|
+
// A send held for uploads or a human card resumes live; a refused delivery waits for run end.
|
|
192
|
+
const queuedForLiveDeliveryRef = useRef(false);
|
|
161
193
|
const submittedComposerRef = useRef(null);
|
|
162
194
|
const startingPromptRef = useRef(false);
|
|
163
195
|
const [typedAnswerNotice, setTypedAnswerNotice] = useState(null);
|
|
@@ -221,6 +253,16 @@ export function useChatPane(options) {
|
|
|
221
253
|
validateAttachments: options.validateAttachments,
|
|
222
254
|
} }));
|
|
223
255
|
const activity = resolveChatPaneActivity(selectedAgent, conversation);
|
|
256
|
+
const queuedInputs = useMemo(() => unconsumedQueuedInputs({ inputs: storedQueuedInputs, messages: conversation.messages }), [storedQueuedInputs, conversation.messages]);
|
|
257
|
+
const queuedTurn = useMemo(() => queuedInputs.reduce((queued, input) => appendQueuedTurn({
|
|
258
|
+
queued, next: { text: input.text, secretRedacted: input.secretRedaction?.secretRedacted ?? false,
|
|
259
|
+
count: input.secretRedaction?.count ?? 0 },
|
|
260
|
+
}), null), [queuedInputs]);
|
|
261
|
+
// Release consumed text/files locally as well as hiding them; next-turn fallback uses this same projection.
|
|
262
|
+
useEffect(() => {
|
|
263
|
+
if (queuedInputs.length !== storedQueuedInputs.length)
|
|
264
|
+
setQueuedInputs(queuedInputs);
|
|
265
|
+
}, [queuedInputs, storedQueuedInputs.length]);
|
|
224
266
|
useEffect(() => {
|
|
225
267
|
options.onActivityChange?.(activity);
|
|
226
268
|
}, [activity, options.onActivityChange]);
|
|
@@ -363,7 +405,7 @@ export function useChatPane(options) {
|
|
|
363
405
|
}
|
|
364
406
|
}, [composer, options.conversationId, options.uploadAttachments]);
|
|
365
407
|
/** Acceptance is the only writer of local recall and optimistic in-run bubbles. */
|
|
366
|
-
const recordAcceptedInput = useCallback(({ inputs, bubble },
|
|
408
|
+
const recordAcceptedInput = useCallback(({ inputs, bubble }, { receipt = 'optimistic' } = {}) => {
|
|
367
409
|
composer.history?.record({ inputs });
|
|
368
410
|
submittedComposerRef.current = composer;
|
|
369
411
|
if (bubble === undefined)
|
|
@@ -372,8 +414,9 @@ export function useChatPane(options) {
|
|
|
372
414
|
const last = messages.at(-1);
|
|
373
415
|
if (last?.role !== 'assistant')
|
|
374
416
|
return messages;
|
|
375
|
-
const event = acceptedInputUserEvent({ input: { id: inputs[0].id, text: bubble.text
|
|
376
|
-
|
|
417
|
+
const event = acceptedInputUserEvent({ input: { id: inputs[0].id, text: bubble.text,
|
|
418
|
+
...(inputs[0].deliveryState ? { deliveryState: inputs[0].deliveryState } : {}) },
|
|
419
|
+
inputs, ...bubble }, { receipt });
|
|
377
420
|
return [...messages.slice(0, -1), { ...last,
|
|
378
421
|
events: projectAcceptedInputEvents({ events: [...(last.events ?? []), event] }) }];
|
|
379
422
|
});
|
|
@@ -495,24 +538,37 @@ export function useChatPane(options) {
|
|
|
495
538
|
if (inputs === undefined && latestDraftRef.current === submittedDraft)
|
|
496
539
|
latestComposerRef.current.setDraft('');
|
|
497
540
|
}, []);
|
|
541
|
+
/** HTTP acceptance retains fallback ownership; only an actual consumption receipt removes it. */
|
|
542
|
+
const retainAcceptedMidRunInput = useCallback(({ outcome, inputs, turn, attachments, afterInputId }, _options = {}) => {
|
|
543
|
+
const accepted = inputs ?? [{ id: createChatLocalId({}), text: turn.text, attachments,
|
|
544
|
+
...(turn.secretRedacted ? { secretRedaction: { secretRedacted: true, count: turn.count } } : {}) }];
|
|
545
|
+
const id = typeof outcome === 'object' ? outcome.id : accepted[0].id;
|
|
546
|
+
// A known HTTP id is already authoritative. Do not let equal text reconcile an unrelated
|
|
547
|
+
// wire receipt into this bubble; legacy hosts alone still need optimistic reconciliation.
|
|
548
|
+
const retained = accepted.map((input, index) => ({ ...input,
|
|
549
|
+
...(index === 0 ? { id, aliases: [...(input.aliases ?? []), input.id] } : {}),
|
|
550
|
+
liveMessageId: id, deliveryState: 'queued',
|
|
551
|
+
}));
|
|
552
|
+
queuedConversationIdRef.current = options.conversationId;
|
|
553
|
+
setQueuedInputs(queued => [...queued, ...retained]);
|
|
554
|
+
recordAcceptedInput({ inputs: retained,
|
|
555
|
+
bubble: { text: turn.text, ...(afterInputId === undefined ? {} : { afterInputId }) } }, typeof outcome === 'object' ? { receipt: 'accepted' } : {});
|
|
556
|
+
}, [options.conversationId, recordAcceptedInput]);
|
|
498
557
|
const finishMidRunSend = useCallback(({ outcome, inputs, turn, attachments, afterInputId, submittedDraft }, _options = {}) => {
|
|
499
558
|
if (outcome === 'failed') {
|
|
500
559
|
restoreQueuedDraft({ inputs, text: turn.text, attachments });
|
|
501
560
|
setTypedAnswerNotice('failed');
|
|
502
561
|
return;
|
|
503
562
|
}
|
|
504
|
-
if (outcome === 'delivered') {
|
|
505
|
-
|
|
506
|
-
bubble: { text: turn.text, ...(afterInputId === undefined ? {} : { afterInputId }) } });
|
|
563
|
+
if (outcome === 'delivered' || typeof outcome === 'object') {
|
|
564
|
+
retainAcceptedMidRunInput({ outcome, inputs, turn, attachments, afterInputId });
|
|
507
565
|
clearDeliveredDraft({ inputs, submittedDraft });
|
|
508
566
|
consumeDeliveredAttachments({ attachments });
|
|
509
567
|
return;
|
|
510
568
|
}
|
|
511
569
|
enqueueTurn({ turn, attachments, ...(inputs === undefined ? {} : { inputs }) });
|
|
512
570
|
clearDeliveredDraft({ inputs, submittedDraft });
|
|
513
|
-
|
|
514
|
-
conversation.cancel();
|
|
515
|
-
}, [conversation.cancel, enqueueTurn, recordAcceptedInput, clearDeliveredDraft, consumeDeliveredAttachments, restoreQueuedDraft]);
|
|
571
|
+
}, [enqueueTurn, retainAcceptedMidRunInput, clearDeliveredDraft, consumeDeliveredAttachments, restoreQueuedDraft]);
|
|
516
572
|
// A delivered steer belongs to the existing run; unsupported turns use the same accepted queue.
|
|
517
573
|
// A failed delivery keeps the working draft so retry cannot silently become another run.
|
|
518
574
|
const sendIntoRunningTurn = useCallback(async ({ deliver, runId, turn, inputs, attachments = composer.attachments }, _options = {}) => {
|
|
@@ -541,10 +597,12 @@ export function useChatPane(options) {
|
|
|
541
597
|
}
|
|
542
598
|
const turn = guard.redact({ text: prompt }, {});
|
|
543
599
|
const uploadPending = activeUploadsRef.current.size > 0 || attachmentError !== null;
|
|
544
|
-
|
|
545
|
-
|
|
600
|
+
// Retry live delivery after the human gate settles, using the same queue as pending uploads.
|
|
601
|
+
if (waitingForLiveInput({ uploadPending, messages: conversation.messages })) {
|
|
602
|
+
queuedForLiveDeliveryRef.current = queuedTurn === null || queuedForLiveDeliveryRef.current;
|
|
603
|
+
}
|
|
546
604
|
// MOVE: The existing queue owns its files; a later steer must join it to preserve send order.
|
|
547
|
-
const target = uploadPending ||
|
|
605
|
+
const target = uploadPending || queuedInputs.some(input => input.liveMessageId === undefined) ? null : findMidRunTarget({
|
|
548
606
|
deliver: options.deliverMidRunMessage,
|
|
549
607
|
blocker: sendBlocker,
|
|
550
608
|
attachmentCount: composer.attachments.length,
|
|
@@ -565,6 +623,7 @@ export function useChatPane(options) {
|
|
|
565
623
|
enqueueTurn,
|
|
566
624
|
options.deliverMidRunMessage,
|
|
567
625
|
queuedTurn,
|
|
626
|
+
queuedInputs,
|
|
568
627
|
sendBlocker,
|
|
569
628
|
sendIntoRunningTurn,
|
|
570
629
|
sendPrompt,
|
|
@@ -629,7 +688,7 @@ export function useChatPane(options) {
|
|
|
629
688
|
}, []);
|
|
630
689
|
const cancelQueued = useCallback(() => {
|
|
631
690
|
submittedComposerRef.current = null;
|
|
632
|
-
|
|
691
|
+
queuedForLiveDeliveryRef.current = false;
|
|
633
692
|
queuedConversationIdRef.current = undefined;
|
|
634
693
|
discardQueuedUploads({});
|
|
635
694
|
setQueuedInputs([]);
|
|
@@ -643,7 +702,7 @@ export function useChatPane(options) {
|
|
|
643
702
|
// clear blocker. Clear the slot before awaiting so re-renders cannot send it twice.
|
|
644
703
|
useEffect(() => {
|
|
645
704
|
if (queuedTurn === null) {
|
|
646
|
-
|
|
705
|
+
queuedForLiveDeliveryRef.current = false;
|
|
647
706
|
return;
|
|
648
707
|
}
|
|
649
708
|
if (queuedConversationIdRef.current !== options.conversationId) {
|
|
@@ -655,13 +714,13 @@ export function useChatPane(options) {
|
|
|
655
714
|
}
|
|
656
715
|
if (activeUploadCount > 0 || attachmentError !== null)
|
|
657
716
|
return;
|
|
658
|
-
const target =
|
|
717
|
+
const target = queuedForLiveDeliveryRef.current ? findMidRunTarget({
|
|
659
718
|
deliver: options.deliverMidRunMessage,
|
|
660
719
|
blocker: sendBlocker, attachmentCount: queuedInputs.flatMap(input => input.attachments).length, messages: conversation.messages,
|
|
661
720
|
}) : null;
|
|
662
|
-
if (sendBlocker !== null
|
|
721
|
+
if (target === null && (sendBlocker !== null || awaitingRunReceipts({ inputs: queuedInputs, messages: conversation.messages })))
|
|
663
722
|
return;
|
|
664
|
-
|
|
723
|
+
queuedForLiveDeliveryRef.current = false;
|
|
665
724
|
const inputs = queuedInputs;
|
|
666
725
|
const attachments = inputs.flatMap(input => input.attachments);
|
|
667
726
|
setQueuedInputs([]);
|
|
@@ -690,7 +749,7 @@ export function useChatPane(options) {
|
|
|
690
749
|
// `conversation.cancel()` above clears the streaming blocker — the flush effect fires it into
|
|
691
750
|
// the just-reset conversation instead of discarding it like the rest of this turn's state.
|
|
692
751
|
queuedConversationIdRef.current = undefined;
|
|
693
|
-
|
|
752
|
+
queuedForLiveDeliveryRef.current = false;
|
|
694
753
|
queuedUploadOwnersRef.current.clear();
|
|
695
754
|
setQueuedInputs([]);
|
|
696
755
|
}, [composer, conversation, options.initialMessages]);
|
|
@@ -712,7 +771,9 @@ export function useChatPane(options) {
|
|
|
712
771
|
send,
|
|
713
772
|
sendPrompt,
|
|
714
773
|
queuedPrompt: queuedTurn?.text ?? null,
|
|
715
|
-
|
|
774
|
+
// Accepted live messages already have queued bubbles in the transcript. Their private fallback
|
|
775
|
+
// copy must not create a second composer row whose Cancel button cannot revoke daemon input.
|
|
776
|
+
queuedMessages: queuedInputs.filter(input => input.liveMessageId === undefined),
|
|
716
777
|
cancelQueuedMessage,
|
|
717
778
|
cancelQueued,
|
|
718
779
|
typedAnswerNotice,
|
|
@@ -146,13 +146,16 @@ export declare function isTypedAnswerTurn({ blocker, attachmentCount, messages,
|
|
|
146
146
|
* turn with a known `runId`.
|
|
147
147
|
* Attachment support is decided by the host's delivery, never by dropping them here.
|
|
148
148
|
*
|
|
149
|
-
*
|
|
149
|
+
* A native live-input transport can reject a pending human gate when given another user turn.
|
|
150
|
+
* Keep ordinary text in the pane's queue until the card's tool settles; typed question answers
|
|
151
|
+
* have their own delivery path and never pass this guard.
|
|
152
|
+
* @complexity O(e) in the newest message's events.
|
|
150
153
|
*/
|
|
151
154
|
export declare function findMidRunMessageRunId({ blocker, messages }: {
|
|
152
155
|
blocker: ChatPaneSendBlocker | null;
|
|
153
156
|
attachmentCount: number;
|
|
154
157
|
messages: readonly ChatMessage[];
|
|
155
|
-
}): string | null;
|
|
158
|
+
}, _options?: {}): string | null;
|
|
156
159
|
/**
|
|
157
160
|
* MOVE: Display is per-message; delivery is ONE queued turn. A second message behind the same run
|
|
158
161
|
* is appended to that delivery (a blank line apart) rather than replacing it:
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { configuredPickerModel, pickerModelOptions, isConcretePickerModel } from './model-options.js';
|
|
2
2
|
import { isTerminalRunStatus } from '../../../core/messages.js';
|
|
3
3
|
import { acceptedInputEvent } from '../../../core/accepted-input.js';
|
|
4
|
+
import { isAwaitingAnswer } from '../../../core/run-activity.js';
|
|
4
5
|
function preferredOptionId(options) {
|
|
5
6
|
return options?.find((option) => option.id === 'default')?.id ?? options?.[0]?.id;
|
|
6
7
|
}
|
|
@@ -234,12 +235,17 @@ export function isTypedAnswerTurn({ blocker, attachmentCount, messages, toolName
|
|
|
234
235
|
* turn with a known `runId`.
|
|
235
236
|
* Attachment support is decided by the host's delivery, never by dropping them here.
|
|
236
237
|
*
|
|
237
|
-
*
|
|
238
|
+
* A native live-input transport can reject a pending human gate when given another user turn.
|
|
239
|
+
* Keep ordinary text in the pane's queue until the card's tool settles; typed question answers
|
|
240
|
+
* have their own delivery path and never pass this guard.
|
|
241
|
+
* @complexity O(e) in the newest message's events.
|
|
238
242
|
*/
|
|
239
|
-
export function findMidRunMessageRunId({ blocker, messages }) {
|
|
243
|
+
export function findMidRunMessageRunId({ blocker, messages }, _options = {}) {
|
|
240
244
|
if (!isChatPaneQueueableBlocker({ blocker }))
|
|
241
245
|
return null;
|
|
242
246
|
const last = messages.at(-1);
|
|
247
|
+
if (isAwaitingAnswer({ events: last?.events }))
|
|
248
|
+
return null;
|
|
243
249
|
return last?.role === 'assistant' && last.runId ? last.runId : null;
|
|
244
250
|
}
|
|
245
251
|
/**
|
|
@@ -176,11 +176,16 @@ export type DeliverTypedAnswer = ((input: {
|
|
|
176
176
|
};
|
|
177
177
|
/**
|
|
178
178
|
* What happened to a message sent while a run streams (see {@link ChatPaneProps.deliverMidRunMessage}):
|
|
179
|
-
* `'delivered'` — the live agent
|
|
179
|
+
* `{delivery:'delivered', id}` — accepted by the live agent; keep the local copy until an id-matched
|
|
180
|
+
* consumed event. Legacy hosts may still return `'delivered'`; their receipts reconcile optimistic ids.
|
|
181
|
+
* `'unsupported'` — this agent cannot
|
|
180
182
|
* take input mid-run (it reads one prompt per turn); `'not-running'` — the run has no live process
|
|
181
183
|
* any more (it is ending); `'failed'` — the host could not tell (network error, unexpected status).
|
|
182
184
|
*/
|
|
183
|
-
export type MidRunMessageDelivery = 'delivered' | 'unsupported' | 'not-running' | 'failed'
|
|
185
|
+
export type MidRunMessageDelivery = 'delivered' | 'unsupported' | 'not-running' | 'failed' | {
|
|
186
|
+
readonly delivery: 'delivered';
|
|
187
|
+
readonly id: string;
|
|
188
|
+
};
|
|
184
189
|
/** Delivers a complete turn to the live agent; unsupported attachments must reject the whole turn. */
|
|
185
190
|
export type DeliverMidRunMessage = (input: {
|
|
186
191
|
runId: string;
|