theorum 0.1.2 → 0.1.3

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 (99) hide show
  1. package/README.md +62 -27
  2. package/docs/AGENT_PROFILE_CONTRACT.md +45 -18
  3. package/docs/CLI_SPEC.md +2 -2
  4. package/docs/SECRETS.md +27 -22
  5. package/esm/_dnt.polyfills.d.ts +99 -0
  6. package/esm/_dnt.polyfills.js +130 -1
  7. package/esm/mod.d.ts +19 -3
  8. package/esm/mod.js +17 -3
  9. package/esm/src/cli/commands/profile.d.ts +2 -0
  10. package/esm/src/cli/commands/profile.js +53 -0
  11. package/esm/src/cli/commands/run.d.ts +10 -0
  12. package/esm/src/cli/commands/run.js +55 -0
  13. package/esm/src/cli/commands/test.d.ts +15 -0
  14. package/esm/src/cli/commands/test.js +140 -0
  15. package/esm/src/cli/index.d.ts +4 -0
  16. package/esm/src/cli/index.js +148 -0
  17. package/esm/src/cli/matrix/fixtures.d.ts +13 -0
  18. package/esm/src/cli/matrix/fixtures.js +77 -0
  19. package/esm/src/cli/matrix/synthesizer.d.ts +29 -0
  20. package/esm/src/cli/matrix/synthesizer.js +187 -0
  21. package/esm/src/guardrails/error.d.ts +1 -3
  22. package/esm/src/guardrails/error.js +4 -6
  23. package/esm/src/guardrails/mod.d.ts +1 -1
  24. package/esm/src/guardrails/mod.js +1 -1
  25. package/esm/src/guardrails/sanitize.js +1 -1
  26. package/esm/src/host/mint-trace.d.ts +27 -0
  27. package/esm/src/host/mint-trace.js +33 -0
  28. package/esm/src/host/mod.d.ts +13 -0
  29. package/esm/src/host/mod.js +12 -0
  30. package/esm/src/host/reply.d.ts +14 -0
  31. package/esm/src/host/reply.js +27 -0
  32. package/esm/src/kernel/engine/delta.js +31 -4
  33. package/esm/src/kernel/engine/repair.d.ts +1 -3
  34. package/esm/src/kernel/engine/repair.js +1 -1
  35. package/esm/src/kernel/engine/runner/gates.d.ts +4 -0
  36. package/esm/src/kernel/engine/runner/gates.js +206 -0
  37. package/esm/src/kernel/engine/runner/mod.d.ts +14 -0
  38. package/esm/src/kernel/engine/runner/mod.js +103 -0
  39. package/esm/src/kernel/engine/runner/state.d.ts +16 -0
  40. package/esm/src/kernel/engine/runner/state.js +8 -0
  41. package/esm/src/kernel/engine/runner/steps.d.ts +15 -0
  42. package/esm/src/kernel/engine/runner/steps.js +119 -0
  43. package/esm/src/kernel/engine/runner/stream.d.ts +11 -0
  44. package/esm/src/kernel/engine/runner/stream.js +65 -0
  45. package/esm/src/kernel/engine/runner/tokens.d.ts +3 -0
  46. package/esm/src/kernel/engine/runner/tokens.js +38 -0
  47. package/esm/src/kernel/engine/runner/tools.d.ts +13 -0
  48. package/esm/src/kernel/engine/runner/tools.js +197 -0
  49. package/esm/src/kernel/engine/runner.d.ts +1 -9
  50. package/esm/src/kernel/engine/runner.js +1 -726
  51. package/esm/src/kernel/mod.d.ts +3 -1
  52. package/esm/src/kernel/mod.js +2 -1
  53. package/esm/src/{providers → kernel/registry}/attachments.d.ts +2 -3
  54. package/esm/src/{providers → kernel/registry}/attachments.js +6 -6
  55. package/esm/src/kernel/registry/catalog.d.ts +28 -21
  56. package/esm/src/kernel/registry/catalog.js +55 -133
  57. package/esm/src/kernel/registry/ingress.d.ts +13 -0
  58. package/esm/src/kernel/registry/ingress.js +163 -0
  59. package/esm/src/kernel/registry/profiles.d.ts +2 -4
  60. package/esm/src/kernel/registry/profiles.js +14 -5
  61. package/esm/src/kernel/registry/provider-request.d.ts +4 -0
  62. package/esm/src/kernel/registry/provider-request.js +25 -0
  63. package/esm/src/kernel/registry/resolve.d.ts +0 -1
  64. package/esm/src/kernel/registry/resolve.js +35 -53
  65. package/esm/src/kernel/registry/tools.js +2 -2
  66. package/esm/src/kernel/registry/vault.d.ts +11 -0
  67. package/esm/src/kernel/registry/vault.js +18 -0
  68. package/esm/src/kernel/types.d.ts +102 -78
  69. package/esm/src/observability/trace-attach.js +6 -20
  70. package/esm/src/presets/google.d.ts +50 -0
  71. package/esm/src/presets/google.js +96 -0
  72. package/esm/src/presets/mod.d.ts +11 -0
  73. package/esm/src/presets/mod.js +10 -0
  74. package/esm/src/providers/create-provider.d.ts +29 -0
  75. package/esm/src/providers/create-provider.js +38 -0
  76. package/esm/src/providers/gemini-tape.d.ts +1 -2
  77. package/esm/src/providers/gemini-tape.js +1 -1
  78. package/esm/src/providers/interactions.js +41 -16
  79. package/esm/src/{guardrails → providers}/keys.d.ts +10 -3
  80. package/esm/src/{guardrails → providers}/keys.js +10 -13
  81. package/esm/src/providers/mod.d.ts +4 -7
  82. package/esm/src/providers/mod.js +2 -5
  83. package/esm/src/providers/openrouter-mod.d.ts +13 -0
  84. package/esm/src/providers/openrouter-mod.js +12 -0
  85. package/esm/src/providers/openrouter-payload.d.ts +11 -3
  86. package/esm/src/providers/openrouter-payload.js +28 -26
  87. package/esm/src/providers/openrouter.d.ts +5 -7
  88. package/esm/src/providers/openrouter.js +488 -231
  89. package/esm/src/providers/pcm.d.ts +7 -0
  90. package/esm/src/providers/pcm.js +35 -0
  91. package/esm/src/providers/provider.d.ts +3 -1
  92. package/esm/src/providers/provider.js +45 -2
  93. package/esm/src/providers/speech.d.ts +23 -0
  94. package/esm/src/providers/speech.js +125 -0
  95. package/package.json +20 -3
  96. package/esm/src/providers/media.d.ts +0 -5
  97. package/esm/src/providers/media.js +0 -125
  98. package/esm/src/providers/tts.d.ts +0 -24
  99. package/esm/src/providers/tts.js +0 -144
