@theoremjs/react 0.3.0 → 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.
Files changed (258) hide show
  1. package/README.md +728 -202
  2. package/client/composer-hints.js +1 -1
  3. package/client/decision-images.d.ts +76 -0
  4. package/client/decision-images.js +139 -0
  5. package/client/decision-transport.d.ts +26 -4
  6. package/client/decision-transport.js +79 -6
  7. package/client/embedding-input.d.ts +84 -0
  8. package/client/embedding-input.js +194 -0
  9. package/client/embedding-space.d.ts +82 -0
  10. package/client/embedding-space.js +389 -0
  11. package/client/embedding-transport.d.ts +40 -0
  12. package/client/embedding-transport.js +71 -0
  13. package/client/failure.d.ts +7 -10
  14. package/client/failure.js +9 -9
  15. package/client/host-call.d.ts +1 -1
  16. package/client/host-call.js +7 -2
  17. package/client/host-transport.d.ts +2 -0
  18. package/client/host-transport.js +3 -2
  19. package/client/index.d.ts +19 -11
  20. package/client/index.js +14 -10
  21. package/client/ink-waveform.d.ts +1 -1
  22. package/client/ink-waveform.js +1 -2
  23. package/client/live/apply-live-turn-tool-event.js +1 -1
  24. package/client/live/live-captions.d.ts +30 -0
  25. package/client/live/live-captions.js +105 -1
  26. package/client/live/live-mic-forward.d.ts +1 -0
  27. package/client/live/live-mic-forward.js +11 -2
  28. package/client/live/live-page-tool.d.ts +34 -0
  29. package/client/live/live-page-tool.js +11 -0
  30. package/client/live/live-state.d.ts +4 -2
  31. package/client/live/live-state.js +2 -0
  32. package/client/live/live-video.js +1 -1
  33. package/client/live/run-live-tool-call.d.ts +7 -2
  34. package/client/live/run-live-tool-call.js +48 -10
  35. package/client/live/use-live-caption-log.d.ts +17 -0
  36. package/client/live/use-live-caption-log.js +51 -0
  37. package/client/live-client.d.ts +52 -4
  38. package/client/live-client.js +250 -59
  39. package/client/live-messages.d.ts +51 -13
  40. package/client/live-messages.js +42 -9
  41. package/client/page-dispatch.d.ts +15 -0
  42. package/client/page-dispatch.js +63 -0
  43. package/client/run-commit.d.ts +16 -43
  44. package/client/run-commit.js +26 -101
  45. package/client/run-session.d.ts +40 -41
  46. package/client/run-session.js +104 -112
  47. package/client/schema-fields.js +27 -6
  48. package/client/shaped-data.js +37 -8
  49. package/client/source-citations.js +1 -1
  50. package/client/space-geometry.d.ts +39 -0
  51. package/client/space-geometry.js +183 -0
  52. package/client/state-fields.js +1 -1
  53. package/client/tool-result.js +30 -12
  54. package/client/tool-resume.d.ts +18 -1
  55. package/client/tool-resume.js +4 -1
  56. package/client/trace-feed.d.ts +1 -1
  57. package/client/trace-story.d.ts +2 -2
  58. package/client/trace-story.js +43 -19
  59. package/client/trace-view.d.ts +2 -2
  60. package/client/trace-view.js +56 -16
  61. package/client/transcript-groups.d.ts +28 -2
  62. package/client/transcript-groups.js +63 -13
  63. package/client/transcript-schema.d.ts +10 -0
  64. package/client/transcript-schema.js +120 -0
  65. package/client/transport.d.ts +114 -26
  66. package/client/transport.js +106 -32
  67. package/client/turn-client.d.ts +17 -8
  68. package/client/turn-client.js +26 -31
  69. package/client/voice-recorder.d.ts +1 -1
  70. package/client/voice-recorder.js +2 -5
  71. package/client/wire-line.js +5 -5
  72. package/client/worklets/mic-capture.js +9 -9
  73. package/components/InkWaveform.d.ts +12 -1
  74. package/components/InkWaveform.js +65 -42
  75. package/components/live/use-live-runner-controls.d.ts +3 -4
  76. package/components/live/use-live-runner-controls.js +6 -10
  77. package/components/live/use-live-runner-model.d.ts +10 -6
  78. package/components/live/use-live-runner-model.js +29 -14
  79. package/components/live/use-live-runner-ui.d.ts +10 -8
  80. package/components/live/use-live-runner-ui.js +19 -11
  81. package/components/live/use-live-session-client.d.ts +4 -4
  82. package/components/live/use-live-session-client.js +11 -8
  83. package/components/use-composer-voice.d.ts +4 -4
  84. package/components/use-composer-voice.js +8 -8
  85. package/components/use-editor-selection.js +4 -1
  86. package/components/use-voice-playback.d.ts +2 -1
  87. package/components/use-voice-playback.js +4 -4
  88. package/hooks/use-described.d.ts +5 -1
  89. package/hooks/use-described.js +11 -9
  90. package/hooks/use-embedding-index.d.ts +27 -0
  91. package/hooks/use-embedding-index.js +43 -0
  92. package/hooks/use-one-call.d.ts +31 -0
  93. package/hooks/use-one-call.js +76 -0
  94. package/hooks/use-page-tool-warnings.d.ts +6 -0
  95. package/hooks/use-page-tool-warnings.js +25 -0
  96. package/hooks/use-second-ticker.d.ts +2 -0
  97. package/hooks/use-second-ticker.js +13 -0
  98. package/hooks/use-theorem-chat-actions.d.ts +25 -21
  99. package/hooks/use-theorem-chat-actions.js +67 -45
  100. package/hooks/use-theorem-chat-state.d.ts +6 -6
  101. package/hooks/use-theorem-chat-state.js +13 -9
  102. package/hooks/use-theorem-chat.d.ts +48 -14
  103. package/hooks/use-theorem-chat.js +329 -141
  104. package/hooks/use-theorem-decision.d.ts +5 -10
  105. package/hooks/use-theorem-decision.js +4 -67
  106. package/hooks/use-theorem-embedding.d.ts +18 -0
  107. package/hooks/use-theorem-embedding.js +12 -0
  108. package/hooks/use-theorem-host.d.ts +5 -4
  109. package/hooks/use-theorem-host.js +26 -8
  110. package/hooks/use-theorem-interface.d.ts +6 -3
  111. package/hooks/use-theorem-interface.js +12 -22
  112. package/index.d.ts +6 -3
  113. package/index.js +5 -2
  114. package/live.d.ts +1 -0
  115. package/live.js +1 -1
  116. package/package.json +6 -5
  117. package/server/checkpoint-ownership.d.ts +6 -0
  118. package/server/checkpoint-ownership.js +42 -0
  119. package/server/conversation-store.d.ts +92 -0
  120. package/server/conversation-store.js +131 -0
  121. package/server/conversation.d.ts +193 -0
  122. package/server/conversation.js +552 -0
  123. package/server/decision-handler.d.ts +12 -4
  124. package/server/decision-handler.js +29 -38
  125. package/server/embedding-handler.d.ts +33 -0
  126. package/server/embedding-handler.js +45 -0
  127. package/server/handler.d.ts +58 -20
  128. package/server/handler.js +441 -238
  129. package/server/mod.d.ts +15 -6
  130. package/server/mod.js +13 -5
  131. package/server/page-dispatch.d.ts +22 -0
  132. package/server/page-dispatch.js +127 -0
  133. package/server/request-body.d.ts +2 -0
  134. package/server/request-body.js +42 -0
  135. package/server/request-check.d.ts +6 -1
  136. package/server/request-check.js +27 -12
  137. package/server/session-authority.d.ts +8 -0
  138. package/server/session-authority.js +78 -0
  139. package/server/session-boundary.d.ts +26 -0
  140. package/server/session-boundary.js +64 -0
  141. package/server/session-identity.d.ts +16 -0
  142. package/server/session-identity.js +41 -0
  143. package/server/session-limits.d.ts +12 -0
  144. package/server/session-limits.js +59 -0
  145. package/server/session-store.d.ts +69 -20
  146. package/server/session-store.js +220 -5
  147. package/server/single-action.d.ts +21 -0
  148. package/server/single-action.js +30 -0
  149. package/server/steer-inbox.js +1 -1
  150. package/server/tool-operations.d.ts +6 -0
  151. package/server/tool-operations.js +132 -0
  152. package/server/turn-input.d.ts +10 -0
  153. package/server/turn-input.js +29 -0
  154. package/styles/ink-controls.css +78 -78
  155. package/ui/CardWall.d.ts +10 -0
  156. package/ui/CardWall.js +61 -0
  157. package/ui/ChatComposerBar.d.ts +28 -5
  158. package/ui/ChatComposerBar.js +85 -30
  159. package/ui/ChatTranscript.d.ts +14 -1
  160. package/ui/ChatTranscript.js +110 -57
  161. package/ui/ConsoleFrame.d.ts +61 -4
  162. package/ui/ConsoleFrame.js +109 -9
  163. package/ui/DecisionAnswers.d.ts +40 -2
  164. package/ui/DecisionAnswers.js +166 -35
  165. package/ui/DecisionImages.d.ts +16 -0
  166. package/ui/DecisionImages.js +46 -0
  167. package/ui/EmbeddingDocuments.d.ts +78 -0
  168. package/ui/EmbeddingDocuments.js +278 -0
  169. package/ui/EmbeddingQuery.d.ts +27 -0
  170. package/ui/EmbeddingQuery.js +95 -0
  171. package/ui/EmbeddingResults.d.ts +41 -0
  172. package/ui/EmbeddingResults.js +165 -0
  173. package/ui/EmbeddingSpace.d.ts +26 -0
  174. package/ui/EmbeddingSpace.js +106 -0
  175. package/ui/LiveCaptionsPanel.d.ts +22 -0
  176. package/ui/LiveCaptionsPanel.js +27 -0
  177. package/ui/LiveRunner.d.ts +6 -4
  178. package/ui/LiveRunner.js +42 -55
  179. package/ui/PaneState.d.ts +33 -0
  180. package/ui/PaneState.js +50 -0
  181. package/ui/SchemaFields.d.ts +1 -1
  182. package/ui/SchemaFields.js +54 -26
  183. package/ui/ShapedData.js +73 -28
  184. package/ui/SidePanel.d.ts +74 -5
  185. package/ui/SidePanel.js +82 -9
  186. package/ui/StateFields.d.ts +10 -2
  187. package/ui/StateFields.js +56 -13
  188. package/ui/TheoremChat.d.ts +65 -14
  189. package/ui/TheoremChat.js +66 -78
  190. package/ui/TheoremDecision.d.ts +12 -20
  191. package/ui/TheoremDecision.js +78 -35
  192. package/ui/TheoremEmbedding.d.ts +27 -0
  193. package/ui/TheoremEmbedding.js +122 -0
  194. package/ui/TheoremHost.d.ts +22 -5
  195. package/ui/TheoremHost.js +127 -37
  196. package/ui/ToolGateCard.d.ts +5 -1
  197. package/ui/ToolGateCard.js +30 -23
  198. package/ui/ToolResult.js +12 -10
  199. package/ui/ToolResultCharts.js +29 -12
  200. package/ui/TraceCharts.d.ts +2 -2
  201. package/ui/TraceCharts.js +48 -17
  202. package/ui/TraceGuardrails.d.ts +3 -2
  203. package/ui/TraceGuardrails.js +24 -6
  204. package/ui/TraceInspector.d.ts +11 -1
  205. package/ui/TraceInspector.js +45 -28
  206. package/ui/TraceInspectorPanel.d.ts +31 -9
  207. package/ui/TraceInspectorPanel.js +77 -39
  208. package/ui/TraceOverview.d.ts +7 -3
  209. package/ui/TraceOverview.js +44 -14
  210. package/ui/TraceSpanDetail.js +24 -22
  211. package/ui/TraceStory.js +9 -8
  212. package/ui/TraceValues.js +19 -9
  213. package/ui/TraceWaterfall.js +12 -9
  214. package/ui/VoiceNote.js +4 -4
  215. package/ui/approval-heading.d.ts +4 -0
  216. package/ui/approval-heading.js +9 -0
  217. package/ui/arrive.d.ts +18 -0
  218. package/ui/arrive.js +46 -0
  219. package/ui/built/theme.css +86 -68
  220. package/ui/built/theme.css.d.ts +9 -0
  221. package/ui/built/theorem.d.ts +3 -3
  222. package/ui/built/theorem.js +34 -5
  223. package/ui/built/theorem.variants.d.ts +8 -2
  224. package/ui/code-language.d.ts +12 -0
  225. package/ui/code-language.js +50 -0
  226. package/ui/disclosure-motion.js +53 -9
  227. package/ui/embedding-space-gl.d.ts +37 -0
  228. package/ui/embedding-space-gl.js +663 -0
  229. package/ui/embedding-space-view.d.ts +8 -0
  230. package/ui/embedding-space-view.js +8 -0
  231. package/ui/glide.d.ts +7 -0
  232. package/ui/glide.js +44 -0
  233. package/ui/icons.js +2 -1
  234. package/ui/index.d.ts +20 -9
  235. package/ui/index.js +20 -10
  236. package/ui/labels-provider.js +5 -2
  237. package/ui/labels.d.ts +374 -85
  238. package/ui/labels.js +1028 -188
  239. package/ui/markdown-plugins.d.ts +9 -0
  240. package/ui/markdown-plugins.js +13 -0
  241. package/ui/mcp-icon.d.ts +6 -0
  242. package/ui/mcp-icon.js +8 -0
  243. package/ui/row-keys.d.ts +19 -0
  244. package/ui/row-keys.js +33 -0
  245. package/ui/theme.d.ts +1 -1
  246. package/ui/theme.js +2 -2
  247. package/ui/theorem-theme.js +32 -10
  248. package/ui/trace-palette.d.ts +2 -16
  249. package/ui/trace-palette.js +2 -16
  250. package/ui/trace-patterns.d.ts +11 -3
  251. package/ui/trace-patterns.js +16 -3
  252. package/ui/transcript-copy-text.js +1 -0
  253. package/ui/wall-columns.d.ts +8 -0
  254. package/ui/wall-columns.js +19 -0
  255. package/client/walk-away.d.ts +0 -34
  256. package/client/walk-away.js +0 -48
  257. package/server/walk-away.d.ts +0 -22
  258. package/server/walk-away.js +0 -41
