theorum 0.1.2

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 (87) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +338 -0
  3. package/docs/AGENT_PROFILE_CONTRACT.md +161 -0
  4. package/docs/CLI_SPEC.md +183 -0
  5. package/docs/SECRETS.md +55 -0
  6. package/esm/_dnt.polyfills.d.ts +11 -0
  7. package/esm/_dnt.polyfills.js +15 -0
  8. package/esm/_dnt.shims.d.ts +5 -0
  9. package/esm/_dnt.shims.js +61 -0
  10. package/esm/mod.d.ts +37 -0
  11. package/esm/mod.js +35 -0
  12. package/esm/package.json +3 -0
  13. package/esm/src/guardrails/error.d.ts +35 -0
  14. package/esm/src/guardrails/error.js +116 -0
  15. package/esm/src/guardrails/injection.d.ts +12 -0
  16. package/esm/src/guardrails/injection.js +220 -0
  17. package/esm/src/guardrails/keys.d.ts +12 -0
  18. package/esm/src/guardrails/keys.js +132 -0
  19. package/esm/src/guardrails/mod.d.ts +14 -0
  20. package/esm/src/guardrails/mod.js +14 -0
  21. package/esm/src/guardrails/sanitize.d.ts +22 -0
  22. package/esm/src/guardrails/sanitize.js +133 -0
  23. package/esm/src/guardrails/sensitive.d.ts +12 -0
  24. package/esm/src/guardrails/sensitive.js +88 -0
  25. package/esm/src/kernel/engine/boundary.d.ts +10 -0
  26. package/esm/src/kernel/engine/boundary.js +55 -0
  27. package/esm/src/kernel/engine/delta.d.ts +8 -0
  28. package/esm/src/kernel/engine/delta.js +362 -0
  29. package/esm/src/kernel/engine/hash.d.ts +1 -0
  30. package/esm/src/kernel/engine/hash.js +9 -0
  31. package/esm/src/kernel/engine/record.d.ts +2 -0
  32. package/esm/src/kernel/engine/record.js +7 -0
  33. package/esm/src/kernel/engine/repair.d.ts +9 -0
  34. package/esm/src/kernel/engine/repair.js +38 -0
  35. package/esm/src/kernel/engine/runner.d.ts +14 -0
  36. package/esm/src/kernel/engine/runner.js +731 -0
  37. package/esm/src/kernel/engine/tree.d.ts +2 -0
  38. package/esm/src/kernel/engine/tree.js +17 -0
  39. package/esm/src/kernel/mod.d.ts +16 -0
  40. package/esm/src/kernel/mod.js +15 -0
  41. package/esm/src/kernel/registry/catalog.d.ts +24 -0
  42. package/esm/src/kernel/registry/catalog.js +213 -0
  43. package/esm/src/kernel/registry/profiles.d.ts +36 -0
  44. package/esm/src/kernel/registry/profiles.js +111 -0
  45. package/esm/src/kernel/registry/resolve.d.ts +20 -0
  46. package/esm/src/kernel/registry/resolve.js +235 -0
  47. package/esm/src/kernel/registry/schemas.d.ts +14 -0
  48. package/esm/src/kernel/registry/schemas.js +23 -0
  49. package/esm/src/kernel/registry/tools.d.ts +12 -0
  50. package/esm/src/kernel/registry/tools.js +36 -0
  51. package/esm/src/kernel/types.d.ts +497 -0
  52. package/esm/src/kernel/types.js +10 -0
  53. package/esm/src/observability/mod.d.ts +12 -0
  54. package/esm/src/observability/mod.js +10 -0
  55. package/esm/src/observability/spans.d.ts +16 -0
  56. package/esm/src/observability/spans.js +56 -0
  57. package/esm/src/observability/trace-attach.d.ts +16 -0
  58. package/esm/src/observability/trace-attach.js +81 -0
  59. package/esm/src/observability/trace-record.d.ts +112 -0
  60. package/esm/src/observability/trace-record.js +140 -0
  61. package/esm/src/observability/trace-usage.d.ts +3 -0
  62. package/esm/src/observability/trace-usage.js +32 -0
  63. package/esm/src/observability/trace.d.ts +23 -0
  64. package/esm/src/observability/trace.js +121 -0
  65. package/esm/src/providers/attachments.d.ts +17 -0
  66. package/esm/src/providers/attachments.js +156 -0
  67. package/esm/src/providers/gemini-tape.d.ts +3 -0
  68. package/esm/src/providers/gemini-tape.js +46 -0
  69. package/esm/src/providers/google-tap.d.ts +3 -0
  70. package/esm/src/providers/google-tap.js +48 -0
  71. package/esm/src/providers/interactions.d.ts +5 -0
  72. package/esm/src/providers/interactions.js +127 -0
  73. package/esm/src/providers/media.d.ts +5 -0
  74. package/esm/src/providers/media.js +125 -0
  75. package/esm/src/providers/mod.d.ts +15 -0
  76. package/esm/src/providers/mod.js +13 -0
  77. package/esm/src/providers/openrouter-payload.d.ts +24 -0
  78. package/esm/src/providers/openrouter-payload.js +177 -0
  79. package/esm/src/providers/openrouter.d.ts +17 -0
  80. package/esm/src/providers/openrouter.js +332 -0
  81. package/esm/src/providers/provider.d.ts +13 -0
  82. package/esm/src/providers/provider.js +123 -0
  83. package/esm/src/providers/sse.d.ts +7 -0
  84. package/esm/src/providers/sse.js +53 -0
  85. package/esm/src/providers/tts.d.ts +24 -0
  86. package/esm/src/providers/tts.js +144 -0
  87. package/package.json +48 -0
