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
@@ -0,0 +1,187 @@
1
+ /**
2
+ * Stress / matrix turn synthesis for the THEORUM CLI.
3
+ *
4
+ * Tool enabling walks the profile allowlist and registered catalog metadata
5
+ * (`conflictsWith`). Google `--search` / `--map` flags remain as convenience
6
+ * aliases for `googleSearch` / `googleMaps` when those ids are allowed.
7
+ *
8
+ * @module
9
+ */
10
+ import { getTool } from '../../kernel/registry/catalog.js';
11
+ import { FIXTURE_PNG_BASE64, FIXTURE_WAV_BASE64, getFixtureForMime } from './fixtures.js';
12
+ export function synthesizeLiteCombo(profile) {
13
+ const select = profile.model.select?.fast ? 'fast' : undefined;
14
+ return {
15
+ profile: profile.id,
16
+ select,
17
+ tools: {},
18
+ input: {
19
+ text: `Ping test for profile ${profile.id}. Respond concisely with confirmation.`,
20
+ },
21
+ };
22
+ }
23
+ function resolveStressReasoning(profile) {
24
+ if (profile.model.select?.smart) {
25
+ return 'smart';
26
+ }
27
+ if (profile.model.select) {
28
+ const keys = Object.keys(profile.model.select);
29
+ return keys[keys.length - 1];
30
+ }
31
+ return undefined;
32
+ }
33
+ function isEnabled(active, id) {
34
+ return active[id] === true;
35
+ }
36
+ /** Drop tools whose registered `conflictsWith` siblings are also enabled. */
37
+ function applyRegisteredConflicts(allowed, active) {
38
+ for (const id of allowed) {
39
+ if (!isEnabled(active, id)) {
40
+ continue;
41
+ }
42
+ const conflicts = getTool(id)?.conflictsWith ?? [];
43
+ if (conflicts.some((other) => isEnabled(active, other))) {
44
+ active[id] = false;
45
+ }
46
+ }
47
+ }
48
+ /**
49
+ * Enable allowlisted tools, optionally forcing one preferred tool and clearing
50
+ * what it conflicts with (and tools that conflict with it).
51
+ */
52
+ function resolveStressTools(allowed, prefer) {
53
+ const active = {};
54
+ for (const t of allowed) {
55
+ active[t] = true;
56
+ }
57
+ if (prefer && allowed.includes(prefer)) {
58
+ active[prefer] = true;
59
+ for (const c of getTool(prefer)?.conflictsWith ?? []) {
60
+ if (allowed.includes(c)) {
61
+ active[c] = false;
62
+ }
63
+ }
64
+ for (const t of allowed) {
65
+ if (t === prefer) {
66
+ continue;
67
+ }
68
+ const otherConflicts = getTool(t)?.conflictsWith ?? [];
69
+ if (otherConflicts.includes(prefer)) {
70
+ active[t] = false;
71
+ }
72
+ }
73
+ }
74
+ applyRegisteredConflicts(allowed, active);
75
+ return active;
76
+ }
77
+ /** Builtins on the allowlist that declare conflicts (candidates for matrix variants). */
78
+ function conflictingAllowlistedTools(allowed) {
79
+ return allowed.filter((id) => {
80
+ const entry = getTool(id);
81
+ return entry?.kind === 'builtin' && (entry.conflictsWith?.length ?? 0) > 0;
82
+ });
83
+ }
84
+ function resolveStressAttachments(profile) {
85
+ const attachments = [];
86
+ const accept = profile.inputs.attachments?.accept;
87
+ if (accept && accept.length > 0) {
88
+ const preferredMimes = ['image/png', 'application/pdf', 'text/plain'];
89
+ const chosenMime = preferredMimes.find((m) => accept.includes(m)) ?? accept[0];
90
+ const fixture = getFixtureForMime(chosenMime);
91
+ if (fixture) {
92
+ attachments.push(fixture);
93
+ }
94
+ else {
95
+ attachments.push({ mimeType: 'image/png', data: FIXTURE_PNG_BASE64 });
96
+ }
97
+ }
98
+ return attachments;
99
+ }
100
+ function resolveStressVoice(profile) {
101
+ const voice = [];
102
+ if (profile.inputs.voice?.accept && profile.inputs.voice.accept.length > 0) {
103
+ voice.push({ mimeType: 'audio/wav', data: FIXTURE_WAV_BASE64 });
104
+ }
105
+ return voice;
106
+ }
107
+ export function synthesizeStressCombo(profile, options = {}) {
108
+ const select = resolveStressReasoning(profile);
109
+ const activeTools = resolveStressTools(profile.tools.allow ?? [], options.preferTool);
110
+ const attachments = resolveStressAttachments(profile);
111
+ const voice = resolveStressVoice(profile);
112
+ const promptText = `Execute comprehensive test turn for profile ${profile.id}. Validate all instructions and produce required outputs.`;
113
+ return {
114
+ profile: profile.id,
115
+ select,
116
+ tools: activeTools,
117
+ input: {
118
+ text: promptText,
119
+ attachments: attachments.length > 0 ? attachments : undefined,
120
+ voice: voice.length > 0 ? voice : undefined,
121
+ },
122
+ };
123
+ }
124
+ export function synthesizeMatrixCombos(profile) {
125
+ const combos = [];
126
+ const allowed = profile.tools.allow ?? [];
127
+ combos.push({
128
+ name: 'Lite (connectivity)',
129
+ req: synthesizeLiteCombo(profile),
130
+ });
131
+ const primary = synthesizeStressCombo(profile);
132
+ combos.push({
133
+ name: 'Stress (all modalities + primary tools)',
134
+ req: primary,
135
+ });
136
+ // Variants: each allowlisted conflicting builtin that the primary pass dropped.
137
+ for (const id of conflictingAllowlistedTools(allowed)) {
138
+ if (primary.tools?.[id] === true) {
139
+ continue;
140
+ }
141
+ const variant = synthesizeStressCombo(profile, { preferTool: id });
142
+ if (variant.tools?.[id] !== true) {
143
+ continue;
144
+ }
145
+ combos.push({
146
+ name: `Conflict variant (${id} preferred)`,
147
+ req: variant,
148
+ });
149
+ }
150
+ return combos;
151
+ }
152
+ /** Map legacy Google CLI flags onto tool ids when the profile allowlists them. */
153
+ function applyGoogleFlagAliases(active, allowed, options) {
154
+ if (options.search !== undefined && allowed.includes('googleSearch')) {
155
+ active.googleSearch = options.search;
156
+ }
157
+ if (options.map !== undefined && allowed.includes('googleMaps')) {
158
+ active.googleMaps = options.map;
159
+ }
160
+ }
161
+ function applyExplicitToolOverrides(base, profile, options) {
162
+ const allowed = profile.tools.allow ?? [];
163
+ base.tools = base.tools ?? {};
164
+ applyGoogleFlagAliases(base.tools, allowed, options);
165
+ // If map was explicitly requested on, prefer it against its registered conflicts.
166
+ if (options.map === true && allowed.includes('googleMaps')) {
167
+ const preferred = resolveStressTools(allowed, 'googleMaps');
168
+ for (const id of allowed) {
169
+ base.tools[id] = preferred[id] === true;
170
+ }
171
+ return;
172
+ }
173
+ applyRegisteredConflicts(allowed, base.tools);
174
+ }
175
+ export function buildCustomTurnRequest(profile, options) {
176
+ if (options.lite) {
177
+ return synthesizeLiteCombo(profile);
178
+ }
179
+ const base = synthesizeStressCombo(profile);
180
+ if (options.mode) {
181
+ base.select = options.mode;
182
+ }
183
+ if (options.search !== undefined || options.map !== undefined) {
184
+ applyExplicitToolOverrides(base, profile, options);
185
+ }
186
+ return base;
187
+ }
@@ -30,6 +30,4 @@ 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
- /** Backwards-compatible alias for `publicError`. */
34
- declare function errorMessage(err: unknown): string;
35
- export { errorMessage, 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
+ 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, };
@@ -47,7 +47,9 @@ const EXACT = {
47
47
  };
