theorum 0.1.2 → 0.1.4

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 (150) hide show
  1. package/README.md +83 -27
  2. package/deno.json +57 -0
  3. package/deno.lock +469 -0
  4. package/docs/AGENT_PROFILE_CONTRACT.md +45 -18
  5. package/docs/CLI_SPEC.md +2 -2
  6. package/docs/SECRETS.md +27 -22
  7. package/mod.ts +95 -0
  8. package/package.json +37 -29
  9. package/src/cli/commands/profile.ts +57 -0
  10. package/src/cli/commands/run.ts +66 -0
  11. package/src/cli/commands/test.ts +195 -0
  12. package/src/cli/index.ts +161 -0
  13. package/src/cli/matrix/fixtures.ts +96 -0
  14. package/src/cli/matrix/synthesizer.ts +240 -0
  15. package/src/guardrails/error.ts +142 -0
  16. package/src/guardrails/injection.ts +254 -0
  17. package/src/guardrails/mod.ts +40 -0
  18. package/src/guardrails/quota.ts +87 -0
  19. package/src/guardrails/sanitize.ts +165 -0
  20. package/src/guardrails/sensitive.ts +96 -0
  21. package/src/host/mint-trace.ts +54 -0
  22. package/src/host/mod.ts +20 -0
  23. package/src/host/reply.ts +32 -0
  24. package/src/kernel/engine/assert.ts +29 -0
  25. package/src/kernel/engine/boundary.ts +75 -0
  26. package/src/kernel/engine/delta.ts +440 -0
  27. package/src/kernel/engine/hash.ts +11 -0
  28. package/src/kernel/engine/record.ts +8 -0
  29. package/src/kernel/engine/repair.ts +56 -0
  30. package/src/kernel/engine/runner/gates.ts +338 -0
  31. package/src/kernel/engine/runner/mod.ts +135 -0
  32. package/src/kernel/engine/runner/state.ts +31 -0
  33. package/src/kernel/engine/runner/steps.ts +183 -0
  34. package/src/kernel/engine/runner/stream.ts +83 -0
  35. package/src/kernel/engine/runner/tokens.ts +47 -0
  36. package/src/kernel/engine/runner/tools.ts +266 -0
  37. package/src/kernel/engine/runner.ts +7 -0
  38. package/src/kernel/engine/tree.ts +18 -0
  39. package/src/kernel/mod.ts +39 -0
  40. package/src/kernel/registry/attachments.ts +195 -0
  41. package/src/kernel/registry/catalog.ts +181 -0
  42. package/src/kernel/registry/ingress.ts +223 -0
  43. package/src/kernel/registry/profiles.ts +165 -0
  44. package/src/kernel/registry/provider-request.ts +31 -0
  45. package/src/kernel/registry/resolve.ts +290 -0
  46. package/src/kernel/registry/schemas.ts +29 -0
  47. package/src/kernel/registry/tools.ts +45 -0
  48. package/src/kernel/registry/vault.ts +26 -0
  49. package/src/kernel/types.ts +576 -0
  50. package/{esm/src/observability/mod.js → src/observability/mod.ts} +11 -2
  51. package/src/observability/spans.ts +70 -0
  52. package/src/observability/trace-attach.ts +96 -0
  53. package/src/observability/trace-record.ts +254 -0
  54. package/src/observability/trace-usage.ts +36 -0
  55. package/src/observability/trace.ts +143 -0
  56. package/src/presets/google.ts +147 -0
  57. package/src/presets/mod.ts +24 -0
  58. package/src/providers/create-provider.ts +64 -0
  59. package/src/providers/gemini-tape.ts +59 -0
  60. package/src/providers/google-tap.ts +57 -0
  61. package/src/providers/interactions.ts +182 -0
  62. package/src/providers/keys.ts +182 -0
  63. package/src/providers/mod.ts +12 -0
  64. package/src/providers/openrouter-mod.ts +16 -0
  65. package/src/providers/openrouter-payload.ts +232 -0
  66. package/src/providers/openrouter.ts +758 -0
  67. package/src/providers/pcm.ts +37 -0
  68. package/src/providers/provider.ts +206 -0
  69. package/src/providers/speech.ts +180 -0
  70. package/src/providers/sse.ts +59 -0
  71. package/esm/_dnt.polyfills.d.ts +0 -11
  72. package/esm/_dnt.polyfills.js +0 -15
  73. package/esm/_dnt.shims.d.ts +0 -5
  74. package/esm/_dnt.shims.js +0 -61
  75. package/esm/mod.d.ts +0 -37
  76. package/esm/mod.js +0 -35
  77. package/esm/package.json +0 -3
  78. package/esm/src/guardrails/error.d.ts +0 -35
  79. package/esm/src/guardrails/error.js +0 -116
  80. package/esm/src/guardrails/injection.d.ts +0 -12
  81. package/esm/src/guardrails/injection.js +0 -220
  82. package/esm/src/guardrails/keys.d.ts +0 -12
  83. package/esm/src/guardrails/keys.js +0 -132
  84. package/esm/src/guardrails/mod.d.ts +0 -14
  85. package/esm/src/guardrails/mod.js +0 -14
  86. package/esm/src/guardrails/sanitize.d.ts +0 -22
  87. package/esm/src/guardrails/sanitize.js +0 -133
  88. package/esm/src/guardrails/sensitive.d.ts +0 -12
  89. package/esm/src/guardrails/sensitive.js +0 -88
  90. package/esm/src/kernel/engine/boundary.d.ts +0 -10
  91. package/esm/src/kernel/engine/boundary.js +0 -55
  92. package/esm/src/kernel/engine/delta.d.ts +0 -8
  93. package/esm/src/kernel/engine/delta.js +0 -362
  94. package/esm/src/kernel/engine/hash.d.ts +0 -1
  95. package/esm/src/kernel/engine/hash.js +0 -9
  96. package/esm/src/kernel/engine/record.d.ts +0 -2
  97. package/esm/src/kernel/engine/record.js +0 -7
  98. package/esm/src/kernel/engine/repair.d.ts +0 -9
  99. package/esm/src/kernel/engine/repair.js +0 -38
  100. package/esm/src/kernel/engine/runner.d.ts +0 -14
  101. package/esm/src/kernel/engine/runner.js +0 -731
  102. package/esm/src/kernel/engine/tree.d.ts +0 -2
  103. package/esm/src/kernel/engine/tree.js +0 -17
  104. package/esm/src/kernel/mod.d.ts +0 -16
  105. package/esm/src/kernel/mod.js +0 -15
  106. package/esm/src/kernel/registry/catalog.d.ts +0 -24
  107. package/esm/src/kernel/registry/catalog.js +0 -213
  108. package/esm/src/kernel/registry/profiles.d.ts +0 -36
  109. package/esm/src/kernel/registry/profiles.js +0 -111
  110. package/esm/src/kernel/registry/resolve.d.ts +0 -20
  111. package/esm/src/kernel/registry/resolve.js +0 -235
  112. package/esm/src/kernel/registry/schemas.d.ts +0 -14
  113. package/esm/src/kernel/registry/schemas.js +0 -23
  114. package/esm/src/kernel/registry/tools.d.ts +0 -12
  115. package/esm/src/kernel/registry/tools.js +0 -36
  116. package/esm/src/kernel/types.d.ts +0 -497
  117. package/esm/src/kernel/types.js +0 -10
  118. package/esm/src/observability/mod.d.ts +0 -12
  119. package/esm/src/observability/spans.d.ts +0 -16
  120. package/esm/src/observability/spans.js +0 -56
  121. package/esm/src/observability/trace-attach.d.ts +0 -16
  122. package/esm/src/observability/trace-attach.js +0 -81
  123. package/esm/src/observability/trace-record.d.ts +0 -112
  124. package/esm/src/observability/trace-record.js +0 -140
  125. package/esm/src/observability/trace-usage.d.ts +0 -3
  126. package/esm/src/observability/trace-usage.js +0 -32
  127. package/esm/src/observability/trace.d.ts +0 -23
  128. package/esm/src/observability/trace.js +0 -121
  129. package/esm/src/providers/attachments.d.ts +0 -17
  130. package/esm/src/providers/attachments.js +0 -156
  131. package/esm/src/providers/gemini-tape.d.ts +0 -3
  132. package/esm/src/providers/gemini-tape.js +0 -46
  133. package/esm/src/providers/google-tap.d.ts +0 -3
  134. package/esm/src/providers/google-tap.js +0 -48
  135. package/esm/src/providers/interactions.d.ts +0 -5
  136. package/esm/src/providers/interactions.js +0 -127
  137. package/esm/src/providers/media.d.ts +0 -5
  138. package/esm/src/providers/media.js +0 -125
  139. package/esm/src/providers/mod.d.ts +0 -15
  140. package/esm/src/providers/mod.js +0 -13
  141. package/esm/src/providers/openrouter-payload.d.ts +0 -24
  142. package/esm/src/providers/openrouter-payload.js +0 -177
  143. package/esm/src/providers/openrouter.d.ts +0 -17
  144. package/esm/src/providers/openrouter.js +0 -332
  145. package/esm/src/providers/provider.d.ts +0 -13
  146. package/esm/src/providers/provider.js +0 -123
  147. package/esm/src/providers/sse.d.ts +0 -7
  148. package/esm/src/providers/sse.js +0 -53
  149. package/esm/src/providers/tts.d.ts +0 -24
  150. package/esm/src/providers/tts.js +0 -144
