@theoremjs/react 0.0.0-stage → 0.3.0

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.
Files changed (231) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +246 -2
  3. package/client/attachment-hover-preview.d.ts +11 -0
  4. package/client/attachment-hover-preview.js +36 -0
  5. package/client/audio-level.d.ts +5 -0
  6. package/client/audio-level.js +24 -0
  7. package/client/composer-attachments.d.ts +30 -0
  8. package/client/composer-attachments.js +33 -0
  9. package/client/composer-drawer.d.ts +19 -0
  10. package/client/composer-drawer.js +15 -0
  11. package/client/composer-hints.d.ts +25 -0
  12. package/client/composer-hints.js +20 -0
  13. package/client/composer-primary.d.ts +16 -0
  14. package/client/composer-primary.js +25 -0
  15. package/client/decision-transport.d.ts +48 -0
  16. package/client/decision-transport.js +102 -0
  17. package/client/decode-composer-draft.d.ts +11 -0
  18. package/client/decode-composer-draft.js +12 -0
  19. package/client/encode-composer-draft.d.ts +9 -0
  20. package/client/encode-composer-draft.js +34 -0
  21. package/client/encode-files.d.ts +18 -0
  22. package/client/encode-files.js +54 -0
  23. package/client/failure.d.ts +32 -0
  24. package/client/failure.js +34 -0
  25. package/client/generation-selection.d.ts +15 -0
  26. package/client/generation-selection.js +24 -0
  27. package/client/host-call.d.ts +51 -0
  28. package/client/host-call.js +42 -0
  29. package/client/host-transport.d.ts +68 -0
  30. package/client/host-transport.js +74 -0
  31. package/client/image-output.d.ts +6 -0
  32. package/client/image-output.js +13 -0
  33. package/client/index.d.ts +20 -0
  34. package/client/index.js +18 -0
  35. package/client/ink-waveform.d.ts +17 -0
  36. package/client/ink-waveform.js +69 -0
  37. package/client/live/apply-live-turn-tool-event.d.ts +14 -0
  38. package/client/live/apply-live-turn-tool-event.js +39 -0
  39. package/client/live/live-captions.d.ts +23 -0
  40. package/client/live/live-captions.js +70 -0
  41. package/client/live/live-mic-forward.d.ts +29 -0
  42. package/client/live/live-mic-forward.js +37 -0
  43. package/client/live/live-state.d.ts +11 -0
  44. package/client/live/live-state.js +20 -0
  45. package/client/live/live-tool.d.ts +11 -0
  46. package/client/live/live-tool.js +1 -0
  47. package/client/live/live-video.d.ts +10 -0
  48. package/client/live/live-video.js +71 -0
  49. package/client/live/run-live-tool-call.d.ts +17 -0
  50. package/client/live/run-live-tool-call.js +32 -0
  51. package/client/live-client.d.ts +141 -0
  52. package/client/live-client.js +638 -0
  53. package/client/live-errors.d.ts +2 -0
  54. package/client/live-errors.js +9 -0
  55. package/client/live-messages.d.ts +110 -0
  56. package/client/live-messages.js +67 -0
  57. package/client/mic-capture.d.ts +4 -0
  58. package/client/mic-capture.js +6 -0
  59. package/client/oauth-popup.d.ts +26 -0
  60. package/client/oauth-popup.js +30 -0
  61. package/client/pcm-downsample.d.ts +4 -0
  62. package/client/pcm-downsample.js +45 -0
  63. package/client/run-commit.d.ts +61 -0
  64. package/client/run-commit.js +124 -0
  65. package/client/run-session.d.ts +80 -0
  66. package/client/run-session.js +215 -0
  67. package/client/schema-fields.d.ts +63 -0
  68. package/client/schema-fields.js +178 -0
  69. package/client/shaped-data.d.ts +118 -0
  70. package/client/shaped-data.js +314 -0
  71. package/client/source-citations.d.ts +13 -0
  72. package/client/source-citations.js +56 -0
  73. package/client/state-fields.d.ts +16 -0
  74. package/client/state-fields.js +44 -0
  75. package/client/tool-result.d.ts +78 -0
  76. package/client/tool-result.js +275 -0
  77. package/client/tool-resume.d.ts +33 -0
  78. package/client/tool-resume.js +11 -0
  79. package/client/trace-feed.d.ts +13 -0
  80. package/client/trace-feed.js +18 -0
  81. package/client/trace-story.d.ts +163 -0
  82. package/client/trace-story.js +416 -0
  83. package/client/trace-view.d.ts +130 -0
  84. package/client/trace-view.js +293 -0
  85. package/client/transcript-groups.d.ts +114 -0
  86. package/client/transcript-groups.js +167 -0
  87. package/client/transcript-scroll.d.ts +7 -0
  88. package/client/transcript-scroll.js +6 -0
  89. package/client/transport.d.ts +176 -0
  90. package/client/transport.js +256 -0
  91. package/client/turn-client.d.ts +35 -0
  92. package/client/turn-client.js +93 -0
  93. package/client/voice-label.d.ts +2 -0
  94. package/client/voice-label.js +15 -0
  95. package/client/voice-recorder.d.ts +27 -0
  96. package/client/voice-recorder.js +165 -0
  97. package/client/walk-away.d.ts +34 -0
  98. package/client/walk-away.js +48 -0
  99. package/client/wire-line.d.ts +56 -0
  100. package/client/wire-line.js +90 -0
  101. package/client/worklets/mic-capture.js +16 -0
  102. package/components/InkWaveform.d.ts +10 -0
  103. package/components/InkWaveform.js +108 -0
  104. package/components/live/use-live-runner-controls.d.ts +48 -0
  105. package/components/live/use-live-runner-controls.js +100 -0
  106. package/components/live/use-live-runner-model.d.ts +45 -0
  107. package/components/live/use-live-runner-model.js +175 -0
  108. package/components/live/use-live-runner-ui.d.ts +65 -0
  109. package/components/live/use-live-runner-ui.js +159 -0
  110. package/components/live/use-live-session-client.d.ts +41 -0
  111. package/components/live/use-live-session-client.js +123 -0
  112. package/components/use-composer-voice.d.ts +28 -0
  113. package/components/use-composer-voice.js +90 -0
  114. package/components/use-editor-selection.d.ts +3 -0
  115. package/components/use-editor-selection.js +16 -0
  116. package/components/use-voice-playback.d.ts +16 -0
  117. package/components/use-voice-playback.js +68 -0
  118. package/hooks/use-described.d.ts +8 -0
  119. package/hooks/use-described.js +23 -0
  120. package/hooks/use-theorem-chat-actions.d.ts +75 -0
  121. package/hooks/use-theorem-chat-actions.js +265 -0
  122. package/hooks/use-theorem-chat-state.d.ts +66 -0
  123. package/hooks/use-theorem-chat-state.js +118 -0
  124. package/hooks/use-theorem-chat.d.ts +63 -0
  125. package/hooks/use-theorem-chat.js +340 -0
  126. package/hooks/use-theorem-decision.d.ts +23 -0
  127. package/hooks/use-theorem-decision.js +75 -0
  128. package/hooks/use-theorem-host.d.ts +44 -0
  129. package/hooks/use-theorem-host.js +117 -0
  130. package/hooks/use-theorem-interface.d.ts +19 -0
  131. package/hooks/use-theorem-interface.js +25 -0
  132. package/hooks/use-trace-records.d.ts +4 -0
  133. package/hooks/use-trace-records.js +7 -0
  134. package/index.d.ts +14 -0
  135. package/index.js +14 -0
  136. package/live.d.ts +8 -0
  137. package/live.js +10 -0
  138. package/package.json +73 -4
  139. package/server/credential-store.d.ts +24 -0
  140. package/server/credential-store.js +14 -0
  141. package/server/decision-handler.d.ts +34 -0
  142. package/server/decision-handler.js +55 -0
  143. package/server/handler.d.ts +114 -0
  144. package/server/handler.js +568 -0
  145. package/server/json-response.d.ts +2 -0
  146. package/server/json-response.js +7 -0
  147. package/server/mod.d.ts +16 -0
  148. package/server/mod.js +15 -0
  149. package/server/request-check.d.ts +16 -0
  150. package/server/request-check.js +34 -0
  151. package/server/session-store.d.ts +74 -0
  152. package/server/session-store.js +49 -0
  153. package/server/steer-inbox.d.ts +35 -0
  154. package/server/steer-inbox.js +52 -0
  155. package/server/walk-away.d.ts +22 -0
  156. package/server/walk-away.js +41 -0
  157. package/styles/ink-controls.css +134 -0
  158. package/ui/ChatComposerBar.d.ts +39 -0
  159. package/ui/ChatComposerBar.js +251 -0
  160. package/ui/ChatTranscript.d.ts +30 -0
  161. package/ui/ChatTranscript.js +474 -0
  162. package/ui/ConsoleFrame.d.ts +47 -0
  163. package/ui/ConsoleFrame.js +46 -0
  164. package/ui/DecisionAnswers.d.ts +15 -0
  165. package/ui/DecisionAnswers.js +102 -0
  166. package/ui/LiveRunner.d.ts +28 -0
  167. package/ui/LiveRunner.js +193 -0
  168. package/ui/SchemaFields.d.ts +10 -0
  169. package/ui/SchemaFields.js +122 -0
  170. package/ui/ShapedData.d.ts +11 -0
  171. package/ui/ShapedData.js +151 -0
  172. package/ui/SidePanel.d.ts +57 -0
  173. package/ui/SidePanel.js +50 -0
  174. package/ui/StateFields.d.ts +36 -0
  175. package/ui/StateFields.js +96 -0
  176. package/ui/TheoremChat.d.ts +71 -0
  177. package/ui/TheoremChat.js +146 -0
  178. package/ui/TheoremDecision.d.ts +38 -0
  179. package/ui/TheoremDecision.js +87 -0
  180. package/ui/TheoremHost.d.ts +38 -0
  181. package/ui/TheoremHost.js +150 -0
  182. package/ui/ToolGateCard.d.ts +28 -0
  183. package/ui/ToolGateCard.js +100 -0
  184. package/ui/ToolResult.d.ts +10 -0
  185. package/ui/ToolResult.js +60 -0
  186. package/ui/ToolResultCharts.d.ts +5 -0
  187. package/ui/ToolResultCharts.js +86 -0
  188. package/ui/TraceCharts.d.ts +13 -0
  189. package/ui/TraceCharts.js +235 -0
  190. package/ui/TraceGuardrails.d.ts +10 -0
  191. package/ui/TraceGuardrails.js +56 -0
  192. package/ui/TraceInspector.d.ts +6 -0
  193. package/ui/TraceInspector.js +189 -0
  194. package/ui/TraceInspectorPanel.d.ts +24 -0
  195. package/ui/TraceInspectorPanel.js +57 -0
  196. package/ui/TraceOverview.d.ts +34 -0
  197. package/ui/TraceOverview.js +96 -0
  198. package/ui/TraceSpanDetail.d.ts +10 -0
  199. package/ui/TraceSpanDetail.js +188 -0
  200. package/ui/TraceStory.d.ts +20 -0
  201. package/ui/TraceStory.js +107 -0
  202. package/ui/TraceValues.d.ts +36 -0
  203. package/ui/TraceValues.js +113 -0
  204. package/ui/TraceWaterfall.d.ts +13 -0
  205. package/ui/TraceWaterfall.js +132 -0
  206. package/ui/VoiceNote.d.ts +12 -0
  207. package/ui/VoiceNote.js +28 -0
  208. package/ui/built/theme.css +774 -0
  209. package/ui/built/theorem.d.ts +13 -0
  210. package/ui/built/theorem.js +631 -0
  211. package/ui/built/theorem.variants.d.ts +16 -0
  212. package/ui/disclosure-motion.d.ts +6 -0
  213. package/ui/disclosure-motion.js +163 -0
  214. package/ui/icons.d.ts +4 -0
  215. package/ui/icons.js +37 -0
  216. package/ui/index.d.ts +25 -0
  217. package/ui/index.js +29 -0
  218. package/ui/labels-provider.d.ts +16 -0
  219. package/ui/labels-provider.js +61 -0
  220. package/ui/labels.d.ts +1153 -0
  221. package/ui/labels.js +603 -0
  222. package/ui/theme.d.ts +10 -0
  223. package/ui/theme.js +13 -0
  224. package/ui/theorem-theme.d.ts +10 -0
  225. package/ui/theorem-theme.js +83 -0
  226. package/ui/trace-palette.d.ts +24 -0
  227. package/ui/trace-palette.js +24 -0
  228. package/ui/trace-patterns.d.ts +32 -0
  229. package/ui/trace-patterns.js +44 -0
  230. package/ui/transcript-copy-text.d.ts +4 -0
  231. package/ui/transcript-copy-text.js +39 -0