48
48
  const RULES = [
49
49
  {
50
- match: (t) => /^(Gemini|OpenRouter|TTS|OpenRouter TTS) HTTP/.test(t) || t.includes('TTS HTTP'),
50
+ match: (t) => /^(Gemini|OpenRouter|TTS|OpenRouter TTS|Speech) HTTP/.test(t) ||
51
+ t.includes('TTS HTTP') ||
52
+ t.includes('Speech HTTP'),
51
53
  resolve: () => PUBLIC_UNAVAILABLE,
52
54
  },
53
55
  {
@@ -109,8 +111,4 @@ function publicError(err) {
109
111
  }
110
112
  return PUBLIC_UNAVAILABLE;
111
113
  }
112
- /** Backwards-compatible alias for `publicError`. */
113
- function errorMessage(err) {
114
- return publicError(err);
115
- }
116
- export { errorMessage, PUBLIC_ACTION, PUBLIC_CANARY, PUBLIC_FILE_COUNT, PUBLIC_FILE_SIZE, PUBLIC_FILE_TYPE, PUBLIC_GENERIC, PUBLIC_IMAGE_SIZE, PUBLIC_UNAVAILABLE, publicError, TheorumError, UPSTREAM_FAILED, };
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, };
@@ -8,7 +8,7 @@
8
8
  * @module
