theorum 0.1.3 → 0.1.5

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.
package/README.md CHANGED
@@ -16,6 +16,8 @@ THEORUM is a compact TypeScript agent kernel for apps that need deterministic ag
16
16
 
17
17
  The package is intentionally **not** an agent product. It ships no app profiles, no prompts, no secrets, no database policy, no business rules, and no channel-specific UX. Those belong in the host application.
18
18
 
19
+ OpenRouter chat transport is powered by Vercel AI SDK Core under the adapter. THEORUM keeps the runner contract, guardrails, tool permissions, egress, media buffering, and trace event shape; AI SDK handles the OpenRouter request/stream/tool-call normalization layer.
20
+
19
21
  ---
20
22
 
21
23
  ## Core Principles
@@ -62,7 +64,7 @@ flowchart TD
62
64
  end
63
65
 
64
66
  subgraph Providers["Provider adapters"]
65
- OR["OpenRouter chat"]
67
+ OR["OpenRouter chat via Vercel AI SDK Core"]
66
68
  GI["Google Interactions"]
67
69
  Speech["Speech (Interactions or /audio/speech)"]
68
70
  end
@@ -149,10 +151,11 @@ const profile = defineProfile({
149
151
  model: {
150
152
  protocol: "openAi",
151
153
  provider: "openrouter",
152
- allow: ["gemini35FlashLite"],
154
+ allow: ["hostFastModel"],
153
155
  config: {
154
- gemini35FlashLite: {
155
- apiId: "gemini-3.5-flash-lite",
156
+ hostFastModel: {
157
+ apiId: "perplexity/sonar",
158
+ openRouterId: "perplexity/sonar",
156
159
  thinking: { on: "high", off: "minimal" },
157
160
  thinkingLevels: ["minimal", "low", "medium", "high"],
158
161
  summaries: { on: "auto", off: "none" },
@@ -235,10 +238,11 @@ Inbound and outbound safety are generic kernel hooks.
235
238
  const guardedProfile = defineProfile({
236
239
  id: "assistant.guarded",
237
240
  model: {
238
- allow: ["gemini35FlashLite"],
241
+ allow: ["hostFastModel"],
239
242
  config: {
240
- gemini35FlashLite: {
241
- apiId: "gemini-3.5-flash-lite",
243
+ hostFastModel: {
244
+ apiId: "perplexity/sonar",
245
+ openRouterId: "perplexity/sonar",
242
246
  thinking: { on: "high", off: "minimal" },
243
247
  thinkingLevels: ["minimal", "low", "medium", "high"],
244
248
  summaries: { on: "auto", off: "none" },
@@ -299,7 +303,9 @@ for await (const event of runTurn({ profile: profile.id, input: { text: "…" }
299
303
  | `openAi` + `openrouter` (chat) | OpenRouter chat completions |
300
304
  | `openAi` + `openrouter` (speech role) | OpenRouter `/audio/speech` |
301
305
 
302
- Advanced payload helpers live under `theorum/openrouter` (`toOpenRouterPayload`, …). Prefer `createProvider` for turns.
306
+ OpenRouter uses Vercel AI SDK Core inside THEORUM's provider adapter. The adapter still emits THEORUM `TurnEvent` values and preserves raw provider evidence for citations/provenance where the normalized SDK stream does not expose enough detail.
307
+
308
+ Advanced OpenRouter exports live under `theorum/openrouter` (`createOpenRouterProvider`, `toOpenRouterPayload`, …). Prefer `createProvider` for turns unless the host needs to wire the OpenRouter adapter directly.
303
309
 
304
310
  ---
305
311
 
@@ -310,7 +316,7 @@ Advanced payload helpers live under `theorum/openrouter` (`toOpenRouterPayload`,
310
316
  | `jsr:@theorum/core` / `theorum` | Main kernel API: profiles, schemas, runner, core types, provider constructors. |
311
317
  | `jsr:@theorum/core/kernel` / `theorum/kernel` | Profile/turn types, tool catalog, `requireModelSpec`, thinking clamps over host model maps. |
312
318
  | `jsr:@theorum/core/providers` / `theorum/providers` | `createProvider` + Gemini vault types. |
313
- | `jsr:@theorum/core/openrouter` / `theorum/openrouter` | OpenRouter payload helpers (advanced). |
319
+ | `jsr:@theorum/core/openrouter` / `theorum/openrouter` | Direct OpenRouter provider adapter and payload helpers (advanced). |
314
320
  | `jsr:@theorum/core/guardrails` / `theorum/guardrails` | Sanitization, public error mapping, inbound injection/sensitive-data primitives. |
315
321
  | `jsr:@theorum/core/observability` / `theorum/observability` | Trace sinks and trace record helpers. |
316
322
  | `jsr:@theorum/core/host` / `theorum/host` | Optional Deno HTTP helpers (`json`, status mapping, cutout mint flush). |
@@ -347,6 +353,21 @@ cd npm
347
353
  npm pack
348
354
  ```
349
355
 
356
+ Run a live OpenRouter smoke test with a host-resolved key. The key is passed as an argument and is never read from a Theorum `.env` file.
357
+
358
+ ```bash
359
+ deno run --allow-net scripts/verify-live.ts --api-key "$OPENROUTER_API_KEY"
360
+ ```
361
+
362
+ The default live verifier uses `perplexity/sonar` because it is broadly available on OpenRouter. Hosts can override both the profile-facing model id and provider-native id:
363
+
364
+ ```bash
365
+ deno run --allow-net scripts/verify-live.ts \
366
+ --api-key "$OPENROUTER_API_KEY" \
367
+ --model hostFastModel \
368
+ --api-id perplexity/sonar
369
+ ```
370
+
350
371
  ---
351
372
 
352
373
  ## Package Boundary
package/esm/mod.d.ts CHANGED
@@ -37,10 +37,12 @@
37
37
  * @module
38
38
  */
39
39
  import "./_dnt.polyfills.js";
40
- export { publicError, TheorumError } from './src/guardrails/error.js';
40
+ export { publicError, describeError, TheorumError, toErrorEvent } from './src/guardrails/error.js';
41
+ export type { QuotaSlotStatus } from './src/guardrails/quota.js';
42
+ export { clientIp, quotaMessage, releaseSlot, resetSlots, skipQuota, takeSlot, } from './src/guardrails/quota.js';
41
43
  export { PROJECT_ID_MAX, sanitizeProjectId, sanitizeText, sanitizeTurnRequest, } from './src/guardrails/sanitize.js';
42
44
  export { runTurn } from './src/kernel/engine/runner.js';
43
- export { CATALOG, clampThinkingLevel, clampThinkingLevelForApiId, getTool, listBuiltinIds, modelEntryByApiId, registerTools, requireModelSpec, resetTools, } from './src/kernel/registry/catalog.js';
45
+ export { CATALOG, clampThinkingLevel, clampThinkingLevelForApiId, geminiKindForMime, getTool, listBuiltinIds, mimeAllowed, mimeEssence, modelEntryByApiId, registerTools, requireModelSpec, resetTools, } from './src/kernel/registry/catalog.js';
44
46
  export type { ProfileDefinition } from './src/kernel/registry/profiles.js';
45
47
  export { clearProfiles, defineProfile, getProfile, hasProfile, listProfiles, registerProfile, registerProfiles, } from './src/kernel/registry/profiles.js';
46
48
  export { projectProfile, resolveTurn } from './src/kernel/registry/resolve.js';
package/esm/mod.js CHANGED
@@ -37,10 +37,11 @@
37
37
  * @module
38
38
  */
39
39
  import "./_dnt.polyfills.js";
40
- export { publicError, TheorumError } from './src/guardrails/error.js';
40
+ export { publicError, describeError, TheorumError, toErrorEvent } from './src/guardrails/error.js';
41
+ export { clientIp, quotaMessage, releaseSlot, resetSlots, skipQuota, takeSlot, } from './src/guardrails/quota.js';
41
42
  export { PROJECT_ID_MAX, sanitizeProjectId, sanitizeText, sanitizeTurnRequest, } from './src/guardrails/sanitize.js';
42
43
  export { runTurn } from './src/kernel/engine/runner.js';
43
- export { CATALOG, clampThinkingLevel, clampThinkingLevelForApiId, getTool, listBuiltinIds, modelEntryByApiId, registerTools, requireModelSpec, resetTools, } from './src/kernel/registry/catalog.js';
44
+ export { CATALOG, clampThinkingLevel, clampThinkingLevelForApiId, geminiKindForMime, getTool, listBuiltinIds, mimeAllowed, mimeEssence, modelEntryByApiId, registerTools, requireModelSpec, resetTools, } from './src/kernel/registry/catalog.js';
44
45
  export { clearProfiles, defineProfile, getProfile, hasProfile, listProfiles, registerProfile, registerProfiles, } from './src/kernel/registry/profiles.js';
45
46
  export { projectProfile, resolveTurn } from './src/kernel/registry/resolve.js';
46
47
  export { getStructured, registerStructured } from './src/kernel/registry/schemas.js';
@@ -30,4 +30,16 @@ declare const PUBLIC_FILE_COUNT = "Too many files for one message.";
30
30
  declare const PUBLIC_IMAGE_SIZE = "That image size isn't supported.";
31
31
  /** Convert an unknown thrown value or internal message to user-safe text. */
32
32
  declare function publicError(err: unknown): string;
33
- export { PUBLIC_ACTION, PUBLIC_CANARY, PUBLIC_FILE_COUNT, PUBLIC_FILE_SIZE, PUBLIC_FILE_TYPE, PUBLIC_GENERIC, PUBLIC_IMAGE_SIZE, PUBLIC_UNAVAILABLE, publicError, TheorumError, UPSTREAM_FAILED, };
33
+ /** Raw diagnostic text for hosts, traces, and logs (never shown to end users). */
34
+ declare function describeError(err: unknown): string;
35
+ /**
36
+ * Stream error event with a public-safe `error` and a preserved `errorInternal`.
37
+ * Providers and the runner should emit this instead of public-only error strings
38
+ * so traces and host logs are never a black box.
39
+ */
40
+ declare function toErrorEvent(err: unknown): {
41
+ type: 'error';
42
+ error: string;
43
+ errorInternal: string;
44
+ };
45
+ export { PUBLIC_ACTION, PUBLIC_CANARY, PUBLIC_FILE_COUNT, PUBLIC_FILE_SIZE, PUBLIC_FILE_TYPE, PUBLIC_GENERIC, PUBLIC_IMAGE_SIZE, PUBLIC_UNAVAILABLE, describeError, publicError, TheorumError, toErrorEvent, UPSTREAM_FAILED, };
@@ -89,7 +89,20 @@ const RULES = [
89
89
  resolve: () => PUBLIC_GENERIC,
90
90
  },
91
91
  ];
92
+ const ALREADY_PUBLIC = new Set([
93
+ PUBLIC_GENERIC,
94
+ PUBLIC_UNAVAILABLE,
95
+ PUBLIC_CANARY,
96
+ PUBLIC_ACTION,
97
+ PUBLIC_FILE_TYPE,
98
+ PUBLIC_FILE_SIZE,
99
+ PUBLIC_FILE_COUNT,
100
+ PUBLIC_IMAGE_SIZE,
101
+ ]);
92
102
  function publicText(text) {
103
+ if (ALREADY_PUBLIC.has(text)) {
104
+ return text;
105
+ }
93
106
  const exact = EXACT[text];
94
107
  if (exact) {
95
108
  return exact;
@@ -111,4 +124,26 @@ function publicError(err) {
111
124
  }
112
125
  return PUBLIC_UNAVAILABLE;
113
126
  }
114
- export { PUBLIC_ACTION, PUBLIC_CANARY, PUBLIC_FILE_COUNT, PUBLIC_FILE_SIZE, PUBLIC_FILE_TYPE, PUBLIC_GENERIC, PUBLIC_IMAGE_SIZE, PUBLIC_UNAVAILABLE, publicError, TheorumError, UPSTREAM_FAILED, };
127
+ /** Raw diagnostic text for hosts, traces, and logs (never shown to end users). */
128
+ function describeError(err) {
129
+ if (typeof err === 'string') {
130
+ return err;
131
+ }
132
+ if (err instanceof Error && err.message) {
133
+ return err.message;
134
+ }
135
+ return String(err);
136
+ }
137
+ /**
138
+ * Stream error event with a public-safe `error` and a preserved `errorInternal`.
139
+ * Providers and the runner should emit this instead of public-only error strings
140
+ * so traces and host logs are never a black box.
141
+ */
142
+ function toErrorEvent(err) {
143
+ return {
144
+ type: 'error',
145
+ error: publicError(err),
146
+ errorInternal: describeError(err),
147
+ };
148
+ }
149
+ export { PUBLIC_ACTION, PUBLIC_CANARY, PUBLIC_FILE_COUNT, PUBLIC_FILE_SIZE, PUBLIC_FILE_TYPE, PUBLIC_GENERIC, PUBLIC_IMAGE_SIZE, PUBLIC_UNAVAILABLE, describeError, publicError, TheorumError, toErrorEvent, UPSTREAM_FAILED, };
@@ -8,7 +8,9 @@
8
8
  * @module
9
9
  */
10
10
  import "../../_dnt.polyfills.js";
11
- export { PUBLIC_ACTION, PUBLIC_CANARY, PUBLIC_FILE_COUNT, PUBLIC_FILE_SIZE, PUBLIC_FILE_TYPE, PUBLIC_GENERIC, PUBLIC_IMAGE_SIZE, PUBLIC_UNAVAILABLE, publicError, TheorumError, UPSTREAM_FAILED, } from './error.js';
11
+ export { PUBLIC_ACTION, PUBLIC_CANARY, PUBLIC_FILE_COUNT, PUBLIC_FILE_SIZE, PUBLIC_FILE_TYPE, PUBLIC_GENERIC, PUBLIC_IMAGE_SIZE, PUBLIC_UNAVAILABLE, describeError, publicError, TheorumError, toErrorEvent, UPSTREAM_FAILED, } from './error.js';
12
12
  export { injectionSpans } from './injection.js';
13
+ export type { QuotaSlotStatus } from './quota.js';
14
+ export { clientIp, quotaMessage, releaseSlot, resetSlots, skipQuota, takeSlot, } from './quota.js';
13
15
  export { PROJECT_ID_MAX, sanitizeProjectId, sanitizeText, sanitizeTurnRequest, } from './sanitize.js';
14
16
  export { sensitiveSpans } from './sensitive.js';
@@ -8,7 +8,8 @@
8
8
  * @module
9
9
  */
10
10
  import "../../_dnt.polyfills.js";
11
- export { PUBLIC_ACTION, PUBLIC_CANARY, PUBLIC_FILE_COUNT, PUBLIC_FILE_SIZE, PUBLIC_FILE_TYPE, PUBLIC_GENERIC, PUBLIC_IMAGE_SIZE, PUBLIC_UNAVAILABLE, publicError, TheorumError, UPSTREAM_FAILED, } from './error.js';
11
+ export { PUBLIC_ACTION, PUBLIC_CANARY, PUBLIC_FILE_COUNT, PUBLIC_FILE_SIZE, PUBLIC_FILE_TYPE, PUBLIC_GENERIC, PUBLIC_IMAGE_SIZE, PUBLIC_UNAVAILABLE, describeError, publicError, TheorumError, toErrorEvent, UPSTREAM_FAILED, } from './error.js';
12
12
  export { injectionSpans } from './injection.js';
13
+ export { clientIp, quotaMessage, releaseSlot, resetSlots, skipQuota, takeSlot, } from './quota.js';
13
14
  export { PROJECT_ID_MAX, sanitizeProjectId, sanitizeText, sanitizeTurnRequest, } from './sanitize.js';
14
15
  export { sensitiveSpans } from './sensitive.js';
@@ -0,0 +1,10 @@
1
+ import type { Profile } from '../kernel/types.js';
2
+ type QuotaSlotStatus = 'ok' | 'busy' | 'quota' | 'not_configured';
3
+ declare function skipQuota(peer: string, req: Request): boolean;
4
+ declare function clientIp(peer: string, req: Request): string;
5
+ declare function takeSlot(profile: Profile, ip: string, now: number): QuotaSlotStatus;
6
+ declare function releaseSlot(profile: Profile, ip: string): void;
7
+ declare function quotaMessage(profile: Profile): string;
8
+ declare function resetSlots(): void;
9
+ export type { QuotaSlotStatus };
10
+ export { clientIp, quotaMessage, releaseSlot, resetSlots, skipQuota, takeSlot };
@@ -0,0 +1,64 @@
1
+ const LOOPBACK = new Set(['127.0.0.1', '::1', 'localhost']);
2
+ const slots = new Map();
3
+ function utcDay(now) {
4
+ return new Date(now).toISOString().slice(0, 10);
5
+ }
6
+ function isLoopback(peer) {
7
+ return LOOPBACK.has(peer);
8
+ }
9
+ function cfConnectingIp(req) {
10
+ return req.headers.get('cf-connecting-ip')?.trim() ?? '';
11
+ }
12
+ function slotKey(profileId, ip) {
13
+ return `${profileId}:${ip}`;
14
+ }
15
+ function skipQuota(peer, req) {
16
+ return isLoopback(peer) && !cfConnectingIp(req);
17
+ }
18
+ function clientIp(peer, req) {
19
+ if (isLoopback(peer)) {
20
+ const cf = cfConnectingIp(req);
21
+ if (cf) {
22
+ return cf;
23
+ }
24
+ }
25
+ if (peer) {
26
+ return peer;
27
+ }
28
+ return 'unknown';
29
+ }
30
+ function takeSlot(profile, ip, now) {
31
+ const quota = profile.guardrails.quota;
32
+ if (!quota) {
33
+ return 'not_configured';
34
+ }
35
+ const day = utcDay(now);
36
+ const key = slotKey(profile.id, ip);
37
+ let slot = slots.get(key);
38
+ if (!slot || slot.day !== day) {
39
+ slot = { day, count: 0, busy: false };
40
+ slots.set(key, slot);
41
+ }
42
+ if (slot.busy) {
43
+ return 'busy';
44
+ }
45
+ if (slot.count >= quota.perDay) {
46
+ return 'quota';
47
+ }
48
+ slot.busy = true;
49
+ slot.count += 1;
50
+ return 'ok';
51
+ }
52
+ function releaseSlot(profile, ip) {
53
+ const slot = slots.get(slotKey(profile.id, ip));
54
+ if (slot) {
55
+ slot.busy = false;
56
+ }
57
+ }
58
+ function quotaMessage(profile) {
59
+ return `Enjoying ${profile.identity.handle}? You've reached today's limit`;
60
+ }
61
+ function resetSlots() {
62
+ slots.clear();
63
+ }
64
+ export { clientIp, quotaMessage, releaseSlot, resetSlots, skipQuota, takeSlot };
@@ -1,4 +1,4 @@
1
- import { publicError } from '../../../guardrails/error.js';
1
+ import { toErrorEvent } from '../../../guardrails/error.js';
2
2
  import { sanitizeTurnRequest } from '../../../guardrails/sanitize.js';
3
3
  import { resolveTurn } from '../../registry/resolve.js';
4
4
  import { executeAttempt } from './steps.js';
@@ -59,10 +59,7 @@ async function evaluateEgressOutcome(args) {
59
59
  }
60
60
  return {
61
61
  action: 'withhold',
62
- event: {
63
- type: 'error',
64
- error: publicError('Turn withheld: egress disclosure violation'),
65
- },
62
+ event: toErrorEvent('Turn withheld: egress disclosure violation'),
66
63
  };
67
64
  }
68
65
  async function evaluateValidationOutcome(args) {
@@ -67,6 +67,12 @@ async function* runTurn(req, provider, sink = noopSink()) {
67
67
  gemini,
68
68
  })) {
69
69
  seen.push(event);
70
+ if (event.type === 'error') {
71
+ const detail = event.errorInternal ?? event.error;
72
+ if (detail) {
73
+ console.error(`[theorum] turn error (${req.profile}): ${detail}`);
74
+ }
75
+ }
70
76
  if (shouldSkipStreamEvent(event, profile)) {
71
77
  continue;
72
78
  }
@@ -1,4 +1,4 @@
1
- import { publicError } from '../../../guardrails/error.js';
1
+ import { publicError, toErrorEvent } from '../../../guardrails/error.js';
2
2
  import { providerCompleteRequest } from '../../registry/provider-request.js';
3
3
  import { eventHasCanary, redactCanary } from '../boundary.js';
4
4
  import { dispatchModelTool } from './tools.js';
@@ -39,7 +39,12 @@ function* processNormalEvent(event, profile, generation) {
39
39
  yield* interceptProviderTool(event, profile, generation);
40
40
  }
41
41
  else if (event.type === 'error') {
42
- yield { type: 'error', error: publicError(event.error) };
42
+ const internal = event.errorInternal ?? event.error ?? '';
43
+ yield {
44
+ type: 'error',
45
+ error: publicError(event.error ?? internal),
46
+ ...(internal ? { errorInternal: internal } : {}),
47
+ };
43
48
  }
44
49
  else {
45
50
  yield event;
@@ -56,7 +61,7 @@ async function* yieldProviderEvents(args) {
56
61
  })) {
57
62
  if (canary && eventHasCanary(event, canary)) {
58
63
  yield redactCanary(event, canary);
59
- yield { type: 'error', error: publicError('canary leaked') };
64
+ yield toErrorEvent('canary leaked');
60
65
  return;
61
66
  }
62
67
  yield* processNormalEvent(event, profile, generation);
@@ -1,4 +1,4 @@
1
- import { publicError } from '../../../guardrails/error.js';
1
+ import { toErrorEvent } from '../../../guardrails/error.js';
2
2
  import { CATALOG } from '../../registry/catalog.js';
3
3
  import { executeTool } from '../../registry/tools.js';
4
4
  function formatToolFinding(res) {
@@ -22,13 +22,13 @@ function* invokeFromUi(profile, req) {
22
22
  tool: { name: invoke.name, arguments: invoke.arguments, result },
23
23
  };
24
24
  if (result.status === 'error') {
25
- yield { type: 'error', error: publicError(formatToolFinding(result)) };
25
+ yield toErrorEvent(formatToolFinding(result));
26
26
  return;
27
27
  }
28
28
  yield { type: 'done' };
29
29
  }
30
30
  catch (err) {
31
- yield { type: 'error', error: publicError(err) };
31
+ yield toErrorEvent(err);
32
32
  yield { type: 'done' };
33
33
  }
34
34
  }
@@ -42,12 +42,12 @@ function executeCustomModelTool(profile, name, args) {
42
42
  },
43
43
  ];
44
44
  if (result.status === 'error') {
45
- events.push({ type: 'error', error: publicError(formatToolFinding(result)) });
45
+ events.push(toErrorEvent(formatToolFinding(result)));
46
46
  }
47
47
  return events;
48
48
  }
49
49
  catch (err) {
50
- return [{ type: 'error', error: publicError(err) }];
50
+ return [toErrorEvent(err)];
51
51
  }
52
52
  }
53
53
  function dispatchModelTool(profile, event, gated) {
@@ -60,12 +60,7 @@ function dispatchModelTool(profile, event, gated) {
60
60
  return [event];
61
61
  }
62
62
  if (!gated.includes(name)) {
63
- return [
64
- {
65
- type: 'error',
66
- error: publicError(`Tool '${name}' is not gated on this turn`),
67
- },
68
- ];
63
+ return [toErrorEvent(`Tool '${name}' is not gated on this turn`)];
69
64
  }
70
65
  const args = tool.arguments ?? {};
71
66
  return executeCustomModelTool(profile, name, args);
@@ -9,7 +9,7 @@
9
9
  */
10
10
  import "../../_dnt.polyfills.js";
11
11
  export { runTurn } from './engine/runner.js';
12
- export { CATALOG, clampThinkingLevel, clampThinkingLevelForApiId, getTool, listBuiltinIds, modelEntryByApiId, registerTools, requireModelSpec, resetTools, } from './registry/catalog.js';
12
+ export { CATALOG, clampThinkingLevel, clampThinkingLevelForApiId, geminiKindForMime, getTool, listBuiltinIds, mimeAllowed, mimeEssence, modelEntryByApiId, registerTools, requireModelSpec, resetTools, } from './registry/catalog.js';
13
13
  export type { ProfileDefinition } from './registry/profiles.js';
14
14
  export { clearProfiles, defineProfile, getProfile, hasProfile, listProfiles, registerProfile, registerProfiles, } from './registry/profiles.js';
15
15
  export { projectProfile, resolveTurn } from './registry/resolve.js';
@@ -9,7 +9,7 @@
9
9
  */
10
10
  import "../../_dnt.polyfills.js";
11
11
  export { runTurn } from './engine/runner.js';
12
- export { CATALOG, clampThinkingLevel, clampThinkingLevelForApiId, getTool, listBuiltinIds, modelEntryByApiId, registerTools, requireModelSpec, resetTools, } from './registry/catalog.js';
12
+ export { CATALOG, clampThinkingLevel, clampThinkingLevelForApiId, geminiKindForMime, getTool, listBuiltinIds, mimeAllowed, mimeEssence, modelEntryByApiId, registerTools, requireModelSpec, resetTools, } from './registry/catalog.js';
13
13
  export { clearProfiles, defineProfile, getProfile, hasProfile, listProfiles, registerProfile, registerProfiles, } from './registry/profiles.js';
14
14
  export { projectProfile, resolveTurn } from './registry/resolve.js';
15
15
  export { getStructured, registerStructured } from './registry/schemas.js';
@@ -495,7 +495,10 @@ export interface TurnEvent {
495
495
  evidence?: ProviderEvidenceEvent;
496
496
  tokens?: TurnTokens;
497
497
  interactionId?: string;
498
+ /** Public-safe failure text for hosts to show users. */
498
499
  error?: string;
500
+ /** Raw diagnostic detail for traces/logs; never surface to end users. */
501
+ errorInternal?: string;
499
502
  }
500
503
  /** Provider-neutral request object sent from the kernel to a model adapter. */
501
504
  export interface ProviderCompleteRequest extends ProviderGenerationConfig {
@@ -30,6 +30,7 @@ export interface TraceEvent {
30
30
  grounding?: TurnEvent['grounding'];
31
31
  evidence?: TurnEvent['evidence'];
32
32
  error?: string;
33
+ errorInternal?: string;
33
34
  }
34
35
  /** Complete trace-safe record for one attempted turn. */
35
36
  interface TraceRecord {
@@ -31,6 +31,9 @@ async function snapshotEvent(event) {
31
31
  if (event.error) {
32
32
  row.error = event.error;
33
33
  }
34
+ if (event.errorInternal) {
35
+ row.errorInternal = sanitizeText(event.errorInternal);
36
+ }
34
37
  if (event.structured !== undefined) {
35
38
  row.structured = event.structured;
36
39
  }
@@ -89,7 +92,7 @@ function titleFrom(text) {
89
92
  function attachFailure(record, thrown, lastErr, canary) {
90
93
  if (!record.ok) {
91
94
  record.error = publicError(thrown ?? lastErr?.error);
92
- const inside = internalError(thrown) ?? lastErr?.error;
95
+ const inside = internalError(thrown) ?? lastErr?.errorInternal ?? lastErr?.error;
93
96
  if (inside) {
94
97
  record.errorInternal = inside;
95
98
  }
@@ -35,7 +35,8 @@ function userInputStep(parts) {
35
35
  }
36
36
  function historyStep(msg) {
37
37
  const isAssistant = msg.role === 'assistant';
38
- const type = isAssistant ? 'model_turn' : 'user_input';
38
+ // Google Interactions input steps: assistant history is `model_output` (not `model_turn`).
39
+ const type = isAssistant ? 'model_output' : 'user_input';
39
40
  if (msg.parts && msg.parts.length > 0) {
40
41
  return { type, content: msg.parts.map(wirePart) };
41
42
  }
@@ -9,7 +9,7 @@
9
9
  */
10
10
  import { createOpenRouter } from '@openrouter/ai-sdk-provider';
11
11
  import { jsonSchema, streamText, tool, } from 'ai';
12
- import { publicError } from '../guardrails/error.js';
12
+ import { toErrorEvent } from '../guardrails/error.js';
13
13
  import { tryStructured } from '../kernel/engine/delta.js';
14
14
  import { getTool } from '../kernel/registry/catalog.js';
15
15
  import { resolveOpenRouterModel, toOpenRouterPayload, } from './openrouter-payload.js';
@@ -441,7 +441,7 @@ function primaryEventFromPart(part, acc) {
441
441
  return finishEvent(part, acc);
442
442
  case 'error':
443
443
  acc.errored = true;
444
- return { type: 'error', error: publicError(part.error) };
444
+ return toErrorEvent(part.error);
445
445
  default:
446
446
  return undefined;
447
447
  }
@@ -528,7 +528,7 @@ async function* yieldCapturedRawEventsUnchecked(context) {
528
528
  }
529
529
  }
530
530
  function missingOpenRouterKey() {
531
- return { type: 'error', error: publicError('missing OpenRouter API key') };
531
+ return toErrorEvent('missing OpenRouter API key');
532
532
  }
533
533
  async function* streamOpenRouter(req, config) {
534
534
  const apiKey = trimApiKey(config.apiKey);
@@ -545,7 +545,7 @@ async function* streamOpenRouter(req, config) {
545
545
  }
546
546
  catch (err) {
547
547
  yield* yieldCapturedRawEventsUnchecked(context);
548
- yield { type: 'error', error: publicError(err) };
548
+ yield toErrorEvent(err);
549
549
  }
550
550
  }
551
551
  function* finalEvents(req, acc) {
@@ -8,7 +8,7 @@
8
8
  *
9
9
  * @module
10
10
  */
11
- import { publicError, TheorumError } from '../guardrails/error.js';
11
+ import { TheorumError, toErrorEvent } from '../guardrails/error.js';
12
12
  import { eventsFromComplete, eventsFromDelta, extractTokenEvent, groundingFromEvent, tryStructured, } from '../kernel/engine/delta.js';
13
13
  import { tapFetch } from './google-tap.js';
14
14
  import { toInteractionsBody } from './interactions.js';
@@ -118,19 +118,13 @@ function withTap(req, transport) {
118
118
  }
119
119
  async function* streamComplete(req, transport) {
120
120
  if (!req.geminiBucket) {
121
- yield {
122
- type: 'error',
123
- error: publicError('missing Gemini vault bucket for Interactions'),
124
- };
121
+ yield toErrorEvent('missing Gemini vault bucket for Interactions');
125
122
  return;
126
123
  }
127
124
  const res = await fetchGemini(INTERACTIONS_URL, { method: 'POST', body: JSON.stringify(toInteractionsBody(req)) }, req.geminiBucket, withTap(req, transport));
128
125
  if (res.status !== HTTP_OK) {
129
126
  const errorBody = await res.text().catch(() => '');
130
- yield {
131
- type: 'error',
132
- error: publicError(`Gemini HTTP ${String(res.status)}: ${errorBody}`),
133
- };
127
+ yield toErrorEvent(`Gemini HTTP ${String(res.status)}: ${errorBody}`);
134
128
  return;
135
129
  }
136
130
  const acc = { text: '' };
@@ -154,7 +148,7 @@ async function* streamGuarded(req, transport) {
154
148
  yield* streamComplete(req, transport);
155
149
  }
156
150
  catch (err) {
157
- yield { type: 'error', error: publicError(err) };
151
+ yield toErrorEvent(err);
158
152
  }
159
153
  }
160
154
  /** Create a `ModelProvider` backed by Google Interactions streaming. */
@@ -6,7 +6,7 @@
6
6
  *
7
7
  * @module
8
8
  */
9
- import { publicError } from '../guardrails/error.js';
9
+ import { toErrorEvent } from '../guardrails/error.js';
10
10
  import { wrapPcmAsWav } from './pcm.js';
11
11
  const HTTP_OK = 200;
12
12
  function bytesToBase64(bytes) {
@@ -92,23 +92,23 @@ function* yieldSpeechSuccess(rawBytes, text, format) {
92
92
  async function* streamSpeech(req, config = {}) {
93
93
  const apiKey = config.apiKey?.trim() || undefined;
94
94
  if (!apiKey) {
95
- yield { type: 'error', error: publicError('missing API key for speech') };
95
+ yield toErrorEvent('missing API key for speech');
96
96
  return;
97
97
  }
98
98
  const text = extractInputText(req.input);
99
99
  if (!text) {
100
- yield { type: 'error', error: publicError('empty text for speech') };
100
+ yield toErrorEvent('empty text for speech');
101
101
  return;
102
102
  }
103
103
  const res = await requestSpeech(apiKey, text, req, config);
104
104
  if (res.status !== HTTP_OK) {
105
- yield { type: 'error', error: publicError(`Speech HTTP ${String(res.status)}`) };
105
+ yield toErrorEvent(`Speech HTTP ${String(res.status)}`);
106
106
  return;
107
107
  }
108
108
  const arrayBuffer = await res.arrayBuffer();
109
109
  const rawBytes = new Uint8Array(arrayBuffer);
110
110
  if (rawBytes.length === 0) {
111
- yield { type: 'error', error: publicError('no audio returned from speech') };
111
+ yield toErrorEvent('no audio returned from speech');
112
112
  return;
113
113
  }
114
114
  const format = req.speech?.format ?? 'pcm';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "theorum",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "A flat TypeScript agent kernel for typed profiles, deterministic turn execution, dynamic tools, provider adapters, guardrails, and host-injected traces.",
5
5
  "keywords": [
6
6
  "agent",
@@ -49,7 +49,7 @@
49
49
  },
50
50
  "scripts": {},
51
51
  "bin": {
52
- "theorum": "esm/src/cli/index.js"
52
+ "theorum": "./esm/src/cli/index.js"
53
53
  },
54
54
  "type": "module",
55
55
  "sideEffects": false,
@@ -62,4 +62,4 @@
62
62
  "@types/node": "^20.9.0"
63
63
  },
64
64
  "_generatedBy": "dnt@0.43.2"
65
- }
65
+ }