package/README.md CHANGED
@@ -24,7 +24,7 @@ The package is intentionally **not** an agent product. It ships no app profiles,
24
24
  [kernel_contract]
25
25
  profiles = "Host-owned declarations for model, inputs, outputs, tools, and guardrails"
26
26
  runner = "Single deterministic execution path for one agent turn"
27
- providers = "Adapters for OpenRouter-compatible chat and Google Interactions"
27
+ providers = "createProvider routes protocol/provider; adapters stay internal"
28
28
  tools = "Profile allowlist ceiling plus per-turn dynamic declarations"
29
29
  egress = "Typed host hook for outbound disclosure checks and repair loops"
30
30
  traces = "Host-injected sinks; no environment variables or bundled destinations"
@@ -62,9 +62,9 @@ flowchart TD
62
62
  end
63
63
 
64
64
  subgraph Providers["Provider adapters"]
65
- OR["OpenRouter"]
65
+ OR["OpenRouter chat"]
66
66
  GI["Google Interactions"]
67
- TTS["OpenRouter TTS"]
67
+ Speech["Speech (Interactions or /audio/speech)"]
68
68
  end
69
69
 
70
70
  Profile --> Resolve
@@ -80,6 +80,8 @@ flowchart TD
80
80
  Runner --> TraceSink
81
81
  ```
82
82
 
83
+ Hosts bind transports with `createProvider(profile, { gemini, openRouter })`. One door; protocol/provider (and speech role) pick the adapter.
84
+
83
85
  ### Turn Lifecycle
84
86
 
85
87
  ```mermaid
