@theoremjs/react 0.3.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (258) hide show
  1. package/README.md +728 -202
  2. package/client/composer-hints.js +1 -1
  3. package/client/decision-images.d.ts +76 -0
  4. package/client/decision-images.js +139 -0
  5. package/client/decision-transport.d.ts +26 -4
  6. package/client/decision-transport.js +79 -6
  7. package/client/embedding-input.d.ts +84 -0
  8. package/client/embedding-input.js +194 -0
  9. package/client/embedding-space.d.ts +82 -0
  10. package/client/embedding-space.js +389 -0
  11. package/client/embedding-transport.d.ts +40 -0
  12. package/client/embedding-transport.js +71 -0
  13. package/client/failure.d.ts +7 -10
  14. package/client/failure.js +9 -9
  15. package/client/host-call.d.ts +1 -1
  16. package/client/host-call.js +7 -2
  17. package/client/host-transport.d.ts +2 -0
  18. package/client/host-transport.js +3 -2
  19. package/client/index.d.ts +19 -11
  20. package/client/index.js +14 -10
  21. package/client/ink-waveform.d.ts +1 -1
  22. package/client/ink-waveform.js +1 -2
  23. package/client/live/apply-live-turn-tool-event.js +1 -1
  24. package/client/live/live-captions.d.ts +30 -0
  25. package/client/live/live-captions.js +105 -1
  26. package/client/live/live-mic-forward.d.ts +1 -0
  27. package/client/live/live-mic-forward.js +11 -2
  28. package/client/live/live-page-tool.d.ts +34 -0
  29. package/client/live/live-page-tool.js +11 -0
  30. package/client/live/live-state.d.ts +4 -2
  31. package/client/live/live-state.js +2 -0
  32. package/client/live/live-video.js +1 -1
  33. package/client/live/run-live-tool-call.d.ts +7 -2
  34. package/client/live/run-live-tool-call.js +48 -10
  35. package/client/live/use-live-caption-log.d.ts +17 -0
  36. package/client/live/use-live-caption-log.js +51 -0
  37. package/client/live-client.d.ts +52 -4
  38. package/client/live-client.js +250 -59
  39. package/client/live-messages.d.ts +51 -13
  40. package/client/live-messages.js +42 -9
  41. package/client/page-dispatch.d.ts +15 -0
  42. package/client/page-dispatch.js +63 -0
  43. package/client/run-commit.d.ts +16 -43
  44. package/client/run-commit.js +26 -101
  45. package/client/run-session.d.ts +40 -41
  46. package/client/run-session.js +104 -112
  47. package/client/schema-fields.js +27 -6
  48. package/client/shaped-data.js +37 -8
  49. package/client/source-citations.js +1 -1
  50. package/client/space-geometry.d.ts +39 -0
  51. package/client/space-geometry.js +183 -0
  52. package/client/state-fields.js +1 -1
  53. package/client/tool-result.js +30 -12
  54. package/client/tool-resume.d.ts +18 -1
  55. package/client/tool-resume.js +4 -1
  56. package/client/trace-feed.d.ts +1 -1
  57. package/client/trace-story.d.ts +2 -2
  58. package/client/trace-story.js +43 -19
  59. package/client/trace-view.d.ts +2 -2
  60. package/client/trace-view.js +56 -16
  61. package/client/transcript-groups.d.ts +28 -2
  62. package/client/transcript-groups.js +63 -13
  63. package/client/transcript-schema.d.ts +10 -0
  64. package/client/transcript-schema.js +120 -0
  65. package/client/transport.d.ts +114 -26
  66. package/client/transport.js +106 -32
  67. package/client/turn-client.d.ts +17 -8
  68. package/client/turn-client.js +26 -31
  69. package/client/voice-recorder.d.ts +1 -1
  70. package/client/voice-recorder.js +2 -5
  71. package/client/wire-line.js +5 -5
  72. package/client/worklets/mic-capture.js +9 -9
  73. package/components/InkWaveform.d.ts +12 -1
  74. package/components/InkWaveform.js +65 -42
  75. package/components/live/use-live-runner-controls.d.ts +3 -4
  76. package/components/live/use-live-runner-controls.js +6 -10
  77. package/components/live/use-live-runner-model.d.ts +10 -6
  78. package/components/live/use-live-runner-model.js +29 -14
  79. package/components/live/use-live-runner-ui.d.ts +10 -8
  80. package/components/live/use-live-runner-ui.js +19 -11
  81. package/components/live/use-live-session-client.d.ts +4 -4
  82. package/components/live/use-live-session-client.js +11 -8
  83. package/components/use-composer-voice.d.ts +4 -4
  84. package/components/use-composer-voice.js +8 -8
  85. package/components/use-editor-selection.js +4 -1
  86. package/components/use-voice-playback.d.ts +2 -1
  87. package/components/use-voice-playback.js +4 -4
  88. package/hooks/use-described.d.ts +5 -1
  89. package/hooks/use-described.js +11 -9
  90. package/hooks/use-embedding-index.d.ts +27 -0
  91. package/hooks/use-embedding-index.js +43 -0
  92. package/hooks/use-one-call.d.ts +31 -0
  93. package/hooks/use-one-call.js +76 -0
  94. package/hooks/use-page-tool-warnings.d.ts +6 -0
  95. package/hooks/use-page-tool-warnings.js +25 -0
  96. package/hooks/use-second-ticker.d.ts +2 -0
  97. package/hooks/use-second-ticker.js +13 -0
  98. package/hooks/use-theorem-chat-actions.d.ts +25 -21
  99. package/hooks/use-theorem-chat-actions.js +67 -45
  100. package/hooks/use-theorem-chat-state.d.ts +6 -6
  101. package/hooks/use-theorem-chat-state.js +13 -9
  102. package/hooks/use-theorem-chat.d.ts +48 -14
  103. package/hooks/use-theorem-chat.js +329 -141
  104. package/hooks/use-theorem-decision.d.ts +5 -10
  105. package/hooks/use-theorem-decision.js +4 -67
  106. package/hooks/use-theorem-embedding.d.ts +18 -0
  107. package/hooks/use-theorem-embedding.js +12 -0
  108. package/hooks/use-theorem-host.d.ts +5 -4
  109. package/hooks/use-theorem-host.js +26 -8
  110. package/hooks/use-theorem-interface.d.ts +6 -3
  111. package/hooks/use-theorem-interface.js +12 -22
  112. package/index.d.ts +6 -3
  113. package/index.js +5 -2
  114. package/live.d.ts +1 -0
  115. package/live.js +1 -1
  116. package/package.json +6 -5
  117. package/server/checkpoint-ownership.d.ts +6 -0
  118. package/server/checkpoint-ownership.js +42 -0
  119. package/server/conversation-store.d.ts +92 -0
  120. package/server/conversation-store.js +131 -0
  121. package/server/conversation.d.ts +193 -0
  122. package/server/conversation.js +552 -0
  123. package/server/decision-handler.d.ts +12 -4
  124. package/server/decision-handler.js +29 -38
  125. package/server/embedding-handler.d.ts +33 -0
  126. package/server/embedding-handler.js +45 -0
  127. package/server/handler.d.ts +58 -20
  128. package/server/handler.js +441 -238
  129. package/server/mod.d.ts +15 -6
  130. package/server/mod.js +13 -5
  131. package/server/page-dispatch.d.ts +22 -0
  132. package/server/page-dispatch.js +127 -0
  133. package/server/request-body.d.ts +2 -0
  134. package/server/request-body.js +42 -0
  135. package/server/request-check.d.ts +6 -1
  136. package/server/request-check.js +27 -12
  137. package/server/session-authority.d.ts +8 -0
  138. package/server/session-authority.js +78 -0
  139. package/server/session-boundary.d.ts +26 -0
  140. package/server/session-boundary.js +64 -0
  141. package/server/session-identity.d.ts +16 -0
  142. package/server/session-identity.js +41 -0
  143. package/server/session-limits.d.ts +12 -0
  144. package/server/session-limits.js +59 -0
  145. package/server/session-store.d.ts +69 -20
  146. package/server/session-store.js +220 -5
  147. package/server/single-action.d.ts +21 -0
  148. package/server/single-action.js +30 -0
  149. package/server/steer-inbox.js +1 -1
  150. package/server/tool-operations.d.ts +6 -0
  151. package/server/tool-operations.js +132 -0
  152. package/server/turn-input.d.ts +10 -0
  153. package/server/turn-input.js +29 -0
  154. package/styles/ink-controls.css +78 -78
  155. package/ui/CardWall.d.ts +10 -0
  156. package/ui/CardWall.js +61 -0
  157. package/ui/ChatComposerBar.d.ts +28 -5
  158. package/ui/ChatComposerBar.js +85 -30
  159. package/ui/ChatTranscript.d.ts +14 -1
  160. package/ui/ChatTranscript.js +110 -57
  161. package/ui/ConsoleFrame.d.ts +61 -4
  162. package/ui/ConsoleFrame.js +109 -9
  163. package/ui/DecisionAnswers.d.ts +40 -2
  164. package/ui/DecisionAnswers.js +166 -35
  165. package/ui/DecisionImages.d.ts +16 -0
  166. package/ui/DecisionImages.js +46 -0
  167. package/ui/EmbeddingDocuments.d.ts +78 -0
  168. package/ui/EmbeddingDocuments.js +278 -0
  169. package/ui/EmbeddingQuery.d.ts +27 -0
  170. package/ui/EmbeddingQuery.js +95 -0
  171. package/ui/EmbeddingResults.d.ts +41 -0
  172. package/ui/EmbeddingResults.js +165 -0
  173. package/ui/EmbeddingSpace.d.ts +26 -0
  174. package/ui/EmbeddingSpace.js +106 -0
  175. package/ui/LiveCaptionsPanel.d.ts +22 -0
  176. package/ui/LiveCaptionsPanel.js +27 -0
  177. package/ui/LiveRunner.d.ts +6 -4
  178. package/ui/LiveRunner.js +42 -55
  179. package/ui/PaneState.d.ts +33 -0
  180. package/ui/PaneState.js +50 -0
  181. package/ui/SchemaFields.d.ts +1 -1
  182. package/ui/SchemaFields.js +54 -26
  183. package/ui/ShapedData.js +73 -28
  184. package/ui/SidePanel.d.ts +74 -5
  185. package/ui/SidePanel.js +82 -9
  186. package/ui/StateFields.d.ts +10 -2
  187. package/ui/StateFields.js +56 -13
  188. package/ui/TheoremChat.d.ts +65 -14
  189. package/ui/TheoremChat.js +66 -78
  190. package/ui/TheoremDecision.d.ts +12 -20
  191. package/ui/TheoremDecision.js +78 -35
  192. package/ui/TheoremEmbedding.d.ts +27 -0
  193. package/ui/TheoremEmbedding.js +122 -0
  194. package/ui/TheoremHost.d.ts +22 -5
  195. package/ui/TheoremHost.js +127 -37
  196. package/ui/ToolGateCard.d.ts +5 -1
  197. package/ui/ToolGateCard.js +30 -23
  198. package/ui/ToolResult.js +12 -10
  199. package/ui/ToolResultCharts.js +29 -12
  200. package/ui/TraceCharts.d.ts +2 -2
  201. package/ui/TraceCharts.js +48 -17
  202. package/ui/TraceGuardrails.d.ts +3 -2
  203. package/ui/TraceGuardrails.js +24 -6
  204. package/ui/TraceInspector.d.ts +11 -1
  205. package/ui/TraceInspector.js +45 -28
  206. package/ui/TraceInspectorPanel.d.ts +31 -9
  207. package/ui/TraceInspectorPanel.js +77 -39
  208. package/ui/TraceOverview.d.ts +7 -3
  209. package/ui/TraceOverview.js +44 -14
  210. package/ui/TraceSpanDetail.js +24 -22
  211. package/ui/TraceStory.js +9 -8
  212. package/ui/TraceValues.js +19 -9
  213. package/ui/TraceWaterfall.js +12 -9
  214. package/ui/VoiceNote.js +4 -4
  215. package/ui/approval-heading.d.ts +4 -0
  216. package/ui/approval-heading.js +9 -0
  217. package/ui/arrive.d.ts +18 -0
  218. package/ui/arrive.js +46 -0
  219. package/ui/built/theme.css +86 -68
  220. package/ui/built/theme.css.d.ts +9 -0
  221. package/ui/built/theorem.d.ts +3 -3
  222. package/ui/built/theorem.js +34 -5
  223. package/ui/built/theorem.variants.d.ts +8 -2
  224. package/ui/code-language.d.ts +12 -0
  225. package/ui/code-language.js +50 -0
  226. package/ui/disclosure-motion.js +53 -9
  227. package/ui/embedding-space-gl.d.ts +37 -0
  228. package/ui/embedding-space-gl.js +663 -0
  229. package/ui/embedding-space-view.d.ts +8 -0
  230. package/ui/embedding-space-view.js +8 -0
  231. package/ui/glide.d.ts +7 -0
  232. package/ui/glide.js +44 -0
  233. package/ui/icons.js +2 -1
  234. package/ui/index.d.ts +20 -9
  235. package/ui/index.js +20 -10
  236. package/ui/labels-provider.js +5 -2
  237. package/ui/labels.d.ts +374 -85
  238. package/ui/labels.js +1028 -188
  239. package/ui/markdown-plugins.d.ts +9 -0
  240. package/ui/markdown-plugins.js +13 -0
  241. package/ui/mcp-icon.d.ts +6 -0
  242. package/ui/mcp-icon.js +8 -0
  243. package/ui/row-keys.d.ts +19 -0
  244. package/ui/row-keys.js +33 -0
  245. package/ui/theme.d.ts +1 -1
  246. package/ui/theme.js +2 -2
  247. package/ui/theorem-theme.js +32 -10
  248. package/ui/trace-palette.d.ts +2 -16
  249. package/ui/trace-palette.js +2 -16
  250. package/ui/trace-patterns.d.ts +11 -3
  251. package/ui/trace-patterns.js +16 -3
  252. package/ui/transcript-copy-text.js +1 -0
  253. package/ui/wall-columns.d.ts +8 -0
  254. package/ui/wall-columns.js +19 -0
  255. package/client/walk-away.d.ts +0 -34
  256. package/client/walk-away.js +0 -48
  257. package/server/walk-away.d.ts +0 -22
  258. package/server/walk-away.js +0 -41
