theorum 0.1.2 → 0.1.4

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 (150) hide show
  1. package/README.md +83 -27
  2. package/deno.json +57 -0
  3. package/deno.lock +469 -0
  4. package/docs/AGENT_PROFILE_CONTRACT.md +45 -18
  5. package/docs/CLI_SPEC.md +2 -2
  6. package/docs/SECRETS.md +27 -22
  7. package/mod.ts +95 -0
  8. package/package.json +37 -29
  9. package/src/cli/commands/profile.ts +57 -0
  10. package/src/cli/commands/run.ts +66 -0
  11. package/src/cli/commands/test.ts +195 -0
  12. package/src/cli/index.ts +161 -0
  13. package/src/cli/matrix/fixtures.ts +96 -0
  14. package/src/cli/matrix/synthesizer.ts +240 -0
  15. package/src/guardrails/error.ts +142 -0
  16. package/src/guardrails/injection.ts +254 -0
  17. package/src/guardrails/mod.ts +40 -0
  18. package/src/guardrails/quota.ts +87 -0
  19. package/src/guardrails/sanitize.ts +165 -0
  20. package/src/guardrails/sensitive.ts +96 -0
  21. package/src/host/mint-trace.ts +54 -0
  22. package/src/host/mod.ts +20 -0
  23. package/src/host/reply.ts +32 -0
  24. package/src/kernel/engine/assert.ts +29 -0
  25. package/src/kernel/engine/boundary.ts +75 -0
  26. package/src/kernel/engine/delta.ts +440 -0
  27. package/src/kernel/engine/hash.ts +11 -0
  28. package/src/kernel/engine/record.ts +8 -0
  29. package/src/kernel/engine/repair.ts +56 -0
  30. package/src/kernel/engine/runner/gates.ts +338 -0
  31. package/src/kernel/engine/runner/mod.ts +135 -0
  32. package/src/kernel/engine/runner/state.ts +31 -0
  33. package/src/kernel/engine/runner/steps.ts +183 -0
  34. package/src/kernel/engine/runner/stream.ts +83 -0
  35. package/src/kernel/engine/runner/tokens.ts +47 -0
  36. package/src/kernel/engine/runner/tools.ts +266 -0
  37. package/src/kernel/engine/runner.ts +7 -0
  38. package/src/kernel/engine/tree.ts +18 -0
  39. package/src/kernel/mod.ts +39 -0
  40. package/src/kernel/registry/attachments.ts +195 -0
  41. package/src/kernel/registry/catalog.ts +181 -0
  42. package/src/kernel/registry/ingress.ts +223 -0
  43. package/src/kernel/registry/profiles.ts +165 -0
  44. package/src/kernel/registry/provider-request.ts +31 -0
  45. package/src/kernel/registry/resolve.ts +290 -0
  46. package/src/kernel/registry/schemas.ts +29 -0
  47. package/src/kernel/registry/tools.ts +45 -0
  48. package/src/kernel/registry/vault.ts +26 -0
  49. package/src/kernel/types.ts +576 -0
  50. package/{esm/src/observability/mod.js → src/observability/mod.ts} +11 -2
  51. package/src/observability/spans.ts +70 -0
  52. package/src/observability/trace-attach.ts +96 -0
  53. package/src/observability/trace-record.ts +254 -0
  54. package/src/observability/trace-usage.ts +36 -0
  55. package/src/observability/trace.ts +143 -0
  56. package/src/presets/google.ts +147 -0
  57. package/src/presets/mod.ts +24 -0
  58. package/src/providers/create-provider.ts +64 -0
  59. package/src/providers/gemini-tape.ts +59 -0
  60. package/src/providers/google-tap.ts +57 -0
  61. package/src/providers/interactions.ts +182 -0
  62. package/src/providers/keys.ts +182 -0
  63. package/src/providers/mod.ts +12 -0
  64. package/src/providers/openrouter-mod.ts +16 -0
  65. package/src/providers/openrouter-payload.ts +232 -0
  66. package/src/providers/openrouter.ts +758 -0
  67. package/src/providers/pcm.ts +37 -0
  68. package/src/providers/provider.ts +206 -0
  69. package/src/providers/speech.ts +180 -0
  70. package/src/providers/sse.ts +59 -0
  71. package/esm/_dnt.polyfills.d.ts +0 -11
  72. package/esm/_dnt.polyfills.js +0 -15
  73. package/esm/_dnt.shims.d.ts +0 -5
  74. package/esm/_dnt.shims.js +0 -61
  75. package/esm/mod.d.ts +0 -37
  76. package/esm/mod.js +0 -35
  77. package/esm/package.json +0 -3
  78. package/esm/src/guardrails/error.d.ts +0 -35
  79. package/esm/src/guardrails/error.js +0 -116
  80. package/esm/src/guardrails/injection.d.ts +0 -12
  81. package/esm/src/guardrails/injection.js +0 -220
  82. package/esm/src/guardrails/keys.d.ts +0 -12
  83. package/esm/src/guardrails/keys.js +0 -132
  84. package/esm/src/guardrails/mod.d.ts +0 -14
  85. package/esm/src/guardrails/mod.js +0 -14
  86. package/esm/src/guardrails/sanitize.d.ts +0 -22
  87. package/esm/src/guardrails/sanitize.js +0 -133
  88. package/esm/src/guardrails/sensitive.d.ts +0 -12
  89. package/esm/src/guardrails/sensitive.js +0 -88
  90. package/esm/src/kernel/engine/boundary.d.ts +0 -10
  91. package/esm/src/kernel/engine/boundary.js +0 -55
  92. package/esm/src/kernel/engine/delta.d.ts +0 -8
  93. package/esm/src/kernel/engine/delta.js +0 -362
  94. package/esm/src/kernel/engine/hash.d.ts +0 -1
  95. package/esm/src/kernel/engine/hash.js +0 -9
  96. package/esm/src/kernel/engine/record.d.ts +0 -2
  97. package/esm/src/kernel/engine/record.js +0 -7
  98. package/esm/src/kernel/engine/repair.d.ts +0 -9
  99. package/esm/src/kernel/engine/repair.js +0 -38
  100. package/esm/src/kernel/engine/runner.d.ts +0 -14
  101. package/esm/src/kernel/engine/runner.js +0 -731
  102. package/esm/src/kernel/engine/tree.d.ts +0 -2
  103. package/esm/src/kernel/engine/tree.js +0 -17
  104. package/esm/src/kernel/mod.d.ts +0 -16
  105. package/esm/src/kernel/mod.js +0 -15
  106. package/esm/src/kernel/registry/catalog.d.ts +0 -24
  107. package/esm/src/kernel/registry/catalog.js +0 -213
  108. package/esm/src/kernel/registry/profiles.d.ts +0 -36
  109. package/esm/src/kernel/registry/profiles.js +0 -111
  110. package/esm/src/kernel/registry/resolve.d.ts +0 -20
  111. package/esm/src/kernel/registry/resolve.js +0 -235
  112. package/esm/src/kernel/registry/schemas.d.ts +0 -14
  113. package/esm/src/kernel/registry/schemas.js +0 -23
  114. package/esm/src/kernel/registry/tools.d.ts +0 -12
  115. package/esm/src/kernel/registry/tools.js +0 -36
  116. package/esm/src/kernel/types.d.ts +0 -497
  117. package/esm/src/kernel/types.js +0 -10
  118. package/esm/src/observability/mod.d.ts +0 -12
  119. package/esm/src/observability/spans.d.ts +0 -16
  120. package/esm/src/observability/spans.js +0 -56
  121. package/esm/src/observability/trace-attach.d.ts +0 -16
  122. package/esm/src/observability/trace-attach.js +0 -81
  123. package/esm/src/observability/trace-record.d.ts +0 -112
  124. package/esm/src/observability/trace-record.js +0 -140
  125. package/esm/src/observability/trace-usage.d.ts +0 -3
  126. package/esm/src/observability/trace-usage.js +0 -32
  127. package/esm/src/observability/trace.d.ts +0 -23
  128. package/esm/src/observability/trace.js +0 -121
  129. package/esm/src/providers/attachments.d.ts +0 -17
  130. package/esm/src/providers/attachments.js +0 -156
  131. package/esm/src/providers/gemini-tape.d.ts +0 -3
  132. package/esm/src/providers/gemini-tape.js +0 -46
  133. package/esm/src/providers/google-tap.d.ts +0 -3
  134. package/esm/src/providers/google-tap.js +0 -48
  135. package/esm/src/providers/interactions.d.ts +0 -5
  136. package/esm/src/providers/interactions.js +0 -127
  137. package/esm/src/providers/media.d.ts +0 -5
  138. package/esm/src/providers/media.js +0 -125
  139. package/esm/src/providers/mod.d.ts +0 -15
  140. package/esm/src/providers/mod.js +0 -13
  141. package/esm/src/providers/openrouter-payload.d.ts +0 -24
  142. package/esm/src/providers/openrouter-payload.js +0 -177
  143. package/esm/src/providers/openrouter.d.ts +0 -17
  144. package/esm/src/providers/openrouter.js +0 -332
  145. package/esm/src/providers/provider.d.ts +0 -13
  146. package/esm/src/providers/provider.js +0 -123
  147. package/esm/src/providers/sse.d.ts +0 -7
  148. package/esm/src/providers/sse.js +0 -53
  149. package/esm/src/providers/tts.d.ts +0 -24
  150. package/esm/src/providers/tts.js +0 -144
