@jini-ai/chat 0.5.7 → 0.5.8
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/ag-ui/entry.js +2 -0
- package/dist/core/assistant-client/durable-run-subscription.d.ts +4 -1
- package/dist/core/assistant-client/durable-run-subscription.js +13 -4
- package/dist/core/events.d.ts +3 -0
- package/dist/core/history-tool-metadata.d.ts +23 -0
- package/dist/core/history-tool-metadata.js +13 -0
- package/dist/core/index.d.ts +3 -0
- package/dist/core/index.js +3 -0
- package/dist/core/persistence/title.d.ts +24 -0
- package/dist/core/persistence/title.js +60 -9
- package/dist/core/run-activity.d.ts +15 -2
- package/dist/core/run-activity.js +6 -4
- package/dist/core/run-events/entry.js +2 -0
- package/dist/core/tool-call-detail.d.ts +15 -0
- package/dist/core/tool-call-detail.js +18 -0
- package/dist/core/turn-usage.d.ts +42 -3
- package/dist/core/turn-usage.js +101 -4
- package/dist/core/usage-format.d.ts +10 -0
- package/dist/core/usage-format.js +15 -0
- package/dist/embed-widget/browser/lazy-loader.d.ts +34 -0
- package/dist/embed-widget/browser/lazy-loader.js +116 -0
- package/dist/embed-widget/react/dialog.hooks.d.ts +28 -0
- package/dist/embed-widget/react/dialog.hooks.js +54 -0
- package/dist/embed-widget/react/embed.d.ts +1 -0
- package/dist/embed-widget/react/embed.js +5 -3
- package/dist/embed-widget/react/header.d.ts +3 -1
- package/dist/embed-widget/react/header.hooks.d.ts +13 -0
- package/dist/embed-widget/react/header.hooks.js +32 -0
- package/dist/embed-widget/react/header.js +4 -14
- package/dist/embed-widget/vanilla/index.d.ts +63 -0
- package/dist/embed-widget/vanilla/index.js +319 -0
- package/dist/embed-widget/vanilla/state.d.ts +48 -0
- package/dist/embed-widget/vanilla/state.js +120 -0
- package/dist/embed-widget/vanilla/style.css +29 -0
- package/dist/react/components/A2uiSurfaceCard.d.ts +5 -1
- package/dist/react/components/A2uiSurfaceCard.js +9 -9
- package/dist/react/components/AttachmentPreviewModal.hooks.d.ts +7 -0
- package/dist/react/components/AttachmentPreviewModal.hooks.js +1 -1
- package/dist/react/components/ChatFab.d.ts +4 -1
- package/dist/react/components/ChatFab.js +2 -2
- package/dist/react/components/ConversationList.d.ts +8 -1
- package/dist/react/components/ConversationList.hooks.d.ts +18 -0
- package/dist/react/components/ConversationList.hooks.js +25 -0
- package/dist/react/components/ConversationList.js +12 -5
- package/dist/react/components/ConversationList.rules.d.ts +25 -0
- package/dist/react/components/ConversationList.rules.js +19 -0
- package/dist/react/components/McpUiSurfaceCard.d.ts +1 -1
- package/dist/react/components/McpUiSurfaceCard.js +8 -4
- package/dist/react/components/MessageList.d.ts +1 -1
- package/dist/react/components/MessageList.hooks.d.ts +4 -0
- package/dist/react/components/MessageList.hooks.js +6 -1
- package/dist/react/components/MessageList.js +4 -3
- package/dist/react/components/MessageRow.d.ts +17 -0
- package/dist/react/components/MessageRow.js +54 -35
- package/dist/react/components/MessageRow.usage.d.ts +48 -0
- package/dist/react/components/MessageRow.usage.js +47 -0
- package/dist/react/components/MidRunUserMessage.d.ts +2 -12
- package/dist/react/components/MidRunUserMessage.js +4 -2
- package/dist/react/components/SentAttachment.d.ts +19 -0
- package/dist/react/components/SentAttachment.hooks.d.ts +44 -0
- package/dist/react/components/SentAttachment.hooks.js +56 -0
- package/dist/react/components/SentAttachment.js +14 -0
- package/dist/react/components/ToolCallGroup.d.ts +12 -0
- package/dist/react/components/ToolCallGroup.hooks.d.ts +20 -0
- package/dist/react/components/ToolCallGroup.hooks.js +23 -0
- package/dist/react/components/ToolCallGroup.js +13 -0
- package/dist/react/components/ToolCard.js +25 -17
- package/dist/react/components/mcp-ui-surface-state.d.ts +21 -0
- package/dist/react/components/mcp-ui-surface-state.js +26 -0
- package/dist/react/ext-event-renderer-registry.d.ts +7 -0
- package/dist/react/features/chat-pane/components/ChatPane.d.ts +1 -1
- package/dist/react/features/chat-pane/components/ChatPane.js +11 -6
- package/dist/react/features/chat-pane/hooks/useChatPane.hooks.d.ts +1 -0
- package/dist/react/features/chat-pane/hooks/useChatPane.hooks.js +16 -10
- package/dist/react/features/chat-pane/hooks/useChatPaneRuntimeInventory.hooks.js +4 -0
- package/dist/react/features/chat-pane/hooks/useChatPaneSuggestion.hooks.d.ts +9 -0
- package/dist/react/features/chat-pane/hooks/useChatPaneSuggestion.hooks.js +11 -0
- package/dist/react/features/chat-pane/hooks/useChatPaneSurface.hooks.d.ts +17 -0
- package/dist/react/features/chat-pane/hooks/useChatPaneSurface.hooks.js +15 -0
- package/dist/react/features/chat-pane/rules.d.ts +14 -0
- package/dist/react/features/chat-pane/rules.js +18 -0
- package/dist/react/features/chat-pane/styles.d.ts +1 -1
- package/dist/react/features/chat-pane/styles.js +85 -1
- package/dist/react/features/chat-pane/types.d.ts +16 -1
- package/dist/react/hooks/useAnsweredCards.d.ts +20 -0
- package/dist/react/hooks/useAnsweredCards.js +23 -0
- package/dist/react/hooks/useConversation.d.ts +8 -0
- package/dist/react/hooks/useConversation.js +34 -0
- package/dist/react/hooks/useRunActivity.d.ts +5 -3
- package/dist/react/hooks/useRunActivity.js +4 -3
- package/dist/react/index.d.ts +4 -0
- package/dist/react/index.js +3 -0
- package/dist/react/message-blocks.d.ts +22 -3
- package/dist/react/message-blocks.js +25 -3
- package/dist/react/tool-call-display.d.ts +90 -0
- package/dist/react/tool-call-display.js +97 -0
- package/dist/store/ports.d.ts +25 -2
- package/dist/store/runtime/entry.d.ts +1 -0
- package/dist/store/runtime/entry.js +1 -0
- package/dist/store/runtime/history-tools.d.ts +27 -0
- package/dist/store/runtime/history-tools.js +169 -0
- package/dist/store/sql/store.js +52 -0
- package/dist/testing/index.d.ts +7 -0
- package/dist/testing/index.js +22 -0
- package/package.json +48 -18
package/dist/core/ag-ui/entry.js
CHANGED
|
@@ -15,6 +15,8 @@ function reduceUsagePayload(payload) {
|
|
|
15
15
|
...(typeof usage.input_tokens === "number" ? { inputTokens: usage.input_tokens } : {}),
|
|
16
16
|
...(typeof usage.output_tokens === "number" ? { outputTokens: usage.output_tokens } : {}),
|
|
17
17
|
...(typeof payload.costUsd === "number" ? { costUsd: payload.costUsd } : {}),
|
|
18
|
+
...(typeof payload.cumulativeCostUsd === "number" ? { cumulativeCostUsd: payload.cumulativeCostUsd } : {}),
|
|
19
|
+
...(typeof payload.costSessionId === "string" ? { costSessionId: payload.costSessionId } : {}),
|
|
18
20
|
...(typeof payload.durationMs === "number" ? { durationMs: payload.durationMs } : {}),
|
|
19
21
|
};
|
|
20
22
|
}
|
|
@@ -6,10 +6,13 @@ export interface DurableRunBinding {
|
|
|
6
6
|
readonly conversationId: string;
|
|
7
7
|
}
|
|
8
8
|
export interface DurableSubscriptionPorts {
|
|
9
|
+
/** Absent recover intent preserves legacy recovery; false requests only the saved checkpoint. */
|
|
9
10
|
readonly read: (required: {
|
|
10
11
|
runId: string;
|
|
11
12
|
messageId?: string;
|
|
12
|
-
}, optional: {
|
|
13
|
+
}, optional: {
|
|
14
|
+
recover?: boolean;
|
|
15
|
+
}) => Promise<ChatMessage | null | "gone">;
|
|
13
16
|
readonly stream: (required: {
|
|
14
17
|
runId: string;
|
|
15
18
|
cursor: string;
|
|
@@ -6,6 +6,7 @@ export function followDurableRun({ runId: initialRunId, handlers, signal, bindin
|
|
|
6
6
|
let cursor = "";
|
|
7
7
|
let stopped = false;
|
|
8
8
|
let reading = false;
|
|
9
|
+
let recoveryPending = true;
|
|
9
10
|
let dropped = false;
|
|
10
11
|
let stopStream;
|
|
11
12
|
let stopTimer;
|
|
@@ -26,7 +27,7 @@ export function followDurableRun({ runId: initialRunId, handlers, signal, bindin
|
|
|
26
27
|
function open() {
|
|
27
28
|
dropped = false;
|
|
28
29
|
stopStream?.();
|
|
29
|
-
stopStream = ports.stream({ runId, cursor, frame, dropped: () => { dropped = true; void read().catch(() => undefined); } }, {});
|
|
30
|
+
stopStream = ports.stream({ runId, cursor, frame, dropped: () => { dropped = true; recoveryPending = true; void read().catch(() => undefined); } }, {});
|
|
30
31
|
}
|
|
31
32
|
function frame(kind, data, nextCursor) {
|
|
32
33
|
if (stopped)
|
|
@@ -36,6 +37,7 @@ export function followDurableRun({ runId: initialRunId, handlers, signal, bindin
|
|
|
36
37
|
const outcome = translateRunFrame({ kind, raw: data, notices }, {});
|
|
37
38
|
// An attempt's error/end may be followed by a continuation. Only the row can finalize it.
|
|
38
39
|
if (outcome.terminal || outcome.error) {
|
|
40
|
+
recoveryPending = true;
|
|
39
41
|
void read().catch(() => undefined);
|
|
40
42
|
return;
|
|
41
43
|
}
|
|
@@ -52,8 +54,11 @@ export function followDurableRun({ runId: initialRunId, handlers, signal, bindin
|
|
|
52
54
|
reading = true;
|
|
53
55
|
stopTimer?.();
|
|
54
56
|
stopTimer = undefined;
|
|
57
|
+
const recover = recoveryPending;
|
|
58
|
+
// Consume only when a read starts: faults during its await latch one follow-up recovery.
|
|
59
|
+
recoveryPending = false;
|
|
55
60
|
try {
|
|
56
|
-
const message = await ports.read({ runId, ...(messageId ? { messageId } : {}) }, {});
|
|
61
|
+
const message = await ports.read({ runId, ...(messageId ? { messageId } : {}) }, { recover });
|
|
57
62
|
if (stopped || !message)
|
|
58
63
|
return;
|
|
59
64
|
if (message === "gone") {
|
|
@@ -64,8 +69,12 @@ export function followDurableRun({ runId: initialRunId, handlers, signal, bindin
|
|
|
64
69
|
}
|
|
65
70
|
finally {
|
|
66
71
|
reading = false;
|
|
67
|
-
if (!stopped)
|
|
68
|
-
|
|
72
|
+
if (!stopped) {
|
|
73
|
+
if (recoveryPending)
|
|
74
|
+
void read().catch(() => undefined);
|
|
75
|
+
else
|
|
76
|
+
stopTimer = ports.schedule({ work: () => void read().catch(() => undefined), delayMs: 1_000 }, {});
|
|
77
|
+
}
|
|
69
78
|
}
|
|
70
79
|
}
|
|
71
80
|
function applySavedMessage(message) {
|
package/dist/core/events.d.ts
CHANGED
|
@@ -65,6 +65,9 @@ export type AgentEvent = {
|
|
|
65
65
|
inputTokens?: number;
|
|
66
66
|
outputTokens?: number;
|
|
67
67
|
costUsd?: number;
|
|
68
|
+
/** CLI session snapshot, persisted in events_json; reconciled across turns before display. */
|
|
69
|
+
cumulativeCostUsd?: number;
|
|
70
|
+
costSessionId?: string;
|
|
68
71
|
durationMs?: number;
|
|
69
72
|
stopReason?: string;
|
|
70
73
|
} | {
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** Discovery and read approval for the generic history tools, shared with host projections. */
|
|
2
|
+
export declare const assistantHistoryToolMetadata: {
|
|
3
|
+
readonly assistant_chats_list: {
|
|
4
|
+
readonly search: {
|
|
5
|
+
readonly keywords: "previous earlier interrupted chat conversation history search resume continue what we were doing";
|
|
6
|
+
readonly queries: readonly ["I had to start a new chat. Can you find what we were doing?", "Find my previous conversation."];
|
|
7
|
+
};
|
|
8
|
+
readonly approval: {
|
|
9
|
+
readonly class: "read";
|
|
10
|
+
readonly confirmation: "direct";
|
|
11
|
+
};
|
|
12
|
+
};
|
|
13
|
+
readonly assistant_chat_read: {
|
|
14
|
+
readonly search: {
|
|
15
|
+
readonly keywords: "read previous chat transcript history tail messages resume interrupted continue conversation";
|
|
16
|
+
};
|
|
17
|
+
readonly approval: {
|
|
18
|
+
readonly class: "read";
|
|
19
|
+
readonly confirmation: "direct";
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
//# sourceMappingURL=history-tool-metadata.d.ts.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** Discovery and read approval for the generic history tools, shared with host projections. */
|
|
2
|
+
export const assistantHistoryToolMetadata = {
|
|
3
|
+
assistant_chats_list: {
|
|
4
|
+
search: { keywords: 'previous earlier interrupted chat conversation history search resume continue what we were doing',
|
|
5
|
+
queries: ['I had to start a new chat. Can you find what we were doing?', 'Find my previous conversation.'] },
|
|
6
|
+
approval: { class: 'read', confirmation: 'direct' },
|
|
7
|
+
},
|
|
8
|
+
assistant_chat_read: {
|
|
9
|
+
search: { keywords: 'read previous chat transcript history tail messages resume interrupted continue conversation' },
|
|
10
|
+
approval: { class: 'read', confirmation: 'direct' },
|
|
11
|
+
},
|
|
12
|
+
};
|
|
13
|
+
//# sourceMappingURL=history-tool-metadata.js.map
|
package/dist/core/index.d.ts
CHANGED
|
@@ -33,4 +33,7 @@ export { recoveredRunEvents } from './durable-projection.js';
|
|
|
33
33
|
export { composerHistoryKeyAction, createComposerHistoryState, mergeComposerHistory, normalizeComposerHistory, transitionComposerHistory, } from './composer-history.js';
|
|
34
34
|
export type { ComposerHistoryKeyInput, ComposerHistoryState, ComposerHistoryStoragePort } from './composer-history.js';
|
|
35
35
|
export * from './user-text-redaction.js';
|
|
36
|
+
export * from './history-tool-metadata.js';
|
|
37
|
+
export { turnUsage, conversationUsage, replySegments, isReplyBoundary, MID_RUN_USER_MESSAGE_EVENT_NAME, type ChatUsageSummary } from './turn-usage.js';
|
|
38
|
+
export { formatUsageCost } from './usage-format.js';
|
|
36
39
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/core/index.js
CHANGED
|
@@ -32,4 +32,7 @@ export * from './persistence/index.js';
|
|
|
32
32
|
export { recoveredRunEvents } from './durable-projection.js';
|
|
33
33
|
export { composerHistoryKeyAction, createComposerHistoryState, mergeComposerHistory, normalizeComposerHistory, transitionComposerHistory, } from './composer-history.js';
|
|
34
34
|
export * from './user-text-redaction.js';
|
|
35
|
+
export * from './history-tool-metadata.js';
|
|
36
|
+
export { turnUsage, conversationUsage, replySegments, isReplyBoundary, MID_RUN_USER_MESSAGE_EVENT_NAME } from './turn-usage.js';
|
|
37
|
+
export { formatUsageCost } from './usage-format.js';
|
|
35
38
|
//# sourceMappingURL=index.js.map
|
|
@@ -23,6 +23,18 @@
|
|
|
23
23
|
* 2. `LATIN_NEVER_TRAILING` trims function words off the end. The original's word cap lands
|
|
24
24
|
* mid-clause on question-shaped prompts and strands them ("...Posts Do I").
|
|
25
25
|
*/
|
|
26
|
+
/** Remove presentation markup from title text while preserving link labels and inline code.
|
|
27
|
+
* Shared by prompt derivation, stored-title display and product naming adapters; no DOM or I/O.
|
|
28
|
+
* Fenced programs and reference definitions are not titles. Only paired emphasis is stripped:
|
|
29
|
+
* glob stars and identifier underscores are meaningful text. Older saved titles may already have
|
|
30
|
+
* been cut through a markdown span; cut links keep their partial label, unpaired emphasis stays.
|
|
31
|
+
* @param input.text - A prompt or a previously saved title.
|
|
32
|
+
* @returns Plain text with whitespace collapsed; identifiers keep their internal underscores.
|
|
33
|
+
* @complexity O(n²) worst-case regex time for unmatched spans; O(n) space in title/prompt length.
|
|
34
|
+
*/
|
|
35
|
+
export declare function stripConversationTitleMarkdown({ text }: {
|
|
36
|
+
text: string;
|
|
37
|
+
}, _options?: {}): string;
|
|
26
38
|
/**
|
|
27
39
|
* Returns a short display title for a conversation, or `''` when the prompt yields nothing
|
|
28
40
|
* usable (a bare URL, a lone code fence, pure punctuation). Callers should treat `''` as "leave
|
|
@@ -33,5 +45,17 @@
|
|
|
33
45
|
*/
|
|
34
46
|
export declare function deriveConversationTitle({ prompt }: {
|
|
35
47
|
prompt: string;
|
|
48
|
+
}, { casing }?: {
|
|
49
|
+
casing?: 'title' | 'sentence';
|
|
36
50
|
}): string;
|
|
51
|
+
/** Sentence-case only a known heuristic title using the original prompt's proper names/acronyms.
|
|
52
|
+
* Manual names, model-authored names and unloaded transcripts keep their saved wording, with
|
|
53
|
+
* presentation markup removed and the display ceiling applied; no writes.
|
|
54
|
+
* @complexity O(n) time/space in the first prompt's length, using the existing bounded heuristic.
|
|
55
|
+
*/
|
|
56
|
+
export declare function conversationTitleForDisplay({ title, titleSource, firstPrompt }: {
|
|
57
|
+
title: string | null;
|
|
58
|
+
titleSource: 'fallback' | 'generated' | 'manual';
|
|
59
|
+
firstPrompt?: string | undefined;
|
|
60
|
+
}, _options?: {}): string | null;
|
|
37
61
|
//# sourceMappingURL=title.d.ts.map
|
|
@@ -76,23 +76,60 @@ const LATIN_NEVER_TRAILING = new Set([
|
|
|
76
76
|
'from', 'into', 'about', 'so', 'just', 'also', 'too', 'up', 'out',
|
|
77
77
|
]);
|
|
78
78
|
/**
|
|
79
|
-
* Strips the parts of a prompt that are never a good title: fenced
|
|
79
|
+
* Strips the parts of a prompt that are never a good title: fenced code, URLs, and
|
|
80
80
|
* `@handle`/`#tag` tokens. Each of these is high-entropy and low-meaning in a six-word summary.
|
|
81
|
+
* Inline code keeps its label with delimiters removed, so a filename can still name the task.
|
|
81
82
|
*/
|
|
82
83
|
function cleanPrompt(prompt) {
|
|
83
|
-
return prompt
|
|
84
|
-
.replace(/```[\s\S]*?```/g, ' ')
|
|
85
|
-
.replace(/`[^`]*`/g, ' ')
|
|
84
|
+
return stripConversationTitleMarkdown({ text: prompt })
|
|
86
85
|
.replace(/https?:\/\/\S+/g, ' ')
|
|
87
86
|
.replace(/[@#][\w.-]+/g, ' ')
|
|
88
87
|
.replace(/[“”"']/g, '')
|
|
89
88
|
.replace(/\s+/g, ' ')
|
|
90
89
|
.trim();
|
|
91
90
|
}
|
|
91
|
+
/** Remove presentation markup from title text while preserving link labels and inline code.
|
|
92
|
+
* Shared by prompt derivation, stored-title display and product naming adapters; no DOM or I/O.
|
|
93
|
+
* Fenced programs and reference definitions are not titles. Only paired emphasis is stripped:
|
|
94
|
+
* glob stars and identifier underscores are meaningful text. Older saved titles may already have
|
|
95
|
+
* been cut through a markdown span; cut links keep their partial label, unpaired emphasis stays.
|
|
96
|
+
* @param input.text - A prompt or a previously saved title.
|
|
97
|
+
* @returns Plain text with whitespace collapsed; identifiers keep their internal underscores.
|
|
98
|
+
* @complexity O(n²) worst-case regex time for unmatched spans; O(n) space in title/prompt length.
|
|
99
|
+
*/
|
|
100
|
+
export function stripConversationTitleMarkdown({ text }, _options = {}) {
|
|
101
|
+
return text
|
|
102
|
+
.replace(/```[\s\S]*?(?:```|$)/g, ' ')
|
|
103
|
+
.replace(/^\s{0,3}\[[^\]\n]+\]:[^\n]*(?:\n|$)/gm, ' ')
|
|
104
|
+
.replace(/!?\[([^\[\]\n]+)\]\((?:[^()[\]\n]|\([^()[\]\n]*\))*(?:\)|$)/g, '$1')
|
|
105
|
+
.replace(/\[([^\[\]\n]+)\]\[[^\[\]\n]*(?:\]|$)/g, '$1')
|
|
106
|
+
.replace(/^\s{0,3}#{1,6}\s+|[ \t]+#+[ \t]*$/gm, '')
|
|
107
|
+
.replace(/(?<![\w\\*])(\*\*|\*)(?=[^\s*])([^*\n]*?[^\s*\\])\1(?![\w*])/g, '$2')
|
|
108
|
+
.replace(/(?<![\w\\_])(__|_)(?=[^\s_])([^\n]*?[^\s\\])\1(?![\w_])/g, '$2')
|
|
109
|
+
.replace(/`|~~/g, '')
|
|
110
|
+
.replace(/!?\[([^\]\n]*)(?=\n|$)/g, '$1')
|
|
111
|
+
// Unescape after markup removal so an escaped literal marker cannot become emphasis.
|
|
112
|
+
.replace(/\\([\\`*_[\]~])/g, '$1')
|
|
113
|
+
.replace(/\s+/g, ' ')
|
|
114
|
+
.trim();
|
|
115
|
+
}
|
|
116
|
+
/** Keep complete words at the display ceiling. A single oversized token still gets a bounded
|
|
117
|
+
* prefix: pasted blobs must never become megabyte-sized stored/listed titles.
|
|
118
|
+
* @complexity O(n) time/space, bounded by the title ceiling after the initial length check.
|
|
119
|
+
*/
|
|
120
|
+
function truncateTitle(title) {
|
|
121
|
+
if (title.length <= MAX_TITLE_CHARS)
|
|
122
|
+
return title;
|
|
123
|
+
const prefix = title.slice(0, MAX_TITLE_CHARS);
|
|
124
|
+
if (/\s/.test(title.charAt(MAX_TITLE_CHARS)))
|
|
125
|
+
return prefix.trimEnd();
|
|
126
|
+
const lastSpace = prefix.lastIndexOf(' ');
|
|
127
|
+
return (lastSpace > 0 ? prefix.slice(0, lastSpace) : prefix).trimEnd();
|
|
128
|
+
}
|
|
92
129
|
function toTitleCase(word) {
|
|
93
130
|
return word ? word.charAt(0).toUpperCase() + word.slice(1).toLowerCase() : word;
|
|
94
131
|
}
|
|
95
|
-
function trimLatinTitle(input) {
|
|
132
|
+
function trimLatinTitle(input, { casing }) {
|
|
96
133
|
const words = input
|
|
97
134
|
.replace(LEADING_LATIN_FILLER, '')
|
|
98
135
|
.replace(/[^\p{L}\p{N}\s-]/gu, ' ')
|
|
@@ -104,7 +141,8 @@ function trimLatinTitle(input) {
|
|
|
104
141
|
while (words.length > 0 && LATIN_NEVER_TRAILING.has(words[words.length - 1].toLowerCase())) {
|
|
105
142
|
words.pop();
|
|
106
143
|
}
|
|
107
|
-
|
|
144
|
+
const title = casing === 'title' ? words.map(toTitleCase).join(' ') : words.join(' ');
|
|
145
|
+
return title ? title.charAt(0).toUpperCase() + title.slice(1) : title;
|
|
108
146
|
}
|
|
109
147
|
function trimCjkTitle(input) {
|
|
110
148
|
let title = input.trim();
|
|
@@ -122,16 +160,29 @@ function trimCjkTitle(input) {
|
|
|
122
160
|
*
|
|
123
161
|
* @complexity O(n) in prompt length; no allocation beyond the intermediate strings.
|
|
124
162
|
*/
|
|
125
|
-
export function deriveConversationTitle({ prompt }) {
|
|
163
|
+
export function deriveConversationTitle({ prompt }, { casing = 'title' } = {}) {
|
|
126
164
|
const cleaned = cleanPrompt(prompt);
|
|
127
165
|
if (!cleaned)
|
|
128
166
|
return '';
|
|
129
167
|
const firstClause = cleaned.split(/[\n\r。!?!?]/)[0]?.trim() ?? cleaned;
|
|
130
168
|
if (!firstClause)
|
|
131
169
|
return '';
|
|
132
|
-
const title = CJK_PATTERN.test(firstClause) ? trimCjkTitle(firstClause) : trimLatinTitle(firstClause);
|
|
170
|
+
const title = CJK_PATTERN.test(firstClause) ? trimCjkTitle(firstClause) : trimLatinTitle(firstClause, { casing });
|
|
133
171
|
// Truncated without an ellipsis: this is a working title, not prose, and a `…` would end up stored
|
|
134
172
|
// and shown as if the author wrote it.
|
|
135
|
-
return title
|
|
173
|
+
return truncateTitle(title);
|
|
174
|
+
}
|
|
175
|
+
/** Sentence-case only a known heuristic title using the original prompt's proper names/acronyms.
|
|
176
|
+
* Manual names, model-authored names and unloaded transcripts keep their saved wording, with
|
|
177
|
+
* presentation markup removed and the display ceiling applied; no writes.
|
|
178
|
+
* @complexity O(n) time/space in the first prompt's length, using the existing bounded heuristic.
|
|
179
|
+
*/
|
|
180
|
+
export function conversationTitleForDisplay({ title, titleSource, firstPrompt }, _options = {}) {
|
|
181
|
+
if (!title)
|
|
182
|
+
return title;
|
|
183
|
+
if (titleSource === 'generated' && firstPrompt && title === deriveConversationTitle({ prompt: firstPrompt })) {
|
|
184
|
+
return deriveConversationTitle({ prompt: firstPrompt }, { casing: 'sentence' });
|
|
185
|
+
}
|
|
186
|
+
return truncateTitle(stripConversationTitleMarkdown({ text: title })) || null;
|
|
136
187
|
}
|
|
137
188
|
//# sourceMappingURL=title.js.map
|
|
@@ -34,6 +34,19 @@ export declare function humanizeToolName({ name }: {
|
|
|
34
34
|
}, { input }?: {
|
|
35
35
|
input?: (unknown) | undefined;
|
|
36
36
|
}): string;
|
|
37
|
+
/** Whether the person already answered a card, before its tool call returned. */
|
|
38
|
+
export type IsAnsweredCard = (card: {
|
|
39
|
+
name: string;
|
|
40
|
+
data: unknown;
|
|
41
|
+
}) => boolean;
|
|
42
|
+
export interface RunActivityOptions {
|
|
43
|
+
/**
|
|
44
|
+
* A card the person answered no longer waits on them, even though its tool call stays open while
|
|
45
|
+
* the confirmed work runs (a publish can take minutes). Without this the line kept saying
|
|
46
|
+
* "Waiting for your answer above" under a card that already read "Done." (owner, 2026-10-11).
|
|
47
|
+
*/
|
|
48
|
+
isAnsweredCard?: IsAnsweredCard | undefined;
|
|
49
|
+
}
|
|
37
50
|
/**
|
|
38
51
|
* The latest signal in a running turn's events.
|
|
39
52
|
*
|
|
@@ -46,7 +59,7 @@ export declare function humanizeToolName({ name }: {
|
|
|
46
59
|
*/
|
|
47
60
|
export declare function deriveRunActivity({ events }: {
|
|
48
61
|
events: readonly AgentEvent[] | undefined;
|
|
49
|
-
}): RunActivityState;
|
|
62
|
+
}, { isAnsweredCard }?: RunActivityOptions): RunActivityState;
|
|
50
63
|
/**
|
|
51
64
|
* Whether the run is waiting on the person: a card (form, confirm, sign-in) arrived while its tool
|
|
52
65
|
* call is still open. Hosts use it to hide "still working" notices, which read as a hung run while
|
|
@@ -56,7 +69,7 @@ export declare function deriveRunActivity({ events }: {
|
|
|
56
69
|
*/
|
|
57
70
|
export declare function isAwaitingAnswer({ events }: {
|
|
58
71
|
events: readonly AgentEvent[] | undefined;
|
|
59
|
-
}): boolean;
|
|
72
|
+
}, options?: RunActivityOptions): boolean;
|
|
60
73
|
/** `4` → `"4 s"`, `80` → `"1 m 20 s"`, `120` → `"2 m 0 s"`. */
|
|
61
74
|
export declare function formatActivityClock({ totalSeconds }: {
|
|
62
75
|
totalSeconds: number;
|
|
@@ -67,6 +67,8 @@ function isVisible(event) {
|
|
|
67
67
|
function isAnswerCard(scan, name, data) {
|
|
68
68
|
if (!CARD_EXT_NAMES.has(name))
|
|
69
69
|
return false;
|
|
70
|
+
if (scan.isAnsweredCard?.({ name, data }) === true)
|
|
71
|
+
return false;
|
|
70
72
|
if (name !== A2UI_EXT_NAME)
|
|
71
73
|
return true;
|
|
72
74
|
const displayOnlyId = displayOnlySurfaceIdOf({ message: data });
|
|
@@ -109,8 +111,8 @@ function scanEvent(scan, event, index) {
|
|
|
109
111
|
*
|
|
110
112
|
* @complexity O(n) in events, O(open tool calls) extra space.
|
|
111
113
|
*/
|
|
112
|
-
export function deriveRunActivity({ events }) {
|
|
113
|
-
const scan = { open: [], lastVisible: null, lastCardIndex: -1, retry: null, progress: null, visibleCount: 0, displayOnlySurfaces: new Set() };
|
|
114
|
+
export function deriveRunActivity({ events }, { isAnsweredCard } = {}) {
|
|
115
|
+
const scan = { open: [], lastVisible: null, lastCardIndex: -1, retry: null, progress: null, visibleCount: 0, displayOnlySurfaces: new Set(), isAnsweredCard };
|
|
114
116
|
(events ?? []).forEach((event, index) => scanEvent(scan, event, index));
|
|
115
117
|
const visibleCount = scan.visibleCount;
|
|
116
118
|
const lastVisibleIndex = scan.lastVisible?.index ?? -1;
|
|
@@ -161,8 +163,8 @@ export function deriveRunActivity({ events }) {
|
|
|
161
163
|
*
|
|
162
164
|
* @complexity O(n) in events.
|
|
163
165
|
*/
|
|
164
|
-
export function isAwaitingAnswer({ events }) {
|
|
165
|
-
return deriveRunActivity({ events }).activity.kind === 'awaiting-answer';
|
|
166
|
+
export function isAwaitingAnswer({ events }, options = {}) {
|
|
167
|
+
return deriveRunActivity({ events }, options).activity.kind === 'awaiting-answer';
|
|
166
168
|
}
|
|
167
169
|
/** `4` → `"4 s"`, `80` → `"1 m 20 s"`, `120` → `"2 m 0 s"`. */
|
|
168
170
|
export function formatActivityClock({ totalSeconds }) {
|
|
@@ -32,6 +32,8 @@ export function parseUsageEvent({ payload }, _options = {}) {
|
|
|
32
32
|
...(typeof usage.input_tokens === "number" ? { inputTokens: usage.input_tokens } : {}),
|
|
33
33
|
...(typeof usage.output_tokens === "number" ? { outputTokens: usage.output_tokens } : {}),
|
|
34
34
|
...(typeof payload.costUsd === "number" ? { costUsd: payload.costUsd } : {}),
|
|
35
|
+
...(typeof payload.cumulativeCostUsd === "number" ? { cumulativeCostUsd: payload.cumulativeCostUsd } : {}),
|
|
36
|
+
...(typeof payload.costSessionId === "string" ? { costSessionId: payload.costSessionId } : {}),
|
|
35
37
|
...(typeof payload.durationMs === "number" ? { durationMs: payload.durationMs } : {}),
|
|
36
38
|
};
|
|
37
39
|
}
|
|
@@ -33,4 +33,19 @@ export declare function describeToolCallDetail({ input, result }: {
|
|
|
33
33
|
readonly isError?: boolean;
|
|
34
34
|
} | undefined;
|
|
35
35
|
}, { runStreaming, runSucceeded, maxChars, redact }?: ToolCallDetailOptions): ToolCallDetail;
|
|
36
|
+
/** Per-block cap for the text a collapsed row keeps mounted — see {@link describeCollapsedToolCall}. */
|
|
37
|
+
export declare const COLLAPSED_DETAIL_MAX_CHARS = 400;
|
|
38
|
+
/**
|
|
39
|
+
* The short, single-node text a collapsed row keeps in the DOM in place of its full detail. A long
|
|
40
|
+
* chat mounts hundreds of collapsed rows (713 in a 38-message chat, 3.45 MB of hidden text at the
|
|
41
|
+
* full 10 000-char cap), so the full panel is mounted only while open; this keeps the gist —
|
|
42
|
+
* input, then output or the note explaining its absence — readable through the row's
|
|
43
|
+
* `textContent` for a `page.*` DOM reader that never clicks.
|
|
44
|
+
*
|
|
45
|
+
* @param required - Same as {@link describeToolCallDetail}.
|
|
46
|
+
* @param options - Same as {@link describeToolCallDetail}; `maxChars` defaults to {@link COLLAPSED_DETAIL_MAX_CHARS}.
|
|
47
|
+
* @returns Input (when it has a JSON form) and output or note, joined by a newline — never empty, since a missing output always carries a note.
|
|
48
|
+
* @complexity O(n) in the serialized input plus result length (redaction runs before the cap).
|
|
49
|
+
*/
|
|
50
|
+
export declare function describeCollapsedToolCall(required: Parameters<typeof describeToolCallDetail>[0], options?: ToolCallDetailOptions): string;
|
|
36
51
|
//# sourceMappingURL=tool-call-detail.d.ts.map
|
|
@@ -25,6 +25,24 @@ export function describeToolCallDetail({ input, result }, { runStreaming = false
|
|
|
25
25
|
return { inputText, outputText: null, noteKey: result.isError ? 'Failed without an error message.' : 'Finished with no output.' };
|
|
26
26
|
return { inputText, outputText: show(formatToolOutputForDisplay({ text: result.content })), noteKey: null };
|
|
27
27
|
}
|
|
28
|
+
/** Per-block cap for the text a collapsed row keeps mounted — see {@link describeCollapsedToolCall}. */
|
|
29
|
+
export const COLLAPSED_DETAIL_MAX_CHARS = 400;
|
|
30
|
+
/**
|
|
31
|
+
* The short, single-node text a collapsed row keeps in the DOM in place of its full detail. A long
|
|
32
|
+
* chat mounts hundreds of collapsed rows (713 in a 38-message chat, 3.45 MB of hidden text at the
|
|
33
|
+
* full 10 000-char cap), so the full panel is mounted only while open; this keeps the gist —
|
|
34
|
+
* input, then output or the note explaining its absence — readable through the row's
|
|
35
|
+
* `textContent` for a `page.*` DOM reader that never clicks.
|
|
36
|
+
*
|
|
37
|
+
* @param required - Same as {@link describeToolCallDetail}.
|
|
38
|
+
* @param options - Same as {@link describeToolCallDetail}; `maxChars` defaults to {@link COLLAPSED_DETAIL_MAX_CHARS}.
|
|
39
|
+
* @returns Input (when it has a JSON form) and output or note, joined by a newline — never empty, since a missing output always carries a note.
|
|
40
|
+
* @complexity O(n) in the serialized input plus result length (redaction runs before the cap).
|
|
41
|
+
*/
|
|
42
|
+
export function describeCollapsedToolCall(required, options = {}) {
|
|
43
|
+
const detail = describeToolCallDetail(required, { maxChars: COLLAPSED_DETAIL_MAX_CHARS, ...options });
|
|
44
|
+
return [detail.inputText, detail.outputText ?? detail.noteKey].filter((part) => part !== null).join('\n');
|
|
45
|
+
}
|
|
28
46
|
function missingResultNote({ runStreaming, runSucceeded }) {
|
|
29
47
|
if (runStreaming)
|
|
30
48
|
return 'Waiting for the result or answer…';
|
|
@@ -1,11 +1,50 @@
|
|
|
1
1
|
import type { AgentEvent } from './events.js';
|
|
2
|
+
import type { ChatMessage } from './messages.js';
|
|
2
3
|
type UsageEvent = Extract<AgentEvent, {
|
|
3
4
|
kind: 'usage';
|
|
4
5
|
}>;
|
|
5
|
-
/**
|
|
6
|
-
*
|
|
6
|
+
/** A turn's reported usage plus the conversation's prefix total. `segments` is present only when the
|
|
7
|
+
* message holds more than one reply (a mid-run message started a new one): one entry per reply, in
|
|
8
|
+
* order, each with its own prefix total, so every reply can show its own "Done" line. */
|
|
9
|
+
export type ChatUsageSummary = UsageEvent & {
|
|
10
|
+
totalCostUsd?: number;
|
|
11
|
+
segments?: readonly (ChatUsageSummary | undefined)[];
|
|
12
|
+
};
|
|
13
|
+
type SessionCost = {
|
|
14
|
+
sessionId: string;
|
|
15
|
+
chargedUsd: number;
|
|
16
|
+
};
|
|
17
|
+
/** The ext event a message sent mid-run arrives as (the daemon's `user_message` run event). The agent
|
|
18
|
+
* answers it as a new reply inside the same run, so it ends the reply before it. */
|
|
19
|
+
export declare const MID_RUN_USER_MESSAGE_EVENT_NAME = "user_message";
|
|
20
|
+
/** Whether this event is a mid-run user message, i.e. the boundary between two replies of one run. */
|
|
21
|
+
export declare function isReplyBoundary({ event }: {
|
|
22
|
+
event: AgentEvent;
|
|
23
|
+
}, _optional?: {}): boolean;
|
|
24
|
+
/** Split one assistant message's events into replies at each mid-run user message. A usage event
|
|
25
|
+
* that lands after a boundary but before the next reply produced anything is the interrupted
|
|
26
|
+
* reply's own result (the daemon notes it can arrive after the interrupt ack), so it stays with
|
|
27
|
+
* the reply before. Event order is otherwise preserved. O(n) for n events.
|
|
28
|
+
* @example replySegments({ events }).length // 1 + number of mid-run user messages */
|
|
29
|
+
export declare function replySegments({ events }: {
|
|
30
|
+
events: readonly AgentEvent[] | undefined;
|
|
31
|
+
}, _optional?: {}): AgentEvent[][];
|
|
32
|
+
/** Sum reported segment usage for a turn, including aborted segments and recovered attempts.
|
|
33
|
+
* Optional projection state is updated to reconcile session snapshots across turns.
|
|
34
|
+
* Missing fields stay absent; money is never inferred. O(n) time for n events.
|
|
35
|
+
* @example turnUsage({ events: message.events }) */
|
|
7
36
|
export declare function turnUsage({ events }: {
|
|
8
37
|
events: readonly AgentEvent[] | undefined;
|
|
9
|
-
},
|
|
38
|
+
}, { sessions, agentId }?: {
|
|
39
|
+
sessions?: Map<string, SessionCost>;
|
|
40
|
+
agentId?: string;
|
|
41
|
+
}): UsageEvent | undefined;
|
|
42
|
+
/** Derive per-turn charges and prefix totals from persisted events_json, for live and reloaded
|
|
43
|
+
* chats alike. A message with mid-run replies also gets per-reply `segments`. O(e + m) for e events
|
|
44
|
+
* and m messages; no per-row transcript scans or I/O.
|
|
45
|
+
* @example conversationUsage({ messages: savedMessages }).get('assistant-2') */
|
|
46
|
+
export declare function conversationUsage({ messages }: {
|
|
47
|
+
messages: readonly ChatMessage[];
|
|
48
|
+
}, _optional?: {}): Map<string, ChatUsageSummary>;
|
|
10
49
|
export {};
|
|
11
50
|
//# sourceMappingURL=turn-usage.d.ts.map
|
package/dist/core/turn-usage.js
CHANGED
|
@@ -1,17 +1,114 @@
|
|
|
1
|
-
/**
|
|
2
|
-
*
|
|
3
|
-
export
|
|
1
|
+
/** The ext event a message sent mid-run arrives as (the daemon's `user_message` run event). The agent
|
|
2
|
+
* answers it as a new reply inside the same run, so it ends the reply before it. */
|
|
3
|
+
export const MID_RUN_USER_MESSAGE_EVENT_NAME = 'user_message';
|
|
4
|
+
/** Whether this event is a mid-run user message, i.e. the boundary between two replies of one run. */
|
|
5
|
+
export function isReplyBoundary({ event }, _optional = {}) {
|
|
6
|
+
return event.kind === 'ext' && event.name === MID_RUN_USER_MESSAGE_EVENT_NAME;
|
|
7
|
+
}
|
|
8
|
+
/** Split one assistant message's events into replies at each mid-run user message. A usage event
|
|
9
|
+
* that lands after a boundary but before the next reply produced anything is the interrupted
|
|
10
|
+
* reply's own result (the daemon notes it can arrive after the interrupt ack), so it stays with
|
|
11
|
+
* the reply before. Event order is otherwise preserved. O(n) for n events.
|
|
12
|
+
* @example replySegments({ events }).length // 1 + number of mid-run user messages */
|
|
13
|
+
export function replySegments({ events }, _optional = {}) {
|
|
14
|
+
const segments = [[]];
|
|
15
|
+
let started = false;
|
|
16
|
+
for (const event of events ?? []) {
|
|
17
|
+
if (isReplyBoundary({ event })) {
|
|
18
|
+
segments.push([]);
|
|
19
|
+
started = false;
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
const target = event.kind === 'usage' && !started && segments.length > 1 ? segments.length - 2 : segments.length - 1;
|
|
23
|
+
if (event.kind !== 'usage' && event.kind !== 'status' && event.kind !== 'raw')
|
|
24
|
+
started = true;
|
|
25
|
+
segments[target].push(event);
|
|
26
|
+
}
|
|
27
|
+
return segments;
|
|
28
|
+
}
|
|
29
|
+
/** Resolve one charge against this agent/session's last snapshot. State is local to an
|
|
30
|
+
* ordered transcript projection; revisiting a session keeps its baseline. A lower snapshot
|
|
31
|
+
* starts a new counter epoch (some CLIs reset their counter on resume). O(1) map access. */
|
|
32
|
+
function usageCost({ event, agentId, sessions }, _optional = {}) {
|
|
33
|
+
const sessionId = event.costSessionId;
|
|
34
|
+
if (!sessionId)
|
|
35
|
+
return validCost(event.costUsd);
|
|
36
|
+
const key = JSON.stringify([agentId, sessionId]);
|
|
37
|
+
const chargedUsd = sessions.get(key)?.chargedUsd ?? 0;
|
|
38
|
+
const cumulative = validCost(event.cumulativeCostUsd);
|
|
39
|
+
const cost = cumulative === undefined ? validCost(event.costUsd)
|
|
40
|
+
: cumulative < chargedUsd ? cumulative : cumulative - chargedUsd;
|
|
41
|
+
// Interim interrupted-segment charges are already paid; later snapshots subtract them.
|
|
42
|
+
// After a counter reset the new snapshot, rather than the old epoch's total, is the baseline.
|
|
43
|
+
sessions.set(key, { sessionId, chargedUsd: cumulative ?? chargedUsd + (cost ?? 0) });
|
|
44
|
+
return cost;
|
|
45
|
+
}
|
|
46
|
+
/** Missing or malformed costs stay unknown; never infer charges from tokens. */
|
|
47
|
+
function validCost(value) {
|
|
48
|
+
return typeof value === 'number' && Number.isFinite(value) && value >= 0 ? value : undefined;
|
|
49
|
+
}
|
|
50
|
+
/** Sum reported segment usage for a turn, including aborted segments and recovered attempts.
|
|
51
|
+
* Optional projection state is updated to reconcile session snapshots across turns.
|
|
52
|
+
* Missing fields stay absent; money is never inferred. O(n) time for n events.
|
|
53
|
+
* @example turnUsage({ events: message.events }) */
|
|
54
|
+
export function turnUsage({ events }, { sessions = new Map(), agentId = '' } = {}) {
|
|
4
55
|
let total;
|
|
5
56
|
for (const event of events ?? []) {
|
|
6
57
|
if (event.kind !== 'usage')
|
|
7
58
|
continue;
|
|
8
59
|
total ??= { kind: 'usage' };
|
|
9
|
-
for (const field of ['inputTokens', 'outputTokens', '
|
|
60
|
+
for (const field of ['inputTokens', 'outputTokens', 'durationMs']) {
|
|
10
61
|
const value = event[field];
|
|
11
62
|
if (typeof value === 'number' && Number.isFinite(value) && value >= 0)
|
|
12
63
|
total[field] = (total[field] ?? 0) + value;
|
|
13
64
|
}
|
|
65
|
+
const cost = usageCost({ event, agentId, sessions });
|
|
66
|
+
if (cost !== undefined)
|
|
67
|
+
total.costUsd = (total.costUsd ?? 0) + cost;
|
|
14
68
|
}
|
|
15
69
|
return total;
|
|
16
70
|
}
|
|
71
|
+
/** Add one reply's usage into a message total; fields absent from both stay absent. O(1). */
|
|
72
|
+
function addUsage({ total, usage }, _optional = {}) {
|
|
73
|
+
if (!usage)
|
|
74
|
+
return total;
|
|
75
|
+
const next = { ...(total ?? { kind: 'usage' }) };
|
|
76
|
+
for (const field of ['inputTokens', 'outputTokens', 'durationMs', 'costUsd']) {
|
|
77
|
+
const value = usage[field];
|
|
78
|
+
if (value !== undefined)
|
|
79
|
+
next[field] = (next[field] ?? 0) + value;
|
|
80
|
+
}
|
|
81
|
+
return next;
|
|
82
|
+
}
|
|
83
|
+
/** Derive per-turn charges and prefix totals from persisted events_json, for live and reloaded
|
|
84
|
+
* chats alike. A message with mid-run replies also gets per-reply `segments`. O(e + m) for e events
|
|
85
|
+
* and m messages; no per-row transcript scans or I/O.
|
|
86
|
+
* @example conversationUsage({ messages: savedMessages }).get('assistant-2') */
|
|
87
|
+
export function conversationUsage({ messages }, _optional = {}) {
|
|
88
|
+
const sessions = new Map();
|
|
89
|
+
const summaries = new Map();
|
|
90
|
+
let totalCostUsd;
|
|
91
|
+
for (const message of messages) {
|
|
92
|
+
if (message.role !== 'assistant')
|
|
93
|
+
continue;
|
|
94
|
+
// Replies run in event order through one session map, so their sum equals the whole-message charge.
|
|
95
|
+
const segments = [];
|
|
96
|
+
let usage;
|
|
97
|
+
for (const events of replySegments({ events: message.events })) {
|
|
98
|
+
const segment = turnUsage({ events }, { sessions, agentId: message.agentId ?? '' });
|
|
99
|
+
usage = addUsage({ total: usage, usage: segment });
|
|
100
|
+
if (segment?.costUsd !== undefined)
|
|
101
|
+
totalCostUsd = (totalCostUsd ?? 0) + segment.costUsd;
|
|
102
|
+
segments.push(segment && { ...segment, ...(totalCostUsd === undefined ? {} : { totalCostUsd }) });
|
|
103
|
+
}
|
|
104
|
+
if (!usage)
|
|
105
|
+
continue;
|
|
106
|
+
summaries.set(message.id, {
|
|
107
|
+
...usage,
|
|
108
|
+
...(totalCostUsd === undefined ? {} : { totalCostUsd }),
|
|
109
|
+
...(segments.length > 1 ? { segments } : {}),
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
return summaries;
|
|
113
|
+
}
|
|
17
114
|
//# sourceMappingURL=turn-usage.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Format a known turn charge, and its conversation prefix total only when one is passed; round only
|
|
2
|
+
* for display. The total is the chat's running total as of that turn, so an earlier turn's line never
|
|
3
|
+
* changes when later turns run (owner, 2026-10-10: "Done · 16s · $0.39 · total $3.95").
|
|
4
|
+
* @example formatUsageCost({ costUsd: 0.445, totalCostUsd: 2.5 }) // '$0.45 · total $2.50'
|
|
5
|
+
* @example formatUsageCost({ costUsd: 0.445 }) // '$0.45' */
|
|
6
|
+
export declare function formatUsageCost({ costUsd, totalCostUsd }: {
|
|
7
|
+
costUsd: number;
|
|
8
|
+
totalCostUsd?: number | undefined;
|
|
9
|
+
}, _optional?: {}): string;
|
|
10
|
+
//# sourceMappingURL=usage-format.d.ts.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** Round reported USD upward to cents after clearing arithmetic noise at 1e-8 USD precision.
|
|
2
|
+
* Keep the minimum cent for any positive reported charge, even below the noise precision. */
|
|
3
|
+
function dollars(usd) {
|
|
4
|
+
const cents = usd > 0 ? Math.max(1, Math.ceil(Math.round(usd * 1e8) / 1e6)) : 0;
|
|
5
|
+
return `$${(cents / 100).toFixed(2)}`;
|
|
6
|
+
}
|
|
7
|
+
/** Format a known turn charge, and its conversation prefix total only when one is passed; round only
|
|
8
|
+
* for display. The total is the chat's running total as of that turn, so an earlier turn's line never
|
|
9
|
+
* changes when later turns run (owner, 2026-10-10: "Done · 16s · $0.39 · total $3.95").
|
|
10
|
+
* @example formatUsageCost({ costUsd: 0.445, totalCostUsd: 2.5 }) // '$0.45 · total $2.50'
|
|
11
|
+
* @example formatUsageCost({ costUsd: 0.445 }) // '$0.45' */
|
|
12
|
+
export function formatUsageCost({ costUsd, totalCostUsd }, _optional = {}) {
|
|
13
|
+
return totalCostUsd === undefined ? dollars(costUsd) : `${dollars(costUsd)} · total ${dollars(totalCostUsd)}`;
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=usage-format.js.map
|