@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.
Files changed (128) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +493 -2
  3. package/client/audio-level.d.ts +5 -0
  4. package/client/audio-level.js +24 -0
  5. package/client/chat.d.ts +144 -0
  6. package/client/chat.js +760 -0
  7. package/client/composer-drawer.d.ts +19 -0
  8. package/client/composer-drawer.js +15 -0
  9. package/client/composer-hints.d.ts +25 -0
  10. package/client/composer-hints.js +20 -0
  11. package/client/composer-primary.d.ts +18 -0
  12. package/client/composer-primary.js +29 -0
  13. package/client/decision-transport.d.ts +70 -0
  14. package/client/decision-transport.js +175 -0
  15. package/client/decode-composer-draft.d.ts +11 -0
  16. package/client/decode-composer-draft.js +12 -0
  17. package/client/embedding-input.d.ts +84 -0
  18. package/client/embedding-input.js +194 -0
  19. package/client/embedding-space.d.ts +82 -0
  20. package/client/embedding-space.js +389 -0
  21. package/client/embedding-transport.d.ts +40 -0
  22. package/client/embedding-transport.js +71 -0
  23. package/client/encode-composer-draft.d.ts +9 -0
  24. package/client/encode-composer-draft.js +34 -0
  25. package/client/encode-files.d.ts +18 -0
  26. package/client/encode-files.js +54 -0
  27. package/client/failure.d.ts +29 -0
  28. package/client/failure.js +34 -0
  29. package/client/generation-selection.d.ts +15 -0
  30. package/client/generation-selection.js +24 -0
  31. package/client/host-transport.d.ts +70 -0
  32. package/client/host-transport.js +75 -0
  33. package/client/index.d.ts +37 -0
  34. package/client/index.js +31 -0
  35. package/client/live/live-mic-forward.d.ts +30 -0
  36. package/client/live/live-mic-forward.js +46 -0
  37. package/client/live/live-page-tool.d.ts +39 -0
  38. package/client/live/live-page-tool.js +26 -0
  39. package/client/live/live-state.d.ts +13 -0
  40. package/client/live/live-state.js +22 -0
  41. package/client/live/live-tool.d.ts +11 -0
  42. package/client/live/live-tool.js +1 -0
  43. package/client/live-client.d.ts +189 -0
  44. package/client/live-client.js +829 -0
  45. package/client/live-errors.d.ts +2 -0
  46. package/client/live-errors.js +9 -0
  47. package/client/live-messages.d.ts +148 -0
  48. package/client/live-messages.js +100 -0
  49. package/client/mic-capture.d.ts +4 -0
  50. package/client/mic-capture.js +6 -0
  51. package/client/oauth-popup.d.ts +26 -0
  52. package/client/oauth-popup.js +30 -0
  53. package/client/page-dispatch.d.ts +15 -0
  54. package/client/page-dispatch.js +63 -0
  55. package/client/pcm-downsample.d.ts +4 -0
  56. package/client/pcm-downsample.js +45 -0
  57. package/client/run-commit.d.ts +34 -0
  58. package/client/run-commit.js +49 -0
  59. package/client/run-session.d.ts +79 -0
  60. package/client/run-session.js +207 -0
  61. package/client/schema-fields.d.ts +63 -0
  62. package/client/schema-fields.js +199 -0
  63. package/client/shaped-data.d.ts +118 -0
  64. package/client/shaped-data.js +343 -0
  65. package/client/source-citations.d.ts +13 -0
  66. package/client/source-citations.js +56 -0
  67. package/client/space-geometry.d.ts +39 -0
  68. package/client/space-geometry.js +183 -0
  69. package/client/tool-result.d.ts +78 -0
  70. package/client/tool-result.js +293 -0
  71. package/client/tool-resume.d.ts +50 -0
  72. package/client/tool-resume.js +14 -0
  73. package/client/trace-feed.d.ts +13 -0
  74. package/client/trace-feed.js +18 -0
  75. package/client/transcript-schema.d.ts +10 -0
  76. package/client/transcript-schema.js +120 -0
  77. package/client/transport.d.ts +264 -0
  78. package/client/transport.js +330 -0
  79. package/client/turn-client.d.ts +44 -0
  80. package/client/turn-client.js +88 -0
  81. package/client/wire-line.d.ts +56 -0
  82. package/client/wire-line.js +90 -0
  83. package/client/worklets/mic-capture.js +16 -0
  84. package/package.json +28 -4
  85. package/server/checkpoint-ownership.d.ts +6 -0
  86. package/server/checkpoint-ownership.js +42 -0
  87. package/server/conversation-store.d.ts +92 -0
  88. package/server/conversation-store.js +131 -0
  89. package/server/conversation.d.ts +193 -0
  90. package/server/conversation.js +552 -0
  91. package/server/credential-store.d.ts +24 -0
  92. package/server/credential-store.js +14 -0
  93. package/server/decision-handler.d.ts +42 -0
  94. package/server/decision-handler.js +46 -0
  95. package/server/embedding-handler.d.ts +33 -0
  96. package/server/embedding-handler.js +45 -0
  97. package/server/handler.d.ts +158 -0
  98. package/server/handler.js +775 -0
  99. package/server/json-response.d.ts +2 -0
  100. package/server/json-response.js +7 -0
  101. package/server/mcp-handler.d.ts +46 -0
  102. package/server/mcp-handler.js +312 -0
  103. package/server/mod.d.ts +27 -0
  104. package/server/mod.js +25 -0
  105. package/server/page-dispatch.d.ts +22 -0
  106. package/server/page-dispatch.js +127 -0
  107. package/server/request-body.d.ts +2 -0
  108. package/server/request-body.js +42 -0
  109. package/server/request-check.d.ts +21 -0
  110. package/server/request-check.js +49 -0
  111. package/server/session-authority.d.ts +8 -0
  112. package/server/session-authority.js +78 -0
  113. package/server/session-boundary.d.ts +26 -0
  114. package/server/session-boundary.js +64 -0
  115. package/server/session-identity.d.ts +16 -0
  116. package/server/session-identity.js +41 -0
  117. package/server/session-limits.d.ts +12 -0
  118. package/server/session-limits.js +59 -0
  119. package/server/session-store.d.ts +123 -0
  120. package/server/session-store.js +264 -0
  121. package/server/single-action.d.ts +21 -0
  122. package/server/single-action.js +30 -0
  123. package/server/steer-inbox.d.ts +35 -0
  124. package/server/steer-inbox.js +52 -0
  125. package/server/tool-operations.d.ts +6 -0
  126. package/server/tool-operations.js +132 -0
  127. package/server/turn-input.d.ts +10 -0
  128. package/server/turn-input.js +29 -0