@@ -0,0 +1,552 @@
1
+ /**
2
+ * One reply, run against the conversation its host stores: the history goes
3
+ * to the model from the store, never from the browser, and what the reply
4
+ * leaves (its transcript, its history, a paused reply) is written back.
5
+ *
6
+ * Every host runs replies through here: `createTheoremHandler` with the
7
+ * kernel's `runTurn`, the studio with its own scope. Each run streams its
8
+ * events, then one `conversation` line with the ref the store issued, then
9
+ * the run's error when it failed.
10
+ *
11
+ * The conversation owns the transcript, the history, and the paused reply's
12
+ * events. Which paused calls may run, and with what input, stays the host's
13
+ * session store's: a call is answered there first, and its events settle it here.
14
+ *
15
+ * @module
16
+ */
17
+ import { attachmentsRefused, labelText, TheoremError, toErrorEvent, } from '@theoremjs/agents';
18
+ import { sanitizeTurnRequestWithEvents, withPublicLabels } from '@theoremjs/agents/guardrails';
19
+ import { forClient } from '@theoremjs/agents/host';
20
+ import { appendAssistantEventsToHistory, appendUserDraftToHistory, applyTurnEventsToSession, emptyInterfaceTurnSession, foldTurnEvents, gatedToolsFromEvents, historyFromTranscriptBlocks, prepareUserTurn, settlesToolCall, streamThoughtsEnabled, } from '@theoremjs/agents/interface';
21
+ import { base64ToBytes } from '@theoremjs/agents/kernel';
22
+ import { stampWorked } from "../client/run-commit.js";
23
+ import { emptyConversation, unknownConversation, } from "./conversation-store.js";
24
+ import { kernelTurnInput } from "./turn-input.js";
25
+ /** The paused call a reference names, when its call id and its operation both match. */
26
+ function waitingOn(conversation, reference) {
27
+ return gatedToolsFromEvents(conversation.paused?.events ?? []).some((call) => call.callId === reference.callId && call.operationId === reference.operationId);
28
+ }
29
+ /**
30
+ * The conversation host for a profile as `forClient` shows it to `clientEvents`:
31
+ * its thoughts as its outputs allow, its failures in its own wording.
32
+ */
33
+ export function readerConversationHost(args) {
34
+ const { profile, clientEvents } = args;
35
+ return {
36
+ sessionId: args.sessionId,
37
+ store: args.store,
38
+ showThoughts: streamThoughtsEnabled(profile.outputs),
39
+ forReader: (event) => forClient(event, clientEvents),
40
+ failureText(failure) {
41
+ const worded = forClient(withPublicLabels(failure, profile.labels), clientEvents);
42
+ return worded.type === 'error' && worded.error
43
+ ? worded.error
44
+ : labelText(`error.${failure.errorKind}`, {}, profile.labels);
45
+ },
46
+ };
47
+ }
48
+ /**
49
+ * The conversation under `ref`, or a new one when the browser has none, refused when it does not
50
+ * allow `use`.
51
+ */
52
+ export async function openConversation(host, ref, use, restore) {
53
+ const opened = await openedAt(host, ref, restore);
54
+ if (use.kind === 'message')
55
+ requireWalkAway(opened.conversation, use.abandon);
56
+ if (use.kind === 'answer')
57
+ requireWaitingOn(opened.conversation, use.call);
58
+ if (use.kind === 'rewind')
59
+ requireRewindPoint(opened.conversation, use.at);
60
+ return opened;
61
+ }
62
+ async function openedAt(host, ref, restore) {
63
+ if (ref === undefined) {
64
+ if (restore)
65
+ // labels-exempt: internal diagnostic; the user reads error.request
66
+ throw new TheoremError('request', 'a transcript restores a conversation the host lost, and this message names none');
67
+ return { ref: null, revision: null, conversation: emptyConversation() };
68
+ }
69
+ const snapshot = await host.store.read(host.sessionId, ref);
70
+ if (snapshot)
71
+ return { ref, revision: snapshot.revision, conversation: snapshot.conversation };
72
+ if (!restore)
73
+ throw unknownConversation();
74
+ // why: A null ref makes the reply's write a new conversation, whose ref the `conversation` line hands the page.
75
+ const { conversation, events } = conversationFromBlocks(restore.profile, restore.blocks);
76
+ return { ref: null, revision: null, conversation, events };
77
+ }
78
+ /**
79
+ * A conversation from transcript blocks, read as history by the profile's guardrails at the
80
+ * `history` boundary, as every turn's history is. A match set to `block` refuses them. Returns it
81
+ * with the guardrail events for what matched.
82
+ */
83
+ function conversationFromBlocks(profile, blocks) {
84
+ const { request, events, refusal } = sanitizeTurnRequestWithEvents({ profile: profile.id, input: { history: historyFromTranscriptBlocks(blocks) } }, profile);
85
+ if (refusal)
86
+ throw refusal;
87
+ return {
88
+ conversation: {
89
+ ...emptyConversation(),
90
+ transcript: [...blocks],
91
+ history: request.input?.history ?? [],
92
+ },
93
+ events,
94
+ };
95
+ }
96
+ /**
97
+ * Starts a conversation from blocks the host wrote, not ran: a chat that opens on an exchange
98
+ * already under way. The model reads them as history on the next turn, read first by the
99
+ * profile's guardrails at the `history` boundary, as every turn's history is. A match set to
100
+ * `block` refuses them, and nothing is stored. Returns the new conversation's ref, its view for
101
+ * the chat's `conversationView`, and the guardrail events for what matched.
102
+ */
103
+ export async function startConversation(host, profile, blocks) {
104
+ const { conversation, events } = conversationFromBlocks(profile, blocks);
105
+ const result = await host.store.write(host.sessionId, null, null, conversation);
106
+ if (!result.written) {
107
+ // labels-exempt: internal diagnostic; the user reads error.unavailable
108
+ throw new TheoremError('unavailable', 'the conversation store did not keep the new conversation');
109
+ }
110
+ // why: A new conversation has no paused reply, so its view is the transcript alone.
111
+ return { ref: result.ref, view: { transcript: [...blocks] }, events };
112
+ }
113
+ function foldedFor(host, events) {
114
+ return foldTurnEvents(events.map((event) => host.forReader(event)), { showThoughts: host.showThoughts });
115
+ }
116
+ /** The conversation as the browser draws it. */
117
+ export function conversationView(host, conversation) {
118
+ const { paused } = conversation;
119
+ return {
120
+ transcript: conversation.transcript,
121
+ ...(paused
122
+ ? {
123
+ paused: {
124
+ events: paused.events.map((event) => host.forReader(event)),
125
+ blocks: stampWorked(foldedFor(host, paused.events), {
126
+ workedMs: paused.workedMs,
127
+ endedAt: paused.endedAt,
128
+ }),
129
+ },
130
+ }
131
+ : {}),
132
+ };
133
+ }
134
+ /** Writes `next` over what the request read; a write another request beat is refused. */
135
+ async function save(host, opened, next) {
136
+ const result = await host.store.write(host.sessionId, opened.ref, opened.revision, next);
137
+ if (!result.written) {
138
+ // labels-exempt: internal diagnostic; the user reads error.unavailable
139
+ throw new TheoremError('unavailable', 'the conversation changed while this reply ran', {
140
+ cause: { code: 'conversation_conflict' },
141
+ });
142
+ }
143
+ return { ref: result.ref, revision: result.revision, conversation: next };
144
+ }
145
+ /** The line that tells the browser the ref of what was just written. */
146
+ function refLine(opened, posted) {
147
+ if (opened.ref === null)
148
+ return [];
149
+ return [{ type: 'conversation', ref: opened.ref, ...(posted ? { posted: true } : {}) }];
150
+ }
151
+ /** The model's history once the reply's events enter it. */
152
+ function replyHistory(reply) {
153
+ return appendAssistantEventsToHistory(reply.base, reply.events.slice(reply.baseEvents));
154
+ }
155
+ /** The conversation's meters and checkpoint after the reply's events. */
156
+ function metered(conversation, events) {
157
+ const applied = applyTurnEventsToSession({
158
+ ...emptyInterfaceTurnSession(),
159
+ providerState: conversation.providerState,
160
+ inputTokens: conversation.inputTokens,
161
+ historyTokens: conversation.historyTokens,
162
+ }, events);
163
+ return {
164
+ ...(applied.providerState ? { providerState: applied.providerState } : {}),
165
+ ...(applied.inputTokens === undefined ? {} : { inputTokens: applied.inputTokens }),
166
+ ...(applied.historyTokens === undefined ? {} : { historyTokens: applied.historyTokens }),
167
+ };
168
+ }
169
+ /** The reply is over: its blocks join the transcript and its events the history. */
170
+ function committed(host, conversation, reply) {
171
+ const { paused: _paused, providerState: _state, ...rest } = conversation;
172
+ return {
173
+ ...rest,
174
+ ...metered(conversation, reply.events),
175
+ transcript: [
176
+ ...conversation.transcript,
177
+ ...stampWorked(foldedFor(host, reply.events), {
178
+ workedMs: reply.workedMs,
179
+ endedAt: Date.now(),
180
+ }),
181
+ ],
182
+ history: replyHistory(reply),
183
+ };
184
+ }
185
+ /** The reply waits on gates: its events wait beside the transcript until they settle. */
186
+ function pausedOn(conversation, reply, runner) {
187
+ const paused = {
188
+ events: reply.events,
189
+ base: reply.base,
190
+ baseEvents: reply.baseEvents,
191
+ workedMs: reply.workedMs,
192
+ endedAt: Date.now(),
193
+ ...(runner.model ? { model: runner.model } : {}),
194
+ ...(runner.effort ? { effort: runner.effort } : {}),
195
+ };
196
+ return { ...conversation, ...metered(conversation, reply.events), paused };
197
+ }
198
+ /** The reply failed: it commits as far as it got, closed by its error. */
199
+ function failed(host, conversation, reply, failure) {
200
+ const next = committed(host, conversation, reply);
201
+ if (next.transcript.slice(conversation.transcript.length).some((b) => b.kind === 'error')) {
202
+ return next;
203
+ }
204
+ return {
205
+ ...next,
206
+ transcript: [
207
+ ...next.transcript,
208
+ { id: crypto.randomUUID(), kind: 'error', message: host.failureText(failure) },
209
+ ],
210
+ };
211
+ }
212
+ /**
213
+ * The person stopped the reply: it leaves the transcript, and the history
214
+ * keeps what came before this run, the calls it settled included.
215
+ */
216
+ function stopped(conversation, reply) {
217
+ const { paused: _paused, ...rest } = conversation;
218
+ return {
219
+ ...rest,
220
+ transcript: [
221
+ ...conversation.transcript,
222
+ { id: crypto.randomUUID(), kind: 'turn-done', stop: { kind: 'cancelled' } },
223
+ ],
224
+ history: appendAssistantEventsToHistory(reply.base, reply.events.slice(reply.baseEvents, Math.max(reply.baseEvents, reply.seed))),
225
+ };
226
+ }
227
+ /**
228
+ * Runs the model for `reply` and writes what it leaves. A compaction moves the
229
+ * reply's base to the compacted history; `withMessage` puts the person's
230
+ * message back on it when the reply answers one.
231
+ */
232
+ async function* runReply(host, opened, reply, runner, input, withMessage) {
233
+ const { conversation } = opened;
234
+ const startedAt = Date.now();
235
+ const worked = () => ({ ...reply, workedMs: reply.workedMs + (Date.now() - startedAt) });
236
+ let errorEvent;
237
+ let current = opened;
238
+ try {
239
+ const events = runner.run({
240
+ input,
241
+ providerState: conversation.providerState,
242
+ model: runner.model,
243
+ effort: runner.effort,
244
+ });
245
+ for await (const event of events) {
246
+ reply.events.push(event);
247
+ if (event.type === 'error') {
248
+ // why: The browser throws on an error line, so it goes out after the conversation line.
249
+ errorEvent = event;
250
+ continue;
251
+ }
252
+ if (event.type === 'compaction') {
253
+ reply.base = withMessage(event.history);
254
+ reply.baseEvents = reply.events.length;
255
+ }
256
+ yield event;
257
+ }
258
+ }
259
+ catch (err) {
260
+ current = await save(host, current, runner.signal.aborted
261
+ ? stopped(conversation, worked())
262
+ : failed(host, conversation, worked(), toErrorEvent(err)));
263
+ yield* refLine(current);
264
+ throw err;
265
+ }
266
+ const settled = worked();
267
+ if (runner.signal.aborted) {
268
+ current = await save(host, current, stopped(conversation, settled));
269
+ }
270
+ else if (errorEvent) {
271
+ current = await save(host, current, failed(host, conversation, settled, errorEvent));
272
+ }
273
+ else if (gatedToolsFromEvents(settled.events).length) {
274
+ current = await save(host, current, pausedOn(conversation, settled, runner));
275
+ }
276
+ else {
277
+ current = await save(host, current, committed(host, conversation, settled));
278
+ yield* refLine(current);
279
+ // why: The reply is the browser's before compaction runs, so a failed compaction loses none of it.
280
+ const compacted = await compactedAfter(host, current, settled, runner, withMessage);
281
+ if (compacted)
282
+ yield* refLine(compacted);
283
+ return;
284
+ }
285
+ yield* refLine(current);
286
+ if (errorEvent)
287
+ yield errorEvent;
288
+ }
289
+ /**
290
+ * The profile compacts after a reply (`done.compaction.needed`): the history
291
+ * before the reply is compacted, and the reply's own messages follow it.
292
+ */
293
+ async function compactedAfter(host, opened, reply, runner, withMessage) {
294
+ const done = reply.events.findLast((event) => event.type === 'done');
295
+ const signal = done?.type === 'done' ? done.compaction : undefined;
296
+ if (!signal?.needed)
297
+ return undefined;
298
+ const compacted = await runner.compact(signal.history, signal.tokens, runner.model);
299
+ if (!compacted)
300
+ return undefined;
301
+ const history = replyHistory({ ...reply, base: withMessage(compacted), baseEvents: 0 });
302
+ return save(host, opened, { ...opened.conversation, history });
303
+ }
304
+ /** A file off the wire as the profile's input limits read it. */
305
+ function pendingOf(blob) {
306
+ let sizeBytes;
307
+ try {
308
+ sizeBytes = base64ToBytes(blob.data).length;
309
+ }
310
+ catch {
311
+ // invariant: A file that is not base64 is a malformed request; its bytes must not enter diagnostics.
312
+ throw new TheoremError('request', labelText('error.request'), {
313
+ cause: { code: 'request_file_encoding' },
314
+ });
315
+ }
316
+ return { name: blob.name, mimeType: blob.mimeType, sizeBytes, data: blob.data };
317
+ }
318
+ /**
319
+ * The person's message as the profile accepts it: refused when its files break the profile's
320
+ * limits. `server` is the host's own context for the run (the handler's `context` option).
321
+ */
322
+ export function postedMessage(iface, input, server) {
323
+ const prepared = prepareUserTurn(iface.inputs, {
324
+ ...(input.text !== undefined ? { text: input.text } : {}),
325
+ ...(input.attachments ? { attachments: input.attachments.map(pendingOf) } : {}),
326
+ ...(input.voice ? { voice: input.voice.map(pendingOf) } : {}),
327
+ }, iface.guardrails);
328
+ if (!prepared.ok)
329
+ throw attachmentsRefused(prepared.issues);
330
+ const { text: _sent, ...rest } = input;
331
+ const { text } = prepared.draft;
332
+ return {
333
+ draft: prepared.draft,
334
+ blocks: prepared.blocks,
335
+ media: {
336
+ ...(input.attachments ? { attachments: input.attachments } : {}),
337
+ ...(input.voice ? { voice: input.voice } : {}),
338
+ },
339
+ input: kernelTurnInput(text === undefined ? rest : { ...rest, text }, server),
340
+ };
341
+ }
342
+ /**
343
+ * Refuses a walk-away that does not name exactly the calls the paused reply
344
+ * waits on, by call id and operation; a message to a paused reply must walk away.
345
+ */
346
+ function requireWalkAway(conversation, abandon) {
347
+ const waiting = gatedToolsFromEvents(conversation.paused?.events ?? []);
348
+ if (waiting.length && !abandon) {
349
+ // labels-exempt: internal diagnostic; the user reads the copy key's wording
350
+ throw new TheoremError('request', 'a message to a paused reply must walk away from it', {
351
+ copy: { key: 'session.gate_pending' },
352
+ });
353
+ }
354
+ const named = abandon ?? [];
355
+ if (waiting.length !== named.length ||
356
+ !named.every((reference) => waitingOn(conversation, reference))) {
357
+ // labels-exempt: internal diagnostic; the user reads the copy key's wording
358
+ throw new TheoremError('request', 'the message walks away from calls the reply is not waiting on', {
359
+ copy: { key: 'session.gate_expired' },
360
+ });
361
+ }
362
+ }
363
+ /** Refuses an answer to a call the paused reply is not waiting on, by call id and operation. */
364
+ function requireWaitingOn(conversation, reference) {
365
+ if (waitingOn(conversation, reference))
366
+ return;
367
+ // labels-exempt: internal diagnostic; the user reads the copy key's wording
368
+ throw new TheoremError('request', `the reply is not waiting on ${reference.callId}`, {
369
+ copy: { key: 'session.gate_expired' },
370
+ });
371
+ }
372
+ /**
373
+ * Streams each walked-away call's tool events into the paused reply as it
374
+ * settles; each run's other events stay with that run. Returns the error that
375
+ * ended a run, or undefined once every call settled.
376
+ */
377
+ async function* settleWalkedAway(paused, calls) {
378
+ for (const call of calls) {
379
+ for await (const event of call.events) {
380
+ if (event.type === 'error')
381
+ return event;
382
+ if (event.type !== 'tool')
383
+ continue;
384
+ paused.events.push(event);
385
+ yield event;
386
+ }
387
+ if (!paused.events.some((event) => settlesToolCall(event, call.callId))) {
388
+ // labels-exempt: internal diagnostic; the user reads error.internal
389
+ throw new TheoremError('internal', `walked-away call ${call.callId} ended without settling`);
390
+ }
391
+ }
392
+ return undefined;
393
+ }
394
+ function pausedReply(paused) {
395
+ return {
396
+ base: paused.base,
397
+ events: [...paused.events],
398
+ baseEvents: paused.baseEvents,
399
+ seed: paused.events.length,
400
+ workedMs: paused.workedMs,
401
+ };
402
+ }
403
+ /** A run's error as a failure to throw. */
404
+ function failureOf(error) {
405
+ return new TheoremError(error.errorKind, error.error ?? labelText(`error.${error.errorKind}`));
406
+ }
407
+ /**
408
+ * The person sends a message. A reply that waits on gates is walked away
409
+ * from first: each call settles cancelled and the paused reply commits; then
410
+ * the message posts and its reply runs.
411
+ */
412
+ export async function* messageReply(args) {
413
+ const { host, message, runner } = args;
414
+ const { events: restored = [], ...stored } = args.opened;
415
+ let opened = stored;
416
+ yield* restored;
417
+ const { paused } = opened.conversation;
418
+ requireWalkAway(opened.conversation, args.walkAway);
419
+ if (paused) {
420
+ const walked = pausedReply(paused);
421
+ const error = yield* settleWalkedAway(walked, args.walkAway ?? []);
422
+ if (error) {
423
+ opened = await save(host, opened, {
424
+ ...opened.conversation,
425
+ paused: { ...paused, events: walked.events, endedAt: Date.now() },
426
+ });
427
+ yield* refLine(opened);
428
+ yield error;
429
+ return;
430
+ }
431
+ opened = { ...opened, conversation: committed(host, opened.conversation, walked) };
432
+ }
433
+ const { conversation } = opened;
434
+ const withMessage = (history) => appendUserDraftToHistory(history, message.draft, message.media);
435
+ // why: The message posts before the model runs, so a reply the browser stops still leaves it, in the transcript and the history.
436
+ opened = await save(host, opened, {
437
+ ...conversation,
438
+ transcript: [...conversation.transcript, ...message.blocks],
439
+ history: withMessage(conversation.history),
440
+ });
441
+ yield* refLine(opened, 'posted');
442
+ const reply = {
443
+ base: withMessage(conversation.history),
444
+ events: [],
445
+ baseEvents: 0,
446
+ seed: 0,
447
+ workedMs: 0,
448
+ };
449
+ yield* runReply(host, opened, reply, runner, {
450
+ ...message.input,
451
+ history: conversation.history,
452
+ ...(conversation.inputTokens === undefined ? {} : { inputTokens: conversation.inputTokens }),
453
+ ...(conversation.historyTokens === undefined
454
+ ? {}
455
+ : { historyTokens: conversation.historyTokens }),
456
+ }, withMessage);
457
+ }
458
+ /**
459
+ * The person answers one call the reply waits on. `settling` streams the
460
+ * call's events; once no call waits, the reply goes on in the same stream.
461
+ */
462
+ export async function* answerReply(args) {
463
+ const { host, runner } = args;
464
+ let opened = args.opened;
465
+ requireWaitingOn(opened.conversation, args.call);
466
+ const { paused, ...waiting } = opened.conversation;
467
+ if (!paused)
468
+ return;
469
+ const reply = pausedReply(paused);
470
+ const startedAt = Date.now();
471
+ let error;
472
+ for await (const event of args.settling) {
473
+ if (event.type === 'error') {
474
+ error = event;
475
+ break;
476
+ }
477
+ reply.events.push(event);
478
+ yield event;
479
+ }
480
+ reply.workedMs += Date.now() - startedAt;
481
+ reply.seed = reply.events.length;
482
+ if (error || gatedToolsFromEvents(reply.events).length) {
483
+ // why: An answer that failed before its call settled leaves the reply as it was, to answer again.
484
+ const settledCall = reply.events.some((event) => settlesToolCall(event, args.call.callId));
485
+ const events = error && !settledCall ? paused.events : reply.events;
486
+ opened = await save(host, opened, {
487
+ ...opened.conversation,
488
+ paused: { ...paused, events, workedMs: reply.workedMs, endedAt: Date.now() },
489
+ });
490
+ yield* refLine(opened);
491
+ if (error)
492
+ yield error;
493
+ return;
494
+ }
495
+ yield* runReply(host, { ...opened, conversation: waiting }, reply, { ...runner, model: runner.model ?? paused.model, effort: runner.effort ?? paused.effort }, {
496
+ history: replyHistory(reply),
497
+ ...(waiting.inputTokens === undefined ? {} : { inputTokens: waiting.inputTokens }),
498
+ ...(waiting.historyTokens === undefined ? {} : { historyTokens: waiting.historyTokens }),
499
+ }, (history) => history);
500
+ }
501
+ /** Refuses a rewind to a block the transcript does not hold; -1 empties it. */
502
+ function requireRewindPoint(conversation, at) {
503
+ if (Number.isSafeInteger(at) && at >= -1 && at < conversation.transcript.length)
504
+ return;
505
+ // labels-exempt: internal diagnostic; the user reads error.request
506
+ throw new TheoremError('request', `no transcript block ${at} to rewind to`);
507
+ }
508
+ /**
509
+ * The conversation cut after transcript block `at` (-1 empties it): the
510
+ * history is rebuilt from what is kept, and the model's checkpoint and meters
511
+ * start over. A paused reply is walked away from first, and commits before the cut.
512
+ */
513
+ export async function rewindConversation(args) {
514
+ const { host, opened, at } = args;
515
+ let { conversation } = opened;
516
+ requireRewindPoint(conversation, at);
517
+ requireWalkAway(conversation, args.walkAway);
518
+ if (conversation.paused) {
519
+ const walked = pausedReply(conversation.paused);
520
+ const settling = settleWalkedAway(walked, args.walkAway ?? []);
521
+ let step = await settling.next();
522
+ while (!step.done)
523
+ step = await settling.next();
524
+ if (step.value)
525
+ throw failureOf(step.value);
526
+ conversation = committed(host, conversation, walked);
527
+ }
528
+ const transcript = conversation.transcript.slice(0, at + 1);
529
+ return save(host, opened, {
530
+ version: 1,
531
+ transcript,
532
+ history: historyFromTranscriptBlocks(transcript),
533
+ });
534
+ }
535
+ /** A paused call's operation: every call a session issued carries one. */
536
+ export function requiredOperationId(call) {
537
+ if (!call.operationId)
538
+ throw new TheoremError('internal', labelText('error.internal'));
539
+ return call.operationId;
540
+ }
541
+ /** The calls a paused reply waits on: the ones a message or a rewind walks away from. */
542
+ export function waitingCalls(conversation) {
543
+ return gatedToolsFromEvents(conversation.paused?.events ?? []).map((call) => ({
544
+ callId: call.callId,
545
+ operationId: requiredOperationId(call),
546
+ }));
547
+ }
548
+ /** Refuses what only a kept conversation answers, for a profile that keeps none. */
549
+ export function statelessRefusal(what) {
550
+ // labels-exempt: internal diagnostic; the user reads error.request
551
+ return new TheoremError('request', `a stateless chat keeps no conversation to ${what}`);
552
+ }
@@ -4,18 +4,24 @@
4
4
  * - `GET <base>` → `{ interface }` the profile's handle, contract, and questions
