@theoremjs/react 0.0.0-stage → 0.3.1
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/LICENSE +21 -0
- package/README.md +772 -2
- package/client/attachment-hover-preview.d.ts +11 -0
- package/client/attachment-hover-preview.js +36 -0
- package/client/audio-level.d.ts +5 -0
- package/client/audio-level.js +24 -0
- package/client/composer-attachments.d.ts +30 -0
- package/client/composer-attachments.js +33 -0
- package/client/composer-drawer.d.ts +19 -0
- package/client/composer-drawer.js +15 -0
- package/client/composer-hints.d.ts +25 -0
- package/client/composer-hints.js +20 -0
- package/client/composer-primary.d.ts +16 -0
- package/client/composer-primary.js +25 -0
- package/client/decision-images.d.ts +76 -0
- package/client/decision-images.js +139 -0
- package/client/decision-transport.d.ts +70 -0
- package/client/decision-transport.js +175 -0
- package/client/decode-composer-draft.d.ts +11 -0
- package/client/decode-composer-draft.js +12 -0
- package/client/embedding-input.d.ts +84 -0
- package/client/embedding-input.js +194 -0
- package/client/embedding-space.d.ts +82 -0
- package/client/embedding-space.js +389 -0
- package/client/embedding-transport.d.ts +40 -0
- package/client/embedding-transport.js +71 -0
- package/client/encode-composer-draft.d.ts +9 -0
- package/client/encode-composer-draft.js +34 -0
- package/client/encode-files.d.ts +18 -0
- package/client/encode-files.js +54 -0
- package/client/failure.d.ts +29 -0
- package/client/failure.js +34 -0
- package/client/generation-selection.d.ts +15 -0
- package/client/generation-selection.js +24 -0
- package/client/host-call.d.ts +51 -0
- package/client/host-call.js +47 -0
- package/client/host-transport.d.ts +70 -0
- package/client/host-transport.js +75 -0
- package/client/image-output.d.ts +6 -0
- package/client/image-output.js +13 -0
- package/client/index.d.ts +28 -0
- package/client/index.js +22 -0
- package/client/ink-waveform.d.ts +17 -0
- package/client/ink-waveform.js +68 -0
- package/client/live/apply-live-turn-tool-event.d.ts +14 -0
- package/client/live/apply-live-turn-tool-event.js +39 -0
- package/client/live/live-captions.d.ts +53 -0
- package/client/live/live-captions.js +174 -0
- package/client/live/live-mic-forward.d.ts +30 -0
- package/client/live/live-mic-forward.js +46 -0
- package/client/live/live-page-tool.d.ts +34 -0
- package/client/live/live-page-tool.js +11 -0
- package/client/live/live-state.d.ts +13 -0
- package/client/live/live-state.js +22 -0
- package/client/live/live-tool.d.ts +11 -0
- package/client/live/live-tool.js +1 -0
- package/client/live/live-video.d.ts +10 -0
- package/client/live/live-video.js +71 -0
- package/client/live/run-live-tool-call.d.ts +22 -0
- package/client/live/run-live-tool-call.js +70 -0
- package/client/live/use-live-caption-log.d.ts +17 -0
- package/client/live/use-live-caption-log.js +51 -0
- package/client/live-client.d.ts +189 -0
- package/client/live-client.js +829 -0
- package/client/live-errors.d.ts +2 -0
- package/client/live-errors.js +9 -0
- package/client/live-messages.d.ts +148 -0
- package/client/live-messages.js +100 -0
- package/client/mic-capture.d.ts +4 -0
- package/client/mic-capture.js +6 -0
- package/client/oauth-popup.d.ts +26 -0
- package/client/oauth-popup.js +30 -0
- package/client/page-dispatch.d.ts +15 -0
- package/client/page-dispatch.js +63 -0
- package/client/pcm-downsample.d.ts +4 -0
- package/client/pcm-downsample.js +45 -0
- package/client/run-commit.d.ts +34 -0
- package/client/run-commit.js +49 -0
- package/client/run-session.d.ts +79 -0
- package/client/run-session.js +207 -0
- package/client/schema-fields.d.ts +63 -0
- package/client/schema-fields.js +199 -0
- package/client/shaped-data.d.ts +118 -0
- package/client/shaped-data.js +343 -0
- package/client/source-citations.d.ts +13 -0
- package/client/source-citations.js +56 -0
- package/client/space-geometry.d.ts +39 -0
- package/client/space-geometry.js +183 -0
- package/client/state-fields.d.ts +16 -0
- package/client/state-fields.js +44 -0
- package/client/tool-result.d.ts +78 -0
- package/client/tool-result.js +293 -0
- package/client/tool-resume.d.ts +50 -0
- package/client/tool-resume.js +14 -0
- package/client/trace-feed.d.ts +13 -0
- package/client/trace-feed.js +18 -0
- package/client/trace-story.d.ts +163 -0
- package/client/trace-story.js +440 -0
- package/client/trace-view.d.ts +130 -0
- package/client/trace-view.js +333 -0
- package/client/transcript-groups.d.ts +140 -0
- package/client/transcript-groups.js +217 -0
- package/client/transcript-schema.d.ts +10 -0
- package/client/transcript-schema.js +120 -0
- package/client/transcript-scroll.d.ts +7 -0
- package/client/transcript-scroll.js +6 -0
- package/client/transport.d.ts +264 -0
- package/client/transport.js +330 -0
- package/client/turn-client.d.ts +44 -0
- package/client/turn-client.js +88 -0
- package/client/voice-label.d.ts +2 -0
- package/client/voice-label.js +15 -0
- package/client/voice-recorder.d.ts +27 -0
- package/client/voice-recorder.js +162 -0
- package/client/wire-line.d.ts +56 -0
- package/client/wire-line.js +90 -0
- package/client/worklets/mic-capture.js +16 -0
- package/components/InkWaveform.d.ts +21 -0
- package/components/InkWaveform.js +131 -0
- package/components/live/use-live-runner-controls.d.ts +47 -0
- package/components/live/use-live-runner-controls.js +96 -0
- package/components/live/use-live-runner-model.d.ts +49 -0
- package/components/live/use-live-runner-model.js +190 -0
- package/components/live/use-live-runner-ui.d.ts +67 -0
- package/components/live/use-live-runner-ui.js +167 -0
- package/components/live/use-live-session-client.d.ts +41 -0
- package/components/live/use-live-session-client.js +126 -0
- package/components/use-composer-voice.d.ts +28 -0
- package/components/use-composer-voice.js +90 -0
- package/components/use-editor-selection.d.ts +3 -0
- package/components/use-editor-selection.js +19 -0
- package/components/use-voice-playback.d.ts +17 -0
- package/components/use-voice-playback.js +68 -0
- package/hooks/use-described.d.ts +12 -0
- package/hooks/use-described.js +25 -0
- package/hooks/use-embedding-index.d.ts +27 -0
- package/hooks/use-embedding-index.js +43 -0
- package/hooks/use-one-call.d.ts +31 -0
- package/hooks/use-one-call.js +76 -0
- package/hooks/use-page-tool-warnings.d.ts +6 -0
- package/hooks/use-page-tool-warnings.js +25 -0
- package/hooks/use-second-ticker.d.ts +2 -0
- package/hooks/use-second-ticker.js +13 -0
- package/hooks/use-theorem-chat-actions.d.ts +79 -0
- package/hooks/use-theorem-chat-actions.js +287 -0
- package/hooks/use-theorem-chat-state.d.ts +66 -0
- package/hooks/use-theorem-chat-state.js +122 -0
- package/hooks/use-theorem-chat.d.ts +97 -0
- package/hooks/use-theorem-chat.js +528 -0
- package/hooks/use-theorem-decision.d.ts +18 -0
- package/hooks/use-theorem-decision.js +12 -0
- package/hooks/use-theorem-embedding.d.ts +18 -0
- package/hooks/use-theorem-embedding.js +12 -0
- package/hooks/use-theorem-host.d.ts +45 -0
- package/hooks/use-theorem-host.js +135 -0
- package/hooks/use-theorem-interface.d.ts +22 -0
- package/hooks/use-theorem-interface.js +15 -0
- package/hooks/use-trace-records.d.ts +4 -0
- package/hooks/use-trace-records.js +7 -0
- package/index.d.ts +17 -0
- package/index.js +17 -0
- package/live.d.ts +9 -0
- package/live.js +10 -0
- package/package.json +74 -4
- package/server/checkpoint-ownership.d.ts +6 -0
- package/server/checkpoint-ownership.js +42 -0
- package/server/conversation-store.d.ts +92 -0
- package/server/conversation-store.js +131 -0
- package/server/conversation.d.ts +193 -0
- package/server/conversation.js +552 -0
- package/server/credential-store.d.ts +24 -0
- package/server/credential-store.js +14 -0
- package/server/decision-handler.d.ts +42 -0
- package/server/decision-handler.js +46 -0
- package/server/embedding-handler.d.ts +33 -0
- package/server/embedding-handler.js +45 -0
- package/server/handler.d.ts +152 -0
- package/server/handler.js +771 -0
- package/server/json-response.d.ts +2 -0
- package/server/json-response.js +7 -0
- package/server/mod.d.ts +25 -0
- package/server/mod.js +23 -0
- package/server/page-dispatch.d.ts +22 -0
- package/server/page-dispatch.js +127 -0
- package/server/request-body.d.ts +2 -0
- package/server/request-body.js +42 -0
- package/server/request-check.d.ts +21 -0
- package/server/request-check.js +49 -0
- package/server/session-authority.d.ts +8 -0
- package/server/session-authority.js +78 -0
- package/server/session-boundary.d.ts +26 -0
- package/server/session-boundary.js +64 -0
- package/server/session-identity.d.ts +16 -0
- package/server/session-identity.js +41 -0
- package/server/session-limits.d.ts +12 -0
- package/server/session-limits.js +59 -0
- package/server/session-store.d.ts +123 -0
- package/server/session-store.js +264 -0
- package/server/single-action.d.ts +21 -0
- package/server/single-action.js +30 -0
- package/server/steer-inbox.d.ts +35 -0
- package/server/steer-inbox.js +52 -0
- package/server/tool-operations.d.ts +6 -0
- package/server/tool-operations.js +132 -0
- package/server/turn-input.d.ts +10 -0
- package/server/turn-input.js +29 -0
- package/styles/ink-controls.css +134 -0
- package/ui/CardWall.d.ts +10 -0
- package/ui/CardWall.js +61 -0
- package/ui/ChatComposerBar.d.ts +62 -0
- package/ui/ChatComposerBar.js +306 -0
- package/ui/ChatTranscript.d.ts +43 -0
- package/ui/ChatTranscript.js +527 -0
- package/ui/ConsoleFrame.d.ts +104 -0
- package/ui/ConsoleFrame.js +146 -0
- package/ui/DecisionAnswers.d.ts +53 -0
- package/ui/DecisionAnswers.js +233 -0
- package/ui/DecisionImages.d.ts +16 -0
- package/ui/DecisionImages.js +46 -0
- package/ui/EmbeddingDocuments.d.ts +78 -0
- package/ui/EmbeddingDocuments.js +278 -0
- package/ui/EmbeddingQuery.d.ts +27 -0
- package/ui/EmbeddingQuery.js +95 -0
- package/ui/EmbeddingResults.d.ts +41 -0
- package/ui/EmbeddingResults.js +165 -0
- package/ui/EmbeddingSpace.d.ts +26 -0
- package/ui/EmbeddingSpace.js +106 -0
- package/ui/LiveCaptionsPanel.d.ts +22 -0
- package/ui/LiveCaptionsPanel.js +27 -0
- package/ui/LiveRunner.d.ts +30 -0
- package/ui/LiveRunner.js +180 -0
- package/ui/PaneState.d.ts +33 -0
- package/ui/PaneState.js +50 -0
- package/ui/SchemaFields.d.ts +10 -0
- package/ui/SchemaFields.js +150 -0
- package/ui/ShapedData.d.ts +11 -0
- package/ui/ShapedData.js +196 -0
- package/ui/SidePanel.d.ts +126 -0
- package/ui/SidePanel.js +123 -0
- package/ui/StateFields.d.ts +44 -0
- package/ui/StateFields.js +139 -0
- package/ui/TheoremChat.d.ts +122 -0
- package/ui/TheoremChat.js +134 -0
- package/ui/TheoremDecision.d.ts +30 -0
- package/ui/TheoremDecision.js +130 -0
- package/ui/TheoremEmbedding.d.ts +27 -0
- package/ui/TheoremEmbedding.js +122 -0
- package/ui/TheoremHost.d.ts +55 -0
- package/ui/TheoremHost.js +240 -0
- package/ui/ToolGateCard.d.ts +32 -0
- package/ui/ToolGateCard.js +107 -0
- package/ui/ToolResult.d.ts +10 -0
- package/ui/ToolResult.js +62 -0
- package/ui/ToolResultCharts.d.ts +5 -0
- package/ui/ToolResultCharts.js +103 -0
- package/ui/TraceCharts.d.ts +13 -0
- package/ui/TraceCharts.js +266 -0
- package/ui/TraceGuardrails.d.ts +11 -0
- package/ui/TraceGuardrails.js +74 -0
- package/ui/TraceInspector.d.ts +16 -0
- package/ui/TraceInspector.js +206 -0
- package/ui/TraceInspectorPanel.d.ts +46 -0
- package/ui/TraceInspectorPanel.js +95 -0
- package/ui/TraceOverview.d.ts +38 -0
- package/ui/TraceOverview.js +126 -0
- package/ui/TraceSpanDetail.d.ts +10 -0
- package/ui/TraceSpanDetail.js +190 -0
- package/ui/TraceStory.d.ts +20 -0
- package/ui/TraceStory.js +108 -0
- package/ui/TraceValues.d.ts +36 -0
- package/ui/TraceValues.js +123 -0
- package/ui/TraceWaterfall.d.ts +13 -0
- package/ui/TraceWaterfall.js +135 -0
- package/ui/VoiceNote.d.ts +12 -0
- package/ui/VoiceNote.js +28 -0
- package/ui/approval-heading.d.ts +4 -0
- package/ui/approval-heading.js +9 -0
- package/ui/arrive.d.ts +18 -0
- package/ui/arrive.js +46 -0
- package/ui/built/theme.css +792 -0
- package/ui/built/theme.css.d.ts +9 -0
- package/ui/built/theorem.d.ts +13 -0
- package/ui/built/theorem.js +660 -0
- package/ui/built/theorem.variants.d.ts +22 -0
- package/ui/code-language.d.ts +12 -0
- package/ui/code-language.js +50 -0
- package/ui/disclosure-motion.d.ts +6 -0
- package/ui/disclosure-motion.js +207 -0
- package/ui/embedding-space-gl.d.ts +37 -0
- package/ui/embedding-space-gl.js +663 -0
- package/ui/embedding-space-view.d.ts +8 -0
- package/ui/embedding-space-view.js +8 -0
- package/ui/glide.d.ts +7 -0
- package/ui/glide.js +44 -0
- package/ui/icons.d.ts +4 -0
- package/ui/icons.js +38 -0
- package/ui/index.d.ts +36 -0
- package/ui/index.js +39 -0
- package/ui/labels-provider.d.ts +16 -0
- package/ui/labels-provider.js +64 -0
- package/ui/labels.d.ts +1442 -0
- package/ui/labels.js +1443 -0
- package/ui/markdown-plugins.d.ts +9 -0
- package/ui/markdown-plugins.js +13 -0
- package/ui/mcp-icon.d.ts +6 -0
- package/ui/mcp-icon.js +8 -0
- package/ui/row-keys.d.ts +19 -0
- package/ui/row-keys.js +33 -0
- package/ui/theme.d.ts +10 -0
- package/ui/theme.js +13 -0
- package/ui/theorem-theme.d.ts +10 -0
- package/ui/theorem-theme.js +105 -0
- package/ui/trace-palette.d.ts +10 -0
- package/ui/trace-palette.js +10 -0
- package/ui/trace-patterns.d.ts +40 -0
- package/ui/trace-patterns.js +57 -0
- package/ui/transcript-copy-text.d.ts +4 -0
- package/ui/transcript-copy-text.js +40 -0
- package/ui/wall-columns.d.ts +8 -0
- package/ui/wall-columns.js +19 -0
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
import { inlineContent, TRACE_ATTRIBUTE_GROUPS, TRACE_FIELDS, TRACE_SPAN_TYPES, TRACE_STATUS, traceAttributeMeta, traceEventMeta, traceSpanMeta, } from '@theoremjs/agents';
|
|
2
|
+
import { isRecord } from '@theoremjs/agents/kernel';
|
|
3
|
+
/** A unix-nanosecond timestamp string as unix milliseconds. */
|
|
4
|
+
export function nanosToMs(nanos) {
|
|
5
|
+
return Number(BigInt(nanos) / 1000n) / 1_000;
|
|
6
|
+
}
|
|
7
|
+
function spanId(traceId, id) {
|
|
8
|
+
return `${traceId}:${id}`;
|
|
9
|
+
}
|
|
10
|
+
function byStart(a, b) {
|
|
11
|
+
return a.startMs - b.startMs;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Every span of `records` as one tree. A span written twice keeps its later
|
|
15
|
+
* copy; a span whose parent has not arrived yet (a Live response before its
|
|
16
|
+
* session root) is a root until the parent does.
|
|
17
|
+
*/
|
|
18
|
+
export function traceTree(records) {
|
|
19
|
+
const nodes = new Map();
|
|
20
|
+
for (const record of records) {
|
|
21
|
+
for (const span of record.spans) {
|
|
22
|
+
const startMs = nanosToMs(span.startTimeUnixNano);
|
|
23
|
+
nodes.set(spanId(span.traceId, span.spanId), {
|
|
24
|
+
id: spanId(span.traceId, span.spanId),
|
|
25
|
+
span,
|
|
26
|
+
record,
|
|
27
|
+
meta: traceSpanMeta(span),
|
|
28
|
+
startMs,
|
|
29
|
+
durationMs: nanosToMs(span.endTimeUnixNano) - startMs,
|
|
30
|
+
children: [],
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
const roots = [];
|
|
35
|
+
for (const node of nodes.values()) {
|
|
36
|
+
const parent = node.span.parentSpanId
|
|
37
|
+
? nodes.get(spanId(node.span.traceId, node.span.parentSpanId))
|
|
38
|
+
: undefined;
|
|
39
|
+
(parent ? parent.children : roots).push(node);
|
|
40
|
+
}
|
|
41
|
+
for (const node of nodes.values())
|
|
42
|
+
node.children.sort(byStart);
|
|
43
|
+
return roots.sort(byStart);
|
|
44
|
+
}
|
|
45
|
+
/** Every node of `nodes`, depth first. */
|
|
46
|
+
export function traceSpans(nodes) {
|
|
47
|
+
return nodes.flatMap((node) => [node, ...traceSpans(node.children)]);
|
|
48
|
+
}
|
|
49
|
+
const MODEL_CALL_TYPES = new Set(['call', 'response']);
|
|
50
|
+
function numberAttribute(span, key) {
|
|
51
|
+
const value = span.attributes[key];
|
|
52
|
+
return typeof value === 'number' ? value : undefined;
|
|
53
|
+
}
|
|
54
|
+
function isEstimated(span, side) {
|
|
55
|
+
const estimated = span.attributes['theorem.usage.estimated'];
|
|
56
|
+
return Array.isArray(estimated) && estimated.includes(side);
|
|
57
|
+
}
|
|
58
|
+
function sum(calls, key, partial) {
|
|
59
|
+
const values = calls.map((span) => numberAttribute(span, key));
|
|
60
|
+
const reported = values.filter((value) => value !== undefined);
|
|
61
|
+
if (reported.length === 0)
|
|
62
|
+
return undefined;
|
|
63
|
+
return {
|
|
64
|
+
value: reported.reduce((total, value) => total + value, 0),
|
|
65
|
+
complete: reported.length === calls.length && !calls.some((span) => partial?.(span) === true),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function tokenSum(calls, key, side) {
|
|
69
|
+
const total = sum(calls, key);
|
|
70
|
+
return total && { ...total, estimated: calls.some((span) => isEstimated(span, side)) };
|
|
71
|
+
}
|
|
72
|
+
export function traceTotals(nodes) {
|
|
73
|
+
const spans = traceSpans(nodes);
|
|
74
|
+
const calls = spans
|
|
75
|
+
.filter((node) => MODEL_CALL_TYPES.has(node.meta.type))
|
|
76
|
+
.map((node) => node.span);
|
|
77
|
+
const start = Math.min(...spans.map((node) => node.startMs));
|
|
78
|
+
const end = Math.max(...spans.map((node) => node.startMs + node.durationMs));
|
|
79
|
+
const input = tokenSum(calls, 'gen_ai.usage.input_tokens', 'input');
|
|
80
|
+
const cached = sum(calls, 'gen_ai.usage.cache_read.input_tokens');
|
|
81
|
+
const output = tokenSum(calls, 'gen_ai.usage.output_tokens', 'output');
|
|
82
|
+
const cost = sum(calls, 'theorem.usage.cost_usd', (span) => span.attributes['theorem.usage.cost_partial'] === true);
|
|
83
|
+
return {
|
|
84
|
+
spans: spans.length,
|
|
85
|
+
errors: spans.filter((node) => node.span.status.code === 'ERROR').length,
|
|
86
|
+
durationMs: spans.length === 0 ? 0 : end - start,
|
|
87
|
+
calls: calls.length,
|
|
88
|
+
...(input && { input }),
|
|
89
|
+
...(cached && { cached }),
|
|
90
|
+
...(output && { output }),
|
|
91
|
+
...(cost && { cost }),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
/** The operators each kind offers, by PowerSearch operator key. */
|
|
95
|
+
export const TRACE_SEARCH_OPERATORS = {
|
|
96
|
+
options: ['isAnyOf', 'isNoneOf'],
|
|
97
|
+
number: ['greaterThan', 'lessThan'],
|
|
98
|
+
text: ['contains', 'notContains'],
|
|
99
|
+
flag: ['isTrue', 'isFalse'],
|
|
100
|
+
};
|
|
101
|
+
/** The field that searches any text a span holds: its name, its values, its stored text. */
|
|
102
|
+
export const TRACE_TEXT_FIELD = 'text';
|
|
103
|
+
/** Span fields beside the attributes. */
|
|
104
|
+
const TYPE_FIELD = 'type';
|
|
105
|
+
const STATUS_FIELD = 'status';
|
|
106
|
+
const DURATION_FIELD = 'duration';
|
|
107
|
+
const EVENTS_FIELD = 'events';
|
|
108
|
+
const NUMBER_FORMATS = new Set([
|
|
109
|
+
'number',
|
|
110
|
+
'tokens',
|
|
111
|
+
'seconds',
|
|
112
|
+
'milliseconds',
|
|
113
|
+
'usd',
|
|
114
|
+
]);
|
|
115
|
+
const TEXT_FORMATS = new Set(['text', 'id', 'list']);
|
|
116
|
+
function optionsOf(seen, known) {
|
|
117
|
+
return [...seen].sort().map((value) => {
|
|
118
|
+
const meta = known[value];
|
|
119
|
+
return meta ? { value, label: meta.label, doc: meta.doc } : { value, label: value };
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
function attributeValues(value) {
|
|
123
|
+
return Array.isArray(value) ? value : [value];
|
|
124
|
+
}
|
|
125
|
+
function attributeKind(meta, value) {
|
|
126
|
+
if (meta?.options)
|
|
127
|
+
return 'options';
|
|
128
|
+
const format = meta?.format;
|
|
129
|
+
if (format === 'boolean' || (!meta && typeof value === 'boolean'))
|
|
130
|
+
return 'flag';
|
|
131
|
+
if ((format && NUMBER_FORMATS.has(format)) || (!meta && typeof value === 'number'))
|
|
132
|
+
return 'number';
|
|
133
|
+
if ((format && TEXT_FORMATS.has(format)) || (!meta && typeof value === 'string'))
|
|
134
|
+
return 'text';
|
|
135
|
+
return undefined;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* The fields a search over `nodes` offers: type, status, duration, and events,
|
|
139
|
+
* then every attribute the spans recorded that compares as a value. Stored
|
|
140
|
+
* text, JSON, and structured values are reached by the text field.
|
|
141
|
+
*/
|
|
142
|
+
export function traceSearchFields(nodes) {
|
|
143
|
+
const spans = traceSpans(nodes);
|
|
144
|
+
const types = new Set(spans.map((node) => node.meta.type));
|
|
145
|
+
const statuses = new Set(spans.map((node) => node.span.status.code));
|
|
146
|
+
const events = new Set(spans.flatMap((node) => node.span.events.map((event) => event.name)));
|
|
147
|
+
const attributes = new Map();
|
|
148
|
+
for (const node of spans) {
|
|
149
|
+
for (const [key, value] of Object.entries(node.span.attributes)) {
|
|
150
|
+
const kind = attributeKind(traceAttributeMeta(key), value);
|
|
151
|
+
if (!kind)
|
|
152
|
+
continue;
|
|
153
|
+
const field = attributes.get(key) ?? { kind, seen: new Set() };
|
|
154
|
+
if (kind === 'options')
|
|
155
|
+
for (const item of attributeValues(value))
|
|
156
|
+
if (typeof item === 'string')
|
|
157
|
+
field.seen.add(item);
|
|
158
|
+
attributes.set(key, field);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
const eventLabels = Object.fromEntries([...events].flatMap((name) => {
|
|
162
|
+
const meta = traceEventMeta(name);
|
|
163
|
+
return meta ? [[name, meta]] : [];
|
|
164
|
+
}));
|
|
165
|
+
const fields = [
|
|
166
|
+
{
|
|
167
|
+
key: TYPE_FIELD,
|
|
168
|
+
...TRACE_FIELDS.type,
|
|
169
|
+
kind: 'options',
|
|
170
|
+
options: optionsOf(types, TRACE_SPAN_TYPES),
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
key: STATUS_FIELD,
|
|
174
|
+
...TRACE_FIELDS.status,
|
|
175
|
+
kind: 'options',
|
|
176
|
+
options: optionsOf(statuses, TRACE_STATUS),
|
|
177
|
+
},
|
|
178
|
+
{ key: DURATION_FIELD, ...TRACE_FIELDS.duration, kind: 'number', format: 'milliseconds' },
|
|
179
|
+
];
|
|
180
|
+
if (events.size > 0)
|
|
181
|
+
fields.push({
|
|
182
|
+
key: EVENTS_FIELD,
|
|
183
|
+
...TRACE_FIELDS.events,
|
|
184
|
+
kind: 'options',
|
|
185
|
+
options: optionsOf(events, eventLabels),
|
|
186
|
+
});
|
|
187
|
+
for (const [key, { kind, seen }] of [...attributes].sort(([a], [b]) => a.localeCompare(b))) {
|
|
188
|
+
const meta = traceAttributeMeta(key);
|
|
189
|
+
fields.push({
|
|
190
|
+
key,
|
|
191
|
+
label: meta?.label ?? key,
|
|
192
|
+
doc: meta?.doc ?? key,
|
|
193
|
+
kind,
|
|
194
|
+
...(meta && { group: TRACE_ATTRIBUTE_GROUPS[meta.group].label, format: meta.format }),
|
|
195
|
+
...(kind === 'options' && { options: optionsOf(seen, meta?.options ?? {}) }),
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
return fields;
|
|
199
|
+
}
|
|
200
|
+
/** Every string a span holds: what it is, its name, and status, and its attributes and events with stored text inlined. */
|
|
201
|
+
function spanText(node) {
|
|
202
|
+
const strings = [
|
|
203
|
+
node.meta.label,
|
|
204
|
+
node.meta.subject ?? '',
|
|
205
|
+
node.span.name,
|
|
206
|
+
node.span.status.message ?? '',
|
|
207
|
+
];
|
|
208
|
+
const collect = (value) => {
|
|
209
|
+
if (typeof value === 'string')
|
|
210
|
+
strings.push(value);
|
|
211
|
+
else if (Array.isArray(value))
|
|
212
|
+
for (const item of value)
|
|
213
|
+
collect(item);
|
|
214
|
+
else if (value && typeof value === 'object')
|
|
215
|
+
for (const nested of Object.values(value))
|
|
216
|
+
collect(nested);
|
|
217
|
+
};
|
|
218
|
+
collect(inlineContent(node.record, { attributes: node.span.attributes, events: node.span.events }));
|
|
219
|
+
for (const event of node.span.events)
|
|
220
|
+
strings.push(traceEventMeta(event.name)?.label ?? '');
|
|
221
|
+
return strings.join('\n').toLocaleLowerCase();
|
|
222
|
+
}
|
|
223
|
+
/** A field's values on one span: strings for options and text, a number, or a yes/no. */
|
|
224
|
+
function fieldValues(node, field) {
|
|
225
|
+
switch (field) {
|
|
226
|
+
case TYPE_FIELD:
|
|
227
|
+
return [node.meta.type];
|
|
228
|
+
case STATUS_FIELD:
|
|
229
|
+
return [node.span.status.code];
|
|
230
|
+
case DURATION_FIELD:
|
|
231
|
+
return [node.durationMs];
|
|
232
|
+
case EVENTS_FIELD:
|
|
233
|
+
return node.span.events.map((event) => event.name);
|
|
234
|
+
default: {
|
|
235
|
+
const value = node.span.attributes[field];
|
|
236
|
+
if (value === undefined)
|
|
237
|
+
return [];
|
|
238
|
+
return attributeValues(value).filter((item) => typeof item === 'string' || typeof item === 'number' || typeof item === 'boolean');
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
function stringList(value) {
|
|
243
|
+
return Array.isArray(value) ? value.filter((item) => typeof item === 'string') : [];
|
|
244
|
+
}
|
|
245
|
+
function matchesFilter(node, filter) {
|
|
246
|
+
const { value } = filter.value;
|
|
247
|
+
if (filter.field === TRACE_TEXT_FIELD) {
|
|
248
|
+
const found = typeof value === 'string' && spanText(node).includes(value.toLocaleLowerCase());
|
|
249
|
+
return filter.operator === 'notContains' ? !found : found;
|
|
250
|
+
}
|
|
251
|
+
const values = fieldValues(node, filter.field);
|
|
252
|
+
switch (filter.operator) {
|
|
253
|
+
case 'isAnyOf':
|
|
254
|
+
return values.some((item) => typeof item === 'string' && stringList(value).includes(item));
|
|
255
|
+
case 'isNoneOf':
|
|
256
|
+
return !values.some((item) => typeof item === 'string' && stringList(value).includes(item));
|
|
257
|
+
case 'greaterThan':
|
|
258
|
+
return (typeof value === 'number' && values.some((item) => typeof item === 'number' && item > value));
|
|
259
|
+
case 'lessThan':
|
|
260
|
+
return (typeof value === 'number' && values.some((item) => typeof item === 'number' && item < value));
|
|
261
|
+
case 'contains':
|
|
262
|
+
case 'notContains': {
|
|
263
|
+
const found = typeof value === 'string' &&
|
|
264
|
+
values.some((item) => String(item).toLocaleLowerCase().includes(value.toLocaleLowerCase()));
|
|
265
|
+
return filter.operator === 'contains' ? found : !found;
|
|
266
|
+
}
|
|
267
|
+
case 'isTrue':
|
|
268
|
+
return values.includes(true);
|
|
269
|
+
case 'isFalse':
|
|
270
|
+
return values.includes(false);
|
|
271
|
+
default:
|
|
272
|
+
return false;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
/** Whether a span meets every filter. */
|
|
276
|
+
function traceMatches(node, filters) {
|
|
277
|
+
return filters.every((filter) => matchesFilter(node, filter));
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* The tree cut to the spans that meet every filter, each kept under its
|
|
281
|
+
* ancestors so it reads in place; `matches` counts the spans that met them.
|
|
282
|
+
*/
|
|
283
|
+
export function filterTraceTree(nodes, filters) {
|
|
284
|
+
if (filters.length === 0)
|
|
285
|
+
return { nodes: [...nodes], matches: traceSpans(nodes).length };
|
|
286
|
+
let matches = 0;
|
|
287
|
+
const keep = (node) => {
|
|
288
|
+
const hit = traceMatches(node, filters);
|
|
289
|
+
if (hit)
|
|
290
|
+
matches += 1;
|
|
291
|
+
const children = node.children.flatMap((child) => keep(child) ?? []);
|
|
292
|
+
return hit || children.length > 0 ? { ...node, children } : undefined;
|
|
293
|
+
};
|
|
294
|
+
return { nodes: nodes.flatMap((node) => keep(node) ?? []), matches };
|
|
295
|
+
}
|
|
296
|
+
/** The ids of the spans under `nodes` that meet every filter; undefined when there are none to meet. */
|
|
297
|
+
export function traceMatchIds(nodes, filters) {
|
|
298
|
+
if (filters.length === 0)
|
|
299
|
+
return undefined;
|
|
300
|
+
return new Set(traceSpans(nodes)
|
|
301
|
+
.filter((node) => traceMatches(node, filters))
|
|
302
|
+
.map((node) => node.id));
|
|
303
|
+
}
|
|
304
|
+
/** Formats whose value is stored text or structure: shown on click, never inline. */
|
|
305
|
+
const STORED_FORMATS = new Set([
|
|
306
|
+
'content',
|
|
307
|
+
'json',
|
|
308
|
+
'messages',
|
|
309
|
+
'parts',
|
|
310
|
+
]);
|
|
311
|
+
function isStringList(value) {
|
|
312
|
+
return Array.isArray(value) && value.every((item) => typeof item === 'string');
|
|
313
|
+
}
|
|
314
|
+
/** An object, or a list of objects, as items to read by a catalog entry's fields. */
|
|
315
|
+
function fieldItems(value) {
|
|
316
|
+
if (isRecord(value))
|
|
317
|
+
return [value];
|
|
318
|
+
return Array.isArray(value) && value.every(isRecord) ? value : undefined;
|
|
319
|
+
}
|
|
320
|
+
export function traceValueShape(meta, value) {
|
|
321
|
+
if (meta && STORED_FORMATS.has(meta.format))
|
|
322
|
+
return { kind: 'stored' };
|
|
323
|
+
if (typeof value === 'string')
|
|
324
|
+
return { kind: 'text', value };
|
|
325
|
+
if (typeof value === 'number' || typeof value === 'boolean')
|
|
326
|
+
return { kind: 'scalar', value };
|
|
327
|
+
if (isStringList(value))
|
|
328
|
+
return { kind: 'list', value };
|
|
329
|
+
const items = meta?.fields ? fieldItems(value) : undefined;
|
|
330
|
+
return meta?.fields && items
|
|
331
|
+
? { kind: 'fields', fields: meta.fields, items }
|
|
332
|
+
: { kind: 'stored' };
|
|
333
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Group flat transcript blocks into user / assistant turns for message-shell UI.
|
|
3
|
+
* Tools, reasoning, and mid-turn narration stay inside the assistant turn
|
|
4
|
+
* (Seance-style), not as separate messages.
|
|
5
|
+
*/
|
|
6
|
+
import type { TurnStop } from '@theoremjs/agents';
|
|
7
|
+
import { type TranscriptBlock } from '@theoremjs/agents/interface';
|
|
8
|
+
/** What a reply or a called agent used: its tokens, and its cost when the provider gave one. */
|
|
9
|
+
export type TurnUsage = {
|
|
10
|
+
total: number;
|
|
11
|
+
cost?: {
|
|
12
|
+
usd: number;
|
|
13
|
+
partial?: true;
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
export type TranscriptTurnGroup = {
|
|
17
|
+
kind: 'user';
|
|
18
|
+
key: string;
|
|
19
|
+
blocks: TranscriptBlock[];
|
|
20
|
+
/** How its turn stopped short before any reply. A message with a stop is closed. */
|
|
21
|
+
stop?: TurnStop;
|
|
22
|
+
} | {
|
|
23
|
+
kind: 'assistant';
|
|
24
|
+
key: string;
|
|
25
|
+
blocks: TranscriptBlock[];
|
|
26
|
+
/** The reply's work and when it stopped, from its latest stamped `turn-done`. */
|
|
27
|
+
workedMs?: number;
|
|
28
|
+
endedAt?: number;
|
|
29
|
+
/** What the reply used, summed over its runs: a reply resumed at a gate has several. */
|
|
30
|
+
usage?: TurnUsage;
|
|
31
|
+
/** How its latest run stopped. */
|
|
32
|
+
stop?: TurnStop;
|
|
33
|
+
};
|
|
34
|
+
export type TraceItem = {
|
|
35
|
+
kind: 'reasoning';
|
|
36
|
+
id: string;
|
|
37
|
+
text: string;
|
|
38
|
+
} | {
|
|
39
|
+
kind: 'narration';
|
|
40
|
+
id: string;
|
|
41
|
+
text: string;
|
|
42
|
+
} | {
|
|
43
|
+
kind: 'tool';
|
|
44
|
+
id: string;
|
|
45
|
+
block: Extract<TranscriptBlock, {
|
|
46
|
+
kind: 'tool';
|
|
47
|
+
}>;
|
|
48
|
+
};
|
|
49
|
+
export type ComposedAssistantTurn = {
|
|
50
|
+
/** Ordered reasoning / narration / tools for the disclosure. */
|
|
51
|
+
trace: TraceItem[];
|
|
52
|
+
/** Gate tools rendered as interactive cards outside the collapsed list. */
|
|
53
|
+
gatedTools: Extract<TranscriptBlock, {
|
|
54
|
+
kind: 'tool';
|
|
55
|
+
}>[];
|
|
56
|
+
/** Final answer segments (text / media / structured / grounding / evidence / error). */
|
|
57
|
+
body: TranscriptBlock[];
|
|
58
|
+
/** True when the disclosure control should appear. */
|
|
59
|
+
hasTrace: boolean;
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* Blocks as turns: neighbours from the same side share one, unless a block is
|
|
63
|
+
* in `breaks`, which always starts its own. A message whose turn stopped short
|
|
64
|
+
* is closed: the next message starts its own turn.
|
|
65
|
+
*/
|
|
66
|
+
export declare function groupTranscriptBlocks(blocks: readonly TranscriptBlock[], breaks?: ReadonlySet<string>): TranscriptTurnGroup[];
|
|
67
|
+
/** A reply's text to copy; a failure is the message's status, not part of the reply. */
|
|
68
|
+
export declare function assistantTurnCopyText(blocks: readonly TranscriptBlock[]): string;
|
|
69
|
+
/** A turn's work, for the status line: running, or done (with its duration when known). */
|
|
70
|
+
export type WorkStatus = {
|
|
71
|
+
phase: 'working' | 'worked';
|
|
72
|
+
elapsedMs?: number;
|
|
73
|
+
};
|
|
74
|
+
/** Working while streaming; worked after, when it has a duration or a trace; else nothing to show. */
|
|
75
|
+
export declare function workStatus(args: {
|
|
76
|
+
streaming: boolean;
|
|
77
|
+
hasTrace: boolean;
|
|
78
|
+
elapsedMs?: number;
|
|
79
|
+
}): WorkStatus | null;
|
|
80
|
+
/**
|
|
81
|
+
* Split an assistant turn into Seance-style trace + final body.
|
|
82
|
+
*
|
|
83
|
+
* - `thought` → always reasoning in the trace
|
|
84
|
+
* - non-gate `tool` → tool item in the trace
|
|
85
|
+
* - first gate `tool` → interactive card outside the collapsed list; later gates → trace
|
|
86
|
+
* - `text` before/between tools → narration; text and answer kinds after the
|
|
87
|
+
* latest tool → body (while streaming too, so the answer streams formatted)
|
|
88
|
+
* - No tools: thoughts still go to trace; remaining kinds → body
|
|
89
|
+
*/
|
|
90
|
+
export declare function composeAssistantTurn(blocks: readonly TranscriptBlock[]): ComposedAssistantTurn;
|
|
91
|
+
/** The trailing user group while its reply has not started streaming back. */
|
|
92
|
+
export declare function pendingPromptOf(groups: readonly TranscriptTurnGroup[]): TranscriptTurnGroup | undefined;
|
|
93
|
+
export type AssistantTurnTiming = {
|
|
94
|
+
/**
|
|
95
|
+
* Keyed by its prompt, not its blocks: the reply stays mounted from the
|
|
96
|
+
* "Working…" placeholder through streaming and commit (which re-keys blocks).
|
|
97
|
+
*/
|
|
98
|
+
key: string;
|
|
99
|
+
live: boolean;
|
|
100
|
+
/** While live: when the reply started, its approval waits skipped. */
|
|
101
|
+
startedAt?: number;
|
|
102
|
+
/** Once stopped: how long it worked and when it stopped, as its blocks record. */
|
|
103
|
+
workedMs?: number;
|
|
104
|
+
endedAt?: number;
|
|
105
|
+
};
|
|
106
|
+
/**
|
|
107
|
+
* A turn sent in this session, keyed by its prompt: when it last stopped, and
|
|
108
|
+
* how long it sat paused on approvals, which doesn't count as work.
|
|
109
|
+
*/
|
|
110
|
+
export type TurnSpan = {
|
|
111
|
+
endedAt?: number;
|
|
112
|
+
pausedMs: number;
|
|
113
|
+
};
|
|
114
|
+
/**
|
|
115
|
+
* The key of the assistant group at `index`: its prompt's, so the reply keeps
|
|
116
|
+
* one identity from "Working…" through commit. Block ids restart every reply.
|
|
117
|
+
*/
|
|
118
|
+
export declare function replyKey(groups: readonly TranscriptTurnGroup[], index: number): string;
|
|
119
|
+
/** The key of the reply to `prompt`, before and after it streams. */
|
|
120
|
+
export declare function promptReplyKey(prompt: TranscriptTurnGroup): string;
|
|
121
|
+
/**
|
|
122
|
+
* When the prompt at `index` was sent. Once its reply has stopped, its blocks
|
|
123
|
+
* date it (less any approval waits), so a restored chat keeps its times; until
|
|
124
|
+
* then, when this session first showed it.
|
|
125
|
+
*/
|
|
126
|
+
export declare function promptTime(groups: readonly TranscriptTurnGroup[], index: number, timeOf: (key: string) => number): number;
|
|
127
|
+
/**
|
|
128
|
+
* Key and timing for the assistant group at `index`. A stopped reply reads its
|
|
129
|
+
* time from its blocks; a live one counts from its span in this session.
|
|
130
|
+
*/
|
|
131
|
+
export declare function assistantTurnTiming(args: {
|
|
132
|
+
groups: readonly TranscriptTurnGroup[];
|
|
133
|
+
index: number;
|
|
134
|
+
streaming: boolean;
|
|
135
|
+
timeOf: (key: string) => number;
|
|
136
|
+
spans: ReadonlyMap<string, TurnSpan>;
|
|
137
|
+
}): AssistantTurnTiming;
|
|
138
|
+
export declare function toolCallLabel(tool: Extract<TranscriptBlock, {
|
|
139
|
+
kind: 'tool';
|
|
140
|
+
}>['tool']): string;
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Group flat transcript blocks into user / assistant turns for message-shell UI.
|
|
3
|
+
* Tools, reasoning, and mid-turn narration stay inside the assistant turn
|
|
4
|
+
* (Seance-style), not as separate messages.
|
|
5
|
+
*/
|
|
6
|
+
import { WORDED_STOP_KINDS } from '@theoremjs/agents/interface';
|
|
7
|
+
import { humanize } from "./shaped-data.js";
|
|
8
|
+
/** `used` with `more` added. A cost only one side reported is partial. */
|
|
9
|
+
function addUsage(used, more) {
|
|
10
|
+
if (!used)
|
|
11
|
+
return more;
|
|
12
|
+
const total = used.total + more.total;
|
|
13
|
+
if (!used.cost && !more.cost)
|
|
14
|
+
return { total };
|
|
15
|
+
const usd = (used.cost?.usd ?? 0) + (more.cost?.usd ?? 0);
|
|
16
|
+
const partial = !used.cost || !more.cost || used.cost.partial || more.cost.partial;
|
|
17
|
+
return { total, cost: { usd, ...(partial ? { partial: true } : {}) } };
|
|
18
|
+
}
|
|
19
|
+
const WORDED = new Set(WORDED_STOP_KINDS);
|
|
20
|
+
const USER_KINDS = new Set(['user-text', 'user-attachment', 'user-voice']);
|
|
21
|
+
function isUserTranscriptBlock(block) {
|
|
22
|
+
return USER_KINDS.has(block.kind);
|
|
23
|
+
}
|
|
24
|
+
/** Hide bookkeeping from the message body. */
|
|
25
|
+
function isHiddenTranscriptBlock(block) {
|
|
26
|
+
return block.kind === 'turn-done';
|
|
27
|
+
}
|
|
28
|
+
/** A `turn-done` on the turn it closes: a reply's work and usage, or a message's stop. */
|
|
29
|
+
function closeTurn(last, done) {
|
|
30
|
+
if (last?.kind === 'user') {
|
|
31
|
+
if (done.stop && WORDED.has(done.stop.kind))
|
|
32
|
+
last.stop = done.stop;
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
if (!last)
|
|
36
|
+
return;
|
|
37
|
+
if (done.workedMs !== undefined) {
|
|
38
|
+
last.workedMs = done.workedMs;
|
|
39
|
+
last.endedAt = done.endedAt;
|
|
40
|
+
}
|
|
41
|
+
if (done.tokens)
|
|
42
|
+
last.usage = addUsage(last.usage, done.tokens);
|
|
43
|
+
if (done.stop)
|
|
44
|
+
last.stop = done.stop;
|
|
45
|
+
}
|
|
46
|
+
/** Whether `block` joins the turn before it: the same side, and that turn still open. */
|
|
47
|
+
function joinsTurn(last, isUser) {
|
|
48
|
+
if (!last || last.kind !== (isUser ? 'user' : 'assistant'))
|
|
49
|
+
return false;
|
|
50
|
+
return !(last.kind === 'user' && last.stop);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Blocks as turns: neighbours from the same side share one, unless a block is
|
|
54
|
+
* in `breaks`, which always starts its own. A message whose turn stopped short
|
|
55
|
+
* is closed: the next message starts its own turn.
|
|
56
|
+
*/
|
|
57
|
+
export function groupTranscriptBlocks(blocks, breaks) {
|
|
58
|
+
const groups = [];
|
|
59
|
+
for (const block of blocks) {
|
|
60
|
+
if (block.kind === 'turn-done')
|
|
61
|
+
closeTurn(groups.at(-1), block);
|
|
62
|
+
if (isHiddenTranscriptBlock(block))
|
|
63
|
+
continue;
|
|
64
|
+
const isUser = isUserTranscriptBlock(block);
|
|
65
|
+
const last = groups.at(-1);
|
|
66
|
+
if (last && joinsTurn(last, isUser) && !breaks?.has(block.id)) {
|
|
67
|
+
last.blocks.push(block);
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
groups.push({
|
|
71
|
+
kind: isUser ? 'user' : 'assistant',
|
|
72
|
+
key: block.id,
|
|
73
|
+
blocks: [block],
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
return groups;
|
|
77
|
+
}
|
|
78
|
+
/** A reply's text to copy; a failure is the message's status, not part of the reply. */
|
|
79
|
+
export function assistantTurnCopyText(blocks) {
|
|
80
|
+
return blocks
|
|
81
|
+
.filter((block) => block.kind === 'text')
|
|
82
|
+
.map((block) => block.text)
|
|
83
|
+
.filter(Boolean)
|
|
84
|
+
.join('\n\n');
|
|
85
|
+
}
|
|
86
|
+
/** Working while streaming; worked after, when it has a duration or a trace; else nothing to show. */
|
|
87
|
+
export function workStatus(args) {
|
|
88
|
+
const elapsed = args.elapsedMs !== undefined ? { elapsedMs: args.elapsedMs } : {};
|
|
89
|
+
if (args.streaming)
|
|
90
|
+
return { phase: 'working', ...elapsed };
|
|
91
|
+
if (args.elapsedMs !== undefined && args.elapsedMs > 0)
|
|
92
|
+
return { phase: 'worked', elapsedMs: args.elapsedMs };
|
|
93
|
+
return args.hasTrace ? { phase: 'worked' } : null;
|
|
94
|
+
}
|
|
95
|
+
function isGatedTool(block) {
|
|
96
|
+
return block.kind === 'tool' && block.tool.state?.phase === 'gate';
|
|
97
|
+
}
|
|
98
|
+
function lastToolIndexOf(blocks) {
|
|
99
|
+
for (let i = blocks.length - 1; i >= 0; i -= 1) {
|
|
100
|
+
if (blocks[i]?.kind === 'tool')
|
|
101
|
+
return i;
|
|
102
|
+
}
|
|
103
|
+
return -1;
|
|
104
|
+
}
|
|
105
|
+
function pushTextBlock(block, args, trace, body) {
|
|
106
|
+
// invariant: Text before a tool call is narration; text after the latest one is the
|
|
107
|
+
// answer, streamed in place. If another tool call follows, it becomes narration.
|
|
108
|
+
if (args.hasTools && args.index < args.lastToolIndex) {
|
|
109
|
+
if (block.text.trim()) {
|
|
110
|
+
trace.push({ kind: 'narration', id: block.id, text: block.text });
|
|
111
|
+
}
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
body.push(block);
|
|
115
|
+
}
|
|
116
|
+
function classifyNonGateBlock(block, args, trace, body) {
|
|
117
|
+
if (block.kind === 'thought') {
|
|
118
|
+
if (block.text.trim()) {
|
|
119
|
+
trace.push({ kind: 'reasoning', id: block.id, text: block.text });
|
|
120
|
+
}
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
if (block.kind === 'tool') {
|
|
124
|
+
trace.push({ kind: 'tool', id: block.id, block });
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (block.kind === 'text') {
|
|
128
|
+
pushTextBlock(block, args, trace, body);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
body.push(block);
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Split an assistant turn into Seance-style trace + final body.
|
|
135
|
+
*
|
|
136
|
+
* - `thought` → always reasoning in the trace
|
|
137
|
+
* - non-gate `tool` → tool item in the trace
|
|
138
|
+
* - first gate `tool` → interactive card outside the collapsed list; later gates → trace
|
|
139
|
+
* - `text` before/between tools → narration; text and answer kinds after the
|
|
140
|
+
* latest tool → body (while streaming too, so the answer streams formatted)
|
|
141
|
+
* - No tools: thoughts still go to trace; remaining kinds → body
|
|
142
|
+
*/
|
|
143
|
+
export function composeAssistantTurn(blocks) {
|
|
144
|
+
const visible = blocks.filter((block) => !isHiddenTranscriptBlock(block));
|
|
145
|
+
// why: One card at a time: the gate answered next. The step's later gates wait in the trace.
|
|
146
|
+
const nextGate = visible.find(isGatedTool);
|
|
147
|
+
const gatedTools = nextGate ? [nextGate] : [];
|
|
148
|
+
const nonGate = visible.filter((block) => block !== nextGate);
|
|
149
|
+
const lastToolIndex = lastToolIndexOf(nonGate);
|
|
150
|
+
const hasTools = lastToolIndex >= 0;
|
|
151
|
+
const trace = [];
|
|
152
|
+
const body = [];
|
|
153
|
+
for (const [index, block] of nonGate.entries()) {
|
|
154
|
+
classifyNonGateBlock(block, { hasTools, index, lastToolIndex }, trace, body);
|
|
155
|
+
}
|
|
156
|
+
return {
|
|
157
|
+
trace,
|
|
158
|
+
gatedTools,
|
|
159
|
+
body,
|
|
160
|
+
hasTrace: trace.length > 0,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
/** The trailing user group while its reply has not started streaming back. */
|
|
164
|
+
export function pendingPromptOf(groups) {
|
|
165
|
+
const last = groups.at(-1);
|
|
166
|
+
return last?.kind === 'user' ? last : undefined;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* The key of the assistant group at `index`: its prompt's, so the reply keeps
|
|
170
|
+
* one identity from "Working…" through commit. Block ids restart every reply.
|
|
171
|
+
*/
|
|
172
|
+
export function replyKey(groups, index) {
|
|
173
|
+
const prompt = groups[index - 1];
|
|
174
|
+
return prompt?.kind === 'user' ? promptReplyKey(prompt) : (groups[index]?.key ?? String(index));
|
|
175
|
+
}
|
|
176
|
+
/** The key of the reply to `prompt`, before and after it streams. */
|
|
177
|
+
export function promptReplyKey(prompt) {
|
|
178
|
+
return `${prompt.key}:reply`;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* When the prompt at `index` was sent. Once its reply has stopped, its blocks
|
|
182
|
+
* date it (less any approval waits), so a restored chat keeps its times; until
|
|
183
|
+
* then, when this session first showed it.
|
|
184
|
+
*/
|
|
185
|
+
export function promptTime(groups, index, timeOf) {
|
|
186
|
+
const reply = groups[index + 1];
|
|
187
|
+
if (reply?.kind === 'assistant' && reply.endedAt !== undefined && reply.workedMs !== undefined) {
|
|
188
|
+
return reply.endedAt - reply.workedMs;
|
|
189
|
+
}
|
|
190
|
+
return timeOf(groups[index]?.key ?? '');
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Key and timing for the assistant group at `index`. A stopped reply reads its
|
|
194
|
+
* time from its blocks; a live one counts from its span in this session.
|
|
195
|
+
*/
|
|
196
|
+
export function assistantTurnTiming(args) {
|
|
197
|
+
const key = replyKey(args.groups, args.index);
|
|
198
|
+
const live = args.streaming && args.index === args.groups.length - 1;
|
|
199
|
+
const group = args.groups[args.index];
|
|
200
|
+
if (!live) {
|
|
201
|
+
return group?.kind === 'assistant' && group.workedMs !== undefined
|
|
202
|
+
? { key, live, workedMs: group.workedMs, endedAt: group.endedAt }
|
|
203
|
+
: { key, live };
|
|
204
|
+
}
|
|
205
|
+
const prompt = args.groups[args.index - 1];
|
|
206
|
+
const span = prompt?.kind === 'user' ? args.spans.get(prompt.key) : undefined;
|
|
207
|
+
if (!prompt || !span)
|
|
208
|
+
return { key, live };
|
|
209
|
+
return { key, live, startedAt: args.timeOf(prompt.key) + span.pausedMs };
|
|
210
|
+
}
|
|
211
|
+
export function toolCallLabel(tool) {
|
|
212
|
+
const phase = tool.state?.phase;
|
|
213
|
+
if (phase === 'cancel')
|
|
214
|
+
return humanize(tool.name);
|
|
215
|
+
const settled = phase === 'complete' || phase === 'error';
|
|
216
|
+
return (settled ? tool.activityPast : tool.activity) ?? humanize(tool.name);
|
|
217
|
+
}
|