@@ -0,0 +1,2 @@
1
+ /** A JSON reply no cache keeps. */
2
+ export declare function jsonResponse(status: number, body: unknown): Response;
@@ -0,0 +1,7 @@
1
+ /** A JSON reply no cache keeps. */
2
+ export function jsonResponse(status, body) {
3
+ return new Response(JSON.stringify(body), {
4
+ status,
5
+ headers: { 'content-type': 'application/json', 'cache-control': 'no-store' },
6
+ });
7
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * One Web-standard handler that serves a host profile's tools to MCP clients over
3
+ * Streamable HTTP, at one endpoint that takes POST.
4
+ *
5
+ * - Clients on the current revision (`2026-07-28`) send each request with its version and
6
+ * capabilities in `_meta`; nothing is kept between requests.
7
+ * - Clients on an earlier revision open with `initialize`; the handler answers it and serves
8
+ * them the same way, without a session.
9
+ *
10
+ * Every call runs through the kernel as a host call does: the profile's gates, guardrails and
11
+ * trace. The client reads what a model would: the call's guarded text, never its raw output.
12
+ *
13
+ * @module
14
+ */
15
+ import { type ClientTurnOptions } from '@theoremjs/agents/host';
16
+ import { type TheoremHostHandlerOptions } from './handler.ts';
17
+ export type TheoremMcpHandlerOptions = {
18
+ /** Host profile to serve. Registered with the kernel when the handler is created. */
19
+ profile: TheoremHostHandlerOptions['profile'];
20
+ /** The server's version, as clients show it. */
21
+ version: string;
22
+ /** The server's name, as clients show it. Defaults to the profile's id. */
23
+ name?: string;
24
+ /** How a model should use these tools; clients may add it to the model's context. */
25
+ instructions?: string;
26
+ /**
27
+ * Origins a browser page may call from, besides the endpoint's own. A request whose
28
+ * `Origin` is neither is refused with 403, as MCP requires against DNS rebinding.
29
+ */
30
+ origins?: readonly string[];
31
+ /** Handed to each tool as `ctx.host`. */
32
+ host?: (request: Request) => unknown;
33
+ /** Largest request body read, in bytes. */
34
+ maxRequestBytes?: number;
35
+ /** Who reads this host's replies: forwarded to `clientError` for a failure the handler catches. */
36
+ clientEvents?: ClientTurnOptions;
37
+ /** Called with any error the handler catches, for reporting. */
38
+ onError?: (err: unknown, ctx: {
39
+ request: Request;
40
+ }) => void;
41
+ };
42
+ /**
43
+ * Serves a host profile's tools to MCP clients. Every tool the profile allows must run
44
+ * without a person's approval: an MCP client has no one to ask.
45
+ */
46
+ export declare function createTheoremMcpHandler(options: TheoremMcpHandlerOptions): (request: Request) => Promise<Response>;
@@ -0,0 +1,312 @@
1
+ /**
2
+ * One Web-standard handler that serves a host profile's tools to MCP clients over
3
+ * Streamable HTTP, at one endpoint that takes POST.
4
+ *
5
+ * - Clients on the current revision (`2026-07-28`) send each request with its version and
6
+ * capabilities in `_meta`; nothing is kept between requests.
7
+ * - Clients on an earlier revision open with `initialize`; the handler answers it and serves
8
+ * them the same way, without a session.
9
+ *
10
+ * Every call runs through the kernel as a host call does: the profile's gates, guardrails and
11
+ * trace. The client reads what a model would: the call's guarded text, never its raw output.
12
+ *
13
+ * @module
14
+ */
15
+ import { invokeTool, labelText, registerProfile, } from '@theoremjs/agents';
16
+ import { caughtStatus, clientError } from '@theoremjs/agents/host';
17
+ import { hostInterface } from "../client/host-transport.js";
18
+ import { defineHostProfile, hostTool } from "./handler.js";
19
+ import { boundedRequestJson, requestByteLimit } from "./request-body.js";
20
+ /** The revision that carries its version on every request. */
21
+ const MODERN_VERSION = '2026-07-28';
22
+ /** Revisions that open with `initialize`, newest first. */
23
+ const LEGACY_VERSIONS = ['2025-11-25', '2025-06-18', '2025-03-26'];
24
+ const SUPPORTED_VERSIONS = [MODERN_VERSION, ...LEGACY_VERSIONS];
25
+ const META_VERSION = 'io.modelcontextprotocol/protocolVersion';
26
+ const META_CAPABILITIES = 'io.modelcontextprotocol/clientCapabilities';
27
+ const META_SERVER = 'io.modelcontextprotocol/serverInfo';
28
+ /**
29
+ * How long a current-revision client may keep each cacheable result, in milliseconds. Neither
30
+ * holds anything of the caller's, so any cache may share them.
31
+ */
32
+ const CACHED = {
33
+ 'server/discover': 3_600_000,
34
+ 'tools/list': 300_000,
35
+ };
36
+ const PARSE_ERROR = -32700;
37
+ const INVALID_REQUEST = -32600;
38
+ const METHOD_NOT_FOUND = -32601;
39
+ const INVALID_PARAMS = -32602;
40
+ const INTERNAL_ERROR = -32603;
41
+ const HEADER_MISMATCH = -32020;
42
+ const UNSUPPORTED_VERSION = -32022;
43
+ class RpcError extends Error {
44
+ status;
45
+ code;
46
+ data;
47
+ constructor(status, code, message, data) {
48
+ super(message);
49
+ this.status = status;
50
+ this.code = code;
51
+ this.data = data;
52
+ }
53
+ }
54
+ function reply(status, body) {
55
+ return new Response(JSON.stringify(body), {
56
+ status,
57
+ headers: { 'content-type': 'application/json', 'cache-control': 'no-store' },
58
+ });
59
+ }
60
+ function rpcError(id, error) {
61
+ return reply(error.status, {
62
+ jsonrpc: '2.0',
63
+ id,
64
+ error: {
65
+ code: error.code,
66
+ message: error.message,
67
+ ...(error.data !== undefined ? { data: error.data } : {}),
68
+ },
69
+ });
70
+ }
71
+ function isRecord(value) {
72
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
73
+ }
74
+ /** The JSON-RPC message in `body`; one message per POST, never a batch or a response. */
75
+ function rpcRequest(body) {
76
+ if (!isRecord(body) ||
77
+ body.jsonrpc !== '2.0' ||
78
+ typeof body.method !== 'string' ||
79
+ (body.id !== undefined && typeof body.id !== 'string' && typeof body.id !== 'number') ||
80
+ (body.params !== undefined && !isRecord(body.params)))
81
+ // labels-exempt: protocol diagnostic for the MCP client, not end-user copy
82
+ throw new RpcError(400, INVALID_REQUEST, 'Invalid request');
83
+ return {
84
+ ...(body.id !== undefined ? { id: body.id } : {}),
85
+ method: body.method,
86
+ params: isRecord(body.params) ? body.params : {},
87
+ };
88
+ }
89
+ /** A mirrored header's value, decoded from the `=?base64?…?=` form MCP uses for unsafe values. */
90
+ function headerValue(value) {
91
+ const encoded = /^=\?base64\?(.*)\?=$/.exec(value);
92
+ if (!encoded)
93
+ return value;
94
+ const bytes = Uint8Array.from(atob(encoded[1]), (char) => char.charCodeAt(0));
95
+ return new TextDecoder().decode(bytes);
96
+ }
97
+ /**
98
+ * The current revision's request checks: a version this server speaks, the capabilities the
99
+ * client declares, and headers that agree with the body they mirror.
100
+ */
101
+ function checkModern(request, message, meta) {
102
+ const version = meta[META_VERSION];
103
+ if (version !== MODERN_VERSION)
104
+ // labels-exempt: protocol diagnostic for the MCP client, not end-user copy
105
+ throw new RpcError(400, UNSUPPORTED_VERSION, 'Unsupported protocol version', {
106
+ supported: SUPPORTED_VERSIONS,
107
+ requested: version,
108
+ });
109
+ if (!isRecord(meta[META_CAPABILITIES]))
110
+ // labels-exempt: protocol diagnostic for the MCP client, not end-user copy
111
+ throw new RpcError(400, INVALID_PARAMS, `Missing _meta field ${META_CAPABILITIES}`);
112
+ const mirrored = [
113
+ ['mcp-protocol-version', version],
114
+ ['mcp-method', message.method],
115
+ ];
116
+ if (message.method === 'tools/call')
117
+ mirrored.push(['mcp-name', message.params.name]);
118
+ for (const [name, body] of mirrored) {
119
+ const header = request.headers.get(name);
120
+ if (header === null || headerValue(header) !== body)
121
+ // labels-exempt: protocol diagnostic for the MCP client, not end-user copy
122
+ throw new RpcError(400, HEADER_MISMATCH, `Header mismatch: ${name}`);
123
+ }
124
+ }
125
+ /** The legacy revision a client asked for in `initialize`, or the newest one this server speaks. */
126
+ function legacyVersion(params) {
127
+ const asked = params.protocolVersion;
128
+ return LEGACY_VERSIONS.find((version) => version === asked) ?? LEGACY_VERSIONS[0];
129
+ }
130
+ /** What the client reads of one call, from the kernel's events for it. */
131
+ function callResult(events, labels) {
132
+ for (const event of events) {
133
+ if (event.type === 'error')
134
+ return {
135
+ content: [
136
+ { type: 'text', text: event.error ?? labelText(`error.${event.errorKind}`, {}, labels) },
137
+ ],
138
+ isError: true,
139
+ };
140
+ if (event.type !== 'tool' || !('phase' in event.tool))
141
+ continue;
142
+ const tool = event.tool;
143
+ if (tool.phase === 'complete')
144
+ return {
145
+ content: [
146
+ { type: 'text', text: tool.readBack ?? '' },
147
+ ...(tool.parts ?? []).map(partContent),
148
+ ],
149
+ isError: false,
150
+ };
151
+ if (tool.phase === 'error')
152
+ return {
153
+ content: [{ type: 'text', text: tool.readBack ?? tool.failure.message }],
154
+ isError: true,
155
+ };
156
+ // why: No one can answer a gate here; the client reads why the call stopped, as a model would.
157
+ if (tool.phase === 'gate')
158
+ return {
159
+ content: [{ type: 'text', text: tool.readBack ?? labelText('error.blocked', {}, labels) }],
160
+ isError: true,
161
+ };
162
+ }
163
+ return {
164
+ content: [{ type: 'text', text: labelText('error.internal', {}, labels) }],
165
+ isError: true,
166
+ };
167
+ }
168
+ function partContent(part, index) {
169
+ if (part.type === 'text')
170
+ return { type: 'text', text: part.text };
171
+ if ('uri' in part)
172
+ return { type: 'resource_link', uri: part.uri, name: part.uri, mimeType: part.mimeType };
173
+ if (part.type === 'image' || part.type === 'audio')
174
+ return { type: part.type, data: part.data, mimeType: part.mimeType };
175
+ return {
176
+ type: 'resource',
177
+ resource: { uri: `part:${index}`, mimeType: part.mimeType, blob: part.data },
178
+ };
179
+ }
180
+ /** Same origin, or one the host lists; a request with no `Origin` is not from a page. */
181
+ function originAllowed(request, origins) {
182
+ const origin = request.headers.get('origin');
183
+ return origin === null || origin === new URL(request.url).origin || origins.includes(origin);
184
+ }
185
+ /**
186
+ * Serves a host profile's tools to MCP clients. Every tool the profile allows must run
187
+ * without a person's approval: an MCP client has no one to ask.
188
+ */
189
+ export function createTheoremMcpHandler(options) {
190
+ const host = defineHostProfile('createTheoremMcpHandler', options.profile);
191
+ for (const name of host.tools.allow) {
192
+ const permission = hostTool(name)?.permission;
193
+ if (permission !== undefined && permission !== 'auto') {
194
+ // labels-exempt: builder config error at setup; no user sees it
195
+ throw new Error(`createTheoremMcpHandler serves tools that run without approval; '${name}' has permission '${permission}'.`);
196
+ }
197
+ }
198
+ const maxBytes = requestByteLimit(options.maxRequestBytes);
199
+ registerProfile(host);
200
+ const serverInfo = { name: options.name ?? host.id, version: options.version };
201
+ const instructions = options.instructions ? { instructions: options.instructions } : {};
202
+ const listed = () => hostInterface(host, hostTool).tools.map((tool) => ({
203
+ name: tool.name,
204
+ description: tool.description,
205
+ inputSchema: tool.inputSchema,
206
+ annotations: {
207
+ readOnlyHint: tool.access === 'read-only',
208
+ ...(tool.access === 'destructive' ? { destructiveHint: true } : {}),
209
+ },
210
+ }));
211
+ async function call(request, params) {
212
+ const name = params.name;
213
+ if (typeof name !== 'string' || !listed().some((tool) => tool.name === name))
214
+ // labels-exempt: protocol diagnostic for the MCP client, not end-user copy
215
+ throw new RpcError(200, INVALID_PARAMS, `Unknown tool: ${String(name)}`);
216
+ const events = [];
217
+ for await (const event of invokeTool({
218
+ profile: host.id,
219
+ name,
220
+ input: params.arguments ?? {},
221
+ signal: request.signal,
222
+ host: await options.host?.(request),
223
+ }))
224
+ events.push(event);
225
+ return callResult(events, host.labels);
226
+ }
227
+ async function result(request, message, modern) {
228
+ switch (message.method) {
229
+ case 'server/discover':
230
+ if (!modern)
231
+ break;
232
+ return {
233
+ supportedVersions: SUPPORTED_VERSIONS,
234
+ capabilities: { tools: {} },
235
+ ...instructions,
236
+ };
237
+ case 'initialize':
238
+ if (modern)
239
+ break;
240
+ return {
241
+ protocolVersion: legacyVersion(message.params),
242
+ capabilities: { tools: {} },
243
+ serverInfo,
244
+ ...instructions,
245
+ };
246
+ case 'ping':
247
+ if (modern)
248
+ break;
249
+ return {};
250
+ case 'tools/list':
251
+ return { tools: listed() };
252
+ case 'tools/call':
253
+ return await call(request, message.params);
254
+ }
255
+ // labels-exempt: protocol diagnostic for the MCP client, not end-user copy
256
+ throw new RpcError(modern ? 404 : 200, METHOD_NOT_FOUND, `Method not found: ${message.method}`);
257
+ }
258
+ return async (request) => {
259
+ if (!originAllowed(request, options.origins ?? []))
260
+ // labels-exempt: protocol diagnostic for the MCP client, not end-user copy
261
+ return rpcError(null, new RpcError(403, INVALID_REQUEST, 'Origin not allowed'));
262
+ if (request.method !== 'POST')
263
+ return new Response(null, { status: 405, headers: { allow: 'POST' } });
264
+ let id = null;
265
+ try {
266
+ let body;
267
+ try {
268
+ body = await boundedRequestJson(request, maxBytes);
269
+ }
270
+ catch (err) {
271
+ if (err instanceof SyntaxError)
272
+ // labels-exempt: protocol diagnostic for the MCP client, not end-user copy
273
+ throw new RpcError(400, PARSE_ERROR, 'Parse error');
274
+ throw err;
275
+ }
276
+ const message = rpcRequest(body);
277
+ id = message.id ?? null;
278
+ const meta = isRecord(message.params._meta) ? message.params._meta : {};
279
+ const modern = META_VERSION in meta;
280
+ if (modern)
281
+ checkModern(request, message, meta);
282
+ else if (request.headers.get('mcp-protocol-version') === MODERN_VERSION)
283
+ // labels-exempt: protocol diagnostic for the MCP client, not end-user copy
284
+ throw new RpcError(400, INVALID_PARAMS, `Missing _meta field ${META_VERSION}`);
285
+ // invariant: A notification has no reply; the client needs only to know it arrived.
286
+ if (message.id === undefined)
287
+ return new Response(null, { status: 202 });
288
+ const answered = await result(request, message, modern);
289
+ const ttlMs = CACHED[message.method];
290
+ return reply(200, {
291
+ jsonrpc: '2.0',
292
+ id: message.id,
293
+ result: modern
294
+ ? {
295
+ resultType: 'complete',
296
+ ...answered,
297
+ ...(ttlMs !== undefined ? { ttlMs, cacheScope: 'public' } : {}),
298
+ _meta: { [META_SERVER]: serverInfo },
299
+ }
300
+ : answered,
301
+ });
302
+ }
303
+ catch (err) {
304
+ if (err instanceof RpcError)
305
+ return rpcError(id, err);
306
+ options.onError?.(err, { request });
307
+ const failure = clientError(err, host.labels, options.clientEvents);
308
+ const code = failure.errorKind === 'request' ? INVALID_REQUEST : INTERNAL_ERROR;
309
+ return rpcError(id, new RpcError(caughtStatus(err), code, failure.error));
310
+ }
311
+ };
312
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Server half of `@theoremjs/web`: serve one profile to the chat UI, a
3
+ * decision profile to the decision UI, or a host profile's tools to the host UI or to MCP clients.
4
+ * `@theoremjs/react/server` re-exports it.
5
+ *
6
+ * @module
7
+ */
8
+ export { theoremHostCallRequestSchema } from '../client/host-transport.ts';
9
+ export type { LiveClientMessage, LiveOpenMessage } from '../client/live-messages.ts';
10
+ export { theoremConversationRequestSchema, theoremInvokeRequestSchema, theoremReplaySchema, theoremRewindRequestSchema, theoremSteerRequestSchema, theoremTurnRequestSchema, } from '../client/transport.ts';
11
+ export { answerReply, type ConversationHost, type ConversationUse, conversationView, type IssuedWalkAway, messageReply, type OpenedConversation, openConversation, type PostedMessage, postedMessage, type ReplyLine, type ReplyRunner, type ReplyTurn, readerConversationHost, rewindConversation, startConversation, statelessRefusal, type WalkedAwayCall, waitingCalls, } from './conversation.ts';
12
+ export { type ConversationSnapshot, type ConversationWriteResult, createMemoryConversationStore, createSealedConversationStore, emptyConversation, type PausedReply, type SealedConversationStoreOptions, type StoredConversation, storedConversationSchema, type TheoremConversationStore, unknownConversation, } from './conversation-store.ts';
13
+ export { createMemoryCredentialStore, type TheoremCredentialStore, type TheoremCredentials, } from './credential-store.ts';
14
+ export { createTheoremDecisionHandler, type TheoremDecisionHandlerOptions, } from './decision-handler.ts';
15
+ export { createTheoremEmbeddingHandler, embedBodySchema, embedRequest, type TheoremEmbeddingHandlerOptions, } from './embedding-handler.ts';
16
+ export { createTheoremHandler, createTheoremHostHandler, readBody, type TheoremHandlerOptions, type TheoremHostHandlerOptions, type TheoremRequestContext, theoremSession, theoremSessionId, } from './handler.ts';
17
+ export { createTheoremMcpHandler, type TheoremMcpHandlerOptions } from './mcp-handler.ts';
18
+ export { createMemoryPageToolBroker, type PageToolBroker, type PageToolResult, pageToolResultSchema, sessionPageDispatch, } from './page-dispatch.ts';
19
+ export { checkRequest, parseLiveClientMessage, parseLiveOpenMessage } from './request-check.ts';
20
+ export { sessionStateStore } from './session-authority.ts';
21
+ export { createTheoremSessionBoundary, type PreparedLiveOpen, type SessionBoundaryOptions, type TheoremSessionBoundary, } from './session-boundary.ts';
22
+ export type { TheoremSessionLimits } from './session-limits.ts';
23
+ export { resolveSessionLimits } from './session-limits.ts';
24
+ export { createMemorySessionStore, DEFAULT_OPERATION_RETENTION_MS, type MemorySessionStoreOptions, type PendingToolGate, type SessionSnapshot, type SessionToolOperation, type SessionWriteResult, type TheoremSessionState, type TheoremSessionStore, theoremSessionStateSchema, } from './session-store.ts';
25
+ export { createMemorySteerInbox, type SteerInbox, type SteerUnit, steerStage, steerUnitOf, } from './steer-inbox.ts';
26
+ export { sessionToolCoordinator } from './tool-operations.ts';
27
+ export { kernelTurnInput, liveSessionOpen } from './turn-input.ts';
package/server/mod.js ADDED
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Server half of `@theoremjs/web`: serve one profile to the chat UI, a
3
+ * decision profile to the decision UI, or a host profile's tools to the host UI or to MCP clients.
4
+ * `@theoremjs/react/server` re-exports it.
5
+ *
6
+ * @module
7
+ */
8
+ export { theoremHostCallRequestSchema } from "../client/host-transport.js";
9
+ export { theoremConversationRequestSchema, theoremInvokeRequestSchema, theoremReplaySchema, theoremRewindRequestSchema, theoremSteerRequestSchema, theoremTurnRequestSchema, } from "../client/transport.js";
10
+ export { answerReply, conversationView, messageReply, openConversation, postedMessage, readerConversationHost, rewindConversation, startConversation, statelessRefusal, waitingCalls, } from "./conversation.js";
11
+ export { createMemoryConversationStore, createSealedConversationStore, emptyConversation, storedConversationSchema, unknownConversation, } from "./conversation-store.js";
12
+ export { createMemoryCredentialStore, } from "./credential-store.js";
13
+ export { createTheoremDecisionHandler, } from "./decision-handler.js";
14
+ export { createTheoremEmbeddingHandler, embedBodySchema, embedRequest, } from "./embedding-handler.js";
15
+ export { createTheoremHandler, createTheoremHostHandler, readBody, theoremSession, theoremSessionId, } from "./handler.js";
16
+ export { createTheoremMcpHandler } from "./mcp-handler.js";
17
+ export { createMemoryPageToolBroker, pageToolResultSchema, sessionPageDispatch, } from "./page-dispatch.js";
18
+ export { checkRequest, parseLiveClientMessage, parseLiveOpenMessage } from "./request-check.js";
19
+ export { sessionStateStore } from "./session-authority.js";
20
+ export { createTheoremSessionBoundary, } from "./session-boundary.js";
21
+ export { resolveSessionLimits } from "./session-limits.js";
22
+ export { createMemorySessionStore, DEFAULT_OPERATION_RETENTION_MS, theoremSessionStateSchema, } from "./session-store.js";
23
+ export { createMemorySteerInbox, steerStage, steerUnitOf, } from "./steer-inbox.js";
24
+ export { sessionToolCoordinator } from "./tool-operations.js";
25
+ export { kernelTurnInput, liveSessionOpen } from "./turn-input.js";
@@ -0,0 +1,22 @@
1
+ import { type PageAnswer, type PageToolExecution, z } from '@theoremjs/agents';
2
+ import type { MutableStore } from './session-authority.ts';
3
+ import type { TheoremSessionState } from './session-store.ts';
4
+ export type PageToolResult = {
5
+ callId: string;
6
+ operationId: string;
7
+ grantId: string;
8
+ page: PageAnswer;
9
+ };
10
+ export declare const pageToolResultSchema: z.ZodType<PageToolResult>;
11
+ export interface PageToolBroker {
12
+ reserve(sessionId: string, grantId: string, signal?: AbortSignal): {
13
+ result: Promise<PageAnswer>;
14
+ cancel(): void;
15
+ };
16
+ deliver(sessionId: string, grantId: string, page: PageAnswer): Promise<void>;
17
+ }
18
+ export declare function createMemoryPageToolBroker(maxPending?: number): PageToolBroker;
19
+ export declare function sessionPageDispatch(store: MutableStore<TheoremSessionState>, sessionId: string, broker: PageToolBroker, timeoutMs?: number): {
20
+ execution: PageToolExecution;
21
+ answer(result: PageToolResult): Promise<boolean>;
22
+ };
@@ -0,0 +1,127 @@
1
+ import { jsonValueSchema, labelText, TheoremError, toolInvocationHash, z, } from '@theoremjs/agents';
2
+ export const pageToolResultSchema = z.strictObject({
3
+ callId: z.string().min(1),
4
+ operationId: z.string().min(1).max(256),
5
+ grantId: z.string().min(1),
6
+ page: z.union([
7
+ z.strictObject({ output: jsonValueSchema }),
8
+ z.strictObject({ unanswered: z.literal(true) }),
9
+ z.strictObject({ timedOut: z.literal(true) }),
10
+ ]),
11
+ });
12
+ function unavailable() {
13
+ throw new TheoremError('unavailable', labelText('tool.execution_uncertain'), {
14
+ copy: { key: 'tool.execution_uncertain' },
15
+ });
16
+ }
17
+ export function createMemoryPageToolBroker(maxPending = 128) {
18
+ if (!Number.isSafeInteger(maxPending) || maxPending <= 0)
19
+ throw new TheoremError('config', labelText('error.config'));
20
+ const waiting = new Map();
21
+ const key = (sessionId, grantId) => JSON.stringify([sessionId, grantId]);
22
+ return {
23
+ reserve(sessionId, grantId, signal) {
24
+ const id = key(sessionId, grantId);
25
+ if (signal?.aborted || waiting.has(id) || waiting.size >= maxPending)
26
+ unavailable();
27
+ let finish = () => { };
28
+ const result = new Promise((resolve) => {
29
+ finish = resolve;
30
+ });
31
+ const cancel = () => {
32
+ waiting.delete(id);
33
+ signal?.removeEventListener('abort', cancel);
34
+ finish({ timedOut: true });
35
+ };
36
+ waiting.set(id, (answer) => {
37
+ waiting.delete(id);
38
+ signal?.removeEventListener('abort', cancel);
39
+ finish(answer);
40
+ });
41
+ signal?.addEventListener('abort', cancel, { once: true });
42
+ return { result, cancel };
43
+ },
44
+ deliver(sessionId, grantId, page) {
45
+ const finish = waiting.get(key(sessionId, grantId));
46
+ if (!finish)
47
+ return Promise.reject(new TheoremError('unavailable', labelText('tool.execution_uncertain')));
48
+ finish(page);
49
+ return Promise.resolve();
50
+ },
51
+ };
52
+ }
53
+ export function sessionPageDispatch(store, sessionId, broker, timeoutMs = 20_000) {
54
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0 || timeoutMs > 2147483647)
55
+ throw new TheoremError('config', labelText('error.config'));
56
+ return {
57
+ execution: {
58
+ async dispatch(claim, signal) {
59
+ if (!claim.invocation)
60
+ unavailable();
61
+ const grantId = crypto.randomUUID();
62
+ const expiresAt = Date.now() + timeoutMs;
63
+ const pending = broker.reserve(sessionId, grantId, signal);
64
+ try {
65
+ await store.mutate(sessionId, (state) => {
66
+ const record = state.operations[claim.operationId];
67
+ if (record?.status !== 'running' ||
68
+ record.claimToken !== claim.claimToken ||
69
+ record.invocationHash !== claim.invocationHash ||
70
+ record.pageGrant)
71
+ unavailable();
72
+ record.pageGrant = { grantId, expiresAt };
73
+ });
74
+ }
75
+ catch (error) {
76
+ pending.cancel();
77
+ throw error;
78
+ }
79
+ const remaining = expiresAt - Date.now();
80
+ if (remaining <= 0 || signal?.aborted) {
81
+ pending.cancel();
82
+ unavailable();
83
+ }
84
+ let expire = () => { };
85
+ const expired = new Promise((resolve) => {
86
+ expire = () => resolve({ timedOut: true });
87
+ });
88
+ const timer = setTimeout(expire, remaining);
89
+ signal?.addEventListener('abort', expire, { once: true });
90
+ return {
91
+ grantId,
92
+ result: Promise.race([pending.result, expired]).finally(() => {
93
+ clearTimeout(timer);
94
+ signal?.removeEventListener('abort', expire);
95
+ pending.cancel();
96
+ }),
97
+ };
98
+ },
99
+ },
100
+ async answer(raw) {
101
+ const result = pageToolResultSchema.parse(raw);
102
+ const hash = await toolInvocationHash(result.page);
103
+ const duplicate = await store.mutate(sessionId, (state) => {
104
+ const record = state.operations[result.operationId];
105
+ if (!record ||
106
+ record.status === 'ready' ||
107
+ record.callId !== result.callId ||
108
+ record.pageGrant?.grantId !== result.grantId)
109
+ unavailable();
110
+ if (record.pageGrant.resultHash !== undefined) {
111
+ if (record.pageGrant.resultHash !== hash)
112
+ unavailable();
113
+ if (record.status === 'uncertain')
114
+ unavailable();
115
+ return { duplicate: true, settled: record.status === 'settled' };
116
+ }
117
+ if (record.status !== 'running' || Date.now() >= record.pageGrant.expiresAt)
118
+ unavailable();
119
+ record.pageGrant.resultHash = hash;
120
+ return { duplicate: false, settled: false };
121
+ });
122
+ if (!duplicate.duplicate)
123
+ await broker.deliver(sessionId, result.grantId, result.page);
124
+ return duplicate.settled;
125
+ },
126
+ };
127
+ }
@@ -0,0 +1,2 @@
1
+ export declare function requestByteLimit(bytes?: number): number;
2
+ export declare function boundedRequestJson(request: Request, maxBytes: number): Promise<unknown>;
@@ -0,0 +1,42 @@
1
+ import { labelText, TheoremError, throwIfAborted } from '@theoremjs/agents';
2
+ const DEFAULT_REQUEST_BYTES = 16 * 1024 * 1024;
3
+ export function requestByteLimit(bytes = DEFAULT_REQUEST_BYTES) {
4
+ if (!Number.isSafeInteger(bytes) || bytes <= 0) {
5
+ throw new TheoremError('config', labelText('error.config'));
6
+ }
7
+ return bytes;
8
+ }
9
+ export async function boundedRequestJson(request, maxBytes) {
10
+ if (Number(request.headers.get('content-length')) > maxBytes || !request.body) {
11
+ throw new TheoremError('request', labelText('error.request'));
12
+ }
13
+ const reader = request.body.getReader();
14
+ const abort = () => {
15
+ void reader.cancel(request.signal.reason).catch(() => undefined);
16
+ };
17
+ request.signal.addEventListener('abort', abort, { once: true });
18
+ const decoder = new TextDecoder();
19
+ const text = [];
20
+ let bytes = 0;
21
+ try {
22
+ for (;;) {
23
+ throwIfAborted(request.signal);
24
+ const next = await reader.read();
25
+ throwIfAborted(request.signal);
26
+ if (next.done)
27
+ break;
28
+ bytes += next.value.byteLength;
29
+ if (bytes > maxBytes)
30
+ throw new TheoremError('request', labelText('error.request'));
31
+ if (next.value.byteLength)
32
+ text.push(decoder.decode(next.value, { stream: true }));
33
+ }
34
+ text.push(decoder.decode());
35
+ return JSON.parse(text.join(''));
36
+ }
37
+ finally {
38
+ request.signal.removeEventListener('abort', abort);
39
+ void reader.cancel().catch(() => undefined);
40
+ reader.releaseLock();
41
+ }
42
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * What a browser sends a host, checked: the handler's request bodies and a
3
+ * relay's live messages. A malformed one is a `request` error, which names
4
+ * only the paths and codes that broke, never the value.
5
+ *
6
+ * @module
7
+ */
8
+ import { type z } from '@theoremjs/agents';
9
+ import { type LiveClientMessage, type LiveOpenMessage } from '../client/live-messages.ts';
10
+ /** `raw` checked against `schema`; a missing or malformed field is a `request` error. */
11
+ export declare function checkRequest<T>(schema: z.ZodType<T>, raw: unknown, what: string): T;
12
+ /**
13
+ * One text frame from the live client, as a relay reads it: JSON that passes
14
+ * `liveClientMessageSchema`, else a `request` error.
15
+ */
16
+ export declare function parseLiveClientMessage(text: string): LiveClientMessage;
17
+ /**
18
+ * The live client's first frame, as a relay reads it before it opens the
19
+ * session: JSON that passes `liveOpenMessageSchema`, else a `request` error.
20
+ */
21
+ export declare function parseLiveOpenMessage(text: string): LiveOpenMessage;