9
9
  */
10
10
  import "../../_dnt.polyfills.js";
11
- export { errorMessage, 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, publicError, TheorumError, UPSTREAM_FAILED, } from './error.js';
12
12
  export { injectionSpans } from './injection.js';
13
13
  export { PROJECT_ID_MAX, sanitizeProjectId, sanitizeText, sanitizeTurnRequest, } from './sanitize.js';
14
14
  export { sensitiveSpans } from './sensitive.js';
@@ -8,7 +8,7 @@
8
8
  * @module
9
9
  */
10
10
  import "../../_dnt.polyfills.js";
11
- export { errorMessage, 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, publicError, TheorumError, UPSTREAM_FAILED, } from './error.js';
12
12
  export { injectionSpans } from './injection.js';
13
13
  export { PROJECT_ID_MAX, sanitizeProjectId, sanitizeText, sanitizeTurnRequest, } from './sanitize.js';
14
14
  export { sensitiveSpans } from './sensitive.js';
@@ -8,9 +8,9 @@
8
8
  * @module
9
9
  */
10
10
  import { mapStrings } from '../kernel/engine/tree.js';
11
+ import { sanitizeTurnBlobsForProfile } from '../kernel/registry/attachments.js';
11
12
  import { getProfile } from '../kernel/registry/profiles.js';
12
13
  import { applySpans } from '../observability/spans.js';
13
- import { sanitizeTurnBlobsForProfile } from '../providers/attachments.js';
14
14
  import { injectionSpans } from './injection.js';
15
15
  import { sensitiveSpans } from './sensitive.js';
