theorum 0.1.3 → 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 (187) hide show
  1. package/README.md +30 -9
  2. package/deno.json +57 -0
  3. package/deno.lock +469 -0
  4. package/{esm/mod.d.ts → mod.ts} +57 -15
  5. package/package.json +35 -44
  6. package/src/cli/commands/profile.ts +57 -0
  7. package/src/cli/commands/run.ts +66 -0
  8. package/src/cli/commands/test.ts +195 -0
  9. package/src/cli/index.ts +161 -0
  10. package/src/cli/matrix/fixtures.ts +96 -0
  11. package/src/cli/matrix/synthesizer.ts +240 -0
  12. package/src/guardrails/error.ts +142 -0
  13. package/src/guardrails/injection.ts +254 -0
  14. package/src/guardrails/mod.ts +40 -0
  15. package/src/guardrails/quota.ts +87 -0
  16. package/src/guardrails/sanitize.ts +165 -0
  17. package/src/guardrails/sensitive.ts +96 -0
  18. package/src/host/mint-trace.ts +54 -0
  19. package/{esm/src/host/mod.js → src/host/mod.ts} +11 -3
  20. package/src/host/reply.ts +32 -0
  21. package/src/kernel/engine/assert.ts +29 -0
  22. package/src/kernel/engine/boundary.ts +75 -0
  23. package/src/kernel/engine/delta.ts +440 -0
  24. package/src/kernel/engine/hash.ts +11 -0
  25. package/src/kernel/engine/record.ts +8 -0
  26. package/src/kernel/engine/repair.ts +56 -0
  27. package/src/kernel/engine/runner/gates.ts +338 -0
  28. package/src/kernel/engine/runner/mod.ts +135 -0
  29. package/src/kernel/engine/runner/state.ts +31 -0
  30. package/src/kernel/engine/runner/steps.ts +183 -0
  31. package/src/kernel/engine/runner/stream.ts +83 -0
  32. package/src/kernel/engine/runner/tokens.ts +47 -0
  33. package/src/kernel/engine/runner/tools.ts +266 -0
  34. package/{esm/src/kernel/engine/runner.d.ts → src/kernel/engine/runner.ts} +2 -1
  35. package/src/kernel/engine/tree.ts +18 -0
  36. package/src/kernel/mod.ts +39 -0
  37. package/src/kernel/registry/attachments.ts +195 -0
  38. package/src/kernel/registry/catalog.ts +181 -0
  39. package/src/kernel/registry/ingress.ts +223 -0
  40. package/src/kernel/registry/profiles.ts +165 -0
  41. package/src/kernel/registry/provider-request.ts +31 -0
  42. package/src/kernel/registry/resolve.ts +290 -0
  43. package/src/kernel/registry/schemas.ts +29 -0
  44. package/src/kernel/registry/tools.ts +45 -0
  45. package/{esm/src/kernel/registry/vault.d.ts → src/kernel/registry/vault.ts} +17 -2
  46. package/src/kernel/types.ts +576 -0
  47. package/{esm/src/observability/mod.js → src/observability/mod.ts} +11 -2
  48. package/src/observability/spans.ts +70 -0
  49. package/src/observability/trace-attach.ts +96 -0
  50. package/src/observability/trace-record.ts +254 -0
  51. package/src/observability/trace-usage.ts +36 -0
  52. package/src/observability/trace.ts +143 -0
  53. package/src/presets/google.ts +147 -0
  54. package/src/presets/mod.ts +24 -0
  55. package/src/providers/create-provider.ts +64 -0
  56. package/src/providers/gemini-tape.ts +59 -0
  57. package/src/providers/google-tap.ts +57 -0
  58. package/src/providers/interactions.ts +182 -0
  59. package/src/providers/keys.ts +182 -0
  60. package/{esm/src/providers/mod.js → src/providers/mod.ts} +4 -2
  61. package/{esm/src/providers/openrouter-mod.js → src/providers/openrouter-mod.ts} +7 -3
  62. package/src/providers/openrouter-payload.ts +232 -0
  63. package/src/providers/openrouter.ts +758 -0
  64. package/src/providers/pcm.ts +37 -0
  65. package/src/providers/provider.ts +206 -0
  66. package/src/providers/speech.ts +180 -0
  67. package/src/providers/sse.ts +59 -0
  68. package/esm/_dnt.polyfills.d.ts +0 -110
  69. package/esm/_dnt.polyfills.js +0 -144
  70. package/esm/_dnt.shims.d.ts +0 -5
  71. package/esm/_dnt.shims.js +0 -61
  72. package/esm/mod.js +0 -49
  73. package/esm/package.json +0 -3
  74. package/esm/src/cli/commands/profile.d.ts +0 -2
  75. package/esm/src/cli/commands/profile.js +0 -53
  76. package/esm/src/cli/commands/run.d.ts +0 -10
  77. package/esm/src/cli/commands/run.js +0 -55
  78. package/esm/src/cli/commands/test.d.ts +0 -15
  79. package/esm/src/cli/commands/test.js +0 -140
  80. package/esm/src/cli/index.d.ts +0 -4
  81. package/esm/src/cli/index.js +0 -148
  82. package/esm/src/cli/matrix/fixtures.d.ts +0 -13
  83. package/esm/src/cli/matrix/fixtures.js +0 -77
  84. package/esm/src/cli/matrix/synthesizer.d.ts +0 -29
  85. package/esm/src/cli/matrix/synthesizer.js +0 -187
  86. package/esm/src/guardrails/error.d.ts +0 -33
  87. package/esm/src/guardrails/error.js +0 -114
  88. package/esm/src/guardrails/injection.d.ts +0 -12
  89. package/esm/src/guardrails/injection.js +0 -220
  90. package/esm/src/guardrails/mod.d.ts +0 -14
  91. package/esm/src/guardrails/mod.js +0 -14
  92. package/esm/src/guardrails/sanitize.d.ts +0 -22
  93. package/esm/src/guardrails/sanitize.js +0 -133
  94. package/esm/src/guardrails/sensitive.d.ts +0 -12
  95. package/esm/src/guardrails/sensitive.js +0 -88
  96. package/esm/src/host/mint-trace.d.ts +0 -27
  97. package/esm/src/host/mint-trace.js +0 -33
  98. package/esm/src/host/mod.d.ts +0 -13
  99. package/esm/src/host/reply.d.ts +0 -14
  100. package/esm/src/host/reply.js +0 -27
  101. package/esm/src/kernel/engine/boundary.d.ts +0 -10
  102. package/esm/src/kernel/engine/boundary.js +0 -55
  103. package/esm/src/kernel/engine/delta.d.ts +0 -8
  104. package/esm/src/kernel/engine/delta.js +0 -389
  105. package/esm/src/kernel/engine/hash.d.ts +0 -1
  106. package/esm/src/kernel/engine/hash.js +0 -9
  107. package/esm/src/kernel/engine/record.d.ts +0 -2
  108. package/esm/src/kernel/engine/record.js +0 -7
  109. package/esm/src/kernel/engine/repair.d.ts +0 -7
  110. package/esm/src/kernel/engine/repair.js +0 -38
  111. package/esm/src/kernel/engine/runner/gates.d.ts +0 -4
  112. package/esm/src/kernel/engine/runner/gates.js +0 -206
  113. package/esm/src/kernel/engine/runner/mod.d.ts +0 -14
  114. package/esm/src/kernel/engine/runner/mod.js +0 -103
  115. package/esm/src/kernel/engine/runner/state.d.ts +0 -16
  116. package/esm/src/kernel/engine/runner/state.js +0 -8
  117. package/esm/src/kernel/engine/runner/steps.d.ts +0 -15
  118. package/esm/src/kernel/engine/runner/steps.js +0 -119
  119. package/esm/src/kernel/engine/runner/stream.d.ts +0 -11
  120. package/esm/src/kernel/engine/runner/stream.js +0 -65
  121. package/esm/src/kernel/engine/runner/tokens.d.ts +0 -3
  122. package/esm/src/kernel/engine/runner/tokens.js +0 -38
  123. package/esm/src/kernel/engine/runner/tools.d.ts +0 -13
  124. package/esm/src/kernel/engine/runner/tools.js +0 -197
  125. package/esm/src/kernel/engine/runner.js +0 -6
  126. package/esm/src/kernel/engine/tree.d.ts +0 -2
  127. package/esm/src/kernel/engine/tree.js +0 -17
  128. package/esm/src/kernel/mod.d.ts +0 -18
  129. package/esm/src/kernel/mod.js +0 -16
  130. package/esm/src/kernel/registry/attachments.d.ts +0 -16
  131. package/esm/src/kernel/registry/attachments.js +0 -156
  132. package/esm/src/kernel/registry/catalog.d.ts +0 -31
  133. package/esm/src/kernel/registry/catalog.js +0 -135
  134. package/esm/src/kernel/registry/ingress.d.ts +0 -13
  135. package/esm/src/kernel/registry/ingress.js +0 -163
  136. package/esm/src/kernel/registry/profiles.d.ts +0 -34
  137. package/esm/src/kernel/registry/profiles.js +0 -120
  138. package/esm/src/kernel/registry/provider-request.d.ts +0 -4
  139. package/esm/src/kernel/registry/provider-request.js +0 -25
  140. package/esm/src/kernel/registry/resolve.d.ts +0 -19
  141. package/esm/src/kernel/registry/resolve.js +0 -217
  142. package/esm/src/kernel/registry/schemas.d.ts +0 -14
  143. package/esm/src/kernel/registry/schemas.js +0 -23
  144. package/esm/src/kernel/registry/tools.d.ts +0 -12
  145. package/esm/src/kernel/registry/tools.js +0 -36
  146. package/esm/src/kernel/registry/vault.js +0 -18
  147. package/esm/src/kernel/types.d.ts +0 -521
  148. package/esm/src/kernel/types.js +0 -10
  149. package/esm/src/observability/mod.d.ts +0 -12
  150. package/esm/src/observability/spans.d.ts +0 -16
  151. package/esm/src/observability/spans.js +0 -56
  152. package/esm/src/observability/trace-attach.d.ts +0 -16
  153. package/esm/src/observability/trace-attach.js +0 -67
  154. package/esm/src/observability/trace-record.d.ts +0 -112
  155. package/esm/src/observability/trace-record.js +0 -140
  156. package/esm/src/observability/trace-usage.d.ts +0 -3
  157. package/esm/src/observability/trace-usage.js +0 -32
  158. package/esm/src/observability/trace.d.ts +0 -23
  159. package/esm/src/observability/trace.js +0 -121
  160. package/esm/src/presets/google.d.ts +0 -50
  161. package/esm/src/presets/google.js +0 -96
  162. package/esm/src/presets/mod.d.ts +0 -11
  163. package/esm/src/presets/mod.js +0 -10
  164. package/esm/src/providers/create-provider.d.ts +0 -29
  165. package/esm/src/providers/create-provider.js +0 -38
  166. package/esm/src/providers/gemini-tape.d.ts +0 -2
  167. package/esm/src/providers/gemini-tape.js +0 -46
  168. package/esm/src/providers/google-tap.d.ts +0 -3
  169. package/esm/src/providers/google-tap.js +0 -48
  170. package/esm/src/providers/interactions.d.ts +0 -5
  171. package/esm/src/providers/interactions.js +0 -152
  172. package/esm/src/providers/keys.d.ts +0 -19
  173. package/esm/src/providers/keys.js +0 -129
  174. package/esm/src/providers/mod.d.ts +0 -12
  175. package/esm/src/providers/openrouter-mod.d.ts +0 -13
  176. package/esm/src/providers/openrouter-payload.d.ts +0 -32
  177. package/esm/src/providers/openrouter-payload.js +0 -179
  178. package/esm/src/providers/openrouter.d.ts +0 -15
  179. package/esm/src/providers/openrouter.js +0 -589
  180. package/esm/src/providers/pcm.d.ts +0 -7
  181. package/esm/src/providers/pcm.js +0 -35
  182. package/esm/src/providers/provider.d.ts +0 -15
  183. package/esm/src/providers/provider.js +0 -166
  184. package/esm/src/providers/speech.d.ts +0 -23
  185. package/esm/src/providers/speech.js +0 -125
  186. package/esm/src/providers/sse.d.ts +0 -7
  187. package/esm/src/providers/sse.js +0 -53
@@ -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 };