@@ -0,0 +1,731 @@
1
+ /**
2
+ * Deterministic turn runner for THEORUM.
3
+ *
4
+ * `runTurn` resolves a profile, sanitizes input, binds canary boundaries,
5
+ * streams provider events, executes allowed tools, applies validation and
6
+ * egress repair loops, writes traces, and emits one terminal `done` event.
7
+ *
8
+ * @module
9
+ */
10
+ import { publicError } from '../../guardrails/error.js';
11
+ import { sanitizeTurnRequest } from '../../guardrails/sanitize.js';
12
+ import { noopSink, writeTrace } from '../../observability/trace.js';
13
+ import { buildRecord } from '../../observability/trace-record.js';
14
+ import { CATALOG } from '../registry/catalog.js';
15
+ import { pickSystemRole, resolveTurn } from '../registry/resolve.js';
16
+ import { executeTool } from '../registry/tools.js';
17
+ import { bindCanary, eventHasCanary, redactCanary } from './boundary.js';
18
+ function* invokeFromUi(profile, req) {
19
+ const invoke = req.toolInvoke;
20
+ if (!invoke) {
21
+ return;
22
+ }
23
+ try {
24
+ const result = executeTool(profile, invoke.name, invoke.arguments);
25
+ yield {
26
+ type: 'tool',
27
+ tool: { name: invoke.name, arguments: invoke.arguments, result },
28
+ };
29
+ if (result.status === 'error') {
30
+ yield { type: 'error', error: publicError(formatToolFinding(result)) };
31
+ return;
32
+ }
33
+ yield { type: 'done' };
34
+ }
35
+ catch (err) {
36
+ yield { type: 'error', error: publicError(err) };
37
+ yield { type: 'done' };
38
+ }
39
+ }
40
+ function executeCustomModelTool(profile, name, args) {
41
+ try {
42
+ const result = executeTool(profile, name, args);
43
+ const events = [
44
+ {
45
+ type: 'tool',
46
+ tool: { name, arguments: args, result },
47
+ },
48
+ ];
49
+ if (result.status === 'error') {
50
+ events.push({ type: 'error', error: publicError(formatToolFinding(result)) });
51
+ }
52
+ return events;
53
+ }
54
+ catch (err) {
55
+ return [{ type: 'error', error: publicError(err) }];
56
+ }
57
+ }
58
+ function dispatchModelTool(profile, event, gated) {
59
+ const { tool } = event;
60
+ if (!tool) {
61
+ return [];
62
+ }
63
+ const name = tool.name;
64
+ if (CATALOG.tools[name]?.kind === 'builtin') {
65
+ return [event];
66
+ }
67
+ if (!gated.includes(name)) {
68
+ return [
69
+ {
70
+ type: 'error',
71
+ error: publicError(`Tool '${name}' is not gated on this turn`),
72
+ },
73
+ ];
74
+ }
75
+ const args = tool.arguments ?? {};
76
+ return executeCustomModelTool(profile, name, args);
77
+ }
78
+ function systemFromProfile(profile, role) {
79
+ const { identity } = profile;
80
+ const { systemByRole, system } = identity;
81
+ if (systemByRole) {
82
+ const byRole = systemByRole[role];
83
+ if (byRole) {
84
+ return byRole;
85
+ }
86
+ }
87
+ if (system) {
88
+ return system;
89
+ }
90
+ return '';
91
+ }
92
+ function* interceptProviderTool(event, profile, generation) {
93
+ const tool = event.tool;
94
+ if (!tool) {
95
+ return;
96
+ }
97
+ const isDynamic = generation.dynamicTools?.some((d) => d.name === tool.name);
98
+ if (isDynamic) {
99
+ yield event;
100
+ }
101
+ else {
102
+ for (const item of dispatchModelTool(profile, event, generation.custom)) {
103
+ yield item;
104
+ }
105
+ }
106
+ }
107
+ function shouldSkipStreamEvent(event, profile) {
108
+ return event.type === 'thought' && profile.outputs.streaming?.streamThoughts === false;
109
+ }
110
+ function* processNormalEvent(event, profile, generation) {
111
+ if (event.type === 'tool') {
112
+ yield* interceptProviderTool(event, profile, generation);
113
+ }
114
+ else if (event.type === 'error') {
115
+ yield { type: 'error', error: publicError(event.error) };
116
+ }
117
+ else {
118
+ yield event;
119
+ }
120
+ }
121
+ async function* yieldProviderEvents(args) {
122
+ const { profile, generation, system, provider, gemini } = args;
123
+ const { canary } = generation;
124
+ for await (const event of provider.complete({
125
+ model: generation.model,
126
+ previousInteractionId: generation.previousInteractionId,
127
+ store: generation.store,
128
+ thinking: generation.thinking,
129
+ summaries: generation.summaries,
130
+ maxOutputTokens: generation.maxOutputTokens,
131
+ temperature: generation.temperature,
132
+ builtins: generation.builtins,
133
+ system,
134
+ input: generation.input,
135
+ history: generation.history,
136
+ dynamicTools: generation.dynamicTools,
137
+ dynamicToolLoader: generation.dynamicToolLoader,
138
+ structured: generation.structured,
139
+ image: generation.image,
140
+ voice: generation.voice,
141
+ geminiBucket: generation.geminiBucket,
142
+ tapGemini: (row) => {
143
+ gemini.push(row);
144
+ },
145
+ })) {
146
+ if (canary && eventHasCanary(event, canary)) {
147
+ yield redactCanary(event, canary);
148
+ yield { type: 'error', error: publicError('canary leaked') };
149
+ return;
150
+ }
151
+ yield* processNormalEvent(event, profile, generation);
152
+ }
153
+ }
154
+ function isStepLimitReached(step, maxSteps) {
155
+ if (maxSteps <= 0) {
156
+ return false;
157
+ }
158
+ return step >= maxSteps;
159
+ }
160
+ function findDynamicDeclaration(tools, name) {
161
+ return tools?.find((t) => t.name === name);
162
+ }
163
+ function formatToolFinding(res) {
164
+ if (res.finding) {
165
+ return res.finding;
166
+ }
167
+ if (res.data) {
168
+ return JSON.stringify(res.data);
169
+ }
170
+ return 'ok';
171
+ }
172
+ function collectAttemptText(events) {
173
+ return events
174
+ .filter((e) => e.type === 'text' && e.text)
175
+ .map((e) => e.text)
176
+ .join('');
177
+ }
178
+ async function checkDynamicAuthorization(decl, args, profile, sessionPermissions) {
179
+ if (!decl.canExecute) {
180
+ return null;
181
+ }
182
+ try {
183
+ const decision = await decl.canExecute({
184
+ args,
185
+ profile,
186
+ sessionPermissions,
187
+ });
188
+ if (typeof decision === 'boolean' && !decision) {
189
+ return {
190
+ status: 'error',
191
+ finding: `Tool '${decl.name}' execution not authorized.`,
192
+ };
193
+ }
194
+ if (typeof decision === 'object' && decision !== null) {
195
+ return decision;
196
+ }
197
+ return null;
198
+ }
199
+ catch (err) {
200
+ const msg = err instanceof Error ? err.message : String(err);
201
+ return {
202
+ status: 'error',
203
+ finding: `Authorization error for '${decl.name}': ${msg}`,
204
+ };
205
+ }
206
+ }
207
+ function checkDynamicPermissionTier(decl, args, sessionPermissions) {
208
+ if (decl.permissionTier === 'session_consent' || decl.permissionTier === 'always_confirm') {
209
+ const isGranted = sessionPermissions?.includes(decl.name) || sessionPermissions?.includes('*');
210
+ if (!isGranted) {
211
+ return {
212
+ status: 'pause',
213
+ finding: `Tool '${decl.name}' requires ${decl.permissionTier} authorization.`,
214
+ data: { tool: decl.name, permissionTier: decl.permissionTier, args },
215
+ };
216
+ }
217
+ }
218
+ return null;
219
+ }
220
+ async function runDynamicHandler(decl, args) {
221
+ if (!decl.handler) {
222
+ return {
223
+ status: 'ok',
224
+ finding: `${decl.name} accepted (no handler)`,
225
+ data: args,
226
+ };
227
+ }
228
+ try {
229
+ return await decl.handler(args);
230
+ }
231
+ catch (err) {
232
+ const msg = err instanceof Error ? err.message : String(err);
233
+ return { status: 'error', finding: msg };
234
+ }
235
+ }
236
+ async function executeDynamicTool(decl, args, profile, sessionPermissions) {
237
+ const authEnvelope = await checkDynamicAuthorization(decl, args, profile, sessionPermissions);
238
+ if (authEnvelope) {
239
+ return authEnvelope;
240
+ }
241
+ const permissionEnvelope = checkDynamicPermissionTier(decl, args, sessionPermissions);
242
+ if (permissionEnvelope) {
243
+ return permissionEnvelope;
244
+ }
245
+ return runDynamicHandler(decl, args);
246
+ }
247
+ function mergeDynamicTools(current, loaded) {
248
+ const merged = [...(current ?? [])];
249
+ for (const tool of loaded) {
250
+ const existing = merged.findIndex((item) => item.name === tool.name);
251
+ if (existing >= 0) {
252
+ merged[existing] = tool;
253
+ }
254
+ else {
255
+ merged.push(tool);
256
+ }
257
+ }
258
+ return merged;
259
+ }
260
+ async function executeDynamicToolLoader(args) {
261
+ const { decl, toolArgs, profile, generation } = args;
262
+ const loader = generation.dynamicToolLoader;
263
+ if (!loader) {
264
+ return {
265
+ status: 'error',
266
+ finding: `Tool '${decl.name}' is marked as a loader but no loader is configured.`,
267
+ };
268
+ }
269
+ const loaded = await loader({
270
+ name: decl.name,
271
+ args: toolArgs,
272
+ profile,
273
+ currentTools: generation.dynamicTools ?? [],
274
+ sessionPermissions: generation.sessionPermissions,
275
+ });
276
+ generation.dynamicTools = mergeDynamicTools(generation.dynamicTools, loaded);
277
+ return {
278
+ status: 'ok',
279
+ finding: `Loaded ${String(loaded.length)} dynamic tool schema(s).`,
280
+ data: { loadedTools: loaded.map((tool) => tool.name) },
281
+ };
282
+ }
283
+ function isActionableDynamicDeclaration(decl) {
284
+ return Boolean(decl?.handler || decl?.canExecute || decl?.permissionTier || decl?.loadsDynamicTools);
285
+ }
286
+ async function executeDynamicDeclaration(args) {
287
+ const { decl, toolArgs, profile, generation } = args;
288
+ const res = await executeDynamicTool(decl, toolArgs, profile, generation.sessionPermissions);
289
+ if (res.status === 'ok' && decl.loadsDynamicTools) {
290
+ return await executeDynamicToolLoader({
291
+ decl,
292
+ toolArgs,
293
+ profile,
294
+ generation,
295
+ });
296
+ }
297
+ return res;
298
+ }
299
+ function recordStepEvent(event, state) {
300
+ if (event.type === 'tokens') {
301
+ state.sawTokensEvent = true;
302
+ }
303
+ state.allEmittedEvents.push(event);
304
+ state.attemptEvents.push(event);
305
+ }
306
+ async function* executeAutonomousStep(args, state, bufferOutputs = false) {
307
+ const { profile, generation, system, provider, gemini } = args;
308
+ const genForStep = { ...generation, history: state.currentHistory };
309
+ const pendingTools = [];
310
+ let latestStructured;
311
+ for await (const event of yieldProviderEvents({
312
+ profile,
313
+ generation: genForStep,
314
+ system,
315
+ provider,
316
+ gemini,
317
+ })) {
318
+ if (event.type === 'structured') {
319
+ latestStructured = event.structured;
320
+ }
321
+ if (event.type === 'done') {
322
+ continue;
323
+ }
324
+ if (event.type === 'tool' && event.tool) {
325
+ pendingTools.push(event);
326
+ continue;
327
+ }
328
+ recordStepEvent(event, state);
329
+ if (!bufferOutputs || event.type === 'tokens') {
330
+ yield event;
331
+ }
332
+ }
333
+ return { pendingTools, latestStructured };
334
+ }
335
+ function appendToolTurnToHistory(history, toolEv, res) {
336
+ const tool = toolEv.tool;
337
+ if (!tool) {
338
+ return;
339
+ }
340
+ const callId = tool.id ?? `call_${tool.name}_${String(Date.now())}`;
341
+ history.push({
342
+ role: 'assistant',
343
+ tool_calls: [
344
+ {
345
+ id: callId,
346
+ type: 'function',
347
+ function: {
348
+ name: tool.name,
349
+ arguments: JSON.stringify(tool.arguments ?? {}),
350
+ },
351
+ },
352
+ ],
353
+ });
354
+ history.push({
355
+ role: 'tool',
356
+ tool_call_id: callId,
357
+ name: tool.name,
358
+ content: formatToolFinding(res),
359
+ });
360
+ }
361
+ function calculateInputChars(safe, system) {
362
+ const input = safe.input ?? {};
363
+ return ((input.text?.length ?? 0) +
364
+ (input.repair?.previousOutput?.length ?? 0) +
365
+ (system?.length ?? 800));
366
+ }
367
+ function calculateOutputChars(events) {
368
+ let outputChars = 0;
369
+ let thinkingChars = 0;
370
+ for (const e of events) {
371
+ if (e.type === 'text' && e.text)
372
+ outputChars += e.text.length;
373
+ if (e.type === 'structured' && e.structured) {
374
+ outputChars += JSON.stringify(e.structured).length;
375
+ }
376
+ if (e.type === 'thought' && e.text)
377
+ thinkingChars += e.text.length;
378
+ }
379
+ return { outputChars, thinkingChars };
380
+ }
381
+ function* calculateFallbackTokens(safe, system, events) {
382
+ const inputChars = calculateInputChars(safe, system);
383
+ const { outputChars, thinkingChars } = calculateOutputChars(events);
384
+ const inputTokens = Math.max(1, Math.round(inputChars / 4));
385
+ const outputTokens = Math.max(1, Math.round(outputChars / 4));
386
+ const thinkingTokens = thinkingChars ? Math.round(thinkingChars / 4) : 0;
387
+ yield {
388
+ type: 'tokens',
389
+ tokens: {
390
+ input: inputTokens,
391
+ output: outputTokens,
392
+ thinking: thinkingTokens,
393
+ toolUse: 0,
394
+ total: inputTokens + outputTokens + thinkingTokens,
395
+ },
396
+ };
397
+ }
398
+ async function* handlePendingDynamicTools(pendingTools, generation, profile, state) {
399
+ let hasRunnableHandler = false;
400
+ for (const toolEv of pendingTools) {
401
+ const tool = toolEv.tool;
402
+ if (!tool) {
403
+ continue;
404
+ }
405
+ const decl = findDynamicDeclaration(generation.dynamicTools, tool.name);
406
+ if (!isActionableDynamicDeclaration(decl)) {
407
+ state.allEmittedEvents.push(toolEv);
408
+ yield toolEv;
409
+ continue;
410
+ }
411
+ hasRunnableHandler = true;
412
+ const finalResult = await executeDynamicDeclaration({
413
+ decl,
414
+ toolArgs: tool.arguments ?? {},
415
+ profile,
416
+ generation,
417
+ });
418
+ const enrichedEvent = {
419
+ type: 'tool',
420
+ tool: { ...tool, result: finalResult },
421
+ };
422
+ state.allEmittedEvents.push(enrichedEvent);
423
+ yield enrichedEvent;
424
+ appendToolTurnToHistory(state.currentHistory, toolEv, finalResult);
425
+ }
426
+ return hasRunnableHandler;
427
+ }
428
+ async function* executeAttempt(args) {
429
+ const { profile, generation, system, provider, gemini, state } = args;
430
+ let latestStructured;
431
+ let pendingTools = [];
432
+ let stepInAttempt = 0;
433
+ const shouldBuffer = Boolean(profile.outputs.validation) || Boolean(profile.guardrails.egress?.enforce);
434
+ while (!isStepLimitReached(stepInAttempt, generation.maxSteps)) {
435
+ stepInAttempt++;
436
+ state.stepCount++;
437
+ const stepResult = yield* executeAutonomousStep({ profile, generation, system, provider, gemini }, state, shouldBuffer);
438
+ if (stepResult.latestStructured !== undefined) {
439
+ latestStructured = stepResult.latestStructured;
440
+ }
441
+ pendingTools = stepResult.pendingTools;
442
+ if (pendingTools.length === 0) {
443
+ break;
444
+ }
445
+ const hasRunnableHandler = yield* handlePendingDynamicTools(pendingTools, generation, profile, state);
446
+ if (!hasRunnableHandler) {
447
+ break;
448
+ }
449
+ }
450
+ return { pendingTools, latestStructured };
451
+ }
452
+ function buildRepairRequest(safe, previousOutput, rejection, repairGuidance) {
453
+ return {
454
+ ...safe,
455
+ input: {
456
+ ...safe.input,
457
+ repair: {
458
+ previousOutput: typeof previousOutput === 'string' ? previousOutput : JSON.stringify(previousOutput),
459
+ rejection,
460
+ guidance: repairGuidance,
461
+ },
462
+ },
463
+ };
464
+ }
465
+ function hasValidatableOutput(validation, latestStructured) {
466
+ if (!validation || latestStructured === undefined) {
467
+ return false;
468
+ }
469
+ const candidateOutput = validation.extract?.(latestStructured) ?? latestStructured;
470
+ return candidateOutput !== undefined && candidateOutput !== null;
471
+ }
472
+ async function evaluateValidationAttempt(validation, latestStructured, slots) {
473
+ const candidateOutput = validation.extract?.(latestStructured) ?? latestStructured;
474
+ const check = await validation.validate(candidateOutput, slots);
475
+ const error = check.error || check.finding || 'Validation failed';
476
+ return { candidateOutput, isValid: Boolean(check.isValid), error };
477
+ }
478
+ async function evaluateEgressOutcome(args) {
479
+ const { egress, attemptEvents, generation, request, profile, canRetry } = args;
480
+ const attemptText = collectAttemptText(attemptEvents);
481
+ const result = await egress.enforce({
482
+ text: attemptText,
483
+ canary: generation.canary,
484
+ slots: request.input?.slots,
485
+ profile,
486
+ role: request.input?.role,
487
+ });
488
+ if (!result.blocked) {
489
+ return { action: 'pass' };
490
+ }
491
+ if (egress.onBlock === 'refuse_to_user') {
492
+ return { action: 'refusal', event: { type: 'text', text: result.text } };
493
+ }
494
+ if (canRetry) {
495
+ const rejectionMsg = result.rejectionMessage || 'Egress disclosure violation detected.';
496
+ const repairGuidance = egress.repairGuidance ||
497
+ 'Rewrite the message as corrected user-visible prose only. Keep the same helpful substance; scrub all internal tool names, leak phrases, and disclosure markers.';
498
+ const nextRequest = buildRepairRequest(request, attemptText, rejectionMsg, repairGuidance);
499
+ return { action: 'retry', nextRequest };
500
+ }
501
+ return {
502
+ action: 'withhold',
503
+ event: {
504
+ type: 'error',
505
+ error: publicError('Turn withheld: egress disclosure violation'),
506
+ },
507
+ };
508
+ }
509
+ async function evaluateValidationOutcome(args) {
510
+ const { validation, latestStructured, request, canRetry } = args;
511
+ if (!hasValidatableOutput(validation, latestStructured)) {
512
+ return { action: 'pass' };
513
+ }
514
+ const { candidateOutput, isValid, error } = await evaluateValidationAttempt(validation, latestStructured, request.input?.slots);
515
+ if (isValid) {
516
+ return { action: 'pass' };
517
+ }
518
+ if (canRetry) {
519
+ const nextRequest = buildRepairRequest(request, candidateOutput, error, validation.repairGuidance);
520
+ return { action: 'retry', nextRequest };
521
+ }
522
+ return {
523
+ action: 'accept',
524
+ event: { type: 'structured', structured: latestStructured },
525
+ };
526
+ }
527
+ function* yieldBufferedAttemptEvents(events) {
528
+ for (const ev of events) {
529
+ if (ev.type !== 'tokens') {
530
+ yield ev;
531
+ }
532
+ }
533
+ }
534
+ function updateFlowForRetry(flow, nextReq) {
535
+ flow.currentAttempt++;
536
+ flow.currentReq = nextReq;
537
+ flow.currentGen = resolveTurn(sanitizeTurnRequest(nextReq)).generation;
538
+ }
539
+ async function* handleEgressGate(egress, flow, state, profile, maxRetries) {
540
+ const canRetry = flow.currentAttempt < maxRetries;
541
+ const outcome = await evaluateEgressOutcome({
542
+ egress,
543
+ attemptEvents: state.attemptEvents,
544
+ generation: flow.currentGen,
545
+ request: flow.currentReq,
546
+ profile,
547
+ canRetry,
548
+ });
549
+ if (outcome.action === 'refusal') {
550
+ state.allEmittedEvents.push(outcome.event);
551
+ yield outcome.event;
552
+ return 'terminal';
553
+ }
554
+ if (outcome.action === 'withhold') {
555
+ yield outcome.event;
556
+ return 'terminal';
557
+ }
558
+ if (outcome.action === 'retry') {
559
+ updateFlowForRetry(flow, outcome.nextRequest);
560
+ return 'continue';
561
+ }
562
+ return 'pass';
563
+ }
564
+ async function* handleValidationGate(validation, flow, state, latestStructured, maxRetries) {
565
+ const canRetry = flow.currentAttempt < maxRetries;
566
+ const outcome = await evaluateValidationOutcome({
567
+ validation,
568
+ latestStructured,
569
+ request: flow.currentReq,
570
+ canRetry,
571
+ });
572
+ if (outcome.action === 'retry') {
573
+ updateFlowForRetry(flow, outcome.nextRequest);
574
+ return 'continue';
575
+ }
576
+ if (outcome.action === 'accept') {
577
+ state.allEmittedEvents.push(outcome.event);
578
+ yield outcome.event;
579
+ return 'terminal';
580
+ }
581
+ return 'pass';
582
+ }
583
+ function gateStatusToAction(status, terminalStatus = 'terminal') {
584
+ if (status === 'terminal') {
585
+ return { status: terminalStatus };
586
+ }
587
+ if (status === 'continue') {
588
+ return { status: 'continue' };
589
+ }
590
+ return null;
591
+ }
592
+ async function* executeSingleAttemptCycle(args) {
593
+ const { flow, state, profile, system, provider, gemini, maxRetries } = args;
594
+ const validation = profile.outputs.validation;
595
+ const egress = profile.guardrails.egress;
596
+ state.attemptEvents = [];
597
+ const { latestStructured } = yield* executeAttempt({
598
+ safe: flow.currentReq,
599
+ profile,
600
+ generation: flow.currentGen,
601
+ system,
602
+ provider,
603
+ gemini,
604
+ state,
605
+ });
606
+ if (egress?.enforce) {
607
+ const status = yield* handleEgressGate(egress, flow, state, profile, maxRetries);
608
+ const action = gateStatusToAction(status, 'terminal');
609
+ if (action) {
610
+ return action;
611
+ }
612
+ }
613
+ if (validation) {
614
+ const status = yield* handleValidationGate(validation, flow, state, latestStructured, maxRetries);
615
+ const action = gateStatusToAction(status, 'success');
616
+ if (action) {
617
+ return action;
618
+ }
619
+ }
620
+ if (validation || egress?.enforce) {
621
+ yield* yieldBufferedAttemptEvents(state.attemptEvents);
622
+ }
623
+ return { status: 'success' };
624
+ }
625
+ async function* runAttemptsWithValidation(safe, profile, generation, system, provider, gemini, state) {
626
+ const maxRetries = Math.max(profile.outputs.validation?.maxRetries ?? 0, profile.guardrails.egress?.maxRetries ?? 2);
627
+ const flow = {
628
+ currentAttempt: 0,
629
+ currentGen: generation,
630
+ currentReq: safe,
631
+ };
632
+ while (flow.currentAttempt <= maxRetries) {
633
+ const step = yield* executeSingleAttemptCycle({
634
+ flow,
635
+ state,
636
+ profile,
637
+ system,
638
+ provider,
639
+ gemini,
640
+ maxRetries,
641
+ });
642
+ if (step.status === 'terminal' || step.status === 'success') {
643
+ break;
644
+ }
645
+ }
646
+ }
647
+ async function* emitTurn(args) {
648
+ const { safe, profile, generation, system, provider, gemini } = args;
649
+ if (safe.toolInvoke) {
650
+ yield* invokeFromUi(profile, safe);
651
+ return;
652
+ }
653
+ const state = {
654
+ currentHistory: [...(generation.history ?? [])],
655
+ stepCount: 0,
656
+ sawTokensEvent: false,
657
+ allEmittedEvents: [],
658
+ attemptEvents: [],
659
+ };
660
+ yield* runAttemptsWithValidation(safe, profile, generation, system, provider, gemini, state);
661
+ if (!state.sawTokensEvent) {
662
+ yield* calculateFallbackTokens(safe, system, state.allEmittedEvents);
663
+ }
664
+ yield { type: 'done' };
665
+ }
666
+ /** Execute one host turn against a provider adapter. */
667
+ async function* runTurn(req, provider, sink = noopSink()) {
668
+ const started = Date.now();
669
+ const seen = [];
670
+ const gemini = [];
671
+ let model;
672
+ let bucket;
673
+ let canary = '';
674
+ let system;
675
+ let generation;
676
+ try {
677
+ const safe = sanitizeTurnRequest(req);
678
+ const { profile, generation: gen } = resolveTurn(safe);
679
+ generation = gen;
680
+ const { model: resolvedModel, geminiBucket, canary: turnCanary } = gen;
681
+ model = resolvedModel;
682
+ bucket = geminiBucket;
683
+ canary = turnCanary;
684
+ const role = pickSystemRole(profile, safe.input?.role);
685
+ const profileSys = systemFromProfile(profile, role);
686
+ const combinedSys = [profileSys, safe.system].filter(Boolean).join('\n\n');
687
+ const bound = bindCanary(combinedSys, turnCanary);
688
+ system = bound;
689
+ for await (const event of emitTurn({
690
+ safe,
691
+ profile,
692
+ generation: gen,
693
+ system: bound,
694
+ provider,
695
+ gemini,
696
+ })) {
697
+ seen.push(event);
698
+ if (shouldSkipStreamEvent(event, profile)) {
699
+ continue;
700
+ }
701
+ yield event;
702
+ }
703
+ }
704
+ catch (err) {
705
+ await writeTrace(sink, buildRecord({
706
+ req,
707
+ events: seen,
708
+ started,
709
+ model,
710
+ bucket,
711
+ thrown: err,
712
+ gemini,
713
+ canary,
714
+ system,
715
+ generation,
716
+ }));
717
+ throw err;
718
+ }
719
+ await writeTrace(sink, buildRecord({
720
+ req,
721
+ events: seen,
722
+ started,
723
+ model,
724
+ bucket,
725
+ gemini,
726
+ canary,
727
+ system,
728
+ generation,
729
+ }));
730
+ }
731
+ export { runTurn };