16
16
  /** Sanitize one text value using prompt-injection and sensitive-data detectors. */
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Host cutout-trace helpers for apps that mint a final audit row after an
3
+ * upstream side effect (for example image cutout).
4
+ *
5
+ * Prefer importing from `theorum/host`.
6
+ *
7
+ * @module
8
+ */
9
+ import { type TraceSink } from '../observability/trace.js';
10
+ import type { TraceRecord } from '../observability/trace-record.js';
11
+ interface CutoutTape {
12
+ ok: boolean;
13
+ ms: number;
14
+ url?: string;
15
+ inSha256?: string;
16
+ outSha256?: string;
17
+ http?: unknown;
18
+ error?: string;
19
+ }
20
+ declare function flushMintTrace(args: {
21
+ held: TraceRecord[];
22
+ app: Record<string, unknown>;
23
+ cutout: CutoutTape;
24
+ sink?: TraceSink;
25
+ }): Promise<void>;
26
+ export type { CutoutTape };
27
+ export { flushMintTrace };
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Host cutout-trace helpers for apps that mint a final audit row after an
3
+ * upstream side effect (for example image cutout).
4
+ *
5
+ * Prefer importing from `theorum/host`.
6
+ *
7
+ * @module
8
+ */
9
+ import { publicError, UPSTREAM_FAILED } from '../guardrails/error.js';
10
+ import { noopSink, writeTrace } from '../observability/trace.js';
11
+ function attachMint(row, app, cutout) {
12
+ row.app = app;
13
+ row.cutout = cutout;
14
+ row.ms = Date.now() - row.ts;
15
+ if (!cutout.ok) {
16
+ row.ok = false;
17
+ const { error: cutoutError } = cutout;
18
+ let error = cutoutError;
19
+ if (!error) {
20
+ error = publicError(UPSTREAM_FAILED);
21
+ }
22
+ row.error = error;
23
+ }
24
+ }
25
+ async function flushMintTrace(args) {
26
+ const [row] = args.held;
27
+ if (!row) {
28
+ return;
29
+ }
30
+ attachMint(row, args.app, args.cutout);
31
+ await writeTrace(args.sink ?? noopSink(), Promise.resolve(row));
32
+ }
33
+ export { flushMintTrace };
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Optional host-application helpers.
3
+ *
4
+ * These are not part of the turn kernel. They exist for Deno HTTP hosts that
5
+ * want shared reply status mapping and cutout-trace flushing without re-
6
+ * implementing the same glue in every app route.
7
+ *
8
+ * @module
9
+ */
10
+ import "../../_dnt.polyfills.js";
11
+ export type { CutoutTape } from './mint-trace.js';
12
+ export { flushMintTrace } from './mint-trace.js';
13
+ export { caughtStatus, HTTP_BUSY, HTTP_METHOD, HTTP_NOT_FOUND, HTTP_OK, json, } from './reply.js';
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Optional host-application helpers.
3
+ *
4
+ * These are not part of the turn kernel. They exist for Deno HTTP hosts that
5
+ * want shared reply status mapping and cutout-trace flushing without re-
6
+ * implementing the same glue in every app route.
7
+ *
8
+ * @module
9
+ */
10
+ import "../../_dnt.polyfills.js";
11
+ export { flushMintTrace } from './mint-trace.js';
12
+ export { caughtStatus, HTTP_BUSY, HTTP_METHOD, HTTP_NOT_FOUND, HTTP_OK, json, } from './reply.js';
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Optional Deno HTTP reply helpers for host applications.
3
+ *
4
+ * Not part of the turn kernel. Prefer importing from `theorum/host`.
5
+ *
6
+ * @module
7
+ */
8
+ declare const HTTP_BUSY = 429;
9
+ declare const HTTP_OK = 200;
10
+ declare const HTTP_NOT_FOUND = 404;
11
+ declare const HTTP_METHOD = 405;
12
+ declare function json(status: number, body: unknown, cors: Record<string, string>): Response;
13
+ declare function caughtStatus(err: unknown): number;
14
+ export { caughtStatus, HTTP_BUSY, HTTP_METHOD, HTTP_NOT_FOUND, HTTP_OK, json };
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Optional Deno HTTP reply helpers for host applications.
3
+ *
4
+ * Not part of the turn kernel. Prefer importing from `theorum/host`.
5
+ *
6
+ * @module
7
+ */
8
+ import { TheorumError } from '../guardrails/error.js';
9
+ const HTTP_BAD = 400;
10
+ const HTTP_BUSY = 429;
11
+ const HTTP_ERR = 500;
12
+ const HTTP_OK = 200;
13
+ const HTTP_NOT_FOUND = 404;
14
+ const HTTP_METHOD = 405;
15
+ function json(status, body, cors) {
16
+ return new Response(JSON.stringify(body), {
17
+ status,
18
+ headers: { ...cors, 'Content-Type': 'application/json' },
19
+ });
20
+ }
21
+ function caughtStatus(err) {
22
+ if (err instanceof TheorumError) {
23
+ return HTTP_BAD;
24
+ }
25
+ return HTTP_ERR;
26
+ }
27
+ export { caughtStatus, HTTP_BUSY, HTTP_METHOD, HTTP_NOT_FOUND, HTTP_OK, json };
@@ -35,6 +35,18 @@ function eventsFromImage(delta) {
35
35
  }
36
36
  return [];
37
37
  }
