@theoremjs/web 0.0.0-stage → 0.3.4-next.375.ga2d1878
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +493 -2
- package/client/audio-level.d.ts +5 -0
- package/client/audio-level.js +24 -0
- package/client/chat.d.ts +144 -0
- package/client/chat.js +760 -0
- package/client/composer-drawer.d.ts +19 -0
- package/client/composer-drawer.js +15 -0
- package/client/composer-hints.d.ts +25 -0
- package/client/composer-hints.js +20 -0
- package/client/composer-primary.d.ts +18 -0
- package/client/composer-primary.js +29 -0
- package/client/decision-transport.d.ts +70 -0
- package/client/decision-transport.js +175 -0
- package/client/decode-composer-draft.d.ts +11 -0
- package/client/decode-composer-draft.js +12 -0
- package/client/embedding-input.d.ts +84 -0
- package/client/embedding-input.js +194 -0
- package/client/embedding-space.d.ts +82 -0
- package/client/embedding-space.js +389 -0
- package/client/embedding-transport.d.ts +40 -0
- package/client/embedding-transport.js +71 -0
- package/client/encode-composer-draft.d.ts +9 -0
- package/client/encode-composer-draft.js +34 -0
- package/client/encode-files.d.ts +18 -0
- package/client/encode-files.js +54 -0
- package/client/failure.d.ts +29 -0
- package/client/failure.js +34 -0
- package/client/generation-selection.d.ts +15 -0
- package/client/generation-selection.js +24 -0
- package/client/host-transport.d.ts +70 -0
- package/client/host-transport.js +75 -0
- package/client/index.d.ts +37 -0
- package/client/index.js +31 -0
- package/client/live/live-mic-forward.d.ts +30 -0
- package/client/live/live-mic-forward.js +46 -0
- package/client/live/live-page-tool.d.ts +39 -0
- package/client/live/live-page-tool.js +26 -0
- package/client/live/live-state.d.ts +13 -0
- package/client/live/live-state.js +22 -0
- package/client/live/live-tool.d.ts +11 -0
- package/client/live/live-tool.js +1 -0
- package/client/live-client.d.ts +189 -0
- package/client/live-client.js +829 -0
- package/client/live-errors.d.ts +2 -0
- package/client/live-errors.js +9 -0
- package/client/live-messages.d.ts +148 -0
- package/client/live-messages.js +100 -0
- package/client/mic-capture.d.ts +4 -0
- package/client/mic-capture.js +6 -0
- package/client/oauth-popup.d.ts +26 -0
- package/client/oauth-popup.js +30 -0
- package/client/page-dispatch.d.ts +15 -0
- package/client/page-dispatch.js +63 -0
- package/client/pcm-downsample.d.ts +4 -0
- package/client/pcm-downsample.js +45 -0
- package/client/run-commit.d.ts +34 -0
- package/client/run-commit.js +49 -0
- package/client/run-session.d.ts +79 -0
- package/client/run-session.js +207 -0
- package/client/schema-fields.d.ts +63 -0
- package/client/schema-fields.js +199 -0
- package/client/shaped-data.d.ts +118 -0
- package/client/shaped-data.js +343 -0
- package/client/source-citations.d.ts +13 -0
- package/client/source-citations.js +56 -0
- package/client/space-geometry.d.ts +39 -0
- package/client/space-geometry.js +183 -0
- package/client/tool-result.d.ts +78 -0
- package/client/tool-result.js +293 -0
- package/client/tool-resume.d.ts +50 -0
- package/client/tool-resume.js +14 -0
- package/client/trace-feed.d.ts +13 -0
- package/client/trace-feed.js +18 -0
- package/client/transcript-schema.d.ts +10 -0
- package/client/transcript-schema.js +120 -0
- package/client/transport.d.ts +264 -0
- package/client/transport.js +330 -0
- package/client/turn-client.d.ts +44 -0
- package/client/turn-client.js +88 -0
- package/client/wire-line.d.ts +56 -0
- package/client/wire-line.js +90 -0
- package/client/worklets/mic-capture.js +16 -0
- package/package.json +28 -4
- package/server/checkpoint-ownership.d.ts +6 -0
- package/server/checkpoint-ownership.js +42 -0
- package/server/conversation-store.d.ts +92 -0
- package/server/conversation-store.js +131 -0
- package/server/conversation.d.ts +193 -0
- package/server/conversation.js +552 -0
- package/server/credential-store.d.ts +24 -0
- package/server/credential-store.js +14 -0
- package/server/decision-handler.d.ts +42 -0
- package/server/decision-handler.js +46 -0
- package/server/embedding-handler.d.ts +33 -0
- package/server/embedding-handler.js +45 -0
- package/server/handler.d.ts +158 -0
- package/server/handler.js +775 -0
- package/server/json-response.d.ts +2 -0
- package/server/json-response.js +7 -0
- package/server/mcp-handler.d.ts +46 -0
- package/server/mcp-handler.js +312 -0
- package/server/mod.d.ts +27 -0
- package/server/mod.js +25 -0
- package/server/page-dispatch.d.ts +22 -0
- package/server/page-dispatch.js +127 -0
- package/server/request-body.d.ts +2 -0
- package/server/request-body.js +42 -0
- package/server/request-check.d.ts +21 -0
- package/server/request-check.js +49 -0
- package/server/session-authority.d.ts +8 -0
- package/server/session-authority.js +78 -0
- package/server/session-boundary.d.ts +26 -0
- package/server/session-boundary.js +64 -0
- package/server/session-identity.d.ts +16 -0
- package/server/session-identity.js +41 -0
- package/server/session-limits.d.ts +12 -0
- package/server/session-limits.js +59 -0
- package/server/session-store.d.ts +123 -0
- package/server/session-store.js +264 -0
- package/server/single-action.d.ts +21 -0
- package/server/single-action.js +30 -0
- package/server/steer-inbox.d.ts +35 -0
- package/server/steer-inbox.js +52 -0
- package/server/tool-operations.d.ts +6 -0
- package/server/tool-operations.js +132 -0
- package/server/turn-input.d.ts +10 -0
- package/server/turn-input.js +29 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ORCHID AI LLC
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,494 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @theoremjs/web
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/@theoremjs/web)
|
|
4
|
+
[](https://github.com/masudl-hub/theoremai/actions/workflows/ci.yml)
|
|
5
|
+
[](https://github.com/masudl-hub/theoremai/blob/main/LICENSE)
|
|
6
|
+
|
|
7
|
+
Serve a Theorem profile over HTTP, and talk to it from a browser, with no UI framework.
|
|
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)). This package puts the profile on the web.
|
|
12
|
+
|
|
13
|
+
- **Server.** `createTheoremHandler` takes a Web `Request` and returns a `Response`. It runs the profile, enforces the guardrails and the tool gates, and keeps the conversation. Each other profile type has its own handler.
|
|
14
|
+
- **Browser.** The transports call the handlers. The client checks each line the server sends, and folds the lines into state that any UI can draw.
|
|
15
|
+
|
|
16
|
+
The package imports no UI framework. [`@theoremjs/react`](https://www.npmjs.com/package/@theoremjs/react) builds its hooks and its ready-made UI on it, and re-exports it as `@theoremjs/react/server` and `@theoremjs/react/client`.
|
|
17
|
+
|
|
18
|
+
## Install
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install @theoremjs/web @theoremjs/agents zod
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Install `@theoremjs/web` and `@theoremjs/agents` at the same version.
|
|
25
|
+
|
|
26
|
+
Requirements:
|
|
27
|
+
|
|
28
|
+
- TypeScript 5.7 or later, for the type declarations.
|
|
29
|
+
- For voice, a bundler that resolves `new URL('./worklets/mic-capture.js', import.meta.url)`. Vite does.
|
|
30
|
+
|
|
31
|
+
## Entry points
|
|
32
|
+
|
|
33
|
+
| Import | Content |
|
|
34
|
+
| --- | --- |
|
|
35
|
+
| `@theoremjs/web/server` | The handlers and the stores they use |
|
|
36
|
+
| `@theoremjs/web/client` | The transports, the wire schemas, and the client logic |
|
|
37
|
+
|
|
38
|
+
## Quick start
|
|
39
|
+
|
|
40
|
+
### 1. Register the provider and create the handler
|
|
41
|
+
|
|
42
|
+
Register the provider before you create the handler. The profile binds models through the registered provider's `.model()` helper.
|
|
43
|
+
The helper takes the protocol and the model ID. Pass credential values through the handler's vault.
|
|
44
|
+
Supply vault slots as own properties. The server ignores inherited credentials and resolvers.
|
|
45
|
+
Replacing the registration affects later runs. An active run keeps the registration that it started with.
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
import { defineProfile, geminiProvider, registerProvider } from '@theoremjs/agents';
|
|
49
|
+
import { createMemoryConversationStore, createTheoremHandler } from '@theoremjs/web/server';
|
|
50
|
+
|
|
51
|
+
const gemini = registerProvider(geminiProvider, { keySlot: 'main' });
|
|
52
|
+
|
|
53
|
+
const support = defineProfile({
|
|
54
|
+
type: 'text',
|
|
55
|
+
id: 'support.agent',
|
|
56
|
+
identity: { handle: 'support', system: 'You help customers with their account.' },
|
|
57
|
+
models: {
|
|
58
|
+
fast: gemini.model('geminiInteractions', 'gemini-3.5-flash-lite'),
|
|
59
|
+
},
|
|
60
|
+
defaultModel: 'fast',
|
|
61
|
+
tools: { allow: [] },
|
|
62
|
+
inputs: { text: true },
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
export const theorem = createTheoremHandler({
|
|
66
|
+
profile: support,
|
|
67
|
+
provider: { vault: { main: process.env.GEMINI_API_KEY } },
|
|
68
|
+
conversationStore: createMemoryConversationStore(),
|
|
69
|
+
});
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
The memory store keeps each conversation in process memory. For other stores, see [Keep the conversation](#keep-the-conversation).
|
|
73
|
+
|
|
74
|
+
### 2. Mount the handler
|
|
75
|
+
|
|
76
|
+
`createTheoremHandler` returns a function: `(request: Request) => Promise<Response>`. It works in Workers, Deno, Bun, Hono, and Next route handlers. In Node, use an adapter.
|
|
77
|
+
|
|
78
|
+
Send all requests under one path to the handler. The default path is `/api/theorem`.
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
export default {
|
|
82
|
+
fetch(request: Request) {
|
|
83
|
+
return new URL(request.url).pathname.startsWith('/api/theorem')
|
|
84
|
+
? theorem(request)
|
|
85
|
+
: new Response('Not found', { status: 404 });
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
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.
|
|
91
|
+
|
|
92
|
+
To show a chat on this handler, use `TheoremChat` from [`@theoremjs/react`](https://www.npmjs.com/package/@theoremjs/react).
|
|
93
|
+
|
|
94
|
+
## Serve a host profile over MCP
|
|
95
|
+
|
|
96
|
+
`createTheoremMcpHandler` serves the same profile to MCP clients, such as coding agents and chat apps, at one endpoint. It answers MCP clients of the current revision and clients that still open with `initialize`, and it keeps no session between requests.
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
import { createTheoremMcpHandler } from '@theoremjs/web/server';
|
|
100
|
+
|
|
101
|
+
export const mcp = createTheoremMcpHandler({ profile: docsTools, version: '1.0.0' }); // mount at /mcp
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
- `tools/list` gives each allowed tool with its input schema. A tool's `access` becomes the read-only and destructive hints of MCP.
|
|
105
|
+
- Clients of the current revision may cache the tool list for 5 minutes and the discovery result for an hour. Neither holds anything about the caller, so a shared cache may keep them.
|
|
106
|
+
- `tools/call` runs the tool through the gates, the guardrails, and the trace of the kernel. The client receives the text a model would read, not the raw output.
|
|
107
|
+
- A tool that fails returns a result marked `isError`, so the model can read it and try again.
|
|
108
|
+
- No one can approve a call over MCP. The handler refuses a profile when an allowed tool has a permission other than `auto`.
|
|
109
|
+
- A browser page on another origin is refused unless `origins` lists it.
|
|
110
|
+
|
|
111
|
+
## Your relay
|
|
112
|
+
|
|
113
|
+
A `live` profile runs a voice or video call through a WebSocket relay that you host.
|
|
114
|
+
|
|
115
|
+
Use a session boundary to authenticate the conversation before you open the upstream session.
|
|
116
|
+
Use the same authenticated session resolver and atomic session store for chat and live requests.
|
|
117
|
+
The boundary rejects native state that this conversation did not save.
|
|
118
|
+
Saved ownership also covers the operation state. An older live checkpoint cannot revive a completed action.
|
|
119
|
+
|
|
120
|
+
```ts
|
|
121
|
+
import { runSession } from '@theoremjs/agents';
|
|
122
|
+
import {
|
|
123
|
+
createTheoremSessionBoundary,
|
|
124
|
+
parseLiveOpenMessage,
|
|
125
|
+
} from '@theoremjs/web/server';
|
|
126
|
+
|
|
127
|
+
const boundary = await createTheoremSessionBoundary({
|
|
128
|
+
request,
|
|
129
|
+
session: authenticatedConversation,
|
|
130
|
+
sessionStore: sharedSessionStore,
|
|
131
|
+
});
|
|
132
|
+
const open = parseLiveOpenMessage(firstFrame);
|
|
133
|
+
const session = await runSession(
|
|
134
|
+
{ profile, ...await boundary.prepareLiveOpen(open, hostContext) },
|
|
135
|
+
{ vault: hostKeyVault, execution: boundary.toolExecution, pageExecution: boundary.pageExecution },
|
|
136
|
+
);
|
|
137
|
+
for await (const event of session.events()) {
|
|
138
|
+
if ((event.type === 'done' || event.type === 'provider_checkpoint') && event.providerState) {
|
|
139
|
+
await boundary.recordCheckpoint(event.providerState);
|
|
140
|
+
}
|
|
141
|
+
sendToBrowser(event);
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
- `prepareLiveOpen` returns validated history, slots, context, native state, and absence duration. It removes system-role history.
|
|
146
|
+
- Its second argument supplies server context. Browser context stays under `client`.
|
|
147
|
+
- Save checkpoint ownership before you send the event. If storage fails, close the session without advertising that checkpoint.
|
|
148
|
+
- The boundary refuses native resumption while a saved operation is ready, running, or uncertain. It does not repeat the operation.
|
|
149
|
+
- Without a session resolver, the boundary uses an anonymous cookie. Forward its `setCookie` value in the upgrade response.
|
|
150
|
+
- `open.host` is the `openMessage` that the page's `connection` returned.
|
|
151
|
+
- Read later messages with `parseLiveClientMessage`. Give context messages to `session.sendContext({ client: message.context })`.
|
|
152
|
+
|
|
153
|
+
## A chat with no framework
|
|
154
|
+
|
|
155
|
+
Use `createChatController` when you draw the chat yourself, in any framework or none. It holds all of the chat's logic, and it draws nothing:
|
|
156
|
+
|
|
157
|
+
- the transcript, and the reply as it streams;
|
|
158
|
+
- the composer's draft, files, and voice note;
|
|
159
|
+
- the pending messages: steer, queue, and stash;
|
|
160
|
+
- the gates, branching, and resuming a conversation.
|
|
161
|
+
|
|
162
|
+
`useTheoremChat` from `@theoremjs/react` is this controller, bound to React.
|
|
163
|
+
|
|
164
|
+
```ts
|
|
165
|
+
import { createChatController, createHttpTransport } from '@theoremjs/web/client';
|
|
166
|
+
|
|
167
|
+
const transport = createHttpTransport({ endpoint: '/api/theorem' });
|
|
168
|
+
const iface = await transport.describe();
|
|
169
|
+
if (iface.type === 'live') throw new Error('Use LiveSessionClient for a live profile.');
|
|
170
|
+
|
|
171
|
+
const chat = createChatController({ transport, iface });
|
|
172
|
+
chat.subscribe(() => draw(chat.getState()));
|
|
173
|
+
|
|
174
|
+
chat.setDraftText('What is in my cart?');
|
|
175
|
+
await chat.submit();
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
- `getState()` returns a `ChatState`. It is a new object after each change, and the same object between changes. So you can compare it by reference, as React's `useSyncExternalStore` does.
|
|
179
|
+
- `subscribe(listener)` calls the listener once for each group of changes, after the code that made them has run. It returns the function that unsubscribes.
|
|
180
|
+
- `update(options)` gives the controller your latest options, such as a new `context`, `slots`, or `pageTools`. Each turn reads the options as they are when it is sent. `conversation`, `conversationView`, and `initialText` are read only when the controller is created.
|
|
181
|
+
- Each method is bound to the controller. Pass it as a callback as it is.
|
|
182
|
+
|
|
183
|
+
`state.phase` is `'idle'`, `'streaming'`, or `'gated'`. The methods for the composer depend on it:
|
|
184
|
+
|
|
185
|
+
| Method | What it does |
|
|
186
|
+
| --- | --- |
|
|
187
|
+
| `submit()` | At `idle`, sends the draft. Otherwise, queues it. |
|
|
188
|
+
| `enqueue('steer')` | Sends the draft to the running turn with `transport.steer`. The steer leaves `pendingMessages` when the turn's `stage` event names it in `injected`. A steer the turn never takes waits at the front of the queue. |
|
|
189
|
+
| `enqueue('queue')` | Keeps the draft to send after the reply. When the reply ends without a failure and no gate waits, the first queued message is sent. |
|
|
190
|
+
| `enqueue('stash')` | Keeps the draft without sending it. `queuePending(id)` moves it to the queue. |
|
|
191
|
+
| `sendNow(message?)` | Sends a pending message, or the draft, at once. A streaming reply is stopped first. A reply that waits on gates is left. |
|
|
192
|
+
| `restorePending(id)` | Puts a pending message back in the composer. The draft that was there is stashed first, so the user loses nothing. |
|
|
193
|
+
| `stop()` | Stops the streaming reply. |
|
|
194
|
+
| `decide(action)`, `authenticate(secret?)` | Answer the gate that the reply waits on. |
|
|
195
|
+
| `branch(index)` | Cuts the conversation after transcript block `index`. |
|
|
196
|
+
| `sendText(text, files?)` | Sends a message from your code. It resolves with the blocks the turn added, or `null` when the chat cannot take a message now. |
|
|
197
|
+
|
|
198
|
+
The controller uses these methods of the transport: `turn`, `invoke` (to answer a gate), `steer`, `conversation`, and `rewind`. `conversation` and `rewind` are called only for a profile whose host keeps the conversation. `pageResult` is called when the reply hands a tool to the page.
|
|
199
|
+
|
|
200
|
+
The controller writes no English text. The helpers below give the composer's controls as values, and you choose the words:
|
|
201
|
+
|
|
202
|
+
| Helper | What it gives |
|
|
203
|
+
| --- | --- |
|
|
204
|
+
| `composerActionState(args)` | The primary button (`send`, `queue`, `stop`, or `none`), the send menu, and whether the button is off. |
|
|
205
|
+
| `composerDrawerSummary(args)` | The count of waiting messages by kind, for a collapsed drawer. |
|
|
206
|
+
| `resolveComposerHint(args)` | The stash hint, when the whole draft is selected. |
|
|
207
|
+
| `isStashShortcut(event)` | Whether a key press is the stash shortcut, `STASH_SHORTCUT`. |
|
|
208
|
+
|
|
209
|
+
## A custom browser transport
|
|
210
|
+
|
|
211
|
+
Use `withPageToolDispatch` when your screen consumes turn events through a custom transport.
|
|
212
|
+
The helper runs callbacks only for dispatch grants received on the active stream.
|
|
213
|
+
It never reads a saved transcript to authorize callback execution.
|
|
214
|
+
Implement the transport's `pageResult` method to return the result to your authenticated server.
|
|
215
|
+
`createHttpTransport` and `createHostTransport` post that result to `/page-result`.
|
|
216
|
+
The ready-made chat and host console of `@theoremjs/react` apply this helper for their `pageTools`.
|
|
217
|
+
In a React host view of your own, pass the callbacks as the second argument to `useTheoremHost`.
|
|
218
|
+
|
|
219
|
+
```ts
|
|
220
|
+
import { createHttpTransport, withPageToolDispatch } from '@theoremjs/web/client';
|
|
221
|
+
|
|
222
|
+
const transport = withPageToolDispatch(
|
|
223
|
+
createHttpTransport({ endpoint: '/api/theorem' }),
|
|
224
|
+
{ highlight: async argumentsFromServer => ({ output: { shown: argumentsFromServer.id } }) },
|
|
225
|
+
);
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
## Server options
|
|
229
|
+
|
|
230
|
+
`createTheoremHandler` requires `profile` and `provider`. All other options are optional.
|
|
231
|
+
|
|
232
|
+
| Option | Use |
|
|
233
|
+
| --- | --- |
|
|
234
|
+
| `profile` | The profile to serve. The kernel registers it when you create the handler. |
|
|
235
|
+
| `provider` | Host options, including the credential vault. A function can return these options per request, for example for tenant-specific credentials. |
|
|
236
|
+
| `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. |
|
|
237
|
+
| `host` | App data for the tool handlers (`ctx.host`), such as the signed-in user. |
|
|
238
|
+
| `pageToolBroker` | Delivers results to active page dispatches. Default: bounded process memory. |
|
|
239
|
+
| `conversationStore` | Where the conversation is kept. Required, except for a stateless profile, which must not have one. See [Keep the conversation](#keep-the-conversation). |
|
|
240
|
+
| `sessionStore`, `steerInbox` | Where open gates and queued steers are kept. Default: process memory. |
|
|
241
|
+
| `credentialStore` | Where tool credentials are kept, by session. Default: process memory. |
|
|
242
|
+
| `authorizationUrl` | Returns the sign-in URL for an OAuth gate. Without it, an OAuth gate has no URL. |
|
|
243
|
+
| `gateTtlMs` | How long a gate waits for an answer. Default: 30 minutes. A later answer fails with `session.gate_expired`. |
|
|
244
|
+
| `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. |
|
|
245
|
+
| `onError` | Receives each error that the handler catches. A visitor sees the wording of the profile for that kind of error, never the error. |
|
|
246
|
+
|
|
247
|
+
### Start a conversation with content
|
|
248
|
+
|
|
249
|
+
`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.
|
|
250
|
+
|
|
251
|
+
```ts
|
|
252
|
+
import { startConversation, theoremSession } from '@theoremjs/web/server';
|
|
253
|
+
|
|
254
|
+
const session = theoremSession(request);
|
|
255
|
+
const { ref } = await startConversation({ sessionId: session.id, store: conversationStore }, profile, [
|
|
256
|
+
{ id: 'ask', kind: 'user-text', text: 'Two days in Lisbon. Where should I stay?' },
|
|
257
|
+
{ id: 'reply', kind: 'text', text: 'Stay in Chiado.' },
|
|
258
|
+
{ id: 'done', kind: 'turn-done' },
|
|
259
|
+
]);
|
|
260
|
+
// Send session.setCookie as Set-Cookie when it is set, and give the page the ref and view.
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
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.
|
|
264
|
+
|
|
265
|
+
```tsx
|
|
266
|
+
<TheoremChat endpoint="/api/theorem" conversation={ref} conversationView={view} />
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
- 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.
|
|
270
|
+
- 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.
|
|
271
|
+
- The result's `events` are the guardrail events for what matched.
|
|
272
|
+
|
|
273
|
+
### Keep the conversation
|
|
274
|
+
|
|
275
|
+
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.
|
|
276
|
+
The session store keeps gates, permissions, and operations. The two stores are separate.
|
|
277
|
+
|
|
278
|
+
| Store | Where the conversation is kept |
|
|
279
|
+
| --- | --- |
|
|
280
|
+
| `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. |
|
|
281
|
+
| `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. |
|
|
282
|
+
| Your own `TheoremConversationStore` | Your database. |
|
|
283
|
+
|
|
284
|
+
A ref that the store cannot open fails with `session.conversation_unknown`. This includes a ref from another session.
|
|
285
|
+
|
|
286
|
+
The sealed store:
|
|
287
|
+
|
|
288
|
+
- Each key is at least 32 bytes. Keep the keys on the server.
|
|
289
|
+
- `keyVersion` names the key that seals new writes. Keep an older key in `keys` while its refs must still open.
|
|
290
|
+
- Each save issues a new ref. An older ref of the same conversation still opens, as a rewind does.
|
|
291
|
+
- 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.
|
|
292
|
+
- The ref holds the whole conversation, including the bytes of each attachment. Every request sends it, so the conversation must stay under `maxRequestBytes`.
|
|
293
|
+
|
|
294
|
+
To keep conversations in your own database, implement these two methods:
|
|
295
|
+
|
|
296
|
+
```ts
|
|
297
|
+
import type {
|
|
298
|
+
ConversationSnapshot,
|
|
299
|
+
ConversationWriteResult,
|
|
300
|
+
StoredConversation,
|
|
301
|
+
} from '@theoremjs/web/server';
|
|
302
|
+
|
|
303
|
+
interface ConversationDatabase {
|
|
304
|
+
read(sessionId: string, ref: string): Promise<ConversationSnapshot | undefined>;
|
|
305
|
+
write(
|
|
306
|
+
sessionId: string,
|
|
307
|
+
ref: string | null,
|
|
308
|
+
expectedRevision: string | null,
|
|
309
|
+
next: StoredConversation,
|
|
310
|
+
): Promise<ConversationWriteResult>;
|
|
311
|
+
}
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
- Every read and write names the session. For a ref that this session did not write, `read` returns `undefined` and `write` returns `{ written: false }`.
|
|
315
|
+
- A null `ref` creates a conversation. Return a new opaque `ref` and `revision`.
|
|
316
|
+
- Otherwise, write only while the stored revision is still `expectedRevision`, as one atomic operation. Return `{ written: false }` on a conflict.
|
|
317
|
+
- Check what you read back with `storedConversationSchema`.
|
|
318
|
+
|
|
319
|
+
### Share session authority
|
|
320
|
+
|
|
321
|
+
Use one shared `TheoremSessionStore` when requests can reach several server instances.
|
|
322
|
+
New execution claims retain accepted request data and final prepared input under `operation.invocation`.
|
|
323
|
+
Use this data in `authorizeToolReplay` to check current access to the affected resource.
|
|
324
|
+
Older records can lack this evidence. Refuse disclosure if your access check needs it.
|
|
325
|
+
It holds approvals and provider checkpoint ownership. Tool credentials use the separate `credentialStore`.
|
|
326
|
+
|
|
327
|
+
```ts
|
|
328
|
+
import type {
|
|
329
|
+
SessionSnapshot,
|
|
330
|
+
SessionWriteResult,
|
|
331
|
+
TheoremSessionState,
|
|
332
|
+
} from '@theoremjs/web/server';
|
|
333
|
+
|
|
334
|
+
interface SharedSessionStore {
|
|
335
|
+
load(sessionId: string): Promise<SessionSnapshot | undefined>;
|
|
336
|
+
compareAndSwap(
|
|
337
|
+
sessionId: string,
|
|
338
|
+
expectedRevision: string | null,
|
|
339
|
+
next: TheoremSessionState,
|
|
340
|
+
): Promise<SessionWriteResult>;
|
|
341
|
+
}
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
Implement `compareAndSwap` as one atomic database operation. A separate read followed by a write is unsafe.
|
|
345
|
+
A null revision creates an absent session. A matching revision replaces the current state.
|
|
346
|
+
Return `{ written: false }` on a conflict. On success, return `{ written: true, revision }` with a new opaque revision.
|
|
347
|
+
Never reuse a revision, including after expiration. Return detached state from `load`.
|
|
348
|
+
Return write success only after the store meets its promised durability.
|
|
349
|
+
|
|
350
|
+
Each tool action has a kernel-issued `operationId`. The provider's `callId` identifies the upstream call.
|
|
351
|
+
An approval must carry both values. A new action receives a new operation ID even if its provider reuses a call ID.
|
|
352
|
+
A settled replay can omit input. If it supplies input, that input must match the accepted invocation.
|
|
353
|
+
The built-in clients copy this reference from the gate event. Custom clients must preserve it.
|
|
354
|
+
Historical events can omit operation IDs for display. They cannot authorize a new action.
|
|
355
|
+
|
|
356
|
+
The handler validates versioned state and retries a conflicting transition at most eight times.
|
|
357
|
+
It does not run tools inside this retry loop. Invalid state fails; it does not become an empty session.
|
|
358
|
+
The previous session `load`/`save` contract is not supported. Existing unversioned state and versions 1 or 2 require an explicit host migration.
|
|
359
|
+
An older pending gate does not prove that its body never ran. Verify the destination before restoring execution authority.
|
|
360
|
+
The current session state uses version 3. Gates and operation records use operation IDs as keys.
|
|
361
|
+
Each operation record contains its retained outcome. There is no separate outcome cache.
|
|
362
|
+
A gate belongs to the profile that issued it. Another profile cannot consume it through shared session storage.
|
|
363
|
+
The handler saves an accepted answer as ready before it invokes the kernel.
|
|
364
|
+
A retry before dispatch must repeat that accepted answer. It cannot change the input or decision.
|
|
365
|
+
Immediately before the tool body, an atomic claim changes ready to running.
|
|
366
|
+
The kernel saves the terminal outcome before it sends completion to the client.
|
|
367
|
+
A later answer receives that outcome without executing the tool or preparation hooks again.
|
|
368
|
+
The handler checks current tool registration and profile allowlisting before disclosing the outcome.
|
|
369
|
+
The handler checks current output rules before replay. Required redaction or refusal withholds the saved result.
|
|
370
|
+
Supply `authorizeToolReplay` for current role or resource access checks. This read-only callback can withhold a result without reopening the action.
|
|
371
|
+
If execution ends without a saved outcome, the claim remains running or becomes uncertain.
|
|
372
|
+
The handler never restores that operation as an approval. The host must check the destination before any new action.
|
|
373
|
+
Saved outcomes include guarded model read-back, media, provenance, and suspicious content signals.
|
|
374
|
+
The handler retains settled operations for 24 hours from settlement by default. Set `operationRetentionMs` to change this window.
|
|
375
|
+
Each saved outcome records its absolute retention deadline. Changing a handler's policy does not shorten that deadline.
|
|
376
|
+
Each issued gate records its absolute expiry. Changing `gateTtlMs` does not extend an issued gate.
|
|
377
|
+
Ready, running, and uncertain operations do not expire.
|
|
378
|
+
|
|
379
|
+
Use `sessionLimits` when your storage budget requires different finite limits. The defaults are:
|
|
380
|
+
|
|
381
|
+
| Limit | Default |
|
|
382
|
+
| --- | --- |
|
|
383
|
+
| `maxGates` | 32 |
|
|
384
|
+
| `maxOperations` | 128 |
|
|
385
|
+
| `maxCheckpoints` | 256 |
|
|
386
|
+
| `maxPermissions` | 2,048 |
|
|
387
|
+
| `maxStateBytes` | 32 MiB |
|
|
388
|
+
| `maxInvocationBytes` | 256 KiB |
|
|
389
|
+
| `maxOutcomeBytes` | 256 KiB |
|
|
390
|
+
|
|
391
|
+
Set `maxRequestBytes` to bound browser JSON before parsing. Its default is 16 MiB.
|
|
392
|
+
The handler rejects oversized requests before provider access or tool preparation.
|
|
393
|
+
Provider host options also accept `limits` for deadlines and upstream output.
|
|
394
|
+
Byte limits measure UTF-8 JSON. The handler reserves one maximum outcome for each gate and unresolved operation.
|
|
395
|
+
A new claim fails before dispatch when it cannot reserve that space.
|
|
396
|
+
An oversized result after dispatch leaves an uncertain claim. It does not restore an approval.
|
|
397
|
+
Limits never remove unresolved authority or shorten retained settlements. The host must reconcile and archive unresolved actions.
|
|
398
|
+
The memory store accepts the same `sessionLimits` options. Shared stores receive checked states through the handler.
|
|
399
|
+
|
|
400
|
+
The default memory store coordinates handlers that share the same store object.
|
|
401
|
+
It retains unexpired gates, unresolved operations, and unexpired outcomes during idle and capacity eviction.
|
|
402
|
+
It refuses a new session when every stored session has protected authority.
|
|
403
|
+
Separate memory stores and process restarts do not retain shared authority.
|
|
404
|
+
Atomic updates prevent two workers from dispatching the same operation. A process crash leaves its running claim protected.
|
|
405
|
+
These claims do not provide exactly-once effects at the destination. Use the operation ID with destination idempotency when available.
|
|
406
|
+
Concurrent pending attempts can reach preparation before one claims the body.
|
|
407
|
+
Permission checks and pre-body hooks must be read-only or independently idempotent. The kernel cannot prove hook purity.
|
|
408
|
+
A live session does not restore a dispatched call after an unexpected failure. It retains an uncertain call on that connection.
|
|
409
|
+
To retain execution claims across connections, supply a durable coordinator through `RunSessionOptions.execution`.
|
|
410
|
+
Browser dispatch uses a host-owned `PageToolExecution` service beside the execution coordinator.
|
|
411
|
+
`createTheoremHandler` supplies both services. Live relays pass the session boundary's `pageExecution` and `toolExecution` to `runSession`.
|
|
412
|
+
Forward live page results to `answerPageResult`. Preserve checkpoint ownership through `beforeEvent` when using the Studio bridge.
|
|
413
|
+
The default `PageToolBroker` keeps active result waiters in process memory. Several workers need a shared broker or request affinity.
|
|
414
|
+
Use `pageToolBroker` to supply that broker. It carries results; the atomic session store retains execution authority.
|
|
415
|
+
Lost result delivery does not permit another dispatch. The operation retains its claim or an uncertain outcome.
|
|
416
|
+
The default result deadline is 20 seconds. An expired or interrupted result cannot reopen approval.
|
|
417
|
+
|
|
418
|
+
## Build your own server
|
|
419
|
+
|
|
420
|
+
`createTheoremHandler` runs each chat reply through a small loop that `@theoremjs/web/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.
|
|
421
|
+
|
|
422
|
+
| Function | Use |
|
|
423
|
+
| --- | --- |
|
|
424
|
+
| `readerConversationHost({ sessionId, store, profile, clientEvents })` | The conversation of one session, and how its events reach the browser (`forClient`). |
|
|
425
|
+
| `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. |
|
|
426
|
+
| `postedMessage(iface, input, context?)` | Checks the person's message against the profile's inputs. `iface` is `interfaceFromProfile(profile, defaultKernelScope.tools)` from `@theoremjs/agents/interface`. |
|
|
427
|
+
| `messageReply({ host, opened, message, walkAway?, runner })` | Posts the message, runs the reply and writes it back. Returns the lines to stream. |
|
|
428
|
+
| `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`. |
|
|
429
|
+
| `rewindConversation({ host, opened, at, walkAway? })` | Cuts the conversation after transcript block `at`. `-1` empties it. |
|
|
430
|
+
| `conversationView(host, conversation)` | The conversation as the chat draws it. |
|
|
431
|
+
| `waitingCalls(conversation)` | The calls a paused reply waits on. A message or a rewind walks away from exactly these. |
|
|
432
|
+
|
|
433
|
+
`use` says what the request opens the conversation for: `{ kind: 'read' }`, `{ kind: 'message', abandon? }`, `{ kind: 'answer', call }`, or `{ kind: 'rewind', at }`.
|
|
434
|
+
|
|
435
|
+
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):
|
|
436
|
+
|
|
437
|
+
```ts
|
|
438
|
+
import { compactHistory, defaultKernelScope, runTurn } from '@theoremjs/agents';
|
|
439
|
+
import { interfaceFromProfile } from '@theoremjs/agents/interface';
|
|
440
|
+
import {
|
|
441
|
+
createMemoryConversationStore,
|
|
442
|
+
messageReply,
|
|
443
|
+
openConversation,
|
|
444
|
+
postedMessage,
|
|
445
|
+
type ReplyRunner,
|
|
446
|
+
readerConversationHost,
|
|
447
|
+
theoremTurnRequestSchema,
|
|
448
|
+
} from '@theoremjs/web/server';
|
|
449
|
+
|
|
450
|
+
const provider = { vault: { main: process.env.GEMINI_API_KEY } };
|
|
451
|
+
const conversations = createMemoryConversationStore();
|
|
452
|
+
|
|
453
|
+
async function* send(sessionId: string, body: unknown, signal: AbortSignal) {
|
|
454
|
+
const request = theoremTurnRequestSchema.parse(body);
|
|
455
|
+
const host = readerConversationHost({ sessionId, store: conversations, profile: support, clientEvents: undefined });
|
|
456
|
+
const runner: ReplyRunner = {
|
|
457
|
+
run: (turn) => runTurn({ profile: support.id, ...turn, signal }, provider),
|
|
458
|
+
async compact(history, tokens, model) {
|
|
459
|
+
const compacted = await compactHistory({ profile: support.id, history, tokens, signal, ...(model ? { model } : {}) }, provider);
|
|
460
|
+
return compacted?.history;
|
|
461
|
+
},
|
|
462
|
+
signal,
|
|
463
|
+
};
|
|
464
|
+
const message = postedMessage(interfaceFromProfile(support, defaultKernelScope.tools), request.input);
|
|
465
|
+
const opened = await openConversation(host, request.conversation, { kind: 'message' });
|
|
466
|
+
yield* messageReply({ host, opened, message, runner });
|
|
467
|
+
}
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
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.
|
|
471
|
+
|
|
472
|
+
What the loop leaves to you:
|
|
473
|
+
|
|
474
|
+
- The session. Every read and write names it, and the store refuses a ref from another session.
|
|
475
|
+
- 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`.
|
|
476
|
+
- 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`.
|
|
477
|
+
|
|
478
|
+
On the browser side, `@theoremjs/web/client` reads what your server sends:
|
|
479
|
+
|
|
480
|
+
| Export | Use |
|
|
481
|
+
| --- | --- |
|
|
482
|
+
| `CHAT_LINES` | The schemas of a reply's lines: each turn event, and the `conversation` line. |
|
|
483
|
+
| `ChatLine`, `ChatLineSink` | A checked line, and the callback that receives each one. |
|
|
484
|
+
| `fetchConversation(url, body, options?)` | POSTs a conversation read, and checks the view your server answers. |
|
|
485
|
+
| `fetchRewound(url, body, options?)` | POSTs a rewind, and checks the rewound conversation. |
|
|
486
|
+
| `failedDecisionTraces(err)` | The trace records that a failed decision left, for the trace inspector. |
|
|
487
|
+
|
|
488
|
+
## More documentation
|
|
489
|
+
|
|
490
|
+
- [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.
|
|
491
|
+
- [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.
|
|
492
|
+
- [`@theoremjs/agents`](https://www.npmjs.com/package/@theoremjs/agents): profiles, tools, guardrails, and providers.
|
|
493
|
+
|
|
494
|
+
License: MIT.
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** Shared mic RMS → waveform level scaling for live and turn-composer paths. */
|
|
2
|
+
export declare const INPUT_LEVEL_GAIN = 4;
|
|
3
|
+
export declare function float32Rms(samples: Float32Array): number;
|
|
4
|
+
export declare function float32RmsToLevel(samples: Float32Array): number;
|
|
5
|
+
export declare function timeDomainBytesToLevel(samples: Uint8Array<ArrayBufferLike>): number;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** Shared mic RMS → waveform level scaling for live and turn-composer paths. */
|
|
2
|
+
export const INPUT_LEVEL_GAIN = 4;
|
|
3
|
+
export function float32Rms(samples) {
|
|
4
|
+
let sum = 0;
|
|
5
|
+
for (const sample of samples) {
|
|
6
|
+
sum += sample * sample;
|
|
7
|
+
}
|
|
8
|
+
if (samples.length === 0)
|
|
9
|
+
return 0;
|
|
10
|
+
return Math.sqrt(sum / samples.length);
|
|
11
|
+
}
|
|
12
|
+
export function float32RmsToLevel(samples) {
|
|
13
|
+
return Math.min(1, float32Rms(samples) * INPUT_LEVEL_GAIN);
|
|
14
|
+
}
|
|
15
|
+
export function timeDomainBytesToLevel(samples) {
|
|
16
|
+
let sum = 0;
|
|
17
|
+
for (const sample of samples) {
|
|
18
|
+
const normalized = (sample - 128) / 128;
|
|
19
|
+
sum += normalized * normalized;
|
|
20
|
+
}
|
|
21
|
+
if (samples.length === 0)
|
|
22
|
+
return 0;
|
|
23
|
+
return Math.min(1, Math.sqrt(sum / samples.length) * INPUT_LEVEL_GAIN);
|
|
24
|
+
}
|