@@ -8,10 +8,15 @@
8
8
  *
9
9
  * @module
10
10
  */
11
+ import { z } from '@theoremjs/agents';
11
12
  import type { ToolGateAuth } from '@theoremjs/agents/interface';
12
- import type { ToolGate, ToolId, TurnEventOf, TurnInput, TurnToolSnapshot } from '@theoremjs/agents/kernel';
13
+ import type { AnsweredGate, ToolGate, ToolId, ToolOperationIdentity, ToolOperationOutcome, ToolOperationUncertainty, TurnInput, TurnToolSnapshot } from '@theoremjs/agents/kernel';
14
+ import { type TheoremSessionLimits } from './session-limits.ts';
13
15
  /** A tool call the kernel paused on a gate, as the server saw it. */
14
16
  export type PendingToolGate = {
17
+ profileId: string;
18
+ operationId: string;
19
+ callId: string;
15
20
  name: string;
16
21
  /** The model's arguments; an approval runs the call with exactly these. */
17
22
  arguments: Record<string, unknown>;
@@ -24,51 +29,95 @@ export type PendingToolGate = {
24
29
  turnInput: TurnInput;
25
30
  model?: string;
26
31
  createdAt: number;
32
+ expiresAt?: number;
27
33
  };
28
- /**
29
- * A paused call the server answered, and the event that settled it. A client
30
- * the stream never reached still shows the call waiting; a message that walks
31
- * away from it gets this answer again, not a refusal.
32
- */
33
- export type SettledToolGate = {
34
- event: TurnEventOf<'tool'>;
35
- /** When the call paused: a settled call is kept as long as its gate would have waited. */
36
- createdAt: number;
34
+ export type SessionPageGrant = {
35
+ grantId: string;
36
+ expiresAt: number;
37
+ resultHash?: string;
37
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;
38
64
  export type TheoremSessionState = {
65
+ version: 3;
66
+ operations: Record<string, SessionToolOperation>;
39
67
  /** Tool ids the user allowed for the rest of this session. */
40
68
  permissions: string[];
41
- /** Paused tool calls by call id — the only calls `/invoke` will run. */
69
+ /** invariant: Operation IDs index issued gates; both IDs must match an answer. */
42
70
  gates: Record<string, PendingToolGate>;
43
- /** Paused calls the server answered and settled, by call id. */
44
- settled: Record<string, SettledToolGate>;
45
- /** Provider interaction ids this session produced, newest last. */
46
- interactions: string[];
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;
47
85
  };
48
86
  /**
49
87
  * Where session state lives. The default is process memory — use a shared
50
88
  * store (KV, Redis, a database row) when requests can reach different instances.
51
89
  */
52
90
  export interface TheoremSessionStore {
53
- load(sessionId: string): TheoremSessionState | undefined | Promise<TheoremSessionState | undefined>;
54
- save(sessionId: string, state: TheoremSessionState): void | Promise<void>;
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>;
55
94
  }
56
95
  export declare function emptySessionState(): TheoremSessionState;
57
96
  /** Drop gates (waiting or settled) older than `ttlMs`. */
58
97
  export declare function pruneGates<Gate extends {
59
98
  createdAt: number;
99
+ expiresAt?: number;
60
100
  }>(gates: Record<string, Gate>, now: number, ttlMs: number): Record<string, Gate>;
61
101
  export type MemorySessionStoreOptions = {
62
- /** Idle sessions are forgotten after this long. Default 24h. */
102
+ /** Finite authority storage bounds. */
103
+ sessionLimits?: Partial<TheoremSessionLimits>;
104
+ /** invariant: Idle retention is in milliseconds. Protected authority remains. Default 24h. */
63
105
  ttlMs?: number;
64
106
  /** Oldest sessions are evicted past this count. Default 10 000. */
65
107
  maxSessions?: number;
66
108
  };
67
- /** A value kept per session id; the shape of every store the handler takes. */
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. */
68
114
  export type PerSessionStore<T> = {
69
115
  load(sessionId: string): T | undefined | Promise<T | undefined>;
70
116
  save(sessionId: string, value: T): void | Promise<void>;
71
117
  };
72
118
  /** A per-session value kept in process memory: forgotten when idle, oldest evicted first. */
73
- export declare function createMemorySessionMap<T>(options?: MemorySessionStoreOptions): PerSessionStore<T>;
119
+ export declare function createMemorySessionMap<T>(options?: MemorySessionStoreOptions): {
120
+ load(sessionId: string): T | undefined;
121
+ save(sessionId: string, value: T): void;
122
+ };
74
123
  export declare function createMemorySessionStore(options?: MemorySessionStoreOptions): TheoremSessionStore;
@@ -8,18 +8,191 @@
8
8
  *
9
9
  * @module
10
10
  */
11
+ import { jsonObjectSchema, jsonValueSchema, labelText, savedToolInvocationSchema, TheoremError, TOOL_AUTH_TYPES, TOOL_GATE_KINDS, TOOL_PERMISSION, toolOperationOutcomeSchema, turnHistoryMessageSchema, turnToolSnapshotSchema, z, } from '@theoremjs/agents';
11
12
  import { gateExpired } from '@theoremjs/agents/kernel';
13
+ import { checkSessionLimits, resolveSessionLimits, } from "./session-limits.js";
14
+ export const DEFAULT_OPERATION_RETENTION_MS = 24 * 60 * 60 * 1000;
15
+ const blobSchema = z.strictObject({
16
+ data: z.string(),
17
+ mimeType: z.string(),
18
+ name: z.string().optional(),
19
+ });
20
+ const inputSchema = z.strictObject({
21
+ text: z.string().optional(),
22
+ role: z.string().optional(),
23
+ slots: z.record(z.string(), z.string()).optional(),
24
+ context: z
25
+ .strictObject({ client: jsonValueSchema.optional(), server: jsonValueSchema.optional() })
26
+ .optional(),
27
+ attachments: z
28
+ .array(z.union([
29
+ blobSchema,
30
+ z.strictObject({
31
+ uri: z.string(),
32
+ mimeType: z.string(),
33
+ name: z.string().optional(),
34
+ }),
35
+ ]))
36
+ .optional(),
37
+ voice: z.array(blobSchema).optional(),
38
+ history: z.array(turnHistoryMessageSchema).optional(),
39
+ repair: z
40
+ .strictObject({ previousOutput: z.string(), rejection: z.string(), guidance: z.string() })
41
+ .optional(),
42
+ historyTokens: z.number().nonnegative().optional(),
43
+ inputTokens: z.number().nonnegative().optional(),
44
+ });
45
+ const pendingSchema = z.strictObject({
46
+ profileId: z.string().min(1),
47
+ operationId: z.string().min(1).max(256),
48
+ callId: z.string().min(1),
49
+ name: z.string().min(1),
50
+ arguments: jsonObjectSchema,
51
+ gate: z
52
+ .strictObject({
53
+ kind: z.enum(TOOL_GATE_KINDS),
54
+ permission: z.enum(TOOL_PERMISSION).optional(),
55
+ auth: z
56
+ .strictObject({
57
+ slot: z.string().min(1),
58
+ authType: z.enum(TOOL_AUTH_TYPES),
59
+ service: z.string().min(1),
60
+ })
61
+ .optional(),
62
+ })
63
+ .superRefine((gate, ctx) => {
64
+ if ((gate.kind === 'auth') !== (gate.auth !== undefined)) {
65
+ ctx.addIssue({ code: 'custom', path: ['auth'] });
66
+ }
67
+ }),
68
+ snapshot: turnToolSnapshotSchema.optional(),
69
+ promoted: z.array(z.string().min(1)),
70
+ turnInput: inputSchema,
71
+ model: z.string().optional(),
72
+ createdAt: z.number().finite().nonnegative(),
73
+ expiresAt: z.number().finite().nonnegative().optional(),
74
+ });
75
+ const acceptedAnswerSchema = z.strictObject({
76
+ input: jsonObjectSchema,
77
+ resume: z.strictObject({
78
+ granted: z.boolean().optional(),
79
+ gateKind: z.enum(TOOL_GATE_KINDS).optional(),
80
+ cause: z.enum(['declined', 'abandoned', 'expired']).optional(),
81
+ signIn: z.boolean().optional(),
82
+ edited: z.strictObject({ from: jsonObjectSchema }).optional(),
83
+ }),
84
+ sessionPermissions: z.array(z.string().min(1)),
85
+ page: z
86
+ .union([
87
+ z.strictObject({ output: jsonValueSchema }),
88
+ z.strictObject({ timedOut: z.literal(true) }),
89
+ z.strictObject({ unanswered: z.literal(true) }),
90
+ ])
91
+ .optional(),
92
+ });
93
+ const identityFields = {
94
+ operationId: z.string().min(1).max(256),
95
+ callId: z.string().min(1),
96
+ profileId: z.string().min(1),
97
+ toolId: z.string().min(1),
98
+ invocationHash: z.string().min(1),
99
+ invocation: savedToolInvocationSchema.optional(),
100
+ };
101
+ const pageGrant = z.strictObject({
102
+ grantId: z.string().min(1),
103
+ expiresAt: z.number().finite().positive(),
104
+ resultHash: z.string().optional(),
105
+ });
106
+ const operationSchema = z.discriminatedUnion('status', [
107
+ z.strictObject({
108
+ status: z.literal('ready'),
109
+ profileId: z.string().min(1),
110
+ pending: pendingSchema,
111
+ answer: acceptedAnswerSchema,
112
+ readyAt: z.number().finite().nonnegative(),
113
+ }),
114
+ z.strictObject({
115
+ ...identityFields,
116
+ status: z.literal('running'),
117
+ pageGrant: pageGrant.optional(),
118
+ claimToken: z.string().min(1),
119
+ startedAt: z.number().finite().nonnegative(),
120
+ }),
121
+ z.strictObject({
122
+ ...identityFields,
123
+ status: z.literal('settled'),
124
+ pageGrant: pageGrant.optional(),
125
+ outcome: toolOperationOutcomeSchema,
126
+ settledAt: z.number().finite().nonnegative(),
127
+ retainUntil: z.number().finite().nonnegative().optional(),
128
+ }),
129
+ z.strictObject({
130
+ ...identityFields,
131
+ status: z.literal('uncertain'),
132
+ pageGrant: pageGrant.optional(),
133
+ claimToken: z.string().min(1),
134
+ reason: z.enum(['execution_interrupted', 'settlement_write_failed', 'worker_lost']),
135
+ detectedAt: z.number().finite().nonnegative(),
136
+ }),
137
+ ]);
138
+ /** Stored authority is checked before the handler reads or changes it. */
139
+ export const theoremSessionStateSchema = z
140
+ .strictObject({
141
+ version: z.literal(3),
142
+ operations: z.record(z.string().min(1), operationSchema),
143
+ permissions: z.array(z.string().min(1)),
144
+ gates: z.record(z.string().min(1), pendingSchema),
145
+ providerCheckpoints: z.array(z.string().min(1)),
146
+ })
147
+ .superRefine((state, ctx) => {
148
+ for (const [operationId, operation] of Object.entries(state.operations)) {
149
+ const reference = operation.status === 'ready' ? operation.pending : operation;
150
+ if (reference.operationId !== operationId ||
151
+ (operation.status === 'ready' && operation.profileId !== operation.pending.profileId) ||
152
+ operationId in state.gates ||
153
+ (operation.status === 'settled' &&
154
+ (operation.outcome.event.tool.operationId !== operationId ||
155
+ operation.outcome.event.tool.callId !== operation.callId ||
156
+ operation.outcome.event.tool.name !== operation.toolId))) {
157
+ ctx.addIssue({ code: 'custom', path: ['operations', operationId] });
158
+ }
159
+ }
160
+ for (const [operationId, gate] of Object.entries(state.gates)) {
161
+ if (operationId !== gate.operationId)
162
+ ctx.addIssue({ code: 'custom', path: ['gates', operationId] });
163
+ }
164
+ });
12
165
  export function emptySessionState() {
13
- return { permissions: [], gates: {}, settled: {}, interactions: [] };
166
+ return {
167
+ version: 3,
168
+ operations: {},
169
+ permissions: [],
170
+ gates: {},
171
+ providerCheckpoints: [],
172
+ };
14
173
  }
15
174
  /** Drop gates (waiting or settled) older than `ttlMs`. */
16
175
  export function pruneGates(gates, now, ttlMs) {
17
- return Object.fromEntries(Object.entries(gates).filter(([, gate]) => !gateExpired(gate.createdAt, now, ttlMs)));
176
+ return Object.fromEntries(Object.entries(gates).filter(([, gate]) => gate.expiresAt !== undefined
177
+ ? now < gate.expiresAt
178
+ : !gateExpired(gate.createdAt, now, ttlMs)));
18
179
  }
19
- /** A per-session value kept in process memory: forgotten when idle, oldest evicted first. */
20
- export function createMemorySessionMap(options = {}) {
180
+ export function memoryStoreLimits(options) {
21
181
  const ttlMs = options.ttlMs ?? 24 * 60 * 60 * 1000;
22
182
  const maxSessions = options.maxSessions ?? 10_000;
183
+ if (!Number.isFinite(ttlMs) ||
184
+ ttlMs <= 0 ||
185
+ !Number.isSafeInteger(maxSessions) ||
186
+ maxSessions <= 0) {
187
+ throw new TheoremError('config', labelText('error.config'), {
188
+ cause: { code: 'session_store_options', ttlMs, maxSessions },
189
+ });
190
+ }
191
+ return { ttlMs, maxSessions };
192
+ }
193
+ /** A per-session value kept in process memory: forgotten when idle, oldest evicted first. */
194
+ export function createMemorySessionMap(options = {}) {
195
+ const { ttlMs, maxSessions } = memoryStoreLimits(options);
23
196
  const sessions = new Map();
24
197
  return {
25
198
  load(sessionId) {
@@ -45,5 +218,47 @@ export function createMemorySessionMap(options = {}) {
45
218
  };
46
219
  }
47
220
  export function createMemorySessionStore(options = {}) {
48
- return createMemorySessionMap(options);
221
+ const { ttlMs, maxSessions } = memoryStoreLimits(options);
222
+ const limits = resolveSessionLimits(options.sessionLimits);
223
+ const sessions = new Map();
224
+ const held = (entry) => Object.values(entry.snapshot.state.gates).some((gate) => gate.expiresAt === undefined || Date.now() < gate.expiresAt) ||
225
+ Object.values(entry.snapshot.state.operations).some((operation) => operation.status !== 'settled' ||
226
+ Date.now() <
227
+ (operation.retainUntil ?? operation.settledAt + DEFAULT_OPERATION_RETENTION_MS));
228
+ const current = (sessionId) => {
229
+ const entry = sessions.get(sessionId);
230
+ if (entry && !held(entry) && Date.now() - entry.touched >= ttlMs) {
231
+ sessions.delete(sessionId);
232
+ return undefined;
233
+ }
234
+ return entry?.snapshot;
235
+ };
236
+ return {
237
+ load(sessionId) {
238
+ return Promise.resolve(structuredClone(current(sessionId)));
239
+ },
240
+ compareAndSwap(sessionId, expectedRevision, next) {
241
+ const snapshot = current(sessionId);
242
+ if ((snapshot?.revision ?? null) !== expectedRevision)
243
+ return Promise.resolve({ written: false });
244
+ const state = theoremSessionStateSchema.parse(next);
245
+ checkSessionLimits(state, limits);
246
+ if (!snapshot && sessions.size >= maxSessions) {
247
+ const removable = [...sessions].find(([, entry]) => !held(entry));
248
+ if (!removable) {
249
+ throw new TheoremError('unavailable', labelText('error.unavailable'), {
250
+ cause: { code: 'session_store_capacity' },
251
+ });
252
+ }
253
+ sessions.delete(removable[0]);
254
+ }
255
+ const revision = crypto.randomUUID();
256
+ sessions.delete(sessionId);
257
+ sessions.set(sessionId, {
258
+ snapshot: { revision, state: structuredClone(state) },
259
+ touched: Date.now(),
260
+ });
261
+ return Promise.resolve({ written: true, revision });
262
+ },
263
+ };
49
264
  }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * The shape the decision and embedding handlers share: `GET <base>` answers the profile's
3
+ * interface, `POST <base>/<action>` answers `{ result }`, and a failure is worded with the
4
+ * profile's labels.
5
+ *
6
+ * @module
7
+ */
8
+ import { type ProfileDefinition } from '@theoremjs/agents';
9
+ import { type ClientTurnOptions } from '@theoremjs/agents/host';
10
+ export type SingleActionOptions = {
11
+ /** The last path segment the POST is sent to, such as `decide`. */
12
+ action: string;
13
+ /** What `GET <base>` answers, as `{ interface }`. */
14
+ iface: unknown;
15
+ labels: ProfileDefinition['labels'];
16
+ clientEvents?: ClientTurnOptions;
17
+ onError?: (err: unknown, ctx: {
18
+ request: Request;
19
+ }) => void;
20
+ };
21
+ export declare function singleActionHandler({ action, iface, labels, clientEvents, onError }: SingleActionOptions, run: (request: Request) => Promise<unknown>): (request: Request) => Promise<Response>;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * The shape the decision and embedding handlers share: `GET <base>` answers the profile's
3
+ * interface, `POST <base>/<action>` answers `{ result }`, and a failure is worded with the
4
+ * profile's labels.
5
+ *
6
+ * @module
7
+ */
8
+ import { labelText } from '@theoremjs/agents';
9
+ import { caughtStatus, clientError, HTTP_METHOD, } from '@theoremjs/agents/host';
10
+ import { jsonResponse } from "./json-response.js";
11
+ export function singleActionHandler({ action, iface, labels, clientEvents, onError }, run) {
12
+ const isAction = (request) => new URL(request.url).pathname.split('/').filter(Boolean).at(-1) === action;
13
+ return async (request) => {
14
+ try {
15
+ if (request.method === 'GET' && !isAction(request))
16
+ return jsonResponse(200, { interface: iface });
17
+ if (request.method !== 'POST' || !isAction(request)) {
18
+ return jsonResponse(HTTP_METHOD, {
19
+ error: labelText('error.request', {}, labels),
20
+ errorKind: 'request',
21
+ });
22
+ }
23
+ return jsonResponse(200, { result: await run(request) });
24
+ }
25
+ catch (err) {
26
+ onError?.(err, { request });
27
+ return jsonResponse(caughtStatus(err), clientError(err, labels, clientEvents));
28
+ }
29
+ };
30
+ }
@@ -14,7 +14,7 @@ const STEER_STAGES = new Set(['pre_turn', 'post_tool', 'before_end']);
14
14
  */
15
15
  export function steerUnitOf(steer) {
16
16
  const messages = steer.inject.filter((message) => message.role === 'user');
17
- // lexicon-exempt: internal diagnostic; the user reads the error kind's (or copy key's) wording
17
+ // labels-exempt: internal diagnostic; the user reads the error kind's (or copy key's) wording
18
18
  if (!messages.length)
19
19
  throw new TheoremError('request', 'inject must contain user messages');
20
20
  return { id: steer.id, messages };
@@ -0,0 +1,6 @@
1
+ import { type ToolExecutionCoordinator } from '@theoremjs/agents';
2
+ import type { TheoremSessionState } from './session-store.ts';
3
+ export declare function sessionToolCoordinator(store: {
4
+ mutate<T>(id: string, change: (state: TheoremSessionState) => T): Promise<T>;
5
+ }, sessionId: string): ToolExecutionCoordinator;
6
+ export declare function sameAcceptedAnswer(left: unknown, right: unknown): boolean;
@@ -0,0 +1,132 @@
1
+ import { labelText, TheoremError, } from '@theoremjs/agents';
2
+ function unavailable() {
3
+ throw new TheoremError('unavailable', labelText('error.unavailable'));
4
+ }
5
+ function sameOperation(record, identity) {
6
+ return (record.operationId === identity.operationId &&
7
+ record.callId === identity.callId &&
8
+ record.profileId === identity.profileId &&
9
+ record.toolId === identity.toolId &&
10
+ record.invocationHash === identity.invocationHash);
11
+ }
12
+ function checkedRecord(state, identity) {
13
+ if (identity.operationId === '__proto__')
14
+ unavailable();
15
+ const record = Object.hasOwn(state.operations, identity.operationId)
16
+ ? state.operations[identity.operationId]
17
+ : undefined;
18
+ if (record?.status === 'ready') {
19
+ if (record.pending.callId !== identity.callId ||
20
+ record.pending.profileId !== identity.profileId ||
21
+ record.pending.name !== identity.toolId ||
22
+ record.profileId !== identity.profileId)
23
+ unavailable();
24
+ }
25
+ else if (record && !sameOperation(record, identity))
26
+ unavailable();
27
+ return record;
28
+ }
29
+ function checkedClaim(state, claim) {
30
+ const record = checkedRecord(state, claim);
31
+ if (!record ||
32
+ (record.status !== 'running' && record.status !== 'uncertain') ||
33
+ record.claimToken !== claim.claimToken)
34
+ unavailable();
35
+ return record;
36
+ }
37
+ function existingResult(record) {
38
+ if (record?.status === 'settled')
39
+ return { kind: 'settled', outcome: record.outcome };
40
+ if (record?.status === 'running')
41
+ return { kind: 'running' };
42
+ if (record?.status === 'uncertain')
43
+ return { kind: 'uncertain' };
44
+ return undefined;
45
+ }
46
+ function saveOutcome(state, identity, outcome) {
47
+ const before = state.operations[identity.operationId];
48
+ const pageGrant = before && before.status !== 'ready' ? before.pageGrant : undefined;
49
+ const settledAt = Date.now();
50
+ const { operationId, callId, profileId, toolId, invocationHash, invocation } = identity;
51
+ state.operations[identity.operationId] = {
52
+ operationId,
53
+ callId,
54
+ profileId,
55
+ toolId,
56
+ invocationHash,
57
+ ...(invocation ? { invocation } : {}),
58
+ status: 'settled',
59
+ ...(pageGrant ? { pageGrant } : {}),
60
+ outcome,
61
+ settledAt,
62
+ };
63
+ }
64
+ export function sessionToolCoordinator(store, sessionId) {
65
+ return {
66
+ lookup(identity) {
67
+ return store.mutate(sessionId, (state) => {
68
+ const record = checkedRecord(state, identity);
69
+ return existingResult(record);
70
+ });
71
+ },
72
+ begin(identity) {
73
+ const claimToken = crypto.randomUUID();
74
+ const startedAt = Date.now();
75
+ return store.mutate(sessionId, (state) => {
76
+ const record = checkedRecord(state, identity);
77
+ const existing = existingResult(record);
78
+ if (existing)
79
+ return existing;
80
+ state.operations[identity.operationId] = {
81
+ ...identity,
82
+ status: 'running',
83
+ claimToken,
84
+ startedAt,
85
+ };
86
+ return { kind: 'acquired', claimToken };
87
+ });
88
+ },
89
+ settle(claim, outcome) {
90
+ return store.mutate(sessionId, (state) => {
91
+ const record = checkedClaim(state, claim);
92
+ saveOutcome(state, { ...claim, invocation: record.invocation }, outcome);
93
+ });
94
+ },
95
+ settleNotStarted(identity, outcome) {
96
+ return store.mutate(sessionId, (state) => {
97
+ const record = checkedRecord(state, identity);
98
+ if (record && record.status !== 'ready')
99
+ unavailable();
100
+ if (outcome.effects !== 'not_started')
101
+ unavailable();
102
+ saveOutcome(state, identity, outcome);
103
+ });
104
+ },
105
+ markUncertain(claim, reason) {
106
+ return store.mutate(sessionId, (state) => {
107
+ const record = checkedClaim(state, claim);
108
+ state.operations[claim.operationId] = {
109
+ ...claim,
110
+ invocation: record.invocation,
111
+ ...(record.pageGrant ? { pageGrant: record.pageGrant } : {}),
112
+ status: 'uncertain',
113
+ reason,
114
+ detectedAt: Date.now(),
115
+ };
116
+ });
117
+ },
118
+ };
119
+ }
120
+ export function sameAcceptedAnswer(left, right) {
121
+ const canonical = (value) => {
122
+ if (Array.isArray(value))
123
+ return value.map(canonical);
124
+ if (value && typeof value === 'object')
125
+ return Object.fromEntries(Object.entries(value)
126
+ .filter(([, item]) => item !== undefined)
127
+ .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
128
+ .map(([key, item]) => [key, canonical(item)]));
129
+ return value;
130
+ };
131
+ return JSON.stringify(canonical(left)) === JSON.stringify(canonical(right));
132
+ }
@@ -0,0 +1,10 @@
1
+ import type { SessionRequest, TurnInput } from '@theoremjs/agents';
2
+ import type { LiveOpenMessage } from '../client/live-messages.ts';
3
+ import type { TheoremTurnInput } from '../client/transport.ts';
4
+ /** A turn input off the wire as the kernel takes it: the page's context is the client's. */
5
+ export declare function kernelTurnInput(input: TheoremTurnInput, server?: unknown): TurnInput;
6
+ /**
7
+ * What a live call's first message asks of the session, as `runSession` takes
8
+ * it: the page's context is the client's, and `server` is the host's own.
9
+ */
10
+ export declare function liveSessionOpen(open: LiveOpenMessage, server?: unknown): Pick<SessionRequest, 'slots' | 'context' | 'providerState' | 'awayMs' | 'history'>;
@@ -0,0 +1,29 @@
1
+ /** The page's context as the client's and the host's as the server's; nothing when neither gave any. */
2
+ function turnContext(client, server) {
3
+ const context = {
4
+ ...(client === undefined ? {} : { client }),
5
+ ...(server === undefined ? {} : { server }),
6
+ };
7
+ return Object.keys(context).length > 0 ? context : undefined;
8
+ }
9
+ /** A turn input off the wire as the kernel takes it: the page's context is the client's. */
10
+ export function kernelTurnInput(input, server) {
11
+ const { context: client, ...rest } = input;
12
+ const context = turnContext(client, server);
13
+ return context ? { ...rest, context } : rest;
14
+ }
15
+ /**
16
+ * What a live call's first message asks of the session, as `runSession` takes
17
+ * it: the page's context is the client's, and `server` is the host's own.
18
+ */
19
+ export function liveSessionOpen(open, server) {
20
+ const context = turnContext(open.context, server);
21
+ return {
22
+ ...(open.history ? { history: open.history } : {}),
23
+ ...(open.slots ? { slots: open.slots } : {}),
24
+ ...(context ? { context } : {}),
25
+ ...(open.resume
26
+ ? { providerState: open.resume.providerState, awayMs: open.resume.awayMs }
27
+ : {}),
28
+ };
29
+ }