38
+ function eventsFromAudio(delta) {
39
+ const media = deltaMedia(delta);
40
+ if (media) {
41
+ return [media];
42
+ }
43
+ // Google TTS deltas may ship data without a mime; treat as raw PCM.
44
+ const { data } = delta;
45
+ if (typeof data === 'string' && data) {
46
+ return [{ type: 'media', media: { mimeType: 'audio/pcm', data } }];
47
+ }
48
+ return [];
49
+ }
38
50
  function eventsFromDelta(deltaValue) {
39
51
  const delta = asRecord(deltaValue);
40
52
  if (!delta) {
@@ -50,6 +62,9 @@ function eventsFromDelta(deltaValue) {
50
62
  if (deltaType === 'image') {
51
63
  return eventsFromImage(delta);
52
64
  }
65
+ if (deltaType === 'audio' || deltaType === 'output_audio') {
66
+ return eventsFromAudio(delta);
67
+ }
53
68
  return [];
54
69
  }
55
70
  function mediaFromRecord(rec) {
@@ -65,7 +80,7 @@ function mediaFromOutputItem(item) {
65
80
  if (!rec) {
66
81
  return undefined;
67
82
  }
68
- if (rec.type !== 'image' && rec.type !== 'media') {
83
+ if (rec.type !== 'image' && rec.type !== 'media' && rec.type !== 'audio') {
69
84
  return undefined;
70
85
  }
71
86
  return mediaFromRecord(rec);
@@ -84,13 +99,25 @@ function mediaFromOutputs(outputs) {
84
99
  }
85
100
  function mediaFromComplete(event) {
86
101
  const interaction = asRecord(event.interaction) ?? event;
87
- const direct = asRecord(interaction.output_image);
88
- if (direct) {
89
- const media = mediaFromRecord(direct);
102
+ const directImage = asRecord(interaction.output_image);
103
+ if (directImage) {
104
+ const media = mediaFromRecord(directImage);
90
105
  if (media) {
91
106
  return media;
92
107
  }
93
108
  }
109
+ const directAudio = asRecord(interaction.output_audio);
110
+ if (directAudio) {
111
+ const media = mediaFromRecord(directAudio);
112
+ if (media) {
113
+ return media;
114
+ }
115
+ // Google TTS convenience field: base64 PCM without mime.
116
+ const { data } = directAudio;
117
+ if (typeof data === 'string' && data) {
118
+ return { type: 'media', media: { mimeType: 'audio/pcm', data } };
119
+ }
120
+ }
94
121
  return mediaFromOutputs(interaction.outputs);
95
122
  }
96
123
  function sourceFromMaps(maps) {
@@ -1,9 +1,7 @@
1
1
  import type { Profile, TurnHistoryMessage, TurnRepairRequest } from '../types.js';
2
- declare const MAX_REPAIR_HISTORY_EXCHANGES = 2;
3
- declare function scopeHistory(history: TurnHistoryMessage[] | undefined): TurnHistoryMessage[];
4
2
  declare function synthesizeRepairPrompt(args: {
5
3
  profile: Profile;
6
4
  repair: TurnRepairRequest;
7
5
  history?: TurnHistoryMessage[];
8
6
  }): string;
9
- export { MAX_REPAIR_HISTORY_EXCHANGES, scopeHistory, synthesizeRepairPrompt };
7
+ export { synthesizeRepairPrompt };
@@ -35,4 +35,4 @@ function synthesizeRepairPrompt(args) {
35
35
  prompt += `3. Return only the corrected assistant output required by the active profile.`;
36
36
  return prompt;
37
37
  }
38
- export { MAX_REPAIR_HISTORY_EXCHANGES, scopeHistory, synthesizeRepairPrompt };
38
+ export { synthesizeRepairPrompt };
@@ -0,0 +1,4 @@
1
+ import type { ModelProvider, Profile, ResolvedGeneration, TurnEvent, TurnRequest } from '../../types.js';
2
+ import type { StepExecutionState } from './state.js';
3
+ declare function runAttemptsWithValidation(safe: TurnRequest, profile: Profile, generation: ResolvedGeneration, system: string, provider: ModelProvider, gemini: Record<string, unknown>[], state: StepExecutionState): AsyncGenerator<TurnEvent>;
4
+ export { runAttemptsWithValidation };
@@ -0,0 +1,206 @@
1
+ import { publicError } from '../../../guardrails/error.js';
2
+ import { sanitizeTurnRequest } from '../../../guardrails/sanitize.js';
3
+ import { resolveTurn } from '../../registry/resolve.js';
4
+ import { executeAttempt } from './steps.js';
5
+ function collectAttemptText(events) {
6
+ return events
7
+ .filter((e) => e.type === 'text' && e.text)
8
+ .map((e) => e.text)
9
+ .join('');
10
+ }
11
+ function buildRepairRequest(safe, previousOutput, rejection, repairGuidance) {
12
+ return {
13
+ ...safe,
14
+ input: {
15
+ ...safe.input,
16
+ repair: {
17
+ previousOutput: typeof previousOutput === 'string' ? previousOutput : JSON.stringify(previousOutput),
18
+ rejection,
19
+ guidance: repairGuidance,
20
+ },
21
+ },
22
+ };
23
+ }
24
+ function hasValidatableOutput(validation, latestStructured) {
25
+ if (!validation || latestStructured === undefined) {
26
+ return false;
27
+ }
28
+ const candidateOutput = validation.extract?.(latestStructured) ?? latestStructured;
29
+ return candidateOutput !== undefined && candidateOutput !== null;
30
+ }
31
+ async function evaluateValidationAttempt(validation, latestStructured, slots) {
32
+ const candidateOutput = validation.extract?.(latestStructured) ?? latestStructured;
33
+ const check = await validation.validate(candidateOutput, slots);
34
+ const error = check.error || check.finding || 'Validation failed';
35
+ return { candidateOutput, isValid: Boolean(check.isValid), error };
36
+ }
37
+ async function evaluateEgressOutcome(args) {
38
+ const { egress, attemptEvents, generation, request, profile, canRetry } = args;
39
+ const attemptText = collectAttemptText(attemptEvents);
40
+ const result = await egress.enforce({
41
+ text: attemptText,
42
+ canary: generation.canary,
43
+ slots: request.input?.slots,
44
+ profile,
45
+ role: request.input?.role,
46
+ });
47
+ if (!result.blocked) {
48
+ return { action: 'pass' };
49
+ }
50
+ if (egress.onBlock === 'refuse_to_user') {
51
+ return { action: 'refusal', event: { type: 'text', text: result.text } };
52
+ }
53
+ if (canRetry) {
54
+ const rejectionMsg = result.rejectionMessage || 'Egress disclosure violation detected.';
55
+ const repairGuidance = egress.repairGuidance ||
56
+ 'Rewrite the message as corrected user-visible prose only. Keep the same helpful substance; scrub all internal tool names, leak phrases, and disclosure markers.';
57
+ const nextRequest = buildRepairRequest(request, attemptText, rejectionMsg, repairGuidance);
58
+ return { action: 'retry', nextRequest };
59
+ }
60
+ return {
61
+ action: 'withhold',
62
+ event: {
63
+ type: 'error',
64
+ error: publicError('Turn withheld: egress disclosure violation'),
65
+ },
66
+ };
67
+ }
68
+ async function evaluateValidationOutcome(args) {
69
+ const { validation, latestStructured, request, canRetry } = args;
70
+ if (!hasValidatableOutput(validation, latestStructured)) {
71
+ return { action: 'pass' };
72
+ }
73
+ const { candidateOutput, isValid, error } = await evaluateValidationAttempt(validation, latestStructured, request.input?.slots);
74
+ if (isValid) {
75
+ return { action: 'pass' };
76
+ }
77
+ if (canRetry) {
78
+ const nextRequest = buildRepairRequest(request, candidateOutput, error, validation.repairGuidance);
79
+ return { action: 'retry', nextRequest };
80
+ }
81
+ return {
82
+ action: 'accept',
83
+ event: { type: 'structured', structured: latestStructured },
84
+ };
85
+ }
86
+ function* yieldBufferedAttemptEvents(events) {
87
+ for (const ev of events) {
88
+ if (ev.type !== 'tokens') {
89
+ yield ev;
90
+ }
91
+ }
92
+ }
93
+ function updateFlowForRetry(flow, nextReq) {
94
+ flow.currentAttempt++;
95
+ flow.currentReq = nextReq;
96
+ flow.currentGen = resolveTurn(sanitizeTurnRequest(nextReq)).generation;
97
+ }
98
+ async function* handleEgressGate(egress, flow, state, profile, maxRetries) {
99
+ const canRetry = flow.currentAttempt < maxRetries;
100
+ const outcome = await evaluateEgressOutcome({
101
+ egress,
102
+ attemptEvents: state.attemptEvents,
103
+ generation: flow.currentGen,
104
+ request: flow.currentReq,
105
+ profile,
106
+ canRetry,
107
+ });
108
+ if (outcome.action === 'refusal') {
109
+ state.allEmittedEvents.push(outcome.event);
110
+ yield outcome.event;
111
+ return 'terminal';
112
+ }
113
+ if (outcome.action === 'withhold') {
114
+ yield outcome.event;
115
+ return 'terminal';
116
+ }
117
+ if (outcome.action === 'retry') {
118
+ updateFlowForRetry(flow, outcome.nextRequest);
119
+ return 'continue';
120
+ }
121
+ return 'pass';
122
+ }
123
+ async function* handleValidationGate(validation, flow, state, latestStructured, maxRetries) {
124
+ const canRetry = flow.currentAttempt < maxRetries;
125
+ const outcome = await evaluateValidationOutcome({
126
+ validation,
127
+ latestStructured,
128
+ request: flow.currentReq,
129
+ canRetry,
130
+ });
131
+ if (outcome.action === 'retry') {
132
+ updateFlowForRetry(flow, outcome.nextRequest);
133
+ return 'continue';
134
+ }
135
+ if (outcome.action === 'accept') {
136
+ state.allEmittedEvents.push(outcome.event);
137
+ yield outcome.event;
138
+ return 'terminal';
139
+ }
140
+ return 'pass';
141
+ }
142
+ function gateStatusToAction(status, terminalStatus = 'terminal') {
143
+ if (status === 'terminal') {
144
+ return { status: terminalStatus };
145
+ }
146
+ if (status === 'continue') {
147
+ return { status: 'continue' };
148
+ }
149
+ return null;
150
+ }
151
+ async function* executeSingleAttemptCycle(args) {
152
+ const { flow, state, profile, system, provider, gemini, maxRetries } = args;
153
+ const validation = profile.outputs.validation;
154
+ const egress = profile.guardrails.egress;
155
+ state.attemptEvents = [];
156
+ const { latestStructured } = yield* executeAttempt({
157
+ safe: flow.currentReq,
158
+ profile,
159
+ generation: flow.currentGen,
160
+ system,
161
+ provider,
162
+ gemini,
163
+ state,
164
+ });
165
+ if (egress?.enforce) {
166
+ const status = yield* handleEgressGate(egress, flow, state, profile, maxRetries);
167
+ const action = gateStatusToAction(status, 'terminal');
168
+ if (action) {
169
+ return action;
170
+ }
171
+ }
172
+ if (validation) {
173
+ const status = yield* handleValidationGate(validation, flow, state, latestStructured, maxRetries);
174
+ const action = gateStatusToAction(status, 'success');
175
+ if (action) {
176
+ return action;
177
+ }
178
+ }
179
+ if (validation || egress?.enforce) {
180
+ yield* yieldBufferedAttemptEvents(state.attemptEvents);
181
+ }
182
+ return { status: 'success' };
183
+ }
184
+ async function* runAttemptsWithValidation(safe, profile, generation, system, provider, gemini, state) {
185
+ const maxRetries = Math.max(profile.outputs.validation?.maxRetries ?? 0, profile.guardrails.egress?.maxRetries ?? 2);
186
+ const flow = {
187
+ currentAttempt: 0,
188
+ currentGen: generation,
189
+ currentReq: safe,
190
+ };
191
+ while (flow.currentAttempt <= maxRetries) {
192
+ const step = yield* executeSingleAttemptCycle({
193
+ flow,
194
+ state,
195
+ profile,
196
+ system,
197
+ provider,
198
+ gemini,
199
+ maxRetries,
200
+ });
201
+ if (step.status === 'terminal' || step.status === 'success') {
202
+ break;
203
+ }
204
+ }
205
+ }
206
+ export { runAttemptsWithValidation };