@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
package/README.md CHANGED
@@ -1,227 +1,741 @@
1
- # `@theoremjs/react`
1
+ # @theoremjs/react
2
2
 
3
- React projection of the headless interface (`@theoremjs/agents/interface`) — runners, transcript, composer, live stage.
3
+ [![npm](https://img.shields.io/npm/v/@theoremjs/react?logo=npm&label=npm&color=cb3837)](https://www.npmjs.com/package/@theoremjs/react)
4
+ [![CI](https://github.com/masudl-hub/theoremai/actions/workflows/ci.yml/badge.svg)](https://github.com/masudl-hub/theoremai/actions/workflows/ci.yml)
5
+ [![MIT License](https://img.shields.io/badge/license-MIT-blue)](https://github.com/masudl-hub/theoremai/blob/main/LICENSE)
6
+
7
+ Define an agent once. Get the server and the user interface from the same definition.
8
+
9
+ ## The idea
10
+
11
+ In Theorem, you write an agent as a **profile** (see [`@theoremjs/agents`](https://www.npmjs.com/package/@theoremjs/agents)). The profile states:
12
+
13
+ - which models the agent can use
14
+ - what the agent accepts: text, images, files, or voice
15
+ - which tools the agent can call
16
+ - what the agent must return
17
+ - how the kernel guards each input and output
18
+
19
+ This package uses the same profile on both sides.
20
+
21
+ - **Server.** `createTheoremHandler` runs the profile. It enforces the guardrails and the tool gates, and it keeps the conversation.
22
+ - **Browser.** The UI asks the server to describe the profile. It then shows what the profile allows. It shows no attachment button if the profile does not accept files. It shows a model picker if the profile lets the user choose the model.
23
+
24
+ ```text
25
+ ┌─────────────────────────────┐
26
+ │ agent profile │
27
+ │ models · inputs · tools │
28
+ │ output · guardrails │
29
+ └──────────────┬──────────────┘
30
+ │
31
+ ┌────────────────┴────────────────┐
32
+ ▼ ▼
33
+ ┌───────────────────────────────┐ ┌───────────────────────────────┐
34
+ │ SERVER │ │ BROWSER │
35
+ │ createTheoremHandler │ │ <TheoremChat /> │
36
+ │ │ │ useTheoremInterface() │
37
+ │ runs the profile │ │ shows only what the │
38
+ │ enforces guardrails and gates │ │ profile allows │
39
+ └───────────────┬───────────────┘ └───────────────┬───────────────┘
40
+ └─ HTTP: describe · turn · steer ─┘
41
+ ```
42
+
43
+ Edit the profile, and both sides change. You do not write the UI code again.
44
+
45
+ When a profile records traces, the chat offers a floating trace toggle. Opening the trace uses the same toggle to close it, with a selected appearance. When the trace replaces the chat, its pane has equal padding on all sides.
46
+
47
+ The package has three parts:
48
+
49
+ - A ready-made chat, voice call, host console, and decision form. They are built on [Astryx](https://www.npmjs.com/package/@astryxdesign/core).
50
+ - Hooks and a transport, for your own UI.
51
+ - A server handler. It takes a Web `Request` and returns a `Response`.
4
52
 
5
53
  ## Install
6
54
 
7
55
  ```bash
8
56
  npm install @theoremjs/react @theoremjs/agents zod react react-dom
9
- # for the chat and live UI (`/ui`, `/live`):
10
- npm install @astryxdesign/core@0.6.3 @stylexjs/stylex@0.19.0
11
- # only to build your own theme from the Theorem one (`/ui/theme`):
12
- npm install @astryxdesign/theme-neutral@0.6.3
13
57
  ```
14
58
 
15
- Published to npm only: the package ships built JavaScript, declarations and
16
- stylesheets. `@theoremjs/agents` is a peer, so the host and the UI share one
17
- kernel. The Astryx peers are optional: the hooks, client and server entry
18
- points run without them. The declarations need TypeScript 5.7 or later.
59
+ For the ready-made UI (`/ui` and `/live`), also install Astryx and StyleX:
19
60
 
20
- `@theoremjs/react/ui` and `@theoremjs/react/live` import their stylesheets
21
- themselves, so a bundler that handles CSS imports (Vite does) needs no extra
22
- step. The theme's type is Figtree with a system-font fallback;
23
- the package does not ship the font, so load it yourself if you want it.
61
+ ```bash
62
+ npm install @astryxdesign/core @stylexjs/stylex
63
+ ```
24
64
 
25
- The live UI loads its microphone worklet as an asset
26
- (`new URL('./worklets/mic-capture.js', import.meta.url)`); Vite ships it
27
- without configuration, and any bundler that resolves that pattern does too.
65
+ To build your own theme from the Theorem theme (`/ui/theme`), also install `@astryxdesign/theme-neutral`.
28
66
 
29
- No Svelte. The playground site (`theoremai-frontend`) hosts a thin Vite SPA at `apps/run` that imports this package; the info-site graph stays Svelte and only writes a `PlaygroundRunPayload` handoff.
67
+ Use the versions that this package lists in `peerDependencies`. The Astryx and StyleX peers are optional. The hooks, the client, and the server handler work without them.
30
68
 
31
- ## Imports
69
+ When a chat turn, a call, a host request, or a decision fails, the ready-made UI shows the error in a banner that screen readers announce at once. The end of a live call is announced when the reader is next idle.
32
70
 
33
- ```ts
34
- import { useTheoremChat, useTheoremInterface } from '@theoremjs/react'; // headless hooks + transport
35
- import { TheoremChat } from '@theoremjs/react/ui'; // Astryx chat UI
36
- import { LiveRunner } from '@theoremjs/react/live'; // Astryx voice / video UI
37
- import { createTheoremHandler } from '@theoremjs/react/server'; // host side
38
- ```
39
-
40
- A `host` profile runs no model: the page calls its tools directly. Serve it
41
- with `createTheoremHostHandler({ profile })` and render `<TheoremHost
42
- endpoint="/api/host" />` (`createHostTransport` and `useTheoremHost` for a UI
43
- of your own). `GET` describes each allowed tool with its input and output JSON
44
- Schema, never its endpoint or credentials; `POST /call` `{ name, input }`
45
- streams the call's events, and `POST /invoke` answers a gate it paused on, as
46
- in chat. The console draws the form from the input schema and lays the result
47
- out from its value: figures, charts, tables, images, audio and Markdown, with
48
- the raw JSON beside them.
49
-
50
- The playground's run-tab handoff (`savePlaygroundRunPayload`,
51
- `readPlaygroundRunIdFromUrl`, …) lives in `@theoremjs/playground`; see
52
- [`playground/README.md`](../playground/README.md).
53
-
54
- ## The wire
55
-
56
- Turn lines, live envelopes, request bodies, live messages and the profile
57
- interface `describe` returns are each checked against their schema, both ways:
58
-
59
- - **Browser → host.** `createTheoremHandler` reads `/turn`, `/invoke` and
60
- `/steer` bodies with `theoremTurnRequestSchema`, `theoremInvokeRequestSchema`
61
- and `theoremSteerRequestSchema`; a missing or malformed field is a `request`
62
- error (400). `/invoke` answers a paused call by its `gateId` with a
63
- `decision`: `approve` (with `input` when the user edited it, `secret` at a
64
- sign-in gate) or `deny`; the host settles each one. An answer whose request
65
- ends before its call settles (the network drops, the host fails) puts the
66
- call back to wait, to be answered again; once it settled, the session keeps
67
- the settle (`TheoremSessionState.settled`, pruned with the gates): a second
68
- answer is refused with `session.gate_expired`, and a walk-away naming the
69
- call reads its result. A message sent while
70
- its reply waits walks away in its own `/turn`: `abandon` names the waiting
71
- calls, its history leaves exactly those open, and the host settles each
72
- cancelled ahead of the message's reply (`checkWalkAway` and `walkAway` for a
73
- host with routes of its own). A relay reads the live client's messages with
74
- `parseLiveClientMessage`. A host with routes of its own reads a body with
75
- `checkRequest(schema, body, what)`, and answers a paused call with
76
- `answerGatedCall` (`@theoremjs/agents/kernel`), the rule the handler uses.
77
- - **Host → browser.** `describe` returns the profile interface as
78
- `profileInterfaceSchema` names it: tool ids, never a tool's definition, and
79
- no host functions. The transport and the live client read each line or
80
- envelope against its kind's schema, and `describe` against
81
- `profileInterfaceSchema`. A kind the client does not know reaches
82
- the event handler as `unsupported`, and one that fails its check (not JSON,
83
- no kind, or a known kind that fails its schema) as `malformed`: a
84
- `bad_response` naming what broke, never the value. Either way the reply or
85
- call goes on without it. The chat names a skipped part in the composer and
86
- the live call in its failure banner, both with the lexicon's
87
- `session.part_skipped`; a run built on `streamInterfaceTurn` hears of it
88
- through its `view.skipped`. A `describe` reply that fails its schema is
89
- `bad_response`.
90
-
91
- ## Local layout
71
+ Requirements:
92
72
 
93
- ```text
94
- Development/
95
- theoremai/ # kernel + this package
96
- react/
97
- theoremai-frontend/ # site; apps/run consumes file:../../../theoremai/react
98
- ```
99
-
100
- ## Tool credentials
101
-
102
- Tool credentials live on the server, never in the browser. `createTheoremHandler`
103
- keeps them in a `credentialStore` keyed by session id (default: process memory;
104
- use a vault or an encrypted row in production) and loads them into every turn
105
- and every resumed call.
106
-
107
- - **Typed keys** — at a bearer or API-key gate, the user types the key into the
108
- gate card. It goes up once as `secret` on the resume request, and the server
109
- saves it under the gate's slot; the card keeps no copy and no event carries it.
110
- The server decides the credential's kind and header from the tool, not the
111
- request.
112
- - **OAuth** — pass `authorizationUrl`: begin the flow with `createOAuthPkceFlow`,
113
- binding it to the `sessionId` you're given, and return its URL; the gate card
114
- opens it in a popup. Your callback route exchanges the code, saves the
115
- credential under that session, then its page calls `notifyOAuthComplete(slot)`.
116
- The card takes that message only from its own popup and origin, and resumes the
117
- gate with its id alone.
118
- - **Refresh** — a refreshed OAuth token is saved to the store before the call
119
- goes on, so a rotated refresh token is never lost.
73
+ - TypeScript 5.7 or later, for the type declarations.
74
+ - A bundler that handles CSS imports. Vite does. `/ui` and `/live` import their own stylesheets.
75
+ - For voice, a bundler that resolves `new URL('./worklets/mic-capture.js', import.meta.url)`. Vite does.
76
+
77
+ The package does not include the Figtree font. The theme uses Figtree, and falls back to the system font if Figtree is not loaded. To use Figtree, load the font in your app.
78
+
79
+ ## Quick start
80
+
81
+ ### 1. Register the provider and create the handler
82
+
83
+ Register the provider before you create the handler. The profile binds models through the registered provider's `.model()` helper.
84
+ The helper takes the protocol and the model ID. Pass credential values through the handler's vault.
85
+ Supply vault slots as own properties. The server ignores inherited credentials and resolvers.
86
+ Replacing the registration affects later runs. An active run keeps the registration that it started with.
120
87
 
121
88
  ```ts
122
- const credentialStore = createMemoryCredentialStore();
123
- const secret = secrets.oauthStateSecret;
124
- const redirectUri = 'https://app.example/oauth/callback';
89
+ import { defineProfile, geminiProvider, registerProvider } from '@theoremjs/agents';
90
+ import { createMemoryConversationStore, createTheoremHandler } from '@theoremjs/react/server';
91
+
92
+ const gemini = registerProvider(geminiProvider, { keySlot: 'main' });
93
+
94
+ const support = defineProfile({
95
+ type: 'text',
96
+ id: 'support.agent',
97
+ identity: { handle: 'support', system: 'You help customers with their account.' },
98
+ models: {
99
+ fast: gemini.model('geminiInteractions', 'gemini-3.5-flash-lite'),
100
+ },
101
+ defaultModel: 'fast',
102
+ tools: { allow: [] },
103
+ inputs: { text: true },
104
+ });
125
105
 
126
106
  export const theorem = createTheoremHandler({
127
- profile,
128
- provider,
129
- credentialStore,
130
- authorizationUrl: async (challenge, { sessionId }) =>
131
- (
132
- await createOAuthPkceFlow({
133
- resourceServerUrl: challenge.resource ?? 'https://api.tracker.example',
134
- clientId: 'https://app.example/oauth/client.json',
135
- redirectUri,
136
- scopes: challenge.requiredScopes,
137
- signingSecret: secret,
138
- sessionBinding: sessionId,
139
- })
140
- ).authorizationUrl,
107
+ profile: support,
108
+ provider: { vault: { main: process.env.GEMINI_API_KEY } },
109
+ conversationStore: createMemoryConversationStore(),
141
110
  });
111
+ ```
112
+
113
+ The memory store keeps each conversation in process memory. For other stores, see [Keep the conversation](#keep-the-conversation).
114
+
115
+ ### 2. Mount the handler
116
+
117
+ `createTheoremHandler` returns a function: `(request: Request) => Promise<Response>`. It works in Workers, Deno, Bun, Hono, and Next route handlers. In Node, use an adapter.
142
118
 
143
- // The callback route: the handler's session cookie comes with the redirect.
144
- export async function oauthCallback(request: Request): Promise<Response> {
145
- const sessionId = theoremSessionId(request);
146
- if (!sessionId) return new Response(null, { status: 401 });
147
- const params = new URL(request.url).searchParams;
148
- const { credential } = await exchangeOAuthPkce({
149
- code: params.get('code') ?? '',
150
- state: params.get('state') ?? '',
151
- iss: params.get('iss') ?? undefined,
152
- redirectUri,
153
- signingSecret: secret,
154
- sessionBinding: sessionId,
155
- });
156
- const saved = (await credentialStore.load(sessionId)) ?? {};
157
- await credentialStore.save(sessionId, { ...saved, tracker: credential });
158
- // That page runs `notifyOAuthComplete('tracker')` from `@theoremjs/react`.
159
- return Response.redirect(new URL('/oauth/done?slot=tracker', request.url), 303);
119
+ Send all requests under one path to the handler. The default path is `/api/theorem`.
120
+
121
+ ```ts
122
+ export default {
123
+ fetch(request: Request) {
124
+ return new URL(request.url).pathname.startsWith('/api/theorem')
125
+ ? theorem(request)
126
+ : new Response('Not found', { status: 404 });
127
+ },
128
+ };
129
+ ```
130
+
131
+ ### 3. Show the chat
132
+
133
+ ```tsx
134
+ import { TheoremChat } from '@theoremjs/react/ui';
135
+
136
+ export function App() {
137
+ return <TheoremChat endpoint="/api/theorem" />;
160
138
  }
161
139
  ```
162
140
 
163
- A host with its own `session` resolver passes its own session id instead of
164
- `theoremSessionId`. For voice, the relay you host receives a typed key as `secret`
165
- on the `executeTool` message: save it with `credentialFromTypedSecret` (from
166
- `@theoremjs/agents/kernel`) under the gate's slot, and pass a
167
- `ToolCredentialSource` over your store as `credentials` on every `executeTool`.
141
+ The profile, the keys, and the system prompt stay on the server. The browser receives only the interface of the profile: the models, the inputs, the tool names, and the wording. The handler removes the system prompt from this description.
142
+
143
+ ## Entry points
144
+
145
+ | Import | Content |
146
+ | --- | --- |
147
+ | `@theoremjs/react` | Hooks, transports, and client logic |
148
+ | `@theoremjs/react/ui` | `TheoremChat`, `TheoremHost`, `TheoremDecision`, `TheoremEmbedding`, and their parts |
149
+ | `@theoremjs/react/live` | `LiveRunner`, the voice and video call UI |
150
+ | `@theoremjs/react/server` | The handlers and the stores they use |
151
+ | `@theoremjs/react/ui/theme` | `theoremTheme`, the base for your own Astryx theme |
152
+ | `@theoremjs/react/ui/icons` | The icon set of the theme |
153
+ | `@theoremjs/react/client` | The client logic, without the hooks |
154
+
155
+ ## Chat
156
+
157
+ `<TheoremChat />` accepts these props. All props are optional.
158
+
159
+ | Prop | Use |
160
+ | --- | --- |
161
+ | `endpoint` | The path where the handler is mounted. Default: `/api/theorem`. |
162
+ | `http` | Fetch options for the default transport, such as auth headers or a custom `fetch`. |
163
+ | `transport` | Your own transport, instead of `endpoint`. Use it for tests or for hosts that do not use HTTP. |
164
+ | `theme`, `mode` | An Astryx theme, and `'system'`, `'light'`, or `'dark'`. Without `theme`, the chat uses your theme or the Theorem theme. |
165
+ | `labels` | Replacement text for each locale. See [Wording](#wording). |
166
+ | `placeholder`, `emptyState` | The composer placeholder, and the content above the composer before the first message. |
167
+ | `density`, `maxWidth`, `className`, `style` | Layout. `maxWidth` is a CSS length. Default: half of the chat width, or the full width on a narrow screen. |
168
+ | `trace` | Shows the trace instead of the chat. Use it when your own control switches the view. The profile must record traces. |
169
+ | `conversation`, `conversationView`, `onConversationChange` | Resume a conversation that the server keeps, or open one you started. See below. |
170
+ | `initialText` | The text that the composer starts with. The user can send it or change it. The component reads it once, when it mounts. |
171
+ | `chatRef` | Sends a message from your code. See below. |
172
+ | `composer` | `false` leaves the composer out. The chat is the transcript alone, and your own control sends through `chatRef`. Default: `true`. |
173
+
174
+ ### Resume a conversation
175
+
176
+ The server keeps the conversation in its `conversationStore`. The browser never sends the history. It holds only a ref that the store issued.
177
+
178
+ `onConversationChange` gives you each new ref. It runs:
179
+
180
+ - when the message reaches the server
181
+ - each time the server saves the reply, while the reply runs and when it ends
182
+
183
+ Keep the latest ref. To resume the conversation, pass it as `conversation`. The component reads this value once, when it mounts. It then draws the conversation as the server keeps it, including a reply that waits for a gate.
184
+
185
+ ```tsx
186
+ <TheoremChat
187
+ endpoint="/api/theorem"
188
+ conversation={savedRef}
189
+ onConversationChange={(ref) => saveRef(ref)}
190
+ />
191
+ ```
192
+
193
+ If the server does not save the message, the message leaves the transcript. It goes back in the composer with the error, and the user can send it again.
194
+ If the browser loses the response to a gate answer, the chat reads the conversation again from the server. The same answer sent again fails with `session.gate_expired`.
195
+ If the server can no longer open the ref, the message fails with `session.conversation_unknown`. The error's `errorCopy` carries that key. The chat then sends the message once more, with the transcript the page shows as `transcript`. `createTheoremHandler` ignores it, because the server owns the conversation, so the message fails. The studio's server owns no conversation of its own: it continues from the transcript under a new ref.
196
+
197
+ A profile whose `providerContinuation.history` is `'stateless'` keeps no conversation. The server stores nothing and issues no ref. The transcript is kept only in the page, and a reload clears it.
198
+
199
+ ### Send a message from your code
200
+
201
+ `chatRef` gives you `send(text, files?)`. It sends the text and optional array of browser files as the user. Files use the same profile validation and encoding as composer attachments. When the reply is complete, it returns the blocks that the turn added. It returns `null` if the chat cannot accept a message now. This happens while a reply streams or waits for a gate.
202
+ `chatRef` also gives you `stop()`. It stops the reply that streams, as the composer's stop button does.
203
+
204
+ ```tsx
205
+ const chat = useRef<TheoremChatHandle>(null);
206
+
207
+ const turn = await chat.current?.send('Summarize my open tickets');
208
+
209
+ <TheoremChat chatRef={chat} />
210
+ ```
211
+
212
+ ### Gates
213
+
214
+ A **gate** is a point where a tool call stops and waits for a person. There are two kinds:
215
+
216
+ - An approval gate asks the person to allow the call.
217
+ - A sign-in gate asks the person for a credential.
218
+
219
+ The chat shows each gate as a card in the transcript. When the person answers, the server settles the call and the turn continues.
220
+
221
+ The server saves a typed key. The browser does not keep it. For OAuth and typed keys, see [Tool credentials](https://github.com/masudl-hub/theoremai/blob/main/react/docs/credentials.md). For the data that each answer sends, see [The wire](https://github.com/masudl-hub/theoremai/blob/main/react/docs/wire.md).
222
+
223
+ If the person sends a new message while a gate is open, the server cancels the open calls. The message names only the calls it cancels. The server reads each call from the reply it saved, not from the browser. The reply to the new message follows in the same stream. See [Composer, queue, and steer](https://github.com/masudl-hub/theoremai/blob/main/react/docs/composer.md).
224
+
225
+ ## Other profile types
226
+
227
+ Each profile type has a handler and a component. Mount the handler. Point the component at it.
228
+
229
+ ### Host
230
+
231
+ A `host` profile does not run a model. The page calls the tools of the profile directly. Each call passes through the gates and the guardrails of the kernel.
232
+
233
+ ```ts
234
+ import { createTheoremHostHandler } from '@theoremjs/react/server';
235
+
236
+ export const host = createTheoremHostHandler({ profile: opsConsole }); // mount at /api/host
237
+ ```
238
+
239
+ ```tsx
240
+ import { TheoremHost } from '@theoremjs/react/ui';
241
+
242
+ <TheoremHost endpoint="/api/host" />
243
+ ```
244
+
245
+ The handler accepts these requests:
246
+
247
+ - `GET` describes each allowed tool with its input and output JSON Schema. It never shows the endpoint or the credentials of a tool.
248
+ - `POST /call` with `{ name, input }` streams the events of the call.
249
+ - `POST /invoke` answers a gate that paused a call.
250
+
251
+ The console builds a form from the input schema. It shows the result as figures, charts, tables, images, audio, or Markdown, with the raw JSON beside it. If the data renderer fails, the view switches to JSON so the selected view matches what is shown. For your own UI, use `createHostTransport` and `useTheoremHost`.
252
+
253
+ ### Decision
254
+
255
+ A `decision` profile has no conversation. You give it a JSON state and questions. It answers the questions about the state.
256
+
257
+ ```ts
258
+ import { createTheoremDecisionHandler } from '@theoremjs/react/server';
259
+
260
+ export const decide = createTheoremDecisionHandler({ profile, questions, vault }); // mount at /api/decision
261
+ ```
262
+
263
+ ```tsx
264
+ import { TheoremDecision } from '@theoremjs/react/ui';
265
+
266
+ <TheoremDecision endpoint="/api/decision" />
267
+ ```
268
+
269
+ - `questions` maps each question id to its definition.
270
+ - `images` is optional. It holds pictures every decision sends, by the reference the questions write, such as `<ghs02>` in "Which item matches <ghs02>?". The page sees them and refuses its own image under one of their names.
271
+ - `vault` holds the keys of the profile, by slot.
272
+ - `metadata` is optional. It adds data, such as the signed-in user, to the trace record of each decision.
273
+
274
+ When the profile sets `maxStateBytes` in its `inputs`, `TheoremDecision` shows the size of the state against that limit under it. Each text box is as tall as the lines of its text, within a limit.
275
+
276
+ In one column, the state sits in the middle of the page until you ask for a decision. It then eases up to the top, and the answers appear under it. With `columns`, the state moves to the left instead and the answers fill the right.
277
+
278
+ A profile whose model takes images (`images: true`, see the TypeSafe options in `docs/contracts/providers.md`) shows a drop area under the state when a question is a box. Each PNG or JPEG dropped there becomes a state field named after the file. The field holds the image's reference, its name in angle brackets: `pantry shelf.jpg` becomes `{ "pantry_shelf": "<pantry_shelf>" }`. The field shows the image, opens it full size in a lightbox when clicked, and can replace it. A question names the field in backticks, such as "Which jar in `pantry_shelf` is open?". TypeSafe reads a reference anywhere it appears as text. The brackets keep a reference from matching an ordinary word. A decision sends only the images its state still names, as `assets`, and the kernel refuses any that are not PNG or JPEG bytes before a request leaves. Pass `defaultImages`, with a `defaultState` that names them, to start with some.
279
+
280
+ A box answer is drawn as the part of the image it covers, with its edges beside it as shares of the image. The image is the one the question's `image` gives, a reference or a state field. Without one, it is the one at the field the instructions first name in backticks, such as `chart` in "Where on `chart` would the green apple in `photo` sit?". Without that, it is the first image the state names, or else the first held. Click the cut-out to open the whole image in a lightbox, with the box outlined in `--color-accent` and the rest shaded in `--color-overlay`.
281
+
282
+ Pass `onQuestion` to open a question where it is set up, such as in an editor. Each answer then has a button that calls it with the question's id.
283
+
284
+ Pass `questionText`, each question's text by its id, to show it under its answer's title, cut to two lines with the rest on hover. The page is never sent a question's instructions, so it shows only the text you give it.
285
+
286
+ For your own UI, use `useTheoremDecision`. Its `decide(state, assets)` takes the images by reference.
287
+
288
+ ### Embedding
289
+
290
+ An `embedding` profile turns inputs into vectors. Each input is text, or an object with `parts`: text and files.
291
+
292
+ ```ts
293
+ import { createTheoremEmbeddingHandler } from '@theoremjs/react/server';
294
+
295
+ export const embed = createTheoremEmbeddingHandler({ profile, vault }); // mount at /api/embedding
296
+ ```
297
+
298
+ ```tsx
299
+ import { TheoremEmbedding } from '@theoremjs/react/ui';
300
+
301
+ <TheoremEmbedding endpoint="/api/embedding" />
302
+ ```
303
+
304
+ `TheoremEmbedding` is a search bench. Paste text and choose where it splits: not at all, at each line, at blank lines, at commas, or at a separator of your own. Or drop files: a text, Markdown, CSV, or JSON file splits the same way, and an image, audio, video, or PDF file is one document. **Index** embeds the documents with the `document` task, in as few requests as the profile's `inputs` and its model allow. The interface holds the profile's inputs to what its default model takes: the file types both accept, the files one input may hold, and the inputs one request may carry, read from the model's protocol. For a protocol of your own, pass it to `embeddingInterface(profile, [protocol])`. Type a query and press **Search**: it embeds the query with the `query` task and ranks the documents by cosine similarity. `defaultText` and `defaultQuery` fill the two cards.
305
+
306
+ Each request is held to the `inputs` of the profile before it is sent, and each shows as JSON and in the trace.
307
+
308
+ For your own UI, use `useTheoremEmbedding` for one request, and `embed(inputs, { task })` to set the task. Use `useEmbeddingIndex` to embed many documents in batches. `splitText`, `embeddingBatches`, and `rankMatches` from `@theoremjs/react/client` split, batch, and rank as the bench does.
309
+
310
+ **Index** also draws the documents as a space: each a dot in a ball, placed by the three directions their vectors differ most along, and joined to its nearest neighbour when that pair is far more alike than the typical pair. A query is a white dot joined to its matches. Drag to turn it; point at a dot to name its document. For your own UI, `EmbeddingSpaceView` from `@theoremjs/react/ui` draws a space from `layoutSpace(vectors)`, or from `layoutSpaceSoon(vectors, signal)`, which lays it out in slices so the page stays responsive, and places a query with `placeQuery(space, vector)`, all from `@theoremjs/react/client`. Where the browser cannot draw it, the view calls `onUnavailable`, and the bench lists the vectors instead.
311
+
312
+ ### Live
313
+
314
+ A `live` profile runs a voice or video call. The call goes through a WebSocket relay that you host with `runSession` from `@theoremjs/agents`.
315
+
316
+ `LiveRunner` needs two props:
317
+
318
+ - `iface`: the interface of the profile. `interfaceFromProfile` (from `@theoremjs/agents/interface`) builds it.
319
+ - `connection`: a function that tells the runner what to connect to.
320
+
321
+ ```tsx
322
+ import { LiveRunner } from '@theoremjs/react/live';
323
+
324
+ <LiveRunner iface={iface} connection={() => ({ profile: 'support.voice' })} />
325
+ ```
326
+
327
+ This code opens `wss://<your host>/api/live/relay?profile=<id>`. To send your own message to the relay, add `openMessage`. To use your own socket, add `createSocket`.
168
328
 
169
- ## Composer pending intents
329
+ `LiveRunner` also accepts `theme`, `mode`, `labels`, and `trace`. Tool gates in a call appear in a dialog over the call.
170
330
 
171
- `src/interface/` owns stash / queue / steer list ops and the action matrix.
172
- This package wires AbortSignal, the pending bar, and playground turn/steer HTTP.
331
+ ### The page's part in a call
173
332
 
174
- | Phase | Empty composer | Filled composer |
333
+ Three more props give the call values from the page. The profile declares each one; the page only supplies the value.
334
+
335
+ | Prop | The profile declares | What the page gives |
175
336
  | --- | --- | --- |
176
- | idle | disabled | Send (+ Stash menu) |
177
- | streaming | Stop | Queue (+ Steer / Send now / Stash) |
178
- | gated | disabled | Queue (+ Send now / Stash; no Steer) |
179
-
180
- Enter matches the primary action. No keyboard shortcuts for stash/steer.
181
-
182
- Send now while gated walks away from every waiting gate in the message's own request
183
- (`walkAway` on `streamInterfaceDraftTurn`): the host settles each call cancelled, the
184
- paused reply commits, and the message's reply follows in the same stream. The message
185
- leaves the composer only once it posts; if the request fails first, the reply still waits.
186
- An answer at a gate that fails before its call settles leaves the gate to answer again,
187
- the failure in the composer; one that fails after keeps the call's result. The gate cards
188
- show the answer on its way from their owner (`useTheoremChat().answering`, passed as
189
- `ChatTranscript`'s `answering`, `ApprovalCard`'s `decided`, `AuthChallengeCard`'s
190
- `submitted`), so a failed answer brings the gate's actions back. Steer POSTs use the Cache API
191
- on Cloudflare (process Map locally) so mid-turn injects work across isolates.
192
- Live sessions key the same inbox by `sessionId` from relay `ready`.
193
-
194
- A chat can be kept and resumed: `onChatChange` (or `useTheoremChat`'s `onChange`) reports the
195
- `{ blocks, session }` each time the conversation comes to rest (a turn finished, a message
196
- added or removed; never while a reply streams or waits on a gate), and `initialChat` (`initial`)
197
- resumes from one, the session carrying what the next turn is sent with. `chatRef` (`sendText`
198
- on the hook) sends a message as the composer would and resolves with the blocks the turn added
199
- once its reply is done, or `null` while a reply streams or waits on a gate.
200
-
201
- Pending rows show attachment / voice previews, text, **Queue** (stash → queue),
202
- and **Send now**. Clicking the text restores the full draft (text + files + voice)
203
- into the composer; if the composer already had a payload, that payload is re-stashed.
337
+ | `slots` | `inputs.slots` | The value chosen for each slot. It is read when the call starts. A value that changes during a call takes the call up again with it when the profile has resumption on; otherwise the next call has it. |
338
+ | `context` | `inputs.context` with `from: ['client']` | Any JSON the agent should know, such as the page the person is on. |
339
+ | `pageTools` | A function tool with `answeredBy: 'page'` | A function for each page tool, by name. It returns `{ output }`. |
340
+
341
+ ```tsx
342
+ <LiveRunner
343
+ iface={iface}
344
+ connection={() => ({ profile: 'support.voice' })}
345
+ slots={{ language: 'fr' }}
346
+ context={{ page: '/pricing', cart }}
347
+ pageTools={{
348
+ highlight: (args) => ({ output: { success: highlight(String(args.target)) } }),
349
+ }}
350
+ />
351
+ ```
352
+
353
+ - `context` goes with the start of the call. When its value changes, the runner sends it again. The agent reads it as background and does not reply to it.
354
+ - A page tool's output schema checks what the page returned. Every other tool runs on the relay, with its gates.
355
+ - `TheoremChat` and `useTheoremChat` take the same `pageTools`. When a chat agent calls a page tool, the reply waits for the page's answer and then goes on. The person is shown no prompt.
356
+ - The runner writes a console warning when the profile has a page tool with no function in `pageTools`, and when `pageTools` has a function for a tool that is not a page tool.
357
+
358
+ The live client checks a tool call with its relay before it invokes a page callback.
359
+ It waits for permission and sign-in decisions first. A claimed dispatch grant supplies the final checked arguments.
360
+ The callback runs only for that grant. It returns its result with the call ID, operation ID, and grant ID.
361
+ The server validates the result against the saved claim before the kernel checks the tool output.
362
+ A restored page gate does not run a callback. Cached terminal replay emits no dispatch grant.
363
+
364
+ To make the agent speak first, set `live.greeting` on the profile. The page sends nothing for it.
365
+
366
+ ### A call that drops
367
+
368
+ Use `live.sessionResumption` to reconnect after a dropped connection or a provider time limit.
369
+ The client stores the native checkpoint beside portable history. It sends both when it opens the next session.
370
+
371
+ The runner shows `reconnecting`. It tries five times, after 0.5, 1, 2, 4, and 8 seconds.
372
+ If all attempts fail, it reports a `network` error.
373
+ The relay receives the absence duration. If the profile sets `live.resumed`, the agent announces its return after the configured `afterMs`.
374
+
375
+ The kernel validates native state before resumption. A protocol, provider, model, connection, key slot, or history mismatch rebuilds from portable history.
376
+ It emits `provider_warning`. The client then discards the incompatible checkpoint and retains the history.
377
+ A rebuilt session with history does not repeat the initial greeting.
378
+
379
+ A failed call does not reconnect. Without `live.sessionResumption`, a provider-ended call is complete.
380
+
381
+ ### Your relay
382
+
383
+ Use a session boundary to authenticate the conversation before you open the upstream session.
384
+ Use the same authenticated session resolver and atomic session store for chat and live requests.
385
+ The boundary rejects native state that this conversation did not save.
386
+ Saved ownership also covers the operation state. An older live checkpoint cannot revive a completed action.
387
+
388
+ ```ts
389
+ import { runSession } from '@theoremjs/agents';
390
+ import {
391
+ createTheoremSessionBoundary,
392
+ parseLiveOpenMessage,
393
+ } from '@theoremjs/react/server';
394
+
395
+ const boundary = await createTheoremSessionBoundary({
396
+ request,
397
+ session: authenticatedConversation,
398
+ sessionStore: sharedSessionStore,
399
+ });
400
+ const open = parseLiveOpenMessage(firstFrame);
401
+ const session = await runSession(
402
+ { profile, ...await boundary.prepareLiveOpen(open, hostContext) },
403
+ { vault: hostKeyVault, execution: boundary.toolExecution, pageExecution: boundary.pageExecution },
404
+ );
405
+ for await (const event of session.events()) {
406
+ if ((event.type === 'done' || event.type === 'provider_checkpoint') && event.providerState) {
407
+ await boundary.recordCheckpoint(event.providerState);
408
+ }
409
+ sendToBrowser(event);
410
+ }
411
+ ```
412
+
413
+ - `prepareLiveOpen` returns validated history, slots, context, native state, and absence duration. It removes system-role history.
414
+ - Its second argument supplies server context. Browser context stays under `client`.
415
+ - Save checkpoint ownership before you send the event. If storage fails, close the session without advertising that checkpoint.
416
+ - The boundary refuses native resumption while a saved operation is ready, running, or uncertain. It does not repeat the operation.
417
+ - Without a session resolver, the boundary uses an anonymous cookie. Forward its `setCookie` value in the upgrade response.
418
+ - `open.host` is the `openMessage` that your `connection` returned.
419
+ - Read later messages with `parseLiveClientMessage`. Give context messages to `session.sendContext({ client: message.context })`.
420
+
421
+ To build your own call view, use `useLiveRunnerModel(iface, connection, { slots, context, pageTools })` from `@theoremjs/react/ui`. It returns the state of the call, its captions, and its controls. `LiveCaptionsPanel` draws the captions.
422
+
423
+ ### A custom browser transport
424
+
425
+ Use `withPageToolDispatch` when your screen consumes turn events through a custom transport.
426
+ The helper runs callbacks only for dispatch grants received on the active stream.
427
+ It never reads a saved transcript to authorize callback execution.
428
+ Implement the transport's `pageResult` method to return the result to your authenticated server.
429
+ `createHttpTransport` and `createHostTransport` post that result to `/page-result`.
430
+ The built-in chat and host console apply this helper for their `pageTools`.
431
+ For a custom host view, pass callbacks as the second argument to `useTheoremHost`.
432
+
433
+ ```ts
434
+ import { createHttpTransport, withPageToolDispatch } from '@theoremjs/react';
435
+
436
+ const transport = withPageToolDispatch(
437
+ createHttpTransport({ endpoint: '/api/theorem' }),
438
+ { highlight: async argumentsFromServer => ({ output: { shown: argumentsFromServer.id } }) },
439
+ );
440
+ ```
441
+
442
+ ## Server options
443
+
444
+ `createTheoremHandler` requires `profile` and `provider`. All other options are optional.
445
+
446
+ | Option | Use |
447
+ | --- | --- |
448
+ | `profile` | The profile to serve. The kernel registers it when you create the handler. |
449
+ | `provider` | Host options, including the credential vault. A function can return these options per request, for example for tenant-specific credentials. |
450
+ | `session` | Returns the session id of the caller, for example `${userId}:${conversationId}`. Return `undefined` to refuse the request with a 401. Default: a random id in an HttpOnly, SameSite=Lax cookie that lasts 30 days. |
451
+ | `host` | App data for the tool handlers (`ctx.host`), such as the signed-in user. |
452
+ | `pageToolBroker` | Delivers results to active page dispatches. Default: bounded process memory. |
453
+ | `conversationStore` | Where the conversation is kept. Required, except for a stateless profile, which must not have one. See [Keep the conversation](#keep-the-conversation). |
454
+ | `sessionStore`, `steerInbox` | Where open gates and queued steers are kept. Default: process memory. |
455
+ | `credentialStore` | Where tool credentials are kept, by session. Default: process memory. |
456
+ | `authorizationUrl` | Returns the sign-in URL for an OAuth gate. Without it, an OAuth gate has no URL. |
457
+ | `gateTtlMs` | How long a gate waits for an answer. Default: 30 minutes. A later answer fails with `session.gate_expired`. |
458
+ | `clientEvents` | Options for `forClient` and `clientError`, which prepare each event and each caught failure for the browser. `reader: 'builder'` sends the raw reason in place of the wording of the profile. Use it only in a development tool on your own machine. |
459
+ | `onError` | Receives each error that the handler catches. A visitor sees the wording of the profile for that kind of error, never the error. |
460
+
461
+ ### Start a conversation with content
462
+
463
+ `startConversation` stores a conversation that opens on blocks you wrote, and returns its ref and its view. The model reads the blocks as history, so the user's next message is a follow-up.
464
+
465
+ ```ts
466
+ import { startConversation, theoremSession } from '@theoremjs/react/server';
467
+
468
+ const session = theoremSession(request);
469
+ const { ref } = await startConversation({ sessionId: session.id, store: conversationStore }, profile, [
470
+ { id: 'ask', kind: 'user-text', text: 'Two days in Lisbon. Where should I stay?' },
471
+ { id: 'reply', kind: 'text', text: 'Stay in Chiado.' },
472
+ { id: 'done', kind: 'turn-done' },
473
+ ]);
474
+ // Send session.setCookie as Set-Cookie when it is set, and give the page the ref and view.
475
+ ```
476
+
477
+ Pass the ref as `conversation` and the view as `conversationView`. The chat draws the view at once, and does not read the conversation from the server again.
478
+
479
+ ```tsx
480
+ <TheoremChat endpoint="/api/theorem" conversation={ref} conversationView={view} />
481
+ ```
482
+
483
+ - The store is the one you pass to the handler as `conversationStore`, and the session is the one the chat's requests carry. `theoremSession(request)` is the handler's cookie session: its `id`, and a `setCookie` header when the request had none. With your own `session` resolver, pass the id it returns.
484
+ - The profile's guardrails read the history first, at the `history` boundary, as they read every turn's history. A detector set to `redact` changes what the model reads; the transcript keeps your text. A detector set to `block` throws `detect.blocked`, and nothing is stored.
485
+ - The result's `events` are the guardrail events for what matched.
486
+
487
+ ### Keep the conversation
488
+
489
+ The conversation store keeps the transcript, the history that the model reads, the provider's native state, and a reply that waits for a gate.
490
+ The session store keeps gates, permissions, and operations. The two stores are separate.
491
+
492
+ | Store | Where the conversation is kept |
493
+ | --- | --- |
494
+ | `createMemoryConversationStore()` | Process memory, for one server instance. A conversation that is not saved for 24 hours expires. At most 10,000 conversations are kept; the one saved longest ago goes first. Set `ttlMs` and `maxSessions` to change these limits. |
495
+ | `createSealedConversationStore({ keys, keyVersion })` | Nowhere on the server. The ref is the conversation itself, encrypted and signed with your key, and bound to the session. The browser cannot read it or change it. |
496
+ | Your own `TheoremConversationStore` | Your database. |
497
+
498
+ A ref that the store cannot open fails with `session.conversation_unknown`. This includes a ref from another session.
499
+
500
+ The sealed store:
501
+
502
+ - Each key is at least 32 bytes. Keep the keys on the server.
503
+ - `keyVersion` names the key that seals new writes. Keep an older key in `keys` while its refs must still open.
504
+ - Each save issues a new ref. An older ref of the same conversation still opens, as a rewind does.
505
+ - If the user stops a reply, or the browser leaves while a reply runs, the server saves the result under a new ref that the browser never receives. The browser keeps the ref from when its message reached the server. The reply, and any tool call it settled, is lost to the browser. With the memory store or your own store, the ref does not change, and the chat shows what the server saved.
506
+ - The ref holds the whole conversation, including the bytes of each attachment. Every request sends it, so the conversation must stay under `maxRequestBytes`.
507
+
508
+ To keep conversations in your own database, implement these two methods:
509
+
510
+ ```ts
511
+ import type {
512
+ ConversationSnapshot,
513
+ ConversationWriteResult,
514
+ StoredConversation,
515
+ } from '@theoremjs/react/server';
516
+
517
+ interface ConversationDatabase {
518
+ read(sessionId: string, ref: string): Promise<ConversationSnapshot | undefined>;
519
+ write(
520
+ sessionId: string,
521
+ ref: string | null,
522
+ expectedRevision: string | null,
523
+ next: StoredConversation,
524
+ ): Promise<ConversationWriteResult>;
525
+ }
526
+ ```
527
+
528
+ - Every read and write names the session. For a ref that this session did not write, `read` returns `undefined` and `write` returns `{ written: false }`.
529
+ - A null `ref` creates a conversation. Return a new opaque `ref` and `revision`.
530
+ - Otherwise, write only while the stored revision is still `expectedRevision`, as one atomic operation. Return `{ written: false }` on a conflict.
531
+ - Check what you read back with `storedConversationSchema`.
532
+
533
+ ### Share session authority
534
+
535
+ Use one shared `TheoremSessionStore` when requests can reach several server instances.
536
+ New execution claims retain accepted request data and final prepared input under `operation.invocation`.
537
+ Use this data in `authorizeToolReplay` to check current access to the affected resource.
538
+ Older records can lack this evidence. Refuse disclosure if your access check needs it.
539
+ It holds approvals and provider checkpoint ownership. Tool credentials use the separate `credentialStore`.
540
+
541
+ ```ts
542
+ import type {
543
+ SessionSnapshot,
544
+ SessionWriteResult,
545
+ TheoremSessionState,
546
+ } from '@theoremjs/react/server';
547
+
548
+ interface SharedSessionStore {
549
+ load(sessionId: string): Promise<SessionSnapshot | undefined>;
550
+ compareAndSwap(
551
+ sessionId: string,
552
+ expectedRevision: string | null,
553
+ next: TheoremSessionState,
554
+ ): Promise<SessionWriteResult>;
555
+ }
556
+ ```
557
+
558
+ Implement `compareAndSwap` as one atomic database operation. A separate read followed by a write is unsafe.
559
+ A null revision creates an absent session. A matching revision replaces the current state.
560
+ Return `{ written: false }` on a conflict. On success, return `{ written: true, revision }` with a new opaque revision.
561
+ Never reuse a revision, including after expiration. Return detached state from `load`.
562
+ Return write success only after the store meets its promised durability.
563
+
564
+ Each tool action has a kernel-issued `operationId`. The provider's `callId` identifies the upstream call.
565
+ An approval must carry both values. A new action receives a new operation ID even if its provider reuses a call ID.
566
+ A settled replay can omit input. If it supplies input, that input must match the accepted invocation.
567
+ The built-in clients copy this reference from the gate event. Custom clients must preserve it.
568
+ Historical events can omit operation IDs for display. They cannot authorize a new action.
569
+
570
+ The handler validates versioned state and retries a conflicting transition at most eight times.
571
+ It does not run tools inside this retry loop. Invalid state fails; it does not become an empty session.
572
+ The previous session `load`/`save` contract is not supported. Existing unversioned state and versions 1 or 2 require an explicit host migration.
573
+ An older pending gate does not prove that its body never ran. Verify the destination before restoring execution authority.
574
+ The current session state uses version 3. Gates and operation records use operation IDs as keys.
575
+ Each operation record contains its retained outcome. There is no separate outcome cache.
576
+ A gate belongs to the profile that issued it. Another profile cannot consume it through shared session storage.
577
+ The handler saves an accepted answer as ready before it invokes the kernel.
578
+ A retry before dispatch must repeat that accepted answer. It cannot change the input or decision.
579
+ Immediately before the tool body, an atomic claim changes ready to running.
580
+ The kernel saves the terminal outcome before it sends completion to the client.
581
+ A later answer receives that outcome without executing the tool or preparation hooks again.
582
+ The handler checks current tool registration and profile allowlisting before disclosing the outcome.
583
+ The handler checks current output rules before replay. Required redaction or refusal withholds the saved result.
584
+ Supply `authorizeToolReplay` for current role or resource access checks. This read-only callback can withhold a result without reopening the action.
585
+ If execution ends without a saved outcome, the claim remains running or becomes uncertain.
586
+ The handler never restores that operation as an approval. The host must check the destination before any new action.
587
+ Saved outcomes include guarded model read-back, media, provenance, and suspicious content signals.
588
+ The handler retains settled operations for 24 hours from settlement by default. Set `operationRetentionMs` to change this window.
589
+ Each saved outcome records its absolute retention deadline. Changing a handler's policy does not shorten that deadline.
590
+ Each issued gate records its absolute expiry. Changing `gateTtlMs` does not extend an issued gate.
591
+ Ready, running, and uncertain operations do not expire.
592
+
593
+ Use `sessionLimits` when your storage budget requires different finite limits. The defaults are:
594
+
595
+ | Limit | Default |
596
+ | --- | --- |
597
+ | `maxGates` | 32 |
598
+ | `maxOperations` | 128 |
599
+ | `maxCheckpoints` | 256 |
600
+ | `maxPermissions` | 2,048 |
601
+ | `maxStateBytes` | 32 MiB |
602
+ | `maxInvocationBytes` | 256 KiB |
603
+ | `maxOutcomeBytes` | 256 KiB |
604
+
605
+ Set `maxRequestBytes` to bound browser JSON before parsing. Its default is 16 MiB.
606
+ The handler rejects oversized requests before provider access or tool preparation.
607
+ Provider host options also accept `limits` for deadlines and upstream output.
608
+ Byte limits measure UTF-8 JSON. The handler reserves one maximum outcome for each gate and unresolved operation.
609
+ A new claim fails before dispatch when it cannot reserve that space.
610
+ An oversized result after dispatch leaves an uncertain claim. It does not restore an approval.
611
+ Limits never remove unresolved authority or shorten retained settlements. The host must reconcile and archive unresolved actions.
612
+ The memory store accepts the same `sessionLimits` options. Shared stores receive checked states through the handler.
613
+
614
+ The default memory store coordinates handlers that share the same store object.
615
+ It retains unexpired gates, unresolved operations, and unexpired outcomes during idle and capacity eviction.
616
+ It refuses a new session when every stored session has protected authority.
617
+ Separate memory stores and process restarts do not retain shared authority.
618
+ Atomic updates prevent two workers from dispatching the same operation. A process crash leaves its running claim protected.
619
+ These claims do not provide exactly-once effects at the destination. Use the operation ID with destination idempotency when available.
620
+ Concurrent pending attempts can reach preparation before one claims the body.
621
+ Permission checks and pre-body hooks must be read-only or independently idempotent. The kernel cannot prove hook purity.
622
+ A live session does not restore a dispatched call after an unexpected failure. It retains an uncertain call on that connection.
623
+ To retain execution claims across connections, supply a durable coordinator through `RunSessionOptions.execution`.
624
+ Browser dispatch uses a host-owned `PageToolExecution` service beside the execution coordinator.
625
+ `createTheoremHandler` supplies both services. Live relays pass the session boundary's `pageExecution` and `toolExecution` to `runSession`.
626
+ Forward live page results to `answerPageResult`. Preserve checkpoint ownership through `beforeEvent` when using the Studio bridge.
627
+ The default `PageToolBroker` keeps active result waiters in process memory. Several workers need a shared broker or request affinity.
628
+ Use `pageToolBroker` to supply that broker. It carries results; the atomic session store retains execution authority.
629
+ Lost result delivery does not permit another dispatch. The operation retains its claim or an uncertain outcome.
630
+ The default result deadline is 20 seconds. An expired or interrupted result cannot reopen approval.
631
+
632
+ ## Build your own server
633
+
634
+ `createTheoremHandler` runs each chat reply through a small loop that `@theoremjs/react/server` exports. Use the loop when your server is not a `fetch` handler, or when your routes differ from the handler's. The conversation store, the guardrails, and the wire stay the same, so `TheoremChat` and the hooks work with your server unchanged.
635
+
636
+ | Function | Use |
637
+ | --- | --- |
638
+ | `readerConversationHost({ sessionId, store, profile, clientEvents })` | The conversation of one session, and how its events reach the browser (`forClient`). |
639
+ | `openConversation(host, ref, use)` | Reads the conversation under `ref`, or starts one when `ref` is `undefined`. It refuses a request that `use` does not allow, before anything runs. |
640
+ | `postedMessage(iface, input, context?)` | Checks the person's message against the profile's inputs. `iface` is `interfaceFromProfile(profile, defaultKernelScope.tools)` from `@theoremjs/agents/interface`. |
641
+ | `messageReply({ host, opened, message, walkAway?, runner })` | Posts the message, runs the reply and writes it back. Returns the lines to stream. |
642
+ | `answerReply({ host, opened, call, settling, runner })` | Continues a reply after one of its calls was answered. `settling` is the call's events from the kernel's `invokeTool`. |
643
+ | `rewindConversation({ host, opened, at, walkAway? })` | Cuts the conversation after transcript block `at`. `-1` empties it. |
644
+ | `conversationView(host, conversation)` | The conversation as the chat draws it. |
645
+ | `waitingCalls(conversation)` | The calls a paused reply waits on. A message or a rewind walks away from exactly these. |
646
+
647
+ `use` says what the request opens the conversation for: `{ kind: 'read' }`, `{ kind: 'message', abandon? }`, `{ kind: 'answer', call }`, or `{ kind: 'rewind', at }`.
648
+
649
+ The runner is how your server runs the model for a reply. This server answers a message for `support`, the profile from the [Quick start](#quick-start):
650
+
651
+ ```ts
652
+ import { compactHistory, defaultKernelScope, runTurn } from '@theoremjs/agents';
653
+ import { interfaceFromProfile } from '@theoremjs/agents/interface';
654
+ import {
655
+ createMemoryConversationStore,
656
+ messageReply,
657
+ openConversation,
658
+ postedMessage,
659
+ type ReplyRunner,
660
+ readerConversationHost,
661
+ theoremTurnRequestSchema,
662
+ } from '@theoremjs/react/server';
663
+
664
+ const provider = { vault: { main: process.env.GEMINI_API_KEY } };
665
+ const conversations = createMemoryConversationStore();
666
+
667
+ async function* send(sessionId: string, body: unknown, signal: AbortSignal) {
668
+ const request = theoremTurnRequestSchema.parse(body);
669
+ const host = readerConversationHost({ sessionId, store: conversations, profile: support, clientEvents: undefined });
670
+ const runner: ReplyRunner = {
671
+ run: (turn) => runTurn({ profile: support.id, ...turn, signal }, provider),
672
+ async compact(history, tokens, model) {
673
+ const compacted = await compactHistory({ profile: support.id, history, tokens, signal, ...(model ? { model } : {}) }, provider);
674
+ return compacted?.history;
675
+ },
676
+ signal,
677
+ };
678
+ const message = postedMessage(interfaceFromProfile(support, defaultKernelScope.tools), request.input);
679
+ const opened = await openConversation(host, request.conversation, { kind: 'message' });
680
+ yield* messageReply({ host, opened, message, runner });
681
+ }
682
+ ```
683
+
684
+ Write each line as one JSON line of an `application/x-ndjson` response. The reply's events come first, then a `conversation` line with the ref the store issued. When the run fails, its error line comes last.
685
+
686
+ What the loop leaves to you:
687
+
688
+ - The session. Every read and write names it, and the store refuses a ref from another session.
689
+ - Gates. A profile whose tools wait for approval needs a session store that issues each gate and checks its answer. `createTheoremHandler` keeps one. With your own server, check the answer, run the call with `invokeTool`, and pass its events to `answerReply`.
690
+ - Walking away. A message to a reply that waits on gates must name exactly `waitingCalls(conversation)` as `abandon`, and pass each call's cancelled run as `walkAway`.
691
+
692
+ On the browser side, `@theoremjs/react/client` reads what your server sends:
693
+
694
+ | Export | Use |
695
+ | --- | --- |
696
+ | `CHAT_LINES` | The schemas of a reply's lines: each turn event, and the `conversation` line. |
697
+ | `ChatLine`, `ChatLineSink` | A checked line, and the callback that receives each one. |
698
+ | `fetchConversation(url, body, options?)` | POSTs a conversation read, and checks the view your server answers. |
699
+ | `fetchRewound(url, body, options?)` | POSTs a rewind, and checks the rewound conversation. |
700
+ | `failedDecisionTraces(err)` | The trace records that a failed decision left, for the trace inspector. |
701
+
702
+ ## Build your own UI
703
+
704
+ The hooks give you the same state and actions that the ready-made components use. They draw nothing.
705
+
706
+ `useTheoremInterface` reads the interface of the profile from the server. `useTheoremChat` keeps the transcript, the streaming state, the drafts, the queue, and the gates.
707
+
708
+ ```tsx
709
+ import { createHttpTransport, useTheoremChat, useTheoremInterface } from '@theoremjs/react';
710
+
711
+ const transport = createHttpTransport({ endpoint: '/api/theorem' });
712
+
713
+ function MyChat() {
714
+ const described = useTheoremInterface(transport);
715
+ const chat = useTheoremChat({
716
+ transport,
717
+ iface: described.status === 'ready' && described.iface.type !== 'live' ? described.iface : null,
718
+ });
719
+ // chat.blocks, chat.streamBlocks, chat.phase, chat.draftText, chat.handleSubmit
720
+ }
721
+ ```
722
+
723
+ `chat.phase` is `'idle'`, `'streaming'`, or `'gated'`. The actions are `handleSubmit`, `handleStop`, `handleSendNow`, and `handleToolDecision`.
724
+
725
+ To show the transcript and the composer, use `ChatTranscript` and `ChatComposerBar` from `/ui`. You can also draw your own from `chat.blocks`.
726
+
727
+ The hooks and the client write no English text. They return kinds, codes, and states. You write the words.
728
+
729
+ - A failure is a `ClientFailure`: `{ error, errorKind, errorInternal? }`. Show `error` to the user. It is the wording of the profile. `errorKind` and `errorInternal` are for your code.
730
+ - An attachment problem is an `AttachmentValidationIssue`. Use `attachmentIssueText(issue, iface.labels)` from `@theoremjs/agents` to get its text.
731
+ - A provider can end a live call after it gives a warning. This is not a failure. `LiveSessionClient` calls `onSessionEnded(session)`. `session.message` is the wording of the profile (`live.session_ended`). `session.ended` has the close code and the timing.
732
+ - The state of the UI is given as values, such as `liveState`, the work status, the drawer parts, and the hint ids. You choose the words for them.
204
733
 
205
734
  ## Wording
206
735
 
207
- The headless layer (hooks, client, components, server handler) writes no
208
- English. It hands the builder kinds, codes, and states:
209
-
210
- - Failures are `ClientFailure` (`{ error, errorKind, errorInternal? }`). `error`
211
- is the profile's wording (`iface.lexicon`, resolved on the host); show it to
212
- the user. `errorKind` and `errorInternal` are for the builder.
213
- - Attachment problems are `AttachmentValidationIssue`s; word one with
214
- `attachmentIssueText(issue, iface.lexicon)` from `@theoremjs/agents`.
215
- - A Live call the provider ended after warning it would is not a failure:
216
- `LiveSessionClient`'s `onSessionEnded(session)` gives `session.message`, the
217
- profile's `live.session_ended` wording, and `session.ended` (close code,
218
- timing, the code's kind) for the builder.
219
- - Chrome is semantic: `liveState`, `workStatus`, drawer `parts`, hint `id`.
220
-
221
- `@theoremjs/react/ui` is the default UI. Every line it shows is an Astryx i18n
222
- message: Theorem's under `@theorem.*` keys (`THEOREM_UI_CATALOG`, each with a
223
- description and the ICU values it takes), Astryx's own under `@astryx.*`. The
224
- builder owns all of them; `labels` replaces any line, per locale:
736
+ Each line of text in the ready-made UI is an Astryx i18n message. Theorem messages use `@theorem.*` keys. `THEOREM_UI_CATALOG` lists them, with a description and the values that each one takes. Astryx messages use `@astryx.*` keys.
737
+
738
+ Use `labels` to replace any message, for each locale:
225
739
 
226
740
  ```tsx
227
741
  <TheoremChat
@@ -236,12 +750,24 @@ builder owns all of them; `labels` replaces any line, per locale:
236
750
  />
237
751
  ```
238
752
 
239
- `LiveRunner` takes the same prop. The locale is the host's Astryx
240
- `InternationalizationProvider` locale (`en` without one); a host's own Astryx
241
- `messages` and `overrides` also apply, and win over the defaults. Labels are
242
- checked on mount: an unknown `@theorem.*` key, a message that is not valid ICU,
243
- a value the line is not given, or a key outside `@theorem.*` / `@astryx.*`
244
- throws, naming the locale and key. Label values render as text, never HTML.
753
+ `LiveRunner`, `TheoremHost`, `TheoremDecision`, and `TheoremEmbedding` accept the same prop.
754
+
755
+ - The locale is the locale of your Astryx `InternationalizationProvider`. Without a provider, it is `en`.
756
+ - Your own Astryx `messages` and `overrides` also apply. They replace the defaults.
757
+ - The component checks the labels when it mounts. It throws an error that names the locale and the key if:
758
+ - a `@theorem.*` key is unknown
759
+ - a message is not valid ICU
760
+ - a message does not use a value that its line is given
761
+ - a key is not in `@theorem.*` or `@astryx.*`
762
+ - Labels are shown as text. They are never shown as HTML.
763
+
764
+ ## More documentation
765
+
766
+ - [The wire](https://github.com/masudl-hub/theoremai/blob/main/react/docs/wire.md): the requests and events between the browser and the server, and the checks on each side.
767
+ - [Tool credentials](https://github.com/masudl-hub/theoremai/blob/main/react/docs/credentials.md): typed keys, OAuth, and token refresh, with a full callback route.
768
+ - [Composer, queue, and steer](https://github.com/masudl-hub/theoremai/blob/main/react/docs/composer.md): the actions for stash, queue, steer, and send now.
769
+ - [`@theoremjs/agents`](https://www.npmjs.com/package/@theoremjs/agents): profiles, tools, guardrails, and providers.
770
+
771
+ License: MIT.
245
772
 
246
- A builder with their own UI replaces all of it.
247
- `scripts/docs-truth/copy-lint.mjs` keeps prose out of the headless directories.
773
+ Set `--theorem-pane-background` on a containing element to change the fill of raised panes within it. The default is `--color-background-card`.