@@ -0,0 +1,338 @@
1
+ import { publicError } from '../../../guardrails/error.ts';
2
+ import { sanitizeTurnRequest } from '../../../guardrails/sanitize.ts';
3
+ import { resolveTurn } from '../../registry/resolve.ts';
4
+ import type {
5
+ ModelProvider,
6
+ Profile,
7
+ ProfileOutputsSpec,
8
+ ResolvedGeneration,
9
+ TurnEvent,
10
+ TurnRequest,
11
+ } from '../../types.ts';
12
+ import type { AttemptFlowState, StepExecutionState } from './state.ts';
13
+ import { executeAttempt } from './steps.ts';
14
+
15
+ function collectAttemptText(events: TurnEvent[]): string {
16
+ return events
17
+ .filter((e) => e.type === 'text' && e.text)
18
+ .map((e) => e.text)
19
+ .join('');
20
+ }
21
+
22
+ function buildRepairRequest(
23
+ safe: TurnRequest,
24
+ previousOutput: unknown,
25
+ rejection: string,
26
+ repairGuidance?: string,
27
+ ): TurnRequest {
28
+ return {
29
+ ...safe,
30
+ input: {
31
+ ...safe.input,
32
+ repair: {
33
+ previousOutput:
34
+ typeof previousOutput === 'string' ? previousOutput : JSON.stringify(previousOutput),
35
+ rejection,
36
+ guidance: repairGuidance,
37
+ },
38
+ },
39
+ };
40
+ }
41
+
42
+ function hasValidatableOutput(
43
+ validation: ProfileOutputsSpec['validation'],
44
+ latestStructured: unknown,
45
+ ): boolean {
46
+ if (!validation || latestStructured === undefined) {
47
+ return false;
48
+ }
49
+ const candidateOutput = validation.extract?.(latestStructured) ?? latestStructured;
50
+ return candidateOutput !== undefined && candidateOutput !== null;
51
+ }
52
+
53
+ async function evaluateValidationAttempt(
54
+ validation: NonNullable<ProfileOutputsSpec['validation']>,
55
+ latestStructured: unknown,
56
+ slots: Record<string, string> | undefined,
57
+ ): Promise<{ candidateOutput: unknown; isValid: boolean; error: string }> {
58
+ const candidateOutput = validation.extract?.(latestStructured) ?? latestStructured;
59
+ const check = await validation.validate(candidateOutput, slots);
60
+ const error = check.error || check.finding || 'Validation failed';
61
+ return { candidateOutput, isValid: Boolean(check.isValid), error };
62
+ }
63
+
64
+ type EgressOutcome =
65
+ | { action: 'pass' }
66
+ | { action: 'refusal'; event: TurnEvent }
67
+ | { action: 'retry'; nextRequest: TurnRequest }
68
+ | { action: 'withhold'; event: TurnEvent };
69
+
70
+ async function evaluateEgressOutcome(args: {
71
+ egress: NonNullable<Profile['guardrails']['egress']>;
72
+ attemptEvents: TurnEvent[];
73
+ generation: ResolvedGeneration;
74
+ request: TurnRequest;
75
+ profile: Profile;
76
+ canRetry: boolean;
77
+ }): Promise<EgressOutcome> {
78
+ const { egress, attemptEvents, generation, request, profile, canRetry } = args;
79
+ const attemptText = collectAttemptText(attemptEvents);
80
+ const result = await egress.enforce({
81
+ text: attemptText,
82
+ canary: generation.canary,
83
+ slots: request.input?.slots,
84
+ profile,
85
+ role: request.input?.role,
86
+ });
87
+
88
+ if (!result.blocked) {
89
+ return { action: 'pass' };
90
+ }
91
+
92
+ if (egress.onBlock === 'refuse_to_user') {
93
+ return { action: 'refusal', event: { type: 'text', text: result.text } };
94
+ }
95
+
96
+ if (canRetry) {
97
+ const rejectionMsg = result.rejectionMessage || 'Egress disclosure violation detected.';
98
+ const repairGuidance =
99
+ egress.repairGuidance ||
100
+ 'Rewrite the message as corrected user-visible prose only. Keep the same helpful substance; scrub all internal tool names, leak phrases, and disclosure markers.';
101
+ const nextRequest = buildRepairRequest(request, attemptText, rejectionMsg, repairGuidance);
102
+ return { action: 'retry', nextRequest };
103
+ }
104
+
105
+ return {
106
+ action: 'withhold',
107
+ event: {
108
+ type: 'error',
109
+ error: publicError('Turn withheld: egress disclosure violation'),
110
+ },
111
+ };
112
+ }
113
+
114
+ type ValidationOutcome =
115
+ | { action: 'pass' }
116
+ | { action: 'retry'; nextRequest: TurnRequest }
117
+ | { action: 'accept'; event: TurnEvent };
118
+
119
+ async function evaluateValidationOutcome(args: {
120
+ validation: NonNullable<ProfileOutputsSpec['validation']>;
121
+ latestStructured: unknown;
122
+ request: TurnRequest;
123
+ canRetry: boolean;
124
+ }): Promise<ValidationOutcome> {
125
+ const { validation, latestStructured, request, canRetry } = args;
126
+ if (!hasValidatableOutput(validation, latestStructured)) {
127
+ return { action: 'pass' };
128
+ }
129
+
130
+ const { candidateOutput, isValid, error } = await evaluateValidationAttempt(
131
+ validation,
132
+ latestStructured,
133
+ request.input?.slots,
134
+ );
135
+
136
+ if (isValid) {
137
+ return { action: 'pass' };
138
+ }
139
+
140
+ if (canRetry) {
141
+ const nextRequest = buildRepairRequest(
142
+ request,
143
+ candidateOutput,
144
+ error,
145
+ validation.repairGuidance,
146
+ );
147
+ return { action: 'retry', nextRequest };
148
+ }
149
+
150
+ return {
151
+ action: 'accept',
152
+ event: { type: 'structured', structured: latestStructured },
153
+ };
154
+ }
155
+
156
+ function* yieldBufferedAttemptEvents(events: TurnEvent[]): Generator<TurnEvent> {
157
+ for (const ev of events) {
158
+ if (ev.type !== 'tokens') {
159
+ yield ev;
160
+ }
161
+ }
162
+ }
163
+
164
+ function updateFlowForRetry(flow: AttemptFlowState, nextReq: TurnRequest): void {
165
+ flow.currentAttempt++;
166
+ flow.currentReq = nextReq;
167
+ flow.currentGen = resolveTurn(sanitizeTurnRequest(nextReq)).generation;
168
+ }
169
+
170
+ async function* handleEgressGate(
171
+ egress: NonNullable<Profile['guardrails']['egress']>,
172
+ flow: AttemptFlowState,
173
+ state: StepExecutionState,
174
+ profile: Profile,
175
+ maxRetries: number,
176
+ ): AsyncGenerator<TurnEvent, 'continue' | 'terminal' | 'pass'> {
177
+ const canRetry = flow.currentAttempt < maxRetries;
178
+ const outcome = await evaluateEgressOutcome({
179
+ egress,
180
+ attemptEvents: state.attemptEvents,
181
+ generation: flow.currentGen,
182
+ request: flow.currentReq,
183
+ profile,
184
+ canRetry,
185
+ });
186
+
187
+ if (outcome.action === 'refusal') {
188
+ state.allEmittedEvents.push(outcome.event);
189
+ yield outcome.event;
190
+ return 'terminal';
191
+ }
192
+ if (outcome.action === 'withhold') {
193
+ yield outcome.event;
194
+ return 'terminal';
195
+ }
196
+ if (outcome.action === 'retry') {
197
+ updateFlowForRetry(flow, outcome.nextRequest);
198
+ return 'continue';
199
+ }
200
+ return 'pass';
201
+ }
202
+
203
+ async function* handleValidationGate(
204
+ validation: NonNullable<ProfileOutputsSpec['validation']>,
205
+ flow: AttemptFlowState,
206
+ state: StepExecutionState,
207
+ latestStructured: unknown,
208
+ maxRetries: number,
209
+ ): AsyncGenerator<TurnEvent, 'continue' | 'terminal' | 'pass'> {
210
+ const canRetry = flow.currentAttempt < maxRetries;
211
+ const outcome = await evaluateValidationOutcome({
212
+ validation,
213
+ latestStructured,
214
+ request: flow.currentReq,
215
+ canRetry,
216
+ });
217
+
218
+ if (outcome.action === 'retry') {
219
+ updateFlowForRetry(flow, outcome.nextRequest);
220
+ return 'continue';
221
+ }
222
+ if (outcome.action === 'accept') {
223
+ state.allEmittedEvents.push(outcome.event);
224
+ yield outcome.event;
225
+ return 'terminal';
226
+ }
227
+ return 'pass';
228
+ }
229
+
230
+ type AttemptStepAction =
231
+ | { status: 'terminal' }
232
+ | { status: 'continue' }
233
+ | {
234
+ status: 'success';
235
+ };
236
+
237
+ function gateStatusToAction(
238
+ status: 'continue' | 'terminal' | 'pass',
239
+ terminalStatus: 'terminal' | 'success' = 'terminal',
240
+ ): AttemptStepAction | null {
241
+ if (status === 'terminal') {
242
+ return { status: terminalStatus };
243
+ }
244
+ if (status === 'continue') {
245
+ return { status: 'continue' };
246
+ }
247
+ return null;
248
+ }
249
+
250
+ async function* executeSingleAttemptCycle(args: {
251
+ flow: AttemptFlowState;
252
+ state: StepExecutionState;
253
+ profile: Profile;
254
+ system: string;
255
+ provider: ModelProvider;
256
+ gemini: Record<string, unknown>[];
257
+ maxRetries: number;
258
+ }): AsyncGenerator<TurnEvent, AttemptStepAction> {
259
+ const { flow, state, profile, system, provider, gemini, maxRetries } = args;
260
+ const validation = profile.outputs.validation;
261
+ const egress = profile.guardrails.egress;
262
+
263
+ state.attemptEvents = [];
264
+ const { latestStructured } = yield* executeAttempt({
265
+ safe: flow.currentReq,
266
+ profile,
267
+ generation: flow.currentGen,
268
+ system,
269
+ provider,
270
+ gemini,
271
+ state,
272
+ });
273
+
274
+ if (egress?.enforce) {
275
+ const status = yield* handleEgressGate(egress, flow, state, profile, maxRetries);
276
+ const action = gateStatusToAction(status, 'terminal');
277
+ if (action) {
278
+ return action;
279
+ }
280
+ }
281
+
282
+ if (validation) {
283
+ const status = yield* handleValidationGate(
284
+ validation,
285
+ flow,
286
+ state,
287
+ latestStructured,
288
+ maxRetries,
289
+ );
290
+ const action = gateStatusToAction(status, 'success');
291
+ if (action) {
292
+ return action;
293
+ }
294
+ }
295
+
296
+ if (validation || egress?.enforce) {
297
+ yield* yieldBufferedAttemptEvents(state.attemptEvents);
298
+ }
299
+
300
+ return { status: 'success' };
301
+ }
302
+
303
+ async function* runAttemptsWithValidation(
304
+ safe: TurnRequest,
305
+ profile: Profile,
306
+ generation: ResolvedGeneration,
307
+ system: string,
308
+ provider: ModelProvider,
309
+ gemini: Record<string, unknown>[],
310
+ state: StepExecutionState,
311
+ ): AsyncGenerator<TurnEvent> {
312
+ const maxRetries = Math.max(
313
+ profile.outputs.validation?.maxRetries ?? 0,
314
+ profile.guardrails.egress?.maxRetries ?? 2,
315
+ );
316
+ const flow: AttemptFlowState = {
317
+ currentAttempt: 0,
318
+ currentGen: generation,
319
+ currentReq: safe,
320
+ };
321
+
322
+ while (flow.currentAttempt <= maxRetries) {
323
+ const step = yield* executeSingleAttemptCycle({
324
+ flow,
325
+ state,
326
+ profile,
327
+ system,
328
+ provider,
329
+ gemini,
330
+ maxRetries,
331
+ });
332
+ if (step.status === 'terminal' || step.status === 'success') {
333
+ break;
334
+ }
335
+ }
336
+ }
337
+
338
+ export { runAttemptsWithValidation };
@@ -0,0 +1,135 @@
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
+
11
+ import { sanitizeTurnRequest } from '../../../guardrails/sanitize.ts';
12
+ import { noopSink, type TraceSink, writeTrace } from '../../../observability/trace.ts';
13
+ import { buildRecord } from '../../../observability/trace-record.ts';
14
+ import { pickSystemRole, resolveTurn } from '../../registry/resolve.ts';
15
+ import type {
16
+ ModelProvider,
17
+ Profile,
18
+ ResolvedGeneration,
19
+ TurnEvent,
20
+ TurnRequest,
21
+ } from '../../types.ts';
22
+ import { bindCanary } from '../boundary.ts';
23
+ import { runAttemptsWithValidation } from './gates.ts';
24
+ import type { StepExecutionState } from './state.ts';
25
+ import { shouldSkipStreamEvent, systemFromProfile } from './stream.ts';
26
+ import { calculateFallbackTokens } from './tokens.ts';
27
+ import { invokeFromUi } from './tools.ts';
28
+
29
+ async function* emitTurn(args: {
30
+ safe: TurnRequest;
31
+ profile: Profile;
32
+ generation: ResolvedGeneration;
33
+ system: string;
34
+ provider: ModelProvider;
35
+ gemini: Record<string, unknown>[];
36
+ }): AsyncGenerator<TurnEvent> {
37
+ const { safe, profile, generation, system, provider, gemini } = args;
38
+ if (safe.toolInvoke) {
39
+ yield* invokeFromUi(profile, safe);
40
+ return;
41
+ }
42
+
43
+ const state: StepExecutionState = {
44
+ currentHistory: [...(generation.history ?? [])],
45
+ stepCount: 0,
46
+ sawTokensEvent: false,
47
+ allEmittedEvents: [],
48
+ attemptEvents: [],
49
+ };
50
+
51
+ yield* runAttemptsWithValidation(safe, profile, generation, system, provider, gemini, state);
52
+
53
+ if (!state.sawTokensEvent) {
54
+ yield* calculateFallbackTokens(safe, system, state.allEmittedEvents);
55
+ }
56
+
57
+ yield { type: 'done' };
58
+ }
59
+
60
+ /** Execute one host turn against a provider adapter. */
61
+ async function* runTurn(
62
+ req: TurnRequest,
63
+ provider: ModelProvider,
64
+ sink: TraceSink = noopSink(),
65
+ ): AsyncGenerator<TurnEvent> {
66
+ const started = Date.now();
67
+ const seen: TurnEvent[] = [];
68
+ const gemini: Record<string, unknown>[] = [];
69
+ let model: string | undefined;
70
+ let bucket: string | undefined;
71
+ let canary = '';
72
+ let system: string | undefined;
73
+ let generation: ResolvedGeneration | undefined;
74
+ try {
75
+ const safe = sanitizeTurnRequest(req);
76
+ const { profile, generation: gen } = resolveTurn(safe);
77
+ generation = gen;
78
+ const { model: resolvedModel, geminiBucket, canary: turnCanary } = gen;
79
+ model = resolvedModel;
80
+ bucket = geminiBucket;
81
+ canary = turnCanary;
82
+ const role = pickSystemRole(profile, safe.input?.role);
83
+ const profileSys = systemFromProfile(profile, role);
84
+ const combinedSys = [profileSys, safe.system].filter(Boolean).join('\n\n');
85
+ const bound = bindCanary(combinedSys, turnCanary);
86
+ system = bound;
87
+ for await (const event of emitTurn({
88
+ safe,
89
+ profile,
90
+ generation: gen,
91
+ system: bound,
92
+ provider,
93
+ gemini,
94
+ })) {
95
+ seen.push(event);
96
+ if (shouldSkipStreamEvent(event, profile)) {
97
+ continue;
98
+ }
99
+ yield event;
100
+ }
101
+ } catch (err) {
102
+ await writeTrace(
103
+ sink,
104
+ buildRecord({
105
+ req,
106
+ events: seen,
107
+ started,
108
+ model,
109
+ bucket,
110
+ thrown: err,
111
+ gemini,
112
+ canary,
113
+ system,
114
+ generation,
115
+ }),
116
+ );
117
+ throw err;
118
+ }
119
+ await writeTrace(
120
+ sink,
121
+ buildRecord({
122
+ req,
123
+ events: seen,
124
+ started,
125
+ model,
126
+ bucket,
127
+ gemini,
128
+ canary,
129
+ system,
130
+ generation,
131
+ }),
132
+ );
133
+ }
134
+
135
+ export { runTurn };
@@ -0,0 +1,31 @@
1
+ import type {
2
+ ResolvedGeneration,
3
+ TurnEvent,
4
+ TurnHistoryMessage,
5
+ TurnRequest,
6
+ } from '../../types.ts';
7
+
8
+ interface StepExecutionState {
9
+ currentHistory: TurnHistoryMessage[];
10
+ stepCount: number;
11
+ sawTokensEvent: boolean;
12
+ allEmittedEvents: TurnEvent[];
13
+ attemptEvents: TurnEvent[];
14
+ }
15
+
16
+ interface AttemptFlowState {
17
+ currentAttempt: number;
18
+ currentReq: TurnRequest;
19
+ currentGen: ResolvedGeneration;
20
+ }
21
+
22
+ function recordStepEvent(event: TurnEvent, state: StepExecutionState): void {
23
+ if (event.type === 'tokens') {
24
+ state.sawTokensEvent = true;
25
+ }
26
+ state.allEmittedEvents.push(event);
27
+ state.attemptEvents.push(event);
28
+ }
29
+
30
+ export type { AttemptFlowState, StepExecutionState };
31
+ export { recordStepEvent };
@@ -0,0 +1,183 @@
1
+ import type {
2
+ ModelProvider,
3
+ Profile,
4
+ ResolvedGeneration,
5
+ ToolEnvelope,
6
+ TurnEvent,
7
+ TurnHistoryMessage,
8
+ TurnRequest,
9
+ } from '../../types.ts';
10
+ import { recordStepEvent, type StepExecutionState } from './state.ts';
11
+ import { yieldProviderEvents } from './stream.ts';
12
+ import {
13
+ executeDynamicDeclaration,
14
+ findDynamicDeclaration,
15
+ formatToolFinding,
16
+ isActionableDynamicDeclaration,
17
+ } from './tools.ts';
18
+
19
+ function isStepLimitReached(step: number, maxSteps: number): boolean {
20
+ if (maxSteps <= 0) {
21
+ return false;
22
+ }
23
+ return step >= maxSteps;
24
+ }
25
+
26
+ async function* executeAutonomousStep(
27
+ args: {
28
+ profile: Profile;
29
+ generation: ResolvedGeneration;
30
+ system: string;
31
+ provider: ModelProvider;
32
+ gemini: Record<string, unknown>[];
33
+ },
34
+ state: StepExecutionState,
35
+ bufferOutputs = false,
36
+ ): AsyncGenerator<TurnEvent, { pendingTools: TurnEvent[]; latestStructured?: unknown }> {
37
+ const { profile, generation, system, provider, gemini } = args;
38
+ const genForStep = { ...generation, history: state.currentHistory };
39
+ const pendingTools: TurnEvent[] = [];
40
+ let latestStructured: unknown;
41
+
42
+ for await (const event of yieldProviderEvents({
43
+ profile,
44
+ generation: genForStep,
45
+ system,
46
+ provider,
47
+ gemini,
48
+ })) {
49
+ if (event.type === 'structured') {
50
+ latestStructured = event.structured;
51
+ }
52
+ if (event.type === 'done') {
53
+ continue;
54
+ }
55
+ if (event.type === 'tool' && event.tool) {
56
+ pendingTools.push(event);
57
+ continue;
58
+ }
59
+ recordStepEvent(event, state);
60
+ if (!bufferOutputs || event.type === 'tokens') {
61
+ yield event;
62
+ }
63
+ }
64
+
65
+ return { pendingTools, latestStructured };
66
+ }
67
+
68
+ function appendToolTurnToHistory(
69
+ history: TurnHistoryMessage[],
70
+ toolEv: TurnEvent,
71
+ res: ToolEnvelope,
72
+ ): void {
73
+ const tool = toolEv.tool;
74
+ if (!tool) {
75
+ return;
76
+ }
77
+ const callId = tool.id ?? `call_${tool.name}_${String(Date.now())}`;
78
+ history.push({
79
+ role: 'assistant',
80
+ tool_calls: [
81
+ {
82
+ id: callId,
83
+ type: 'function',
84
+ function: {
85
+ name: tool.name,
86
+ arguments: JSON.stringify(tool.arguments ?? {}),
87
+ },
88
+ },
89
+ ],
90
+ });
91
+ history.push({
92
+ role: 'tool',
93
+ tool_call_id: callId,
94
+ name: tool.name,
95
+ content: formatToolFinding(res),
96
+ });
97
+ }
98
+
99
+ async function* handlePendingDynamicTools(
100
+ pendingTools: TurnEvent[],
101
+ generation: ResolvedGeneration,
102
+ profile: Profile,
103
+ state: StepExecutionState,
104
+ ): AsyncGenerator<TurnEvent, boolean> {
105
+ let hasRunnableHandler = false;
106
+ for (const toolEv of pendingTools) {
107
+ const tool = toolEv.tool;
108
+ if (!tool) {
109
+ continue;
110
+ }
111
+ const decl = findDynamicDeclaration(generation.dynamicTools, tool.name);
112
+ if (!isActionableDynamicDeclaration(decl)) {
113
+ state.allEmittedEvents.push(toolEv);
114
+ yield toolEv;
115
+ continue;
116
+ }
117
+
118
+ hasRunnableHandler = true;
119
+ const finalResult = await executeDynamicDeclaration({
120
+ decl,
121
+ toolArgs: tool.arguments ?? {},
122
+ profile,
123
+ generation,
124
+ });
125
+ const enrichedEvent: TurnEvent = {
126
+ type: 'tool',
127
+ tool: { ...tool, result: finalResult },
128
+ };
129
+ state.allEmittedEvents.push(enrichedEvent);
130
+ yield enrichedEvent;
131
+ appendToolTurnToHistory(state.currentHistory, toolEv, finalResult);
132
+ }
133
+ return hasRunnableHandler;
134
+ }
135
+
136
+ async function* executeAttempt(args: {
137
+ safe: TurnRequest;
138
+ profile: Profile;
139
+ generation: ResolvedGeneration;
140
+ system: string;
141
+ provider: ModelProvider;
142
+ gemini: Record<string, unknown>[];
143
+ state: StepExecutionState;
144
+ }): AsyncGenerator<TurnEvent, { pendingTools: TurnEvent[]; latestStructured?: unknown }> {
145
+ const { profile, generation, system, provider, gemini, state } = args;
146
+ let latestStructured: unknown;
147
+ let pendingTools: TurnEvent[] = [];
148
+ let stepInAttempt = 0;
149
+ const shouldBuffer =
150
+ Boolean(profile.outputs.validation) || Boolean(profile.guardrails.egress?.enforce);
151
+
152
+ while (!isStepLimitReached(stepInAttempt, generation.maxSteps)) {
153
+ stepInAttempt++;
154
+ state.stepCount++;
155
+ const stepResult = yield* executeAutonomousStep(
156
+ { profile, generation, system, provider, gemini },
157
+ state,
158
+ shouldBuffer,
159
+ );
160
+ if (stepResult.latestStructured !== undefined) {
161
+ latestStructured = stepResult.latestStructured;
162
+ }
163
+ pendingTools = stepResult.pendingTools;
164
+
165
+ if (pendingTools.length === 0) {
166
+ break;
167
+ }
168
+
169
+ const hasRunnableHandler = yield* handlePendingDynamicTools(
170
+ pendingTools,
171
+ generation,
172
+ profile,
173
+ state,
174
+ );
175
+ if (!hasRunnableHandler) {
176
+ break;
177
+ }
178
+ }
179
+
180
+ return { pendingTools, latestStructured };
181
+ }
182
+
183
+ export { executeAttempt };