@@ -0,0 +1,416 @@
1
+ import { inlineContent, traceAttributeMeta, traceEventAttributeMeta, traceEventMeta } from '@theoremjs/agents';
2
+ import { isRecord } from '@theoremjs/agents/kernel';
3
+ import { nanosToMs, traceSpans, traceTotals } from "./trace-view.js";
4
+ export function traceTurns(tree) {
5
+ return tree.map((node) => ({
6
+ node,
7
+ totals: traceTotals([node]),
8
+ split: traceTimeSplit(node),
9
+ failed: node.span.status.code === 'ERROR',
10
+ }));
11
+ }
12
+ /** A host's trace: each root is one tool call, not a conversation's turn. */
13
+ export function isCallTrace(turns) {
14
+ return turns.length > 0 && turns.every((turn) => TOOL_TYPES.has(turn.node.meta.type));
15
+ }
16
+ /** A turn named by what the person asked, a host's call by its tool; a failure by its outcome, else the answer. */
17
+ export function traceTurnRow(turn, isCall) {
18
+ const story = traceStory(turn.node);
19
+ const ask = story.find((step) => step.kind === 'ask');
20
+ const answer = story.findLast((step) => step.kind === 'answer');
21
+ const title = isCall ? turn.node.meta.subject : ask?.kind === 'ask' ? ask.text : undefined;
22
+ const outcome = turn.failed ? traceOutcome(turn.node) : undefined;
23
+ const answered = answer?.kind === 'answer' ? answer.text : undefined;
24
+ return {
25
+ actor: turn.failed ? 'error' : isCall ? 'tool' : 'user',
26
+ ...(title ? { title } : {}),
27
+ ...(outcome ? { outcome } : answered ? { answered } : {}),
28
+ };
29
+ }
30
+ export function traceActor(node) {
31
+ if (node.span.status.code === 'ERROR')
32
+ return 'error';
33
+ switch (node.meta.type) {
34
+ case 'call':
35
+ case 'response':
36
+ case 'decision':
37
+ return 'model';
38
+ case 'tool':
39
+ return 'tool';
40
+ case 'cutout':
41
+ case 'host':
42
+ return 'host';
43
+ default:
44
+ return 'theorem';
45
+ }
46
+ }
47
+ /** Events worth a line of their own: something acted, retried, waited or warned. */
48
+ const STORY_EVENTS = new Set([
49
+ 'theorem.guardrail',
50
+ 'theorem.attempt.retry',
51
+ 'theorem.gate',
52
+ 'theorem.tool.warning',
53
+ 'theorem.tool.cancel',
54
+ ]);
55
+ /** Event keys whose value reads as a detail of the line, in this order. */
56
+ const EVENT_DETAIL_KEYS = ['action', 'stage', 'kind', 'reason', 'code'];
57
+ function stepOf(node) {
58
+ const step = node.span.attributes['theorem.step'];
59
+ return typeof step === 'number' ? step : undefined;
60
+ }
61
+ function messagesOf(node, key) {
62
+ const value = inlineContent(node.record, node.span.attributes[key]);
63
+ return Array.isArray(value) ? value.filter(isRecord) : [];
64
+ }
65
+ function partsText(parts) {
66
+ if (!Array.isArray(parts))
67
+ return '';
68
+ return parts
69
+ .filter((part) => isRecord(part) && part.type === 'text' && typeof part.content === 'string')
70
+ .map((part) => part.content)
71
+ .join('\n')
72
+ .trim();
73
+ }
74
+ /** The last message's text from `role`, read from a span's input or output messages. */
75
+ export function messageText(node, key, role) {
76
+ const found = messagesOf(node, key).filter((message) => message.role === role);
77
+ for (let index = found.length - 1; index >= 0; index -= 1) {
78
+ const text = partsText(found[index]?.parts);
79
+ if (text)
80
+ return text;
81
+ }
82
+ return '';
83
+ }
84
+ /** A model call that wrote text back (rather than only asking for tools). */
85
+ function wroteText(node) {
86
+ return messageText(node, 'gen_ai.output.messages', 'assistant') !== '';
87
+ }
88
+ function eventDetail(name, attributes) {
89
+ return EVENT_DETAIL_KEYS.flatMap((key) => {
90
+ const value = attributes[key];
91
+ if (typeof value !== 'string')
92
+ return [];
93
+ const meta = traceEventAttributeMeta(name, key);
94
+ return [meta?.options?.[value]?.label ?? value];
95
+ });
96
+ }
97
+ function isQuiet(name, attributes) {
98
+ return name === 'theorem.guardrail' && attributes.action === 'allow';
99
+ }
100
+ function storyEvents(node, turnStart) {
101
+ return node.span.events.flatMap((event, index) => {
102
+ if (!STORY_EVENTS.has(event.name) || isQuiet(event.name, event.attributes))
103
+ return [];
104
+ const meta = traceEventMeta(event.name);
105
+ return [
106
+ {
107
+ kind: 'event',
108
+ id: `${node.id}:event:${index}`,
109
+ node,
110
+ offsetMs: nanosToMs(event.timeUnixNano) - turnStart,
111
+ label: meta?.label ?? event.name,
112
+ detail: eventDetail(event.name, event.attributes),
113
+ doc: meta?.doc ?? event.name,
114
+ },
115
+ ];
116
+ });
117
+ }
118
+ /**
119
+ * A turn as it happened: what the user asked, each model call and the tool
120
+ * calls it asked for (nested under it), the events that changed the course,
121
+ * and how it ended. HTTP tries stay in the timeline; they are how a call
122
+ * travelled, not a step of the story.
123
+ */
124
+ export function traceStory(root) {
125
+ const start = root.startMs;
126
+ const spans = traceSpans(root.children)
127
+ .filter((node) => node.meta.type !== 'http')
128
+ .sort((a, b) => a.startMs - b.startMs);
129
+ const toolsByStep = new Map();
130
+ for (const node of spans) {
131
+ const step = stepOf(node);
132
+ if (node.meta.type === 'tool' && step !== undefined)
133
+ toolsByStep.set(step, (toolsByStep.get(step) ?? 0) + 1);
134
+ }
135
+ const calledSteps = new Set();
136
+ const steps = [];
137
+ const asked = messageText(root, 'gen_ai.input.messages', 'user');
138
+ if (asked)
139
+ steps.push({ kind: 'ask', id: `${root.id}:ask`, node: root, offsetMs: 0, text: asked });
140
+ steps.push(...storyEvents(root, start).filter((step) => step.offsetMs <= 0));
141
+ for (const node of spans) {
142
+ const step = stepOf(node);
143
+ const isCall = node.meta.type === 'call' || node.meta.type === 'response';
144
+ if (isCall && step !== undefined)
145
+ calledSteps.add(step);
146
+ steps.push({
147
+ kind: 'span',
148
+ id: node.id,
149
+ node,
150
+ offsetMs: node.startMs - start,
151
+ nested: node.meta.type === 'tool' && step !== undefined && calledSteps.has(step),
152
+ requested: isCall && step !== undefined && !wroteText(node) ? (toolsByStep.get(step) ?? 0) : 0,
153
+ });
154
+ steps.push(...storyEvents(node, start));
155
+ }
156
+ const rootEvents = storyEvents(root, start).filter((step) => step.offsetMs > 0);
157
+ const ordered = [...steps, ...rootEvents].sort((a, b) => a.offsetMs - b.offsetMs || rank(a) - rank(b));
158
+ if (root.meta.type === 'turn') {
159
+ const answer = messageText(root, 'gen_ai.output.messages', 'assistant');
160
+ ordered.push({
161
+ kind: 'answer',
162
+ id: `${root.id}:answer`,
163
+ node: root,
164
+ offsetMs: root.durationMs,
165
+ ...(answer && { text: answer }),
166
+ });
167
+ }
168
+ return ordered;
169
+ }
170
+ /** Keeps the question first and a span ahead of the events it recorded at the same instant. */
171
+ function rank(step) {
172
+ return step.kind === 'ask' ? 0 : step.kind === 'span' ? 1 : 2;
173
+ }
174
+ /** A tool call's arguments as `[key, value]` pairs of plain values, for a one-line summary. */
175
+ export function toolArguments(node) {
176
+ const stored = inlineContent(node.record, node.span.attributes['gen_ai.tool.call.arguments']);
177
+ let value = stored;
178
+ if (typeof stored === 'string') {
179
+ try {
180
+ value = JSON.parse(stored);
181
+ }
182
+ catch {
183
+ return stored ? [['', stored]] : [];
184
+ }
185
+ }
186
+ if (!isRecord(value))
187
+ return [];
188
+ return Object.entries(value).flatMap(([key, item]) => typeof item === 'string' || typeof item === 'number' || typeof item === 'boolean' ? [[key, String(item)]] : []);
189
+ }
190
+ /** A stored JSON or text value, parsed when it is JSON. */
191
+ export function storedValue(node, key) {
192
+ const value = inlineContent(node.record, node.span.attributes[key]);
193
+ if (typeof value !== 'string')
194
+ return value;
195
+ try {
196
+ return JSON.parse(value);
197
+ }
198
+ catch {
199
+ return value;
200
+ }
201
+ }
202
+ function optionOf(key, value) {
203
+ return typeof value === 'string' ? traceAttributeMeta(key)?.options?.[value] : undefined;
204
+ }
205
+ export function traceOutcome(node) {
206
+ const { attributes, status } = node.span;
207
+ const outcome = attributes['theorem.tool.outcome'];
208
+ const code = attributes['theorem.tool.failure.code'];
209
+ const codePart = typeof code === 'string' ? { code } : {};
210
+ const tool = outcome !== 'ok' ? optionOf('theorem.tool.outcome', outcome) : undefined;
211
+ if (tool)
212
+ return { ...tool, ...codePart, tone: status.code === 'ERROR' || outcome === 'error' ? 'error' : 'warning' };
213
+ if (status.code !== 'ERROR')
214
+ return undefined;
215
+ const type = attributes['error.type'];
216
+ const kind = optionOf('error.type', type);
217
+ if (kind)
218
+ return { ...kind, ...codePart, tone: 'error' };
219
+ return { label: typeof type === 'string' ? type : (status.message ?? ''), ...codePart, tone: 'error' };
220
+ }
221
+ function guardrailHits(value) {
222
+ if (!Array.isArray(value))
223
+ return [];
224
+ const fields = traceEventAttributeMeta('theorem.guardrail', 'hits')?.fields;
225
+ return value.filter(isRecord).flatMap((hit) => {
226
+ if (typeof hit.rule !== 'string')
227
+ return [];
228
+ const severity = typeof hit.severity === 'string' ? hit.severity : '';
229
+ // A host names its own rules on the hit; Theorem's are named in the catalog.
230
+ const rule = fields?.rule?.options?.[hit.rule];
231
+ const ruleDoc = typeof hit.doc === 'string' ? hit.doc : rule?.doc;
232
+ return [
233
+ {
234
+ rule: hit.rule,
235
+ ruleLabel: typeof hit.label === 'string' ? hit.label : (rule?.label ?? hit.rule),
236
+ ...(ruleDoc && { ruleDoc }),
237
+ severity,
238
+ severityLabel: fields?.severity?.options?.[severity]?.label ?? severity,
239
+ ...(typeof hit.match === 'string' && { match: hit.match }),
240
+ },
241
+ ];
242
+ });
243
+ }
244
+ function eventOption(key, value) {
245
+ return traceEventAttributeMeta('theorem.guardrail', key)?.options?.[value];
246
+ }
247
+ /** Every guardrail check the turn recorded, in the order they ran: the passes as well as what they caught. */
248
+ export function traceGuardrails(root) {
249
+ return traceSpans([root])
250
+ .flatMap((node) => node.span.events.flatMap((event, index) => {
251
+ if (event.name !== 'theorem.guardrail')
252
+ return [];
253
+ const { attributes } = event;
254
+ const stage = String(attributes.stage ?? '');
255
+ const action = String(attributes.action ?? '');
256
+ const actionMeta = eventOption('action', action);
257
+ const check = typeof attributes.check === 'string' ? attributes.check : undefined;
258
+ const provenance = isRecord(attributes.provenance) ? attributes.provenance : undefined;
259
+ const reason = inlineContent(node.record, attributes.error);
260
+ return [
261
+ {
262
+ id: `${node.id}:guardrail:${index}`,
263
+ node,
264
+ offsetMs: nanosToMs(event.timeUnixNano) - root.startMs,
265
+ stage,
266
+ stageLabel: eventOption('stage', stage)?.label ?? stage,
267
+ ...(check && { checkLabel: eventOption('check', check)?.label ?? check }),
268
+ action,
269
+ actionLabel: actionMeta?.label ?? action,
270
+ ...(actionMeta && { actionDoc: actionMeta.doc }),
271
+ ...(typeof attributes.duration_ms === 'number' && { durationMs: attributes.duration_ms }),
272
+ hits: guardrailHits(attributes.hits),
273
+ ...(typeof provenance?.tool === 'string' && { tool: provenance.tool }),
274
+ ...(typeof reason === 'string' && reason && { reason }),
275
+ },
276
+ ];
277
+ }))
278
+ .sort((a, b) => a.offsetMs - b.offsetMs);
279
+ }
280
+ function union(intervals) {
281
+ const sorted = [...intervals].sort((a, b) => a[0] - b[0]);
282
+ let total = 0;
283
+ let end = Number.NEGATIVE_INFINITY;
284
+ for (const [from, to] of sorted) {
285
+ if (to <= end)
286
+ continue;
287
+ total += to - Math.max(from, end);
288
+ end = to;
289
+ }
290
+ return total;
291
+ }
292
+ function intervalsOf(nodes, types) {
293
+ return nodes.filter((node) => types.has(node.meta.type)).map((node) => [node.startMs, node.startMs + node.durationMs]);
294
+ }
295
+ const MODEL_TYPES = new Set(['call', 'response', 'decision']);
296
+ const TOOL_TYPES = new Set(['tool']);
297
+ /** Milliseconds a span's events of `name` recorded under `key`. */
298
+ function eventMs(node, name, key) {
299
+ return node.span.events.reduce((total, event) => {
300
+ const value = event.name === name ? event.attributes[key] : undefined;
301
+ return total + (typeof value === 'number' ? value : 0);
302
+ }, 0);
303
+ }
304
+ /** Milliseconds all of `nodes`' events of `name` recorded under `key`. */
305
+ function eventsMs(nodes, name, key) {
306
+ return nodes.reduce((total, node) => total + eventMs(node, name, key), 0);
307
+ }
308
+ /**
309
+ * Where a turn's time went. Guardrail checks and hooks run inside the turn's
310
+ * gaps and inside tool calls; their recorded time moves out of those into
311
+ * their own share, so the model and tools keep only their own work.
312
+ */
313
+ function traceTimeSplit(root) {
314
+ // A host's call is its own root: the tool span is the whole trace.
315
+ const spans = TOOL_TYPES.has(root.meta.type) ? [root, ...traceSpans(root.children)] : traceSpans(root.children);
316
+ const model = intervalsOf(spans, MODEL_TYPES);
317
+ const tools = intervalsOf(spans, TOOL_TYPES);
318
+ const toolNodes = spans.filter((node) => TOOL_TYPES.has(node.meta.type));
319
+ const streamGuard = spans.reduce((total, node) => {
320
+ const value = node.span.attributes['theorem.guardrail.stream_ms'];
321
+ return total + (typeof value === 'number' ? value : 0);
322
+ }, 0);
323
+ const modelMs = union(model);
324
+ const toolsMs = union(tools);
325
+ const toolGuard = Math.min(toolsMs, eventsMs(toolNodes, 'theorem.guardrail', 'duration_ms'));
326
+ const toolHooks = Math.min(toolsMs - toolGuard, eventsMs(toolNodes, 'theorem.stage', 'hook_ms'));
327
+ const between = Math.max(0, root.durationMs - union([...model, ...tools]));
328
+ const turnGuard = Math.min(between, eventMs(root, 'theorem.guardrail', 'duration_ms'));
329
+ const turnHooks = Math.min(between - turnGuard, eventMs(root, 'theorem.stage', 'hook_ms'));
330
+ return {
331
+ model: Math.max(0, modelMs - streamGuard),
332
+ tools: toolsMs - toolGuard - toolHooks,
333
+ guardrails: turnGuard + Math.min(streamGuard, modelMs) + toolGuard,
334
+ hooks: turnHooks + toolHooks,
335
+ other: between - turnGuard - turnHooks,
336
+ };
337
+ }
338
+ const MS_PER_S = 1000;
339
+ function secondsAttribute(node, key) {
340
+ const value = node.span.attributes[key];
341
+ return typeof value === 'number' ? value * MS_PER_S : undefined;
342
+ }
343
+ export function traceLatency(root) {
344
+ const spans = traceSpans([root]);
345
+ const calls = spans.filter((node) => MODEL_TYPES.has(node.meta.type));
346
+ const firstTextMs = secondsAttribute(root, 'theorem.turn.time_to_first_text');
347
+ // When the model first wrote, measured from the turn's start.
348
+ const written = calls
349
+ .map((node) => {
350
+ const at = secondsAttribute(node, 'theorem.response.time_to_first_text');
351
+ return at === undefined ? undefined : { node, at };
352
+ })
353
+ .filter((entry) => entry !== undefined);
354
+ const firstWritten = Math.min(...written.map(({ node, at }) => node.startMs - root.startMs + at));
355
+ let tokens = 0;
356
+ let writingMs = 0;
357
+ for (const { node, at } of written) {
358
+ const output = node.span.attributes['gen_ai.usage.output_tokens'];
359
+ if (typeof output !== 'number' || node.durationMs <= at)
360
+ continue;
361
+ tokens += output;
362
+ writingMs += node.durationMs - at;
363
+ }
364
+ let checks = 0;
365
+ let flagged = 0;
366
+ let guardMs = 0;
367
+ for (const node of spans) {
368
+ let streamChecksMs = 0;
369
+ for (const event of node.span.events) {
370
+ if (event.name !== 'theorem.guardrail')
371
+ continue;
372
+ checks += 1;
373
+ if (event.attributes.action !== 'allow')
374
+ flagged += 1;
375
+ const took = event.attributes.duration_ms;
376
+ if (typeof took !== 'number')
377
+ continue;
378
+ guardMs += took;
379
+ if (typeof event.attributes.runs === 'number')
380
+ streamChecksMs += took;
381
+ }
382
+ // A call's stream checks carry their own time; the call's total adds only what they do not (a trace from before they did).
383
+ const stream = node.span.attributes['theorem.guardrail.stream_ms'];
384
+ if (typeof stream === 'number')
385
+ guardMs += Math.max(0, stream - streamChecksMs);
386
+ }
387
+ return {
388
+ ...(firstTextMs !== undefined && { firstTextMs }),
389
+ ...(firstTextMs !== undefined && Number.isFinite(firstWritten) && { heldMs: Math.max(0, firstTextMs - firstWritten) }),
390
+ ...(writingMs > 0 && { tokensPerSecond: tokens / (writingMs / MS_PER_S) }),
391
+ ...(checks > 0 && { guardrails: { checks, ms: guardMs, flagged } }),
392
+ };
393
+ }
394
+ export function traceBars(root) {
395
+ const bars = [];
396
+ const walk = (node, depth) => {
397
+ const from = node.startMs - root.startMs;
398
+ bars.push({ node, depth, from, to: from + node.durationMs });
399
+ for (const child of node.children)
400
+ walk(child, depth + 1);
401
+ };
402
+ walk(root, 0);
403
+ return bars;
404
+ }
405
+ /** How busy the model and the tools were across the turn, in `buckets` equal slices (0 to 1 each). */
406
+ export function traceActivity(root, buckets) {
407
+ const spans = traceSpans(root.children);
408
+ const model = intervalsOf(spans, MODEL_TYPES);
409
+ const tools = intervalsOf(spans, TOOL_TYPES);
410
+ const width = Math.max(root.durationMs, 1) / buckets;
411
+ const share = (intervals, from, to) => Math.min(1, union(intervals.map(([a, b]) => [Math.max(a, from), Math.min(b, to)]).filter(([a, b]) => b > a)) / width);
412
+ return Array.from({ length: buckets + 1 }, (_, index) => {
413
+ const from = root.startMs + index * width;
414
+ return { at: index * width, model: share(model, from, from + width), tools: share(tools, from, from + width) };
415
+ });
416
+ }
@@ -0,0 +1,130 @@
1
+ import { type TraceAttributeMeta, type TraceRecord, type TraceSpan, type TraceSpanMeta } from '@theoremjs/agents';
2
+ /**
3
+ * The trace panel's reading of the records a run delivered: one span tree,
4
+ * totals over it, and search over its spans. Every word comes from the trace
5
+ * catalog; the panel only lays it out.
6
+ *
7
+ * @module
8
+ */
9
+ /** One span, placed: the record that carries it, what it is, its timing, and the spans inside it. */
10
+ export interface TraceNode {
11
+ /** `traceId:spanId`, unique across records. */
12
+ id: string;
13
+ span: TraceSpan;
14
+ record: TraceRecord;
15
+ meta: TraceSpanMeta;
16
+ /** Unix milliseconds. */
17
+ startMs: number;
18
+ durationMs: number;
19
+ /** In start order. */
20
+ children: TraceNode[];
21
+ }
22
+ /** A unix-nanosecond timestamp string as unix milliseconds. */
23
+ export declare function nanosToMs(nanos: string): number;
24
+ /**
25
+ * Every span of `records` as one tree. A span written twice keeps its later
26
+ * copy; a span whose parent has not arrived yet (a Live response before its
27
+ * session root) is a root until the parent does.
28
+ */
29
+ export declare function traceTree(records: readonly TraceRecord[]): TraceNode[];
30
+ /** Every node of `nodes`, depth first. */
31
+ export declare function traceSpans(nodes: readonly TraceNode[]): TraceNode[];
32
+ /** A sum over the model calls; `complete` is false when some call did not report it. */
33
+ export interface TraceSum {
34
+ value: number;
35
+ complete: boolean;
36
+ }
37
+ /** A token sum; `estimated` when THEOREM estimated some call's count rather than the provider reporting it. */
38
+ export interface TraceTokenSum extends TraceSum {
39
+ estimated: boolean;
40
+ }
41
+ /**
42
+ * Totals over a set of spans. Usage and cost sum the model calls (and Live
43
+ * responses) themselves, not the roots, so a specialist's or a compaction's
44
+ * calls count once. A total no call reported is absent: unknown, not zero.
45
+ */
46
+ export interface TraceTotals {
47
+ spans: number;
48
+ errors: number;
49
+ /** First start to last end. */
50
+ durationMs: number;
51
+ /** Model calls and Live responses. */
52
+ calls: number;
53
+ input?: TraceTokenSum;
54
+ cached?: TraceSum;
55
+ output?: TraceTokenSum;
56
+ cost?: TraceSum;
57
+ }
58
+ export declare function traceTotals(nodes: readonly TraceNode[]): TraceTotals;
59
+ /** How a search field compares: one of a set, a number, text, or yes/no. */
60
+ export type TraceSearchKind = 'options' | 'number' | 'text' | 'flag';
61
+ /** The operators each kind offers, by PowerSearch operator key. */
62
+ export declare const TRACE_SEARCH_OPERATORS: {
63
+ readonly options: readonly ["isAnyOf", "isNoneOf"];
64
+ readonly number: readonly ["greaterThan", "lessThan"];
65
+ readonly text: readonly ["contains", "notContains"];
66
+ readonly flag: readonly ["isTrue", "isFalse"];
67
+ };
68
+ /** One searchable field of a span, worded by the catalog. */
69
+ export interface TraceSearchField {
70
+ key: string;
71
+ label: string;
72
+ doc: string;
73
+ kind: TraceSearchKind;
74
+ /** The attribute group's label; absent for a span's own fields and for keys the catalog does not name. */
75
+ group?: string;
76
+ /** For `options`: each value seen, with its meaning when the catalog knows it. */
77
+ options?: readonly TraceSearchOption[];
78
+ /** For a number field: its unit, when it has one. */
79
+ format?: TraceAttributeMeta['format'];
80
+ }
81
+ export interface TraceSearchOption {
82
+ value: string;
83
+ label: string;
84
+ doc?: string;
85
+ }
86
+ /** The field that searches any text a span holds: its name, its values, its stored text. */
87
+ export declare const TRACE_TEXT_FIELD = "text";
88
+ /** A filter as PowerSearch emits it. */
89
+ export interface TraceFilter {
90
+ readonly field: string;
91
+ readonly operator: string;
92
+ readonly value: {
93
+ readonly type: string;
94
+ readonly value?: unknown;
95
+ };
96
+ }
97
+ /**
98
+ * The fields a search over `nodes` offers: type, status, duration and events,
99
+ * then every attribute the spans recorded that compares as a value. Stored
100
+ * text, JSON and structured values are reached by the text field.
101
+ */
102
+ export declare function traceSearchFields(nodes: readonly TraceNode[]): TraceSearchField[];
103
+ /**
104
+ * The tree cut to the spans that meet every filter, each kept under its
105
+ * ancestors so it reads in place; `matches` counts the spans that met them.
106
+ */
107
+ export declare function filterTraceTree(nodes: readonly TraceNode[], filters: readonly TraceFilter[]): {
108
+ nodes: TraceNode[];
109
+ matches: number;
110
+ };
111
+ /** The ids of the spans under `nodes` that meet every filter; undefined when there are none to meet. */
112
+ export declare function traceMatchIds(nodes: readonly TraceNode[], filters: readonly TraceFilter[]): Set<string> | undefined;
113
+ /** How to show an attribute value: by its catalog entry first, then by its shape. */
114
+ export type TraceValueShape = {
115
+ kind: 'stored';
116
+ } | {
117
+ kind: 'text';
118
+ value: string;
119
+ } | {
120
+ kind: 'scalar';
121
+ value: number | boolean;
122
+ } | {
123
+ kind: 'list';
124
+ value: string[];
125
+ } | {
126
+ kind: 'fields';
127
+ fields: Readonly<Record<string, TraceAttributeMeta>>;
128
+ items: Record<string, unknown>[];
129
+ };
130
+ export declare function traceValueShape(meta: TraceAttributeMeta | undefined, value: unknown): TraceValueShape;