@@ -0,0 +1,83 @@
1
+ import { publicError } from '../../../guardrails/error.ts';
2
+ import { providerCompleteRequest } from '../../registry/provider-request.ts';
3
+ import type { ModelProvider, Profile, ResolvedGeneration, TurnEvent } from '../../types.ts';
4
+ import { eventHasCanary, redactCanary } from '../boundary.ts';
5
+ import { dispatchModelTool } from './tools.ts';
6
+
7
+ function systemFromProfile(profile: Profile, role: string): string {
8
+ const { identity } = profile;
9
+ const { systemByRole, system } = identity;
10
+ if (systemByRole) {
11
+ const byRole = systemByRole[role];
12
+ if (byRole) {
13
+ return byRole;
14
+ }
15
+ }
16
+ if (system) {
17
+ return system;
18
+ }
19
+ return '';
20
+ }
21
+
22
+ function* interceptProviderTool(
23
+ event: TurnEvent,
24
+ profile: Profile,
25
+ generation: ResolvedGeneration,
26
+ ): Generator<TurnEvent> {
27
+ const tool = event.tool;
28
+ if (!tool) {
29
+ return;
30
+ }
31
+ const isDynamic = generation.dynamicTools?.some((d) => d.name === tool.name);
32
+ if (isDynamic) {
33
+ yield event;
34
+ } else {
35
+ for (const item of dispatchModelTool(profile, event, generation.custom)) {
36
+ yield item;
37
+ }
38
+ }
39
+ }
40
+
41
+ function shouldSkipStreamEvent(event: TurnEvent, profile: Profile): boolean {
42
+ return event.type === 'thought' && profile.outputs.streaming?.streamThoughts === false;
43
+ }
44
+
45
+ function* processNormalEvent(
46
+ event: TurnEvent,
47
+ profile: Profile,
48
+ generation: ResolvedGeneration,
49
+ ): Generator<TurnEvent> {
50
+ if (event.type === 'tool') {
51
+ yield* interceptProviderTool(event, profile, generation);
52
+ } else if (event.type === 'error') {
53
+ yield { type: 'error', error: publicError(event.error) };
54
+ } else {
55
+ yield event;
56
+ }
57
+ }
58
+
59
+ async function* yieldProviderEvents(args: {
60
+ profile: Profile;
61
+ generation: ResolvedGeneration;
62
+ system: string;
63
+ provider: ModelProvider;
64
+ gemini: Record<string, unknown>[];
65
+ }): AsyncGenerator<TurnEvent> {
66
+ const { profile, generation, system, provider, gemini } = args;
67
+ const { canary } = generation;
68
+ for await (const event of provider.complete({
69
+ ...providerCompleteRequest(generation, system),
70
+ tapGemini: (row) => {
71
+ gemini.push(row);
72
+ },
73
+ })) {
74
+ if (canary && eventHasCanary(event, canary)) {
75
+ yield redactCanary(event, canary);
76
+ yield { type: 'error', error: publicError('canary leaked') };
77
+ return;
78
+ }
79
+ yield* processNormalEvent(event, profile, generation);
80
+ }
81
+ }
82
+
83
+ export { shouldSkipStreamEvent, systemFromProfile, yieldProviderEvents };
@@ -0,0 +1,47 @@
1
+ import type { TurnEvent, TurnRequest } from '../../types.ts';
2
+
3
+ function calculateInputChars(safe: TurnRequest, system: string): number {
4
+ const input = safe.input ?? {};
5
+ return (
6
+ (input.text?.length ?? 0) +
7
+ (input.repair?.previousOutput?.length ?? 0) +
8
+ (system?.length ?? 800)
9
+ );
10
+ }
11
+
12
+ function calculateOutputChars(events: TurnEvent[]): { outputChars: number; thinkingChars: number } {
13
+ let outputChars = 0;
14
+ let thinkingChars = 0;
15
+ for (const e of events) {
16
+ if (e.type === 'text' && e.text) outputChars += e.text.length;
17
+ if (e.type === 'structured' && e.structured) {
18
+ outputChars += JSON.stringify(e.structured).length;
19
+ }
20
+ if (e.type === 'thought' && e.text) thinkingChars += e.text.length;
21
+ }
22
+ return { outputChars, thinkingChars };
23
+ }
24
+
25
+ function* calculateFallbackTokens(
26
+ safe: TurnRequest,
27
+ system: string,
28
+ events: TurnEvent[],
29
+ ): Generator<TurnEvent> {
30
+ const inputChars = calculateInputChars(safe, system);
31
+ const { outputChars, thinkingChars } = calculateOutputChars(events);
32
+ const inputTokens = Math.max(1, Math.round(inputChars / 4));
33
+ const outputTokens = Math.max(1, Math.round(outputChars / 4));
34
+ const thinkingTokens = thinkingChars ? Math.round(thinkingChars / 4) : 0;
35
+ yield {
36
+ type: 'tokens',
37
+ tokens: {
38
+ input: inputTokens,
39
+ output: outputTokens,
40
+ thinking: thinkingTokens,
41
+ toolUse: 0,
42
+ total: inputTokens + outputTokens + thinkingTokens,
43
+ },
44
+ };
45
+ }
46
+
47
+ export { calculateFallbackTokens };
@@ -0,0 +1,266 @@
1
+ import { publicError } from '../../../guardrails/error.ts';
2
+ import { CATALOG } from '../../registry/catalog.ts';
3
+ import { executeTool } from '../../registry/tools.ts';
4
+ import type {
5
+ CustomToolId,
6
+ DynamicToolDeclaration,
7
+ Profile,
8
+ ResolvedGeneration,
9
+ ToolEnvelope,
10
+ ToolId,
11
+ TurnEvent,
12
+ TurnRequest,
13
+ } from '../../types.ts';
14
+
15
+ function formatToolFinding(res: ToolEnvelope): string {
16
+ if (res.finding) {
17
+ return res.finding;
18
+ }
19
+ if (res.data) {
20
+ return JSON.stringify(res.data);
21
+ }
22
+ return 'ok';
23
+ }
24
+
25
+ function* invokeFromUi(profile: Profile, req: TurnRequest): Generator<TurnEvent> {
26
+ const invoke = req.toolInvoke;
27
+ if (!invoke) {
28
+ return;
29
+ }
30
+ try {
31
+ const result = executeTool(profile, invoke.name, invoke.arguments);
32
+ yield {
33
+ type: 'tool',
34
+ tool: { name: invoke.name, arguments: invoke.arguments, result },
35
+ };
36
+ if (result.status === 'error') {
37
+ yield { type: 'error', error: publicError(formatToolFinding(result)) };
38
+ return;
39
+ }
40
+ yield { type: 'done' };
41
+ } catch (err) {
42
+ yield { type: 'error', error: publicError(err) };
43
+ yield { type: 'done' };
44
+ }
45
+ }
46
+
47
+ function executeCustomModelTool(
48
+ profile: Profile,
49
+ name: CustomToolId,
50
+ args: Record<string, unknown>,
51
+ ): TurnEvent[] {
52
+ try {
53
+ const result = executeTool(profile, name, args);
54
+ const events: TurnEvent[] = [
55
+ {
56
+ type: 'tool',
57
+ tool: { name, arguments: args, result },
58
+ },
59
+ ];
60
+ if (result.status === 'error') {
61
+ events.push({ type: 'error', error: publicError(formatToolFinding(result)) });
62
+ }
63
+ return events;
64
+ } catch (err) {
65
+ return [{ type: 'error', error: publicError(err) }];
66
+ }
67
+ }
68
+
69
+ function dispatchModelTool(profile: Profile, event: TurnEvent, gated: CustomToolId[]): TurnEvent[] {
70
+ const { tool } = event;
71
+ if (!tool) {
72
+ return [];
73
+ }
74
+ const name = tool.name as ToolId;
75
+ if (CATALOG.tools[name]?.kind === 'builtin') {
76
+ return [event];
77
+ }
78
+ if (!gated.includes(name as CustomToolId)) {
79
+ return [
80
+ {
81
+ type: 'error',
82
+ error: publicError(`Tool '${name}' is not gated on this turn`),
83
+ },
84
+ ];
85
+ }
86
+ const args = tool.arguments ?? {};
87
+ return executeCustomModelTool(profile, name as CustomToolId, args);
88
+ }
89
+
90
+ function findDynamicDeclaration(
91
+ tools: DynamicToolDeclaration[] | undefined,
92
+ name: string,
93
+ ): DynamicToolDeclaration | undefined {
94
+ return tools?.find((t) => t.name === name);
95
+ }
96
+
97
+ async function checkDynamicAuthorization(
98
+ decl: DynamicToolDeclaration,
99
+ args: Record<string, unknown>,
100
+ profile: Profile,
101
+ sessionPermissions?: string[],
102
+ ): Promise<ToolEnvelope | null> {
103
+ if (!decl.canExecute) {
104
+ return null;
105
+ }
106
+ try {
107
+ const decision = await decl.canExecute({
108
+ args,
109
+ profile,
110
+ sessionPermissions,
111
+ });
112
+ if (typeof decision === 'boolean' && !decision) {
113
+ return {
114
+ status: 'error',
115
+ finding: `Tool '${decl.name}' execution not authorized.`,
116
+ };
117
+ }
118
+ if (typeof decision === 'object' && decision !== null) {
119
+ return decision;
120
+ }
121
+ return null;
122
+ } catch (err) {
123
+ const msg = err instanceof Error ? err.message : String(err);
124
+ return {
125
+ status: 'error',
126
+ finding: `Authorization error for '${decl.name}': ${msg}`,
127
+ };
128
+ }
129
+ }
130
+
131
+ function checkDynamicPermissionTier(
132
+ decl: DynamicToolDeclaration,
133
+ args: Record<string, unknown>,
134
+ sessionPermissions?: string[],
135
+ ): ToolEnvelope | null {
136
+ if (decl.permissionTier === 'session_consent' || decl.permissionTier === 'always_confirm') {
137
+ const isGranted = sessionPermissions?.includes(decl.name) || sessionPermissions?.includes('*');
138
+ if (!isGranted) {
139
+ return {
140
+ status: 'pause',
141
+ finding: `Tool '${decl.name}' requires ${decl.permissionTier} authorization.`,
142
+ data: { tool: decl.name, permissionTier: decl.permissionTier, args },
143
+ };
144
+ }
145
+ }
146
+ return null;
147
+ }
148
+
149
+ async function runDynamicHandler(
150
+ decl: DynamicToolDeclaration,
151
+ args: Record<string, unknown>,
152
+ ): Promise<ToolEnvelope> {
153
+ if (!decl.handler) {
154
+ return {
155
+ status: 'ok',
156
+ finding: `${decl.name} accepted (no handler)`,
157
+ data: args,
158
+ };
159
+ }
160
+ try {
161
+ return await decl.handler(args);
162
+ } catch (err) {
163
+ const msg = err instanceof Error ? err.message : String(err);
164
+ return { status: 'error', finding: msg };
165
+ }
166
+ }
167
+
168
+ async function executeDynamicTool(
169
+ decl: DynamicToolDeclaration,
170
+ args: Record<string, unknown>,
171
+ profile: Profile,
172
+ sessionPermissions?: string[],
173
+ ): Promise<ToolEnvelope> {
174
+ const authEnvelope = await checkDynamicAuthorization(decl, args, profile, sessionPermissions);
175
+ if (authEnvelope) {
176
+ return authEnvelope;
177
+ }
178
+
179
+ const permissionEnvelope = checkDynamicPermissionTier(decl, args, sessionPermissions);
180
+ if (permissionEnvelope) {
181
+ return permissionEnvelope;
182
+ }
183
+
184
+ return runDynamicHandler(decl, args);
185
+ }
186
+
187
+ function mergeDynamicTools(
188
+ current: DynamicToolDeclaration[] | undefined,
189
+ loaded: DynamicToolDeclaration[],
190
+ ): DynamicToolDeclaration[] {
191
+ const merged = [...(current ?? [])];
192
+ for (const tool of loaded) {
193
+ const existing = merged.findIndex((item) => item.name === tool.name);
194
+ if (existing >= 0) {
195
+ merged[existing] = tool;
196
+ } else {
197
+ merged.push(tool);
198
+ }
199
+ }
200
+ return merged;
201
+ }
202
+
203
+ async function executeDynamicToolLoader(args: {
204
+ decl: DynamicToolDeclaration;
205
+ toolArgs: Record<string, unknown>;
206
+ profile: Profile;
207
+ generation: ResolvedGeneration;
208
+ }): Promise<ToolEnvelope> {
209
+ const { decl, toolArgs, profile, generation } = args;
210
+ const loader = generation.dynamicToolLoader;
211
+ if (!loader) {
212
+ return {
213
+ status: 'error',
214
+ finding: `Tool '${decl.name}' is marked as a loader but no loader is configured.`,
215
+ };
216
+ }
217
+ const loaded = await loader({
218
+ name: decl.name,
219
+ args: toolArgs,
220
+ profile,
221
+ currentTools: generation.dynamicTools ?? [],
222
+ sessionPermissions: generation.sessionPermissions,
223
+ });
224
+ generation.dynamicTools = mergeDynamicTools(generation.dynamicTools, loaded);
225
+ return {
226
+ status: 'ok',
227
+ finding: `Loaded ${String(loaded.length)} dynamic tool schema(s).`,
228
+ data: { loadedTools: loaded.map((tool) => tool.name) },
229
+ };
230
+ }
231
+
232
+ function isActionableDynamicDeclaration(
233
+ decl: DynamicToolDeclaration | undefined,
234
+ ): decl is DynamicToolDeclaration {
235
+ return Boolean(
236
+ decl?.handler || decl?.canExecute || decl?.permissionTier || decl?.loadsDynamicTools,
237
+ );
238
+ }
239
+
240
+ async function executeDynamicDeclaration(args: {
241
+ decl: DynamicToolDeclaration;
242
+ toolArgs: Record<string, unknown>;
243
+ profile: Profile;
244
+ generation: ResolvedGeneration;
245
+ }): Promise<ToolEnvelope> {
246
+ const { decl, toolArgs, profile, generation } = args;
247
+ const res = await executeDynamicTool(decl, toolArgs, profile, generation.sessionPermissions);
248
+ if (res.status === 'ok' && decl.loadsDynamicTools) {
249
+ return await executeDynamicToolLoader({
250
+ decl,
251
+ toolArgs,
252
+ profile,
253
+ generation,
254
+ });
255
+ }
256
+ return res;
257
+ }
258
+
259
+ export {
260
+ dispatchModelTool,
261
+ executeDynamicDeclaration,
262
+ findDynamicDeclaration,
263
+ formatToolFinding,
264
+ invokeFromUi,
265
+ isActionableDynamicDeclaration,
266
+ };
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Deterministic turn runner for THEORUM.
3
+ *
4
+ * @module
5
+ */
6
+
7
+ export { runTurn } from './runner/mod.ts';
@@ -0,0 +1,18 @@
1
+ function mapStrings(value: unknown, map: (text: string) => string): unknown {
2
+ if (typeof value === 'string') {
3
+ return map(value);
4
+ }
5
+ if (Array.isArray(value)) {
6
+ return value.map((item) => mapStrings(item, map));
7
+ }
8
+ if (value && typeof value === 'object') {
9
+ const out: Record<string, unknown> = {};
10
+ for (const [key, nested] of Object.entries(value)) {
11
+ out[key] = mapStrings(nested, map);
12
+ }
13
+ return out;
14
+ }
15
+ return value;
16
+ }
17
+
18
+ export { mapStrings };
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Profile, turn, tool, provider, egress, and event contracts for THEORUM.
3
+ *
4
+ * This entrypoint is type-first. Host applications import from here when they
5
+ * want to declare profiles, tool schemas, provider adapters, turn requests, or
6
+ * trace-safe event handlers without importing provider implementations.
7
+ *
8
+ * @module
9
+ */
10
+
11
+ export { runTurn } from './engine/runner.ts';
12
+ export {
13
+ CATALOG,
14
+ clampThinkingLevel,
15
+ clampThinkingLevelForApiId,
16
+ geminiKindForMime,
17
+ getTool,
18
+ listBuiltinIds,
19
+ mimeAllowed,
20
+ mimeEssence,
21
+ modelEntryByApiId,
22
+ registerTools,
23
+ requireModelSpec,
24
+ resetTools,
25
+ } from './registry/catalog.ts';
26
+ export type { ProfileDefinition } from './registry/profiles.ts';
27
+ export {
28
+ clearProfiles,
29
+ defineProfile,
30
+ getProfile,
31
+ hasProfile,
32
+ listProfiles,
33
+ registerProfile,
34
+ registerProfiles,
35
+ } from './registry/profiles.ts';
36
+ export { projectProfile, resolveTurn } from './registry/resolve.ts';
37
+ export { getStructured, registerStructured } from './registry/schemas.ts';
38
+ export { executeTool } from './registry/tools.ts';
39
+ export type * from './types.ts';
@@ -0,0 +1,195 @@
1
+ import { TheorumError } from '../../guardrails/error.ts';
2
+ import { injectionSpans } from '../../guardrails/injection.ts';
3
+ import { sensitiveSpans } from '../../guardrails/sensitive.ts';
4
+ import { applySpans } from '../../observability/spans.ts';
5
+ import type { MediaLimits, MimeInputs, Profile, TurnBlob } from '../types.ts';
6
+ import { getProfile } from './profiles.ts';
7
+
8
+ const B64_PAD = 2;
9
+ const B64_WORD = 4;
10
+ const B64_TRIPLET = 3;
11
+
12
+ const CSV_FORMULA = /(^|,)(\s*)("?)(?:([=@])|([+-])(?![0-9."]))/gm;
13
+ const B64_BODY = /^[A-Za-z0-9+/]*={0,2}$/;
14
+ const TEXT_MIMES = new Set(['text/csv', 'text/plain', 'text/markdown']);
15
+ const BYTES_PER_KIB = 1024;
16
+
17
+ function formatMb(bytes: number): string {
18
+ const mb = bytes / (BYTES_PER_KIB * BYTES_PER_KIB);
19
+ return Number.isInteger(mb) ? `${String(mb)} MB` : `${mb.toFixed(1)} MB`;
20
+ }
21
+
22
+ function tooManyFilesMessage(maxFiles: number): string {
23
+ return maxFiles === 1
24
+ ? 'Only 1 file per message.'
25
+ : `Only ${String(maxFiles)} files per message.`;
26
+ }
27
+
28
+ function fileTooLargeMessage(maxBytes: number): string {
29
+ return `Each file must be ${formatMb(maxBytes)} or smaller.`;
30
+ }
31
+
32
+ function turnTooLargeMessage(maxTurnBytes: number): string {
33
+ return `Those files together are too large for one message (${formatMb(maxTurnBytes)} max).`;
34
+ }
35
+
36
+ function mediaLimits(inputs: MimeInputs): MediaLimits | undefined {
37
+ const { maxFiles, maxBytes, maxTurnBytes, limitsByMime } = inputs;
38
+ if (maxFiles && maxBytes && maxTurnBytes) {
39
+ return { maxFiles, maxBytes, maxTurnBytes, limitsByMime };
40
+ }
41
+ return undefined;
42
+ }
43
+
44
+ function maxBytesForMime(mimeType: string, limits: MediaLimits): number {
45
+ if (limits.limitsByMime) {
46
+ const cleanMime = mimeType.split(';')[0]?.trim().toLowerCase() ?? '';
47
+ if (limits.limitsByMime[cleanMime]) {
48
+ return limits.limitsByMime[cleanMime];
49
+ }
50
+ const [category] = cleanMime.split('/');
51
+ const wildCard = `${category}/*`;
52
+ if (limits.limitsByMime[wildCard]) {
53
+ return limits.limitsByMime[wildCard];
54
+ }
55
+ }
56
+ return limits.maxBytes;
57
+ }
58
+
59
+ function requireMediaLimits(profile: Profile): MediaLimits {
60
+ const limits = mediaLimits(profile.inputs);
61
+ if (!limits) {
62
+ throw new TheorumError(`Profile ${profile.id} must set maxFiles, maxBytes, and maxTurnBytes`);
63
+ }
64
+ return limits;
65
+ }
66
+
67
+ function b64DecodedLen(data: string): number {
68
+ let pad = 0;
69
+ if (data.endsWith('==')) {
70
+ pad = B64_PAD;
71
+ } else if (data.endsWith('=')) {
72
+ pad = 1;
73
+ }
74
+ return Math.floor((data.length * B64_TRIPLET) / B64_WORD) - pad;
75
+ }
76
+
77
+ function decodeB64(data: string): Uint8Array {
78
+ const binary = atob(data);
79
+ const bytes = new Uint8Array(binary.length);
80
+ for (let i = 0; i < binary.length; i += 1) {
81
+ bytes[i] = binary.charCodeAt(i);
82
+ }
83
+ return bytes;
84
+ }
85
+
86
+ function encodeB64(bytes: Uint8Array): string {
87
+ let binary = '';
88
+ for (const byte of bytes) {
89
+ binary += String.fromCharCode(byte);
90
+ }
91
+ return btoa(binary);
92
+ }
93
+
94
+ function decodeText(bytes: Uint8Array): string {
95
+ try {
96
+ return new TextDecoder('utf-8', { fatal: true }).decode(bytes);
97
+ } catch {
98
+ return new TextDecoder('latin1').decode(bytes);
99
+ }
100
+ }
101
+
102
+ function sanitizeCsvText(text: string): string {
103
+ return text.replace(CSV_FORMULA, (_full, ...groups: string[]) => {
104
+ const [a, b, c, d, e] = groups;
105
+ return `${a}${b}${c}'${d ?? ''}${e ?? ''}`;
106
+ });
107
+ }
108
+
109
+ function sanitizeTextBytes(mime: string, bytes: Uint8Array): Uint8Array {
110
+ let text = decodeText(bytes);
111
+ if (mime === 'text/csv') {
112
+ text = sanitizeCsvText(text);
113
+ }
114
+ return new TextEncoder().encode(
115
+ applySpans(text, [...injectionSpans(text), ...sensitiveSpans(text)]),
116
+ );
117
+ }
118
+
119
+ function assertAttachmentLimits(blobs: TurnBlob[], limits: MediaLimits): void {
120
+ if (blobs.length > limits.maxFiles) {
121
+ throw new TheorumError(tooManyFilesMessage(limits.maxFiles));
122
+ }
123
+ let total = 0;
124
+ for (const blob of blobs) {
125
+ const { data, mimeType } = blob;
126
+ if (!B64_BODY.test(data)) {
127
+ throw new TheorumError('attachment data must be base64');
128
+ }
129
+ const size = b64DecodedLen(data);
130
+ const maxAllowed = maxBytesForMime(mimeType, limits);
131
+ if (size > maxAllowed) {
132
+ throw new TheorumError(fileTooLargeMessage(maxAllowed));
133
+ }
134
+ total += size;
135
+ }
136
+ if (total > limits.maxTurnBytes) {
137
+ throw new TheorumError(turnTooLargeMessage(limits.maxTurnBytes));
138
+ }
139
+ }
140
+
141
+ function sanitizeAttachment(blob: TurnBlob): TurnBlob {
142
+ const { mimeType, data } = blob;
143
+ if (!TEXT_MIMES.has(mimeType.split(';')[0]?.trim().toLowerCase() ?? '')) {
144
+ return blob;
145
+ }
146
+ const bytes = sanitizeTextBytes(mimeType, decodeB64(data));
147
+ return { mimeType, data: encodeB64(bytes) };
148
+ }
149
+
150
+ function hasTurnBlobs(attachments?: TurnBlob[], voice?: TurnBlob[]): boolean {
151
+ return (attachments?.length ?? 0) > 0 || (voice?.length ?? 0) > 0;
152
+ }
153
+
154
+ function sanitizeTurnBlobs(
155
+ attachments: TurnBlob[] | undefined,
156
+ voice: TurnBlob[] | undefined,
157
+ limits: MediaLimits | undefined,
158
+ ): { attachments?: TurnBlob[]; voice?: TurnBlob[] } {
159
+ if (!hasTurnBlobs(attachments, voice)) {
160
+ return { attachments, voice };
161
+ }
162
+ const files = attachments ?? [];
163
+ const clips = voice ?? [];
164
+ if (!limits) {
165
+ throw new TheorumError('This profile does not accept files.');
166
+ }
167
+ assertAttachmentLimits([...files, ...clips], limits);
168
+ return {
169
+ attachments: files.length > 0 ? files.map(sanitizeAttachment) : attachments,
170
+ voice: clips.length > 0 ? clips.map(sanitizeAttachment) : voice,
171
+ };
172
+ }
173
+
174
+ function sanitizeTurnBlobsForProfile(
175
+ profileId: string,
176
+ attachments: TurnBlob[] | undefined,
177
+ voice: TurnBlob[] | undefined,
178
+ ): { attachments?: TurnBlob[]; voice?: TurnBlob[] } {
179
+ if (!hasTurnBlobs(attachments, voice)) {
180
+ return { attachments, voice };
181
+ }
182
+ const limits = requireMediaLimits(getProfile(profileId));
183
+ return sanitizeTurnBlobs(attachments, voice, limits);
184
+ }
185
+
186
+ export {
187
+ assertAttachmentLimits,
188
+ fileTooLargeMessage,
189
+ requireMediaLimits,
190
+ sanitizeCsvText,
191
+ sanitizeTurnBlobs,
192
+ sanitizeTurnBlobsForProfile,
193
+ tooManyFilesMessage,
194
+ turnTooLargeMessage,
195
+ };