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 +30 -9
- package/esm/mod.d.ts +4 -2
- package/esm/mod.js +3 -2
- package/esm/src/guardrails/error.d.ts +13 -1
- package/esm/src/guardrails/error.js +36 -1
- package/esm/src/guardrails/mod.d.ts +3 -1
- package/esm/src/guardrails/mod.js +2 -1
- package/esm/src/guardrails/quota.d.ts +10 -0
- package/esm/src/guardrails/quota.js +64 -0
- package/esm/src/kernel/engine/runner/gates.js +2 -5
- package/esm/src/kernel/engine/runner/mod.js +6 -0
- package/esm/src/kernel/engine/runner/stream.js +8 -3
- package/esm/src/kernel/engine/runner/tools.js +6 -11
- package/esm/src/kernel/mod.d.ts +1 -1
- package/esm/src/kernel/mod.js +1 -1
- package/esm/src/kernel/types.d.ts +3 -0
- package/esm/src/observability/trace-record.d.ts +1 -0
- package/esm/src/observability/trace-record.js +4 -1
- package/esm/src/providers/interactions.js +2 -1
- package/esm/src/providers/openrouter.js +4 -4
- package/esm/src/providers/provider.js +4 -10
- package/esm/src/providers/speech.js +5 -5
- package/package.json +3 -3
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: ["
|
|
154
|
+
allow: ["hostFastModel"],
|
|
153
155
|
config: {
|
|
154
|
-
|
|
155
|
-
apiId: "
|
|
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: ["
|
|
241
|
+
allow: ["hostFastModel"],
|
|
239
242
|
config: {
|
|
240
|
-
|
|
241
|
-
apiId: "
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
|
64
|
+
yield toErrorEvent('canary leaked');
|
|
60
65
|
return;
|
|
61
66
|
}
|
|
62
67
|
yield* processNormalEvent(event, profile, generation);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
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
|
|
25
|
+
yield toErrorEvent(formatToolFinding(result));
|
|
26
26
|
return;
|
|
27
27
|
}
|
|
28
28
|
yield { type: 'done' };
|
|
29
29
|
}
|
|
30
30
|
catch (err) {
|
|
31
|
-
yield
|
|
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(
|
|
45
|
+
events.push(toErrorEvent(formatToolFinding(result)));
|
|
46
46
|
}
|
|
47
47
|
return events;
|
|
48
48
|
}
|
|
49
49
|
catch (err) {
|
|
50
|
-
return [
|
|
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);
|
package/esm/src/kernel/mod.d.ts
CHANGED
|
@@ -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';
|
package/esm/src/kernel/mod.js
CHANGED
|
@@ -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 {
|
|
@@ -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
|
-
|
|
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 {
|
|
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
|
|
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
|
|
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
|
|
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 {
|
|
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
|
|
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 {
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
+
"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
|
+
}
|