5
5
  * - `POST <base>/decide` → `{ result }` the answers for the posted `{ state }`
6
6
  *
7
- * The host owns the questions: the browser sends only the state, so a visitor
8
- * can't spend the host's key asking questions of their own.
7
+ * The host owns the questions: the browser sends only the state and the images it names,
8
+ * so a visitor can't spend the host's key asking questions of their own.
9
9
  *
10
10
  * @module
11
11
  */
12
- import { type DecisionProfile, type DecisionQuestion, type KeyVault, type TraceSink } from '@theoremjs/agents';
12
+ import { type DecisionAsset, type DecisionProfile, type DecisionQuestion, type KeyVault, type TraceSink } from '@theoremjs/agents';
13
+ import type { ClientTurnOptions } from '@theoremjs/agents/host';
13
14
  import type { DecisionProfileDefinition } from '@theoremjs/agents/kernel';
14
15
  export type TheoremDecisionHandlerOptions = {
15
16
  /** Decision profile to serve. Registered with the kernel when the handler is created. */
16
17
  profile: DecisionProfile | DecisionProfileDefinition;
17
18
  /** The questions every decision asks, by id. */
18
19
  questions: Record<string, DecisionQuestion>;
20
+ /**
21
+ * Images every decision sends, by the reference its questions write, such as `<ghs02>`. The page
22
+ * sees them, to draw a box on one; a page image with one of their names is refused.
23
+ */
24
+ images?: Record<string, DecisionAsset>;
19
25
  /** The host's keys by slot; the profile's key slot reads from it. */
20
26
  vault?: KeyVault;
21
27
  fetch?: typeof globalThis.fetch;
@@ -23,9 +29,11 @@ export type TheoremDecisionHandlerOptions = {
23
29
  sink?: TraceSink;
24
30
  /** Host metadata kept on each decision's trace record, e.g. the signed-in user. */
25
31
  metadata?: (request: Request) => Record<string, unknown> | undefined;
32
+ /** Who reads this host's replies: forwarded to `clientError` for a failure the handler catches. */
33
+ clientEvents?: ClientTurnOptions;
26
34
  /**
27
35
  * Called with any error the handler catches, for reporting. Users read the
28
- * profile's lexicon wording for the error's kind, never the error itself.
36
+ * profile's labels wording for the error's kind, never the error itself.
29
37
  */
30
38
  onError?: (err: unknown, ctx: {
31
39
  request: Request;
@@ -4,52 +4,43 @@
4
4
  * - `GET <base>` → `{ interface }` the profile's handle, contract, and questions
5
5
  * - `POST <base>/decide` → `{ result }` the answers for the posted `{ state }`
6
6
  *
7
- * The host owns the questions: the browser sends only the state, so a visitor
8
- * can't spend the host's key asking questions of their own.
7
+ * The host owns the questions: the browser sends only the state and the images it names,
8
+ * so a visitor can't spend the host's key asking questions of their own.
9
9
  *
10
10
  * @module
11
11
  */
12
- import { defineProfile, errorKind, lexiconText, publicError, registerProfile, runDecision, z, } from '@theoremjs/agents';
13
- import { caughtStatus, HTTP_METHOD } from '@theoremjs/agents/host';
14
- import { decisionInterface } from "../client/decision-transport.js";
12
+ import { defineProfile, registerProfile, runDecision, z, } from '@theoremjs/agents';
13
+ import { decisionAssets, decisionInterface } from "../client/decision-transport.js";
15
14
  import { readBody } from "./handler.js";
16
- import { jsonResponse } from "./json-response.js";
17
- const decideBodySchema = z.object({ state: z.unknown() });
18
- function isDecide(request) {
19
- return new URL(request.url).pathname.split('/').filter(Boolean).at(-1) === 'decide';
20
- }
15
+ import { singleActionHandler } from "./single-action.js";
16
+ const decideBodySchema = z.object({
17
+ state: z.unknown(),
18
+ assets: z.record(z.string(), z.object({ mimeType: z.string(), data: z.string() })).optional(),
19
+ });
21
20
  export function createTheoremDecisionHandler(options) {
22
21
  const profile = defineProfile(options.profile);
23
22
  if (profile.type !== 'decision') {
24
- // lexicon-exempt: builder config error at setup; no user sees it
23
+ // labels-exempt: builder config error at setup; no user sees it
25
24
  throw new Error(`createTheoremDecisionHandler serves type 'decision'; got type '${profile.type}'.`);
26
25
  }
27
26
  registerProfile(profile);
28
- const iface = decisionInterface(profile, options.questions);
29
- return async (request) => {
30
- try {
31
- if (request.method === 'GET' && !isDecide(request))
32
- return jsonResponse(200, { interface: iface });
33
- if (request.method !== 'POST' || !isDecide(request)) {
34
- return jsonResponse(HTTP_METHOD, {
35
- error: lexiconText('error.request', {}, profile.lexicon),
36
- errorKind: 'request',
37
- });
38
- }
39
- const body = await readBody(request, decideBodySchema);
40
- const result = await runDecision({
41
- profile: profile.id,
42
- // The kernel checks the state is non-null JSON within the profile's size.
43
- state: body.state,
44
- questions: options.questions,
45
- signal: request.signal,
46
- metadata: options.metadata?.(request),
47
- }, { vault: options.vault, fetch: options.fetch, sink: options.sink });
48
- return jsonResponse(200, { result });
49
- }
50
- catch (err) {
51
- options.onError?.(err, { request });
52
- return jsonResponse(caughtStatus(err), { error: publicError(err, profile.lexicon), errorKind: errorKind(err) });
53
- }
54
- };
27
+ return singleActionHandler({
28
+ action: 'decide',
29
+ iface: decisionInterface(profile, options.questions, options.images),
30
+ labels: profile.labels,
31
+ clientEvents: options.clientEvents,
32
+ onError: options.onError,
33
+ }, async (request) => {
34
+ const body = await readBody(request, decideBodySchema);
35
+ return runDecision({
36
+ profile: profile.id,
37
+ // why: The kernel checks the state is non-null JSON within the profile's size.
38
+ state: body.state,
39
+ // why: The kernel checks each image's type and bytes before any request leaves.
40
+ assets: decisionAssets(options.images, body.assets),
41
+ questions: options.questions,
42
+ signal: request.signal,
43
+ metadata: options.metadata?.(request),
44
+ }, { vault: options.vault, fetch: options.fetch, sink: options.sink });
45
+ });
55
46
  }