@@ -148,6 +150,17 @@ const profile = defineProfile({
148
150
  protocol: "openAi",
149
151
  provider: "openrouter",
150
152
  allow: ["gemini35FlashLite"],
153
+ config: {
154
+ gemini35FlashLite: {
155
+ apiId: "gemini-3.5-flash-lite",
156
+ thinking: { on: "high", off: "minimal" },
157
+ thinkingLevels: ["minimal", "low", "medium", "high"],
158
+ summaries: { on: "auto", off: "none" },
159
+ maxOutputTokens: 8192,
160
+ temperature: 1,
161
+ keyBuiltins: [],
162
+ },
163
+ },
151
164
  thinking: "minimal",
152
165
  maxSteps: 1,
153
166
  },
@@ -221,7 +234,20 @@ Inbound and outbound safety are generic kernel hooks.
221
234
  ```ts
222
235
  const guardedProfile = defineProfile({
223
236
  id: "assistant.guarded",
224
- model: { allow: ["gemini35FlashLite"] },
237
+ model: {
238
+ allow: ["gemini35FlashLite"],
239
+ config: {
240
+ gemini35FlashLite: {
241
+ apiId: "gemini-3.5-flash-lite",
242
+ thinking: { on: "high", off: "minimal" },
243
+ thinkingLevels: ["minimal", "low", "medium", "high"],
244
+ summaries: { on: "auto", off: "none" },
245
+ maxOutputTokens: 8192,
246
+ temperature: 1,
247
+ keyBuiltins: [],
248
+ },
249
+ },
250
+ },
225
251
  guardrails: {
226
252
  egress: {
227
253
  onBlock: "reject_to_agent",
@@ -250,33 +276,30 @@ Quota is optional. If a profile omits `guardrails.quota`, the quota helper retur
250
276
 
251
277
  ## Provider Adapters
252
278
 
253
- THEORUM includes provider adapters but does not own credentials.
279
+ THEORUM includes provider adapters but does not own credentials. Bind them with one door:
254
280
 
255
281
  ```ts
256
- import { createOpenRouterProvider } from "jsr:@theorum/core/openrouter";
282
+ import { createProvider, runTurn } from "jsr:@theorum/core";
257
283
 
258
- const provider = createOpenRouterProvider({
259
- apiKey: hostSecrets.openRouterApiKey,
260
- siteName: "Your app",
261
- siteUrl: "https://example.com",
284
+ const provider = createProvider(profile, {
285
+ gemini: { vault: hostGeminiKeyVault, fetch },
286
+ openRouter: { apiKey: hostSecrets.openRouterApiKey },
262
287
  });
263
- ```
264
-
265
- ```ts
266
- import { createInteractionsProvider } from "jsr:@theorum/core/providers";
267
288
 
268
- const provider = createInteractionsProvider({
269
- keys: hostGeminiKeyVault,
270
- fetch,
271
- });
289
+ for await (const event of runTurn({ profile: profile.id, input: { text: "…" } }, provider)) {
290
+ //
291
+ }
272
292
  ```
273
293
 
274
- Provider support is intentionally split by wire protocol:
294
+ `createProvider` routes from `profile.model.protocol` / `provider`. Speech roles use the same call — Interactions when Google, `/audio/speech` when openAi/openrouter (same `openRouter` credentials).
275
295
 
276
- | Provider | Protocol | Use |
277
- | :--- | :--- | :--- |
278
- | OpenRouter | `openAi` | Chat completions, reasoning streams, tool calls, structured output, TTS gateway. |
279
- | Google Interactions | `geminiInteractions` | Native Google Interactions streaming, image response format, interaction continuity, grounding metadata. |
296
+ | Profile | Transport |
297
+ | :--- | :--- |
298
+ | `geminiInteractions` + `google` | Google Interactions (chat, image, speech) |
299
+ | `openAi` + `openrouter` (chat) | OpenRouter chat completions |
300
+ | `openAi` + `openrouter` (speech role) | OpenRouter `/audio/speech` |
301
+
302
+ Advanced payload helpers live under `theorum/openrouter` (`toOpenRouterPayload`, …). Prefer `createProvider` for turns.
280
303
 
281
304
  ---
282
305
 
@@ -285,11 +308,15 @@ Provider support is intentionally split by wire protocol:
285
308
  | Entrypoint | Purpose |
286
309
  | :--- | :--- |
287
310
  | `jsr:@theorum/core` / `theorum` | Main kernel API: profiles, schemas, runner, core types, provider constructors. |
288
- | `jsr:@theorum/core/kernel` / `theorum/kernel` | Profile, turn, event, tool, egress, provider, and schema types. |
289
- | `jsr:@theorum/core/providers` / `theorum/providers` | Provider constructors and provider utility types. |
290
- | `jsr:@theorum/core/openrouter` / `theorum/openrouter` | OpenRouter payload and streaming adapter. |
311
+ | `jsr:@theorum/core/kernel` / `theorum/kernel` | Profile/turn types, tool catalog, `requireModelSpec`, thinking clamps over host model maps. |
312
+ | `jsr:@theorum/core/providers` / `theorum/providers` | `createProvider` + Gemini vault types. |
313
+ | `jsr:@theorum/core/openrouter` / `theorum/openrouter` | OpenRouter payload helpers (advanced). |
291
314
  | `jsr:@theorum/core/guardrails` / `theorum/guardrails` | Sanitization, public error mapping, inbound injection/sensitive-data primitives. |
292
315
  | `jsr:@theorum/core/observability` / `theorum/observability` | Trace sinks and trace record helpers. |
316
+ | `jsr:@theorum/core/host` / `theorum/host` | Optional Deno HTTP helpers (`json`, status mapping, cutout mint flush). |
317
+ | `jsr:@theorum/core/cli` / `theorum/cli` | Profile inspection and stress-test CLI (`theorum` binary on npm). |
318
+ | `jsr:@theorum/core/presets` / `theorum/presets` | Optional convenience packs (`registerGooglePreset`, …). |
319
+ | `jsr:@theorum/core/presets/google` / `theorum/presets/google` | Google builtins (search/maps/urlContext) + Interactions/OpenRouter wire metadata. |
293
320
 
294
321
  Internal files remain present in source for maintainability, but package consumers should use the public entrypoints above.
295
322
 
@@ -304,7 +331,15 @@ npm run lint
304
331
  deno publish --dry-run --allow-dirty
305
332
  ```
306
333
 
307
- Build the npm package from the Deno source:
334
+ Run the packaged CLI locally:
335
+
336
+ ```bash
337
+ deno task theorum --help
338
+ # or after npm install -g / npx:
339
+ # npx theorum --help
340
+ ```
341
+
342
+ Build the npm package from the Deno source (publish only from `npm/`):
308
343
 
309
344
  ```bash
310
345
  npm run build:npm
@@ -6,10 +6,12 @@ Every profile strictly namespaces its capabilities across 6 functional domains:
6
6
  1. `identity` — Persona, display handle, and static base system prompts.
7
7
  2. `model` — Protocol, provider backend, model whitelist, thinking level, and controls.
8
8
  3. `tools` — Tool access ceiling.
9
- 4. `inputs` — Strict ingress constraints, file limits, and routing slots.
10
- 5. `outputs` — Structured schemas, voice, media, streaming, and validation/auto-repair.
9
+ 4. `inputs` — Strict ingress constraints (text, attachments, voice), file limits, and routing slots.
10
+ 5. `outputs` — Structured schemas, image, speech, streaming, and validation/auto-repair.
11
11
  6. `guardrails` — Rate limits, canary leak detection, content safety, and outbound disclosure policies.
12
12
 
13
+ **Vocabulary:** Ingress audio is `inputs.voice`. Generated audio is `outputs.speech` (TTS voice id is `outputs.speech.voice`). Wire container formats for speech (`pcm` / `mp3`) may live in the kernel; vendor voice catalogs and image aspect/size vocabularies live in presets/apps.
14
+
13
15
  ---
14
16
 
15
17
  ## 1. Authoring Shape
@@ -20,17 +22,30 @@ profiles. The minimum useful profile is:
20
22
  ```typescript
21
23
  defineProfile({
22
24
  id: 'host.agent',
23
- model: { allow: ['your-model-id'] },
25
+ model: {
26
+ allow: ['your-model-id'],
27
+ config: {
28
+ 'your-model-id': {
29
+ apiId: 'provider-native-model-id',
30
+ thinking: { on: 'high', off: 'minimal' },
31
+ thinkingLevels: ['minimal', 'low', 'medium', 'high'],
32
+ summaries: { on: 'auto', off: 'none' },
33
+ maxOutputTokens: 8192,
34
+ temperature: 1,
35
+ keyBuiltins: [],
36
+ },
37
+ },
38
+ },
24
39
  });
25
40
  ```
26
41
 
27
- Everything except `id` and `model.allow` is optional at authoring time:
42
+ Everything except `id`, `model.allow`, and `model.config` is optional at authoring time:
28
43
 
29
44
  ```typescript
30
45
  export type ProfileDefinition = {
31
46
  id: ProfileId;
32
47
  identity?: Partial<Profile['identity']>;
33
- model: Partial<Profile['model']> & Pick<Profile['model'], 'allow'>;
48
+ model: Partial<Profile['model']> & Pick<Profile['model'], 'allow' | 'config'>;
34
49
  tools?: Partial<Profile['tools']>;
35
50
  inputs?: Partial<Profile['inputs']>;
36
51
  outputs?: Partial<Profile['outputs']>;
@@ -61,6 +76,8 @@ export interface Profile {
61
76
  protocol: 'geminiInteractions' | 'openAi';
62
77
  provider: 'google' | 'openrouter';
63
78
  allow: ModelId[];
79
+ /** Host-owned wire config for every id in `allow`. */
80
+ config: Record<ModelId, ModelSpec>;
64
81
  select?: Record<string, ModelId>;
65
82
  thinking?: ThinkingLevel | Record<string, ThinkingLevel>;
66
83
  controls?: ControlId[];
@@ -85,11 +102,13 @@ export interface Profile {
85
102
  slots?: Record<string, string[]>;
86
103
  };
87
104
 
88
- /** 5. Outputs (Structured output, voice, media, validation, streaming) */
105
+ /** 5. Outputs (Structured output, image, speech, validation, streaming) */
89
106
  outputs: {
90
107
  structured?: StructuredSchemaId | StructuredBySlot | null;
91
- media?: boolean;
92
- voice?: ProfileVoiceSpec;
108
+ /** Pins for an image-role profile. Model id lives on `model`. */
109
+ image?: ProfileImageSpec;
110
+ /** Pins for a speech-role profile. Model id lives on `model`. */
111
+ speech?: ProfileSpeechSpec;
93
112
  validation?: ProfileValidationSpec;
94
113
  streaming?: ProfileStreamingSpec;
95
114
  };
@@ -118,39 +137,47 @@ export interface Profile {
118
137
  ### `model`
119
138
  - `model.protocol`: Wire framing protocol (`'geminiInteractions'` for Google Interactions API, `'openAi'` for OpenAI/OpenRouter compatible chat completions API).
120
139
  - `model.provider`: Provider execution backend (`'google'` or `'openrouter'`).
121
- - `model.allow`: Whitelist of allowable `ModelId`s for this profile.
122
- - `model.select`: Named model mappings (e.g. `{ fast: 'gemini35FlashLite', deep: 'gemini31ProPreview' }`).
123
- - `model.thinking`: Pinned thinking level (`'minimal' | 'low' | 'medium' | 'high'`) when not user-controllable.
140
+ - `model.allow`: Whitelist of host-defined `ModelId`s for this profile. THEORUM does not ship model names.
141
+ - `model.config`: Host-owned `ModelSpec` map keyed by the same ids as `allow` / `select` (`apiId`, thinking levels, tokens, `keyBuiltins`, optional per-model `key`, etc.).
142
+ - `model.select`: Named model mappings (e.g. `{ fast: 'flash', deep: 'pro' }` ids are host-defined).
143
+ - `model.thinking`: Pinned thinking level (`'none' | 'minimal' | 'low' | 'medium' | 'high' | 'xhigh' | 'max'`) when not user-controllable. Each model’s accepted subset is declared on `ModelSpec.thinkingLevels`.
124
144
  - `model.controls`: User-togglable controls on this profile (e.g. `['thinking']`).
125
145
  - `model.maxSteps`: Step limit on autonomous tool loops (1 = one-shot; >1 = autonomous tool loop).
126
- - `model.key`: Gemini free bucket quota pool (`'freeA' | 'freeB' | 'freeC'`).
146
+ - `model.key`: Default Gemini vault slot for this profile (`'freeA' | 'freeB' | 'freeC'`). Overflow uses `'paid'`.
147
+
148
+ ### `ModelSpec` (host-owned, per id in `config`)
149
+ - `apiId` / optional `openRouterId`: Provider-native wire ids.
150
+ - `keyBuiltins`: Builtins that may use `profile.model.key`. Any other enabled builtin selects the overflow vault slot. Host policy — THEORUM does not infer tool pricing.
151
+ - `key`: Optional vault slot override for this model (e.g. pin an image model to `'paid'`). When set, wins over profile key and builtin routing.
127
152
 
128
153
  ### `tools`
129
154
  - `tools.allow`: Whitelist of tool IDs permitted to run under this profile. Calls to unlisted tools are blocked at the kernel boundary.
155
+ - Harness tools (`askUser`) ship with THEORUM and are always in the catalog.
156
+ - Provider builtins (e.g. Google search/maps/urlContext) are registered by optional presets such as `theorum/presets/google` via `registerGooglePreset()`.
130
157
 
131
158
  ### `inputs`
132
159
  - `inputs.text`: Boolean flag accepting user text input.
133
160
  - `inputs.attachments`: Allowed mime types for uploaded file attachments.
134
- - `inputs.voice`: Allowed mime types for recorded audio clips.
161
+ - `inputs.voice`: Allowed mime types for recorded audio clips (ingress only).
135
162
  - `inputs.maxFiles`: Maximum number of files permitted per message.
136
163
  - `inputs.maxBytes`: Maximum byte size permitted per single file.
137
164
  - `inputs.maxTurnBytes`: Maximum total byte size permitted across all files in one turn.
138
165
  - `inputs.limitsByMime`: Granular per-MIME byte limits (e.g. `{ 'application/pdf': 50 * 1024 * 1024, 'video/*': 100 * 1024 * 1024 }`).
139
- - `inputs.slots`: Allowed values for dynamic routing slots.
166
+ - `inputs.slots`: Allowed values for dynamic routing slots. Image-role overrides use `slots.aspectRatio` / `slots.size`.
140
167
 
141
168
  ### `outputs`
142
169
  - `outputs.structured`: Structured JSON schema specification (or slot-based schema routing).
143
- - `outputs.media`: Flag enabling native image generation output.
144
- - `outputs.voice`: Voice specification for TTS (`voice.voice`, `voice.responseFormat: 'pcm' | 'mp3'`).
170
+ - `outputs.image`: Pins for an image-role profile (`aspectRatio`, `size`, `mimeType`, optional `allowsGrounding`, `maxInputImages`). The image model itself is selected via `model.allow` / `model.config`. Slot overrides use `slots.aspectRatio` / `slots.size` when the profile lists allowlists under `inputs.slots`. Adapters map `size` to provider wire keys (e.g. Google Interactions `imageSize`).
171
+ - `outputs.speech`: Pins for a speech-role profile (`voice`, optional `format: 'pcm' | 'mp3'`). The speech model itself is selected via `model.allow` / `model.config`. Bind with `createProvider(profile, …)` — same door as chat/image. `geminiInteractions` uses Interactions (`response_format: audio` + `speech_config`); `openAi`/`openrouter` speech roles use `/audio/speech` with the same `openRouter` credentials. `format: 'pcm'` (default) yields WAV media on both. `format: 'mp3'` is only valid on `openAi` speech — Interactions rejects it at resolve.
145
172
  - `outputs.validation`: In-harness auto-correction validator (`validate`, optional `extract`, `maxRetries`, `repairGuidance`). If `extract` is omitted, the structured output itself is validated.
146
- - `outputs.streaming`: SSE streaming behaviors (`streamThoughts`, `gateMedia`).
173
+ - `outputs.streaming`: SSE streaming behaviors (`streamThoughts`, `gateMedia`). `gateMedia` controls whether stream `media` events are held until validation/egress — unrelated to a profile output flag.
147
174
 
148
175
  ### `guardrails`
149
176
  - `guardrails.quota.perDay`: Optional daily turn quota enforced per client IP. If omitted, quota enforcement is explicitly `not_configured`.
150
177
  - `guardrails.canary`: Enable unique token canary leak interception (default `true`).
151
178
  - `guardrails.sanitizeInput`: Run prompt injection / jailbreak redaction on ingress text (default `true`).
152
179
  - `guardrails.redactSensitive`: Redact SSN, credit cards, IP addresses, API keys from inputs (default `true`).
153
- - `guardrails.egress`: Generic outbound disclosure control engine (`enforce`, `onBlock: 'reject_to_agent' | 'refuse_to_user'`, `maxRetries`, `repairGuidance`). Runs deterministic auto-repair loops for chat or immediate in-character refusal for voice.
180
+ - `guardrails.egress`: Generic outbound disclosure control engine (`enforce`, `onBlock: 'reject_to_agent' | 'refuse_to_user'`, `maxRetries`, `repairGuidance`). Runs deterministic auto-repair loops for chat or immediate in-character refusal when blocking egress on voice-input turns.
154
181
 
155
182
  ### Per-turn Interactions state
156
183
  - `TurnRequest.input`: Optional turn input object. If omitted, Theorum normalizes it to an empty input and still runs the profile/provider turn.
package/docs/CLI_SPEC.md CHANGED
@@ -48,7 +48,7 @@ theorum test --all [--lite] [--concurrency 4]
48
48
  | `--lite` | `boolean` | Minimal single-turn connectivity ping (fast mode, tools off, text only) |
49
49
  | `--matrix` | `boolean` | Generate and execute all valid permutations for the profile |
50
50
  | `--attachment` | `string[]` | Path(s) to custom attachment files (PNG, PDF, CSV, etc.) |
51
- | `--voice` | `string` | Path to custom voice audio (WAV, PCM) |
51
+ | `--voice` | `string` | Path to custom voice input audio (WAV, PCM) |
52
52
  | `--search` | `boolean` | Force Google Search tool on |
53
53
  | `--map` | `boolean` | Force Google Maps tool on |
54
54
  | `--mode` | `'fast' \| 'smart'` | Force reasoning / model speed tier |
@@ -112,7 +112,7 @@ interface SynthesizedTurn {
112
112
  - If profile supports `smart` mode $\to$ select `smart` (highest pinned thinking tokens).
113
113
  - Otherwise $\to$ use default profile model.
114
114
  2. **Multimodal Fixtures**:
115
- - If `inputs.voice === true` $\to$ inject synthetic 16kHz PCM/WAV speech fixture.
115
+ - If `inputs.voice` is set $\to$ inject synthetic 16kHz PCM/WAV voice-input fixture.
116
116
  - If `inputs.attachments === true` $\to$ inject synthetic test document/image matching allowed MIME types within byte ceilings (`maxBytes`).
117
117
  3. **Tool Resolution & Conflict Handling**:
118
118
  - Turn on all allowed tools in `profile.tools.allow`.
package/docs/SECRETS.md CHANGED
@@ -9,39 +9,44 @@ Host applications own credentials, runtime configuration, and secret storage. Th
9
9
  - Do not create `.env` files in this repository.
10
10
  - Do not commit key templates to this repository.
11
11
  - Do not teach Theorum to discover keys from the shell or process environment.
12
- - Business applications pass credentials into provider constructors or transport objects.
12
+ - Business applications pass credentials into `createProvider`.
13
13
 
14
- ## 2. OpenRouter
14
+ ## 2. Single door: `createProvider`
15
15
 
16
16
  ```ts
17
- import { createOpenRouterProvider } from 'theorum/openrouter';
18
-
19
- const provider = createOpenRouterProvider({
20
- apiKey: hostResolvedOpenRouterKey,
21
- });
22
- ```
23
-
24
- ## 3. Google Interactions
25
-
26
- ```ts
27
- import { createInteractionsProvider } from 'theorum/providers';
28
-
29
- const provider = createInteractionsProvider({
30
- vault: {
31
- freeA: hostResolvedFreeAKey,
32
- freeB: hostResolvedFreeBKey,
33
- freeC: hostResolvedFreeCKey,
34
- paid: hostResolvedPaidKey,
17
+ import { createProvider, runTurn } from 'theorum';
18
+
19
+ const provider = createProvider(profile, {
20
+ // geminiInteractions / google
21
+ gemini: {
22
+ vault: {
23
+ freeA: hostResolvedFreeAKey,
24
+ freeB: hostResolvedFreeBKey,
25
+ freeC: hostResolvedFreeCKey,
26
+ paid: hostResolvedPaidKey,
27
+ },
28
+ },
29
+ // openAi / openrouter (chat or speech role — same credentials)
30
+ openRouter: {
31
+ apiKey: hostResolvedOpenRouterKey,
35
32
  },
36
33
  });
34
+
35
+ for await (const event of runTurn({ profile: profile.id, input: { text: '…' } }, provider)) {
36
+ // …
37
+ }
37
38
  ```
38
39
 
39
- ## 4. Tracing
40
+ `createProvider` picks the transport from `profile.model.protocol` / `provider` (and whether the profile is a speech role). Hosts do not choose a separate speech constructor.
41
+
42
+ ## 3. Tracing
40
43
 
41
44
  Tracing is silent by default. Hosts opt in by passing a sink to `runTurn`.
42
45
 
43
46
  ```ts
44
- import { jsonlSink, runTurn } from 'theorum';
47
+ import { createProvider, jsonlSink, runTurn } from 'theorum';
48
+
49
+ const provider = createProvider(profile, { openRouter: { apiKey: hostResolvedOpenRouterKey } });
45
50
 
46
51
  for await (const event of runTurn(request, provider, jsonlSink(hostTraceDir))) {
47
52
  // stream events
@@ -9,3 +9,102 @@ declare global {
9
9
  }
10
10
  }
11
11
  export {};
12
+ /**
13
+ * Based on [import-meta-ponyfill](https://github.com/gaubee/import-meta-ponyfill),
14
+ * but instead of using npm to install additional dependencies,
15
+ * this approach manually consolidates cjs/mjs/d.ts into a single file.
16
+ *
17
+ * Note that this code might be imported multiple times
18
+ * (for example, both dnt.test.polyfills.ts and dnt.polyfills.ts contain this code;
19
+ * or Node.js might dynamically clear the cache and then force a require).
20
+ * Therefore, it's important to avoid redundant writes to global objects.
21
+ * Additionally, consider that commonjs is used alongside esm,
22
+ * so the two ponyfill functions are stored independently in two separate global objects.
23
+ */
24
+ import { createRequire } from "node:module";
25
+ import { type URL } from "node:url";
26
+ declare global {
27
+ interface ImportMeta {
28
+ /** A string representation of the fully qualified module URL. When the
29
+ * module is loaded locally, the value will be a file URL (e.g.
30
+ * `file:///path/module.ts`).
31
+ *
32
+ * You can also parse the string as a URL to determine more information about
33
+ * how the current module was loaded. For example to determine if a module was
34
+ * local or not:
35
+ *
36
+ * ```ts
37
+ * const url = new URL(import.meta.url);
38
+ * if (url.protocol === "file:") {
39
+ * console.log("this module was loaded locally");
40
+ * }
41
+ * ```
42
+ */
43
+ url: string;
44
+ /**
45
+ * A function that returns resolved specifier as if it would be imported
46
+ * using `import(specifier)`.
47
+ *
48
+ * ```ts
49
+ * console.log(import.meta.resolve("./foo.js"));
50
+ * // file:///dev/foo.js
51
+ * ```
52
+ *
53
+ * @param specifier The module specifier to resolve relative to `parent`.
54
+ * @param parent The absolute parent module URL to resolve from.
55
+ * @returns The absolute (`file:`) URL string for the resolved module.
56
+ */
57
+ resolve(specifier: string, parent?: string | URL | undefined): string;
58
+ /** A flag that indicates if the current module is the main module that was
59
+ * called when starting the program under Deno.
60
+ *
61
+ * ```ts
62
+ * if (import.meta.main) {
63
+ * // this was loaded as the main module, maybe do some bootstrapping
64
+ * }
65
+ * ```
66
+ */
67
+ main: boolean;
68
+ /** The absolute path of the current module.
69
+ *
70
+ * This property is only provided for local modules (ie. using `file://` URLs).
71
+ *
72
+ * Example:
73
+ * ```
74
+ * // Unix
75
+ * console.log(import.meta.filename); // /home/alice/my_module.ts
76
+ *
77
+ * // Windows
78
+ * console.log(import.meta.filename); // C:\alice\my_module.ts
79
+ * ```
80
+ */
81
+ filename: string;
82
+ /** The absolute path of the directory containing the current module.
83
+ *
84
+ * This property is only provided for local modules (ie. using `file://` URLs).
85
+ *
86
+ * * Example:
87
+ * ```
88
+ * // Unix
89
+ * console.log(import.meta.dirname); // /home/alice
90
+ *
91
+ * // Windows
92
+ * console.log(import.meta.dirname); // C:\alice
93
+ * ```
94
+ */
95
+ dirname: string;
96
+ }
97
+ }
98
+ type NodeRequest = ReturnType<typeof createRequire>;
99
+ type NodeModule = NonNullable<NodeRequest["main"]>;
100
+ interface ImportMetaPonyfillCommonjs {
101
+ (require: NodeRequest, module: NodeModule): ImportMeta;
102
+ }
103
+ interface ImportMetaPonyfillEsmodule {
104
+ (importMeta: ImportMeta): ImportMeta;
105
+ }
106
+ interface ImportMetaPonyfill extends ImportMetaPonyfillCommonjs, ImportMetaPonyfillEsmodule {
107
+ }
108
+ export declare let import_meta_ponyfill_commonjs: ImportMetaPonyfillCommonjs;
109
+ export declare let import_meta_ponyfill_esmodule: ImportMetaPonyfillEsmodule;
110
+ export declare let import_meta_ponyfill: ImportMetaPonyfill;
@@ -12,4 +12,133 @@ if (!Object.hasOwn) {
12
12
  writable: true,
13
13
  });
14
14
  }
15
- export {};
15
+ /**
16
+ * Based on [import-meta-ponyfill](https://github.com/gaubee/import-meta-ponyfill),
17
+ * but instead of using npm to install additional dependencies,
18
+ * this approach manually consolidates cjs/mjs/d.ts into a single file.
19
+ *
20
+ * Note that this code might be imported multiple times
21
+ * (for example, both dnt.test.polyfills.ts and dnt.polyfills.ts contain this code;
22
+ * or Node.js might dynamically clear the cache and then force a require).
23
+ * Therefore, it's important to avoid redundant writes to global objects.
24
+ * Additionally, consider that commonjs is used alongside esm,
25
+ * so the two ponyfill functions are stored independently in two separate global objects.
26
+ */
27
+ //@ts-ignore
28
+ import { createRequire } from "node:module";
29
+ //@ts-ignore
30
+ import { fileURLToPath, pathToFileURL } from "node:url";
31
+ //@ts-ignore
32
+ import { dirname } from "node:path";
33
+ const defineGlobalPonyfill = (symbolFor, fn) => {
34
+ if (!Reflect.has(globalThis, Symbol.for(symbolFor))) {
35
+ Object.defineProperty(globalThis, Symbol.for(symbolFor), {
36
+ configurable: true,
37
+ get() {
38
+ return fn;
39
+ },
40
+ });
41
+ }
42
+ };
43
+ export let import_meta_ponyfill_commonjs = (Reflect.get(globalThis, Symbol.for("import-meta-ponyfill-commonjs")) ??
44
+ (() => {
45
+ const moduleImportMetaWM = new WeakMap();
46
+ return (require, module) => {
47
+ let importMetaCache = moduleImportMetaWM.get(module);
48
+ if (importMetaCache == null) {
49
+ const importMeta = Object.assign(Object.create(null), {
50
+ url: pathToFileURL(module.filename).href,
51
+ main: require.main == module,
52
+ resolve: (specifier, parentURL = importMeta.url) => {
53
+ return pathToFileURL((importMeta.url === parentURL
54
+ ? require
55
+ : createRequire(parentURL))
56
+ .resolve(specifier)).href;
57
+ },
58
+ filename: module.filename,
59
+ dirname: module.path,
60
+ });
61
+ moduleImportMetaWM.set(module, importMeta);
62
+ importMetaCache = importMeta;
63
+ }
64
+ return importMetaCache;
65
+ };
66
+ })());
67
+ defineGlobalPonyfill("import-meta-ponyfill-commonjs", import_meta_ponyfill_commonjs);
68
+ export let import_meta_ponyfill_esmodule = (Reflect.get(globalThis, Symbol.for("import-meta-ponyfill-esmodule")) ??
69
+ ((importMeta) => {
70
+ const resolveFunStr = String(importMeta.resolve);
71
+ const shimWs = new WeakSet();
72
+ //@ts-ignore
73
+ const mainUrl = ("file:///" + process.argv[1].replace(/\\/g, "/"))
74
+ .replace(/\/{3,}/, "///");
75
+ const commonShim = (importMeta) => {
76
+ if (typeof importMeta.main !== "boolean") {
77
+ importMeta.main = importMeta.url === mainUrl;
78
+ }
79
+ if (typeof importMeta.filename !== "string") {
80
+ importMeta.filename = fileURLToPath(importMeta.url);
81
+ importMeta.dirname = dirname(importMeta.filename);
82
+ }
83
+ };
84
+ if (
85
+ // v16.2.0+, v14.18.0+: Add support for WHATWG URL object to parentURL parameter.
86
+ resolveFunStr === "undefined" ||
87
+ // v20.0.0+, v18.19.0+"" This API now returns a string synchronously instead of a Promise.
88
+ resolveFunStr.startsWith("async")
89
+ // enable by --experimental-import-meta-resolve flag
90
+ ) {
91
+ import_meta_ponyfill_esmodule = (importMeta) => {
92
+ if (!shimWs.has(importMeta)) {
93
+ shimWs.add(importMeta);
94
+ const importMetaUrlRequire = {
95
+ url: importMeta.url,
96
+ require: createRequire(importMeta.url),
97
+ };
98
+ // note: the parameter types must be explicitly annotated because
99
+ // `@types/node` also declares `ImportMeta.resolve`, which makes
100
+ // the contextual type an overload set that provides no inference
101
+ importMeta.resolve = function resolve(specifier, parentURL = importMeta.url) {
102
+ return pathToFileURL((importMetaUrlRequire.url === parentURL
103
+ ? importMetaUrlRequire.require
104
+ : createRequire(parentURL)).resolve(specifier)).href;
105
+ };
106
+ commonShim(importMeta);
107
+ }
108
+ return importMeta;
109
+ };
110
+ }
111
+ else {
112
+ /// native support
113
+ import_meta_ponyfill_esmodule = (importMeta) => {
114
+ if (!shimWs.has(importMeta)) {
115
+ shimWs.add(importMeta);
116
+ commonShim(importMeta);
117
+ }
118
+ return importMeta;
119
+ };
120
+ }
121
+ return import_meta_ponyfill_esmodule(importMeta);
122
+ }));
123
+ defineGlobalPonyfill("import-meta-ponyfill-esmodule", import_meta_ponyfill_esmodule);
124
+ export let import_meta_ponyfill = ((...args) => {
125
+ const _MODULE = (() => {
126
+ if (typeof require === "function" && typeof module === "object") {
127
+ return "commonjs";
128
+ }
129
+ else {
130
+ // eval("typeof import.meta");
131
+ return "esmodule";
132
+ }
133
+ })();
134
+ if (_MODULE === "commonjs") {
135
+ //@ts-ignore
136
+ import_meta_ponyfill = (r, m) => import_meta_ponyfill_commonjs(r, m);
137
+ }
138
+ else {
139
+ //@ts-ignore
140
+ import_meta_ponyfill = (im) => import_meta_ponyfill_esmodule(im);
141
+ }
142
+ //@ts-ignore
143
+ return import_meta_ponyfill(...args);
144
+ });
package/esm/mod.d.ts CHANGED
@@ -11,7 +11,20 @@
11
11
  *
12
12
  * const profile = defineProfile({
13
13
  * id: "assistant.basic",
14
- * model: { allow: ["gemini35FlashLite"] },
14
+ * model: {
15
+ * allow: ["gemini35FlashLite"],
16
+ * config: {
17
+ * gemini35FlashLite: {
18
+ * apiId: "gemini-3.5-flash-lite",
19
+ * thinking: { on: "high", off: "minimal" },
20
+ * thinkingLevels: ["minimal", "low", "medium", "high"],
21
+ * summaries: { on: "auto", off: "none" },
22
+ * maxOutputTokens: 8192,
23
+ * temperature: 1,
24
+ * keyBuiltins: [],
25
+ * },
26
+ * },
27
+ * },
15
28
  * tools: { allow: [] },
16
29
  * inputs: { text: true },
17
30
  * outputs: {},
@@ -27,11 +40,14 @@ import "./_dnt.polyfills.js";
27
40
  export { publicError, TheorumError } from './src/guardrails/error.js';
28
41
  export { PROJECT_ID_MAX, sanitizeProjectId, sanitizeText, sanitizeTurnRequest, } from './src/guardrails/sanitize.js';
29
42
  export { runTurn } from './src/kernel/engine/runner.js';
30
- export { defineProfile, getProfile, hasProfile, listProfiles, registerProfile, registerProfiles, } from './src/kernel/registry/profiles.js';
43
+ export { CATALOG, clampThinkingLevel, clampThinkingLevelForApiId, getTool, listBuiltinIds, modelEntryByApiId, registerTools, requireModelSpec, resetTools, } from './src/kernel/registry/catalog.js';
44
+ export type { ProfileDefinition } from './src/kernel/registry/profiles.js';
45
+ export { clearProfiles, defineProfile, getProfile, hasProfile, listProfiles, registerProfile, registerProfiles, } from './src/kernel/registry/profiles.js';
31
46
  export { projectProfile, resolveTurn } from './src/kernel/registry/resolve.js';
32
47
  export { getStructured, registerStructured } from './src/kernel/registry/schemas.js';
33
48
  export { executeTool } from './src/kernel/registry/tools.js';
34
49
  export type * from './src/kernel/types.js';
35
50
  export { jsonlSink, memorySink, noopSink, resolveTraceDir, sinkFromDir, writeTrace, } from './src/observability/trace.js';
36
51
  export type { TraceRecord } from './src/observability/trace-record.js';
37
- export { createInteractionsProvider, createOpenRouterProvider, createOpenRouterTtsProvider, resolveOpenRouterApiKey, resolveOpenRouterModel, streamOpenRouterTts, toOpenRouterPayload, wrapPcmAsWav, } from './src/providers/mod.js';
52
+ export type { CreateProviderOptions, GeminiTransport, GeminiVault } from './src/providers/mod.js';
53
+ export { createProvider } from './src/providers/mod.js';