@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,49 @@
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 { labelText, TheoremError } from '@theoremjs/agents';
9
+ import { liveClientMessageSchema, liveOpenMessageSchema, } from "../client/live-messages.js";
10
+ import { issueSummary } from "../client/wire-line.js";
11
+ import { requestByteLimit } from "./request-body.js";
12
+ /** `raw` checked against `schema`; a missing or malformed field is a `request` error. */
13
+ export function checkRequest(schema, raw, what) {
14
+ const parsed = schema.safeParse(raw);
15
+ if (!parsed.success) {
16
+ // labels-exempt: internal diagnostic; the user reads the error kind's (or copy key's) wording
17
+ throw new TheoremError('request', `${what} failed its check: ${issueSummary(parsed.error)}`);
18
+ }
19
+ return parsed.data;
20
+ }
21
+ function liveFrame(text) {
22
+ const limit = requestByteLimit();
23
+ if (text.length > limit || new TextEncoder().encode(text).byteLength > limit)
24
+ throw new TheoremError('request', labelText('error.request'));
25
+ try {
26
+ return JSON.parse(text);
27
+ }
28
+ catch {
29
+ // invariant: JSON parser diagnostics can contain browser values.
30
+ throw new TheoremError('request', labelText('error.request'), {
31
+ cause: { code: 'request_json' },
32
+ });
33
+ }
34
+ }
35
+ /**
36
+ * One text frame from the live client, as a relay reads it: JSON that passes
37
+ * `liveClientMessageSchema`, else a `request` error.
38
+ */
39
+ export function parseLiveClientMessage(text) {
40
+ return checkRequest(liveClientMessageSchema, liveFrame(text), 'live message');
41
+ }
42
+ /**
43
+ * The live client's first frame, as a relay reads it before it opens the
44
+ * session: JSON that passes `liveOpenMessageSchema`, else a `request` error.
45
+ */
46
+ export function parseLiveOpenMessage(text) {
47
+ // labels-exempt: internal diagnostic; the user reads the error kind's wording
48
+ return checkRequest(liveOpenMessageSchema, liveFrame(text), 'live open message');
49
+ }
@@ -0,0 +1,8 @@
1
+ import { type TheoremSessionLimits } from './session-limits.ts';
2
+ import { type TheoremSessionState, type TheoremSessionStore } from './session-store.ts';
3
+ export type MutableStore<T> = {
4
+ read(sessionId: string): Promise<T>;
5
+ mutate<R>(sessionId: string, change: (value: T) => R | Promise<R>): Promise<R>;
6
+ };
7
+ /** Session state, with expired gates dropped as it loads so they can't be answered. */
8
+ export declare function sessionStateStore(store: TheoremSessionStore, gateTtlMs: number, operationRetentionMs: number, limits: TheoremSessionLimits): MutableStore<TheoremSessionState>;
@@ -0,0 +1,78 @@
1
+ import { labelText, TheoremError, z } from '@theoremjs/agents';
2
+ import { checkSessionLimits } from "./session-limits.js";
3
+ import { emptySessionState, pruneGates, theoremSessionStateSchema, } from "./session-store.js";
4
+ /** Session state, with expired gates dropped as it loads so they can't be answered. */
5
+ export function sessionStateStore(store, gateTtlMs, operationRetentionMs, limits) {
6
+ if (typeof store.compareAndSwap !== 'function') {
7
+ throw new TheoremError('config', labelText('error.config'), {
8
+ cause: { code: 'session_store_contract', required: 'compareAndSwap' },
9
+ });
10
+ }
11
+ const writeResult = z.discriminatedUnion('written', [
12
+ z.strictObject({ written: z.literal(false) }),
13
+ z.strictObject({ written: z.literal(true), revision: z.string().min(1) }),
14
+ ]);
15
+ const load = async (sessionId, now) => {
16
+ const snapshot = await store.load(sessionId);
17
+ const parsed = theoremSessionStateSchema.safeParse(snapshot === undefined ? emptySessionState() : snapshot.state);
18
+ if (!parsed.success ||
19
+ (snapshot !== undefined && (typeof snapshot.revision !== 'string' || !snapshot.revision))) {
20
+ throw new TheoremError('config', labelText('error.config'), {
21
+ cause: { code: 'session_store_state' },
22
+ });
23
+ }
24
+ const state = structuredClone(parsed.data);
25
+ for (const gate of Object.values(state.gates))
26
+ gate.expiresAt ??= gate.createdAt + gateTtlMs;
27
+ state.gates = pruneGates(state.gates, now, gateTtlMs);
28
+ for (const operation of Object.values(state.operations)) {
29
+ if (operation.status === 'ready' && operation.answer.resume.granted === true)
30
+ operation.answer.resume.gateKind ??= operation.pending.gate.kind;
31
+ }
32
+ state.operations = Object.fromEntries(Object.entries(state.operations).filter(([, operation]) => operation.status !== 'settled' ||
33
+ now < (operation.retainUntil ?? operation.settledAt + operationRetentionMs)));
34
+ return { revision: snapshot?.revision ?? null, state };
35
+ };
36
+ return {
37
+ async read(sessionId) {
38
+ return (await load(sessionId, Date.now())).state;
39
+ },
40
+ async mutate(sessionId, change) {
41
+ const now = Date.now();
42
+ for (let conflict = 0; conflict < 8; conflict++) {
43
+ const { revision, state } = await load(sessionId, now);
44
+ const result = await change(state);
45
+ for (const gate of Object.values(state.gates))
46
+ gate.expiresAt ??= gate.createdAt + gateTtlMs;
47
+ for (const operation of Object.values(state.operations)) {
48
+ if (operation.status === 'settled')
49
+ operation.retainUntil ??= operation.settledAt + operationRetentionMs;
50
+ }
51
+ checkSessionLimits(state, limits);
52
+ const next = theoremSessionStateSchema.safeParse(state);
53
+ if (!next.success) {
54
+ throw new TheoremError('config', labelText('error.config'), {
55
+ cause: { code: 'session_store_transition' },
56
+ });
57
+ }
58
+ const written = writeResult.safeParse(await store.compareAndSwap(sessionId, revision, next.data));
59
+ if (!written.success) {
60
+ throw new TheoremError('config', labelText('error.config'), {
61
+ cause: { code: 'session_store_write_result' },
62
+ });
63
+ }
64
+ if (written.data.written) {
65
+ if (written.data.revision === revision) {
66
+ throw new TheoremError('config', labelText('error.config'), {
67
+ cause: { code: 'session_store_revision' },
68
+ });
69
+ }
70
+ return result;
71
+ }
72
+ }
73
+ throw new TheoremError('unavailable', labelText('error.unavailable'), {
74
+ cause: { code: 'session_store_contention' },
75
+ });
76
+ },
77
+ };
78
+ }
@@ -0,0 +1,26 @@
1
+ import { type PageToolExecution, type ProviderCheckpoint, type SessionRequest, type ToolExecutionCoordinator, type TurnHistoryMessage } from '@theoremjs/agents';
2
+ import { type LiveOpenMessage } from '../client/live-messages.ts';
3
+ import { type PageToolBroker, type PageToolResult } from './page-dispatch.ts';
4
+ import { type TheoremSessionLimits } from './session-limits.ts';
5
+ import { type TheoremSessionStore } from './session-store.ts';
6
+ export interface SessionBoundaryOptions {
7
+ request: Request;
8
+ session?: (request: Request) => string | undefined | Promise<string | undefined>;
9
+ sessionStore: TheoremSessionStore;
10
+ gateTtlMs?: number;
11
+ operationRetentionMs?: number;
12
+ sessionLimits?: Partial<TheoremSessionLimits>;
13
+ pageToolBroker?: PageToolBroker;
14
+ }
15
+ export type PreparedLiveOpen = Pick<SessionRequest, 'history' | 'slots' | 'context' | 'providerState' | 'awayMs'>;
16
+ export interface TheoremSessionBoundary {
17
+ sessionId: string;
18
+ setCookie?: string;
19
+ prepareLiveOpen(open: LiveOpenMessage, serverContext?: unknown): Promise<PreparedLiveOpen>;
20
+ prepareProviderState(checkpoint: ProviderCheckpoint | undefined, history?: readonly TurnHistoryMessage[]): Promise<ProviderCheckpoint | undefined>;
21
+ recordCheckpoint(checkpoint: ProviderCheckpoint): Promise<void>;
22
+ toolExecution: ToolExecutionCoordinator;
23
+ pageExecution: PageToolExecution;
24
+ answerPageResult(result: PageToolResult): Promise<boolean>;
25
+ }
26
+ export declare function createTheoremSessionBoundary(options: SessionBoundaryOptions): Promise<TheoremSessionBoundary>;
@@ -0,0 +1,64 @@
1
+ import { labelText, TheoremError, } from '@theoremjs/agents';
2
+ import { resolveGateTtlMs } from '@theoremjs/agents/kernel';
3
+ import { liveOpenMessageSchema } from "../client/live-messages.js";
4
+ import { checkpointIdentity, checkpointOwnershipKey, requireOwnedCheckpoint, retainedCheckpointIdentities, } from "./checkpoint-ownership.js";
5
+ import { createMemoryPageToolBroker, sessionPageDispatch, } from "./page-dispatch.js";
6
+ import { checkRequest } from "./request-check.js";
7
+ import { sessionStateStore } from "./session-authority.js";
8
+ import { theoremSession } from "./session-identity.js";
9
+ import { resolveSessionLimits } from "./session-limits.js";
10
+ import { DEFAULT_OPERATION_RETENTION_MS } from "./session-store.js";
11
+ import { sessionToolCoordinator } from "./tool-operations.js";
12
+ import { liveSessionOpen } from "./turn-input.js";
13
+ export async function createTheoremSessionBoundary(options) {
14
+ const cookie = options.session ? undefined : theoremSession(options.request);
15
+ const id = options.session ? (await options.session(options.request))?.trim() : cookie?.id;
16
+ if (!id)
17
+ throw new TheoremError('auth', labelText('session.sign_in'), {
18
+ copy: { key: 'session.sign_in' },
19
+ });
20
+ const retention = options.operationRetentionMs ?? DEFAULT_OPERATION_RETENTION_MS;
21
+ if (!Number.isSafeInteger(retention) || retention <= 0)
22
+ throw new TheoremError('config', labelText('error.config'));
23
+ const limits = resolveSessionLimits(options.sessionLimits);
24
+ const store = sessionStateStore(options.sessionStore, resolveGateTtlMs('session boundary', options.gateTtlMs), retention, limits);
25
+ // invariant: Refuse unavailable or malformed authority before an upstream connection opens.
26
+ await store.read(id);
27
+ const page = sessionPageDispatch(store, id, options.pageToolBroker ?? createMemoryPageToolBroker());
28
+ return {
29
+ pageExecution: page.execution,
30
+ answerPageResult: page.answer,
31
+ sessionId: id,
32
+ ...(cookie?.setCookie ? { setCookie: cookie.setCookie } : {}),
33
+ toolExecution: sessionToolCoordinator(store, id),
34
+ async prepareLiveOpen(raw, server) {
35
+ const open = checkRequest(liveOpenMessageSchema, raw, 'live open');
36
+ const prepared = liveSessionOpen(open, server);
37
+ const state = await store.read(id);
38
+ const providerState = await requireOwnedCheckpoint(state.providerCheckpoints, prepared.providerState, state.operations);
39
+ if (providerState &&
40
+ Object.values(state.operations).some((operation) => operation.status !== 'settled')) {
41
+ throw new TheoremError('unavailable', labelText('tool.execution_uncertain'), {
42
+ copy: { key: 'tool.execution_uncertain' },
43
+ });
44
+ }
45
+ return {
46
+ ...prepared,
47
+ providerState,
48
+ ...(prepared.history
49
+ ? { history: prepared.history.filter((message) => message.role !== 'system') }
50
+ : {}),
51
+ };
52
+ },
53
+ async prepareProviderState(checkpoint, history) {
54
+ const state = await store.read(id);
55
+ return requireOwnedCheckpoint(state.providerCheckpoints, checkpoint, state.operations, history);
56
+ },
57
+ async recordCheckpoint(checkpoint) {
58
+ const identity = await checkpointIdentity(checkpoint);
59
+ await store.mutate(id, async (state) => {
60
+ state.providerCheckpoints = retainedCheckpointIdentities(state.providerCheckpoints, [await checkpointOwnershipKey(identity, state.operations)], limits.maxCheckpoints);
61
+ });
62
+ },
63
+ };
64
+ }
@@ -0,0 +1,16 @@
1
+ export type Session = {
2
+ id: string;
3
+ setCookie?: string;
4
+ };
5
+ /**
6
+ * The session id the handler issued in its cookie, for a host route outside the
7
+ * handler (an OAuth callback) that saves credentials under it. Hosts that pass
8
+ * a `session` resolver use their own id instead.
9
+ */
10
+ export declare function theoremSessionId(request: Request): string | undefined;
11
+ /**
12
+ * The handler's cookie session for `request`: its id, and the `Set-Cookie` header to send when
13
+ * the request had none. A page that starts a conversation before the chat's first request uses
14
+ * it, so the chat and the conversation share one session.
15
+ */
16
+ export declare function theoremSession(request: Request): Session;
@@ -0,0 +1,41 @@
1
+ import { toBase64Url } from '@theoremjs/agents/kernel';
2
+ const SESSION_COOKIE = 'theorem_session';
3
+ const SESSION_ID_PATTERN = /^[A-Za-z0-9_-]{32,128}$/;
4
+ /** How long a visitor keeps their session, so a chat they come back to opens as theirs: 30 days. */
5
+ const SESSION_MAX_AGE_SECONDS = 30 * 24 * 60 * 60;
6
+ function readCookie(request, name) {
7
+ for (const part of (request.headers.get('cookie') ?? '').split(';')) {
8
+ const [key, ...rest] = part.trim().split('=');
9
+ if (key === name)
10
+ return rest.join('=');
11
+ }
12
+ return undefined;
13
+ }
14
+ function newSessionId() {
15
+ return toBase64Url(crypto.getRandomValues(new Uint8Array(32)));
16
+ }
17
+ /**
18
+ * The session id the handler issued in its cookie, for a host route outside the
19
+ * handler (an OAuth callback) that saves credentials under it. Hosts that pass
20
+ * a `session` resolver use their own id instead.
21
+ */
22
+ export function theoremSessionId(request) {
23
+ const existing = readCookie(request, SESSION_COOKIE);
24
+ return existing && SESSION_ID_PATTERN.test(existing) ? existing : undefined;
25
+ }
26
+ /**
27
+ * The handler's cookie session for `request`: its id, and the `Set-Cookie` header to send when
28
+ * the request had none. A page that starts a conversation before the chat's first request uses
29
+ * it, so the chat and the conversation share one session.
30
+ */
31
+ export function theoremSession(request) {
32
+ const existing = theoremSessionId(request);
33
+ if (existing)
34
+ return { id: existing };
35
+ const id = newSessionId();
36
+ const secure = new URL(request.url).protocol === 'https:' ? '; Secure' : '';
37
+ return {
38
+ id,
39
+ setCookie: `${SESSION_COOKIE}=${id}; Path=/; Max-Age=${SESSION_MAX_AGE_SECONDS}; HttpOnly; SameSite=Lax${secure}`,
40
+ };
41
+ }
@@ -0,0 +1,12 @@
1
+ import type { TheoremSessionState } from './session-store.ts';
2
+ export interface TheoremSessionLimits {
3
+ maxGates: number;
4
+ maxOperations: number;
5
+ maxCheckpoints: number;
6
+ maxPermissions: number;
7
+ maxStateBytes: number;
8
+ maxInvocationBytes: number;
9
+ maxOutcomeBytes: number;
10
+ }
11
+ export declare function resolveSessionLimits(options?: Partial<TheoremSessionLimits>): TheoremSessionLimits;
12
+ export declare function checkSessionLimits(state: TheoremSessionState, limits: TheoremSessionLimits): void;
@@ -0,0 +1,59 @@
1
+ import { labelText, TheoremError } from '@theoremjs/agents';
2
+ const DEFAULTS = {
3
+ maxGates: 32,
4
+ maxOperations: 128,
5
+ maxCheckpoints: 256,
6
+ maxPermissions: 2048,
7
+ maxStateBytes: 32 * 1024 * 1024,
8
+ maxInvocationBytes: 256 * 1024,
9
+ maxOutcomeBytes: 256 * 1024,
10
+ };
11
+ export function resolveSessionLimits(options = {}) {
12
+ const limits = { ...DEFAULTS, ...options };
13
+ if (Object.keys(options).some((key) => !Object.hasOwn(DEFAULTS, key)) ||
14
+ Object.values(limits).some((value) => !Number.isSafeInteger(value) || value <= 0)) {
15
+ throw new TheoremError('config', labelText('error.config'), {
16
+ cause: { code: 'session_limits' },
17
+ });
18
+ }
19
+ return limits;
20
+ }
21
+ function bytes(value) {
22
+ return new TextEncoder().encode(JSON.stringify(value)).byteLength;
23
+ }
24
+ function capacity() {
25
+ throw new TheoremError('unavailable', labelText('error.unavailable'), {
26
+ cause: { code: 'session_state_capacity' },
27
+ });
28
+ }
29
+ export function checkSessionLimits(state, limits) {
30
+ const gates = Object.values(state.gates);
31
+ const operations = Object.values(state.operations);
32
+ if (gates.length > limits.maxGates ||
33
+ operations.length > limits.maxOperations ||
34
+ state.providerCheckpoints.length > limits.maxCheckpoints ||
35
+ state.permissions.length > limits.maxPermissions)
36
+ capacity();
37
+ for (const gate of gates)
38
+ if (bytes(gate) > limits.maxInvocationBytes)
39
+ capacity();
40
+ let reserved = gates.length * limits.maxOutcomeBytes;
41
+ for (const operation of operations) {
42
+ if (operation.status !== 'ready' &&
43
+ operation.invocation &&
44
+ bytes(operation.invocation) > limits.maxInvocationBytes)
45
+ capacity();
46
+ if (operation.status === 'settled') {
47
+ if (bytes(operation.outcome) > limits.maxOutcomeBytes)
48
+ capacity();
49
+ }
50
+ else {
51
+ reserved += limits.maxOutcomeBytes;
52
+ if (operation.status === 'ready' && bytes(operation) > limits.maxInvocationBytes)
53
+ capacity();
54
+ }
55
+ }
56
+ // invariant: Reserve terminal evidence before dispatch; unresolved records are never evicted to make room.
57
+ if (bytes(state) + reserved > limits.maxStateBytes)
58
+ capacity();
59
+ }
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Server-held session state for `createTheoremHandler`.
3
+ *
4
+ * Everything that grants authority lives here, never in the request body:
5
+ * which tools the user allowed for the session, which paused tool calls may be
6
+ * resumed (and with exactly what input), and which provider interactions this
7
+ * session may continue.
8
+ *
9
+ * @module
10
+ */
11
+ import { z } from '@theoremjs/agents';
12
+ import type { ToolGateAuth } from '@theoremjs/agents/interface';
13
+ import type { AnsweredGate, ToolGate, ToolId, ToolOperationIdentity, ToolOperationOutcome, ToolOperationUncertainty, TurnInput, TurnToolSnapshot } from '@theoremjs/agents/kernel';
14
+ import { type TheoremSessionLimits } from './session-limits.ts';
15
+ /** A tool call the kernel paused on a gate, as the server saw it. */
16
+ export type PendingToolGate = {
17
+ profileId: string;
18
+ operationId: string;
19
+ callId: string;
20
+ name: string;
21
+ /** The model's arguments; an approval runs the call with exactly these. */
22
+ arguments: Record<string, unknown>;
23
+ /** The gate, and for a sign-in gate the credential slot and kind it waits for. */
24
+ gate: Pick<ToolGate, 'kind' | 'permission'> & {
25
+ auth?: ToolGateAuth;
26
+ };
27
+ snapshot?: TurnToolSnapshot;
28
+ promoted: ToolId[];
29
+ turnInput: TurnInput;
30
+ model?: string;
31
+ createdAt: number;
32
+ expiresAt?: number;
33
+ };
34
+ export type SessionPageGrant = {
35
+ grantId: string;
36
+ expiresAt: number;
37
+ resultHash?: string;
38
+ };
39
+ export type SessionToolOperation = {
40
+ status: 'ready';
41
+ profileId: string;
42
+ pending: PendingToolGate;
43
+ answer: Omit<AnsweredGate, 'typed'>;
44
+ readyAt: number;
45
+ } | (ToolOperationIdentity & {
46
+ status: 'running';
47
+ claimToken: string;
48
+ startedAt: number;
49
+ pageGrant?: SessionPageGrant;
50
+ }) | (ToolOperationIdentity & {
51
+ status: 'settled';
52
+ pageGrant?: SessionPageGrant;
53
+ outcome: ToolOperationOutcome;
54
+ settledAt: number;
55
+ retainUntil?: number;
56
+ }) | (ToolOperationIdentity & {
57
+ status: 'uncertain';
58
+ pageGrant?: SessionPageGrant;
59
+ claimToken: string;
60
+ reason: ToolOperationUncertainty;
61
+ detectedAt: number;
62
+ });
63
+ export declare const DEFAULT_OPERATION_RETENTION_MS: number;
64
+ export type TheoremSessionState = {
65
+ version: 3;
66
+ operations: Record<string, SessionToolOperation>;
67
+ /** Tool ids the user allowed for the rest of this session. */
68
+ permissions: string[];
69
+ /** invariant: Operation IDs index issued gates; both IDs must match an answer. */
70
+ gates: Record<string, PendingToolGate>;
71
+ /** Checkpoint identities this session produced, newest last. */
72
+ providerCheckpoints: string[];
73
+ };
74
+ /** Stored authority is checked before the handler reads or changes it. */
75
+ export declare const theoremSessionStateSchema: z.ZodType<TheoremSessionState>;
76
+ export interface SessionSnapshot {
77
+ revision: string;
78
+ state: TheoremSessionState;
79
+ }
80
+ export type SessionWriteResult = {
81
+ written: true;
82
+ revision: string;
83
+ } | {
84
+ written: false;
85
+ };
86
+ /**
87
+ * Where session state lives. The default is process memory — use a shared
88
+ * store (KV, Redis, a database row) when requests can reach different instances.
89
+ */
90
+ export interface TheoremSessionStore {
91
+ load(sessionId: string): Promise<SessionSnapshot | undefined>;
92
+ /** Compare and replace atomically; null creates only an absent session. */
93
+ compareAndSwap(sessionId: string, expectedRevision: string | null, next: TheoremSessionState): Promise<SessionWriteResult>;
94
+ }
95
+ export declare function emptySessionState(): TheoremSessionState;
96
+ /** Drop gates (waiting or settled) older than `ttlMs`. */
97
+ export declare function pruneGates<Gate extends {
98
+ createdAt: number;
99
+ expiresAt?: number;
100
+ }>(gates: Record<string, Gate>, now: number, ttlMs: number): Record<string, Gate>;
101
+ export type MemorySessionStoreOptions = {
102
+ /** Finite authority storage bounds. */
103
+ sessionLimits?: Partial<TheoremSessionLimits>;
104
+ /** invariant: Idle retention is in milliseconds. Protected authority remains. Default 24h. */
105
+ ttlMs?: number;
106
+ /** Oldest sessions are evicted past this count. Default 10 000. */
107
+ maxSessions?: number;
108
+ };
109
+ export declare function memoryStoreLimits(options: MemorySessionStoreOptions): {
110
+ ttlMs: number;
111
+ maxSessions: number;
112
+ };
113
+ /** A value kept per session id for the separate credential store. */
114
+ export type PerSessionStore<T> = {
115
+ load(sessionId: string): T | undefined | Promise<T | undefined>;
116
+ save(sessionId: string, value: T): void | Promise<void>;
117
+ };
118
+ /** A per-session value kept in process memory: forgotten when idle, oldest evicted first. */
119
+ export declare function createMemorySessionMap<T>(options?: MemorySessionStoreOptions): {
120
+ load(sessionId: string): T | undefined;
121
+ save(sessionId: string, value: T): void;
122
+ };
123
+ export declare function createMemorySessionStore(options?: MemorySessionStoreOptions): TheoremSessionStore;