theorum 1.0.0 → 1.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 (204) hide show
  1. package/README.md +81 -25
  2. package/esm/mod.d.ts +48 -29
  3. package/esm/mod.js +37 -24
  4. package/esm/src/cli/commands/bench.js +13 -16
  5. package/esm/src/cli/commands/fuzz-canary.js +25 -29
  6. package/esm/src/cli/commands/guardrails-eval.d.ts +14 -0
  7. package/esm/src/cli/commands/guardrails-eval.js +15 -0
  8. package/esm/src/cli/commands/profile.js +12 -3
  9. package/esm/src/cli/commands/run.js +8 -9
  10. package/esm/src/cli/commands/test.js +8 -8
  11. package/esm/src/cli/event-log.d.ts +1 -1
  12. package/esm/src/cli/index.js +12 -0
  13. package/esm/src/cli/matrix/synthesizer.d.ts +6 -6
  14. package/esm/src/cli/matrix/synthesizer.js +23 -22
  15. package/esm/src/guardrails/canary-gate.d.ts +2 -1
  16. package/esm/src/guardrails/canary-gate.js +2 -1
  17. package/esm/src/guardrails/canary.d.ts +9 -1
  18. package/esm/src/guardrails/canary.js +21 -10
  19. package/esm/src/guardrails/corpus/canary-egress-attacks.d.ts +1 -0
  20. package/esm/src/guardrails/corpus/fuzz-inbound.js +12 -15
  21. package/esm/src/guardrails/corpus/inbound-payloads.js +1 -0
  22. package/esm/src/guardrails/corpus/live-attacks.js +0 -1
  23. package/esm/src/guardrails/corpus/mod.d.ts +1 -0
  24. package/esm/src/guardrails/corpus/secrets.d.ts +1 -0
  25. package/esm/src/guardrails/corpus/secrets.js +1 -0
  26. package/esm/src/guardrails/corpus/strings.d.ts +1 -0
  27. package/esm/src/guardrails/corpus/strings.js +1 -0
  28. package/esm/src/guardrails/corpus/types.d.ts +1 -0
  29. package/esm/src/guardrails/egress.d.ts +26 -3
  30. package/esm/src/guardrails/egress.js +69 -18
  31. package/esm/src/guardrails/error.d.ts +14 -23
  32. package/esm/src/guardrails/error.js +86 -77
  33. package/esm/src/guardrails/eval/corpus.d.ts +108 -0
  34. package/esm/src/guardrails/eval/corpus.js +978 -0
  35. package/esm/src/guardrails/eval/mod.d.ts +51 -0
  36. package/esm/src/guardrails/eval/mod.js +133 -0
  37. package/esm/src/guardrails/eval/score.d.ts +66 -0
  38. package/esm/src/guardrails/eval/score.js +114 -0
  39. package/esm/src/guardrails/events.d.ts +25 -0
  40. package/esm/src/guardrails/events.js +56 -0
  41. package/esm/src/guardrails/hits.d.ts +24 -0
  42. package/esm/src/guardrails/hits.js +45 -0
  43. package/esm/src/guardrails/lexicon.d.ts +39 -0
  44. package/esm/src/guardrails/lexicon.js +200 -0
  45. package/esm/src/guardrails/live-outbound-gate.d.ts +15 -13
  46. package/esm/src/guardrails/live-outbound-gate.js +131 -89
  47. package/esm/src/guardrails/mod.d.ts +19 -4
  48. package/esm/src/guardrails/mod.js +12 -3
  49. package/esm/src/guardrails/network.d.ts +19 -0
  50. package/esm/src/guardrails/network.js +234 -0
  51. package/esm/src/guardrails/policy.d.ts +35 -0
  52. package/esm/src/guardrails/policy.js +50 -0
  53. package/esm/src/guardrails/progressive-yield.d.ts +51 -0
  54. package/esm/src/guardrails/progressive-yield.js +98 -0
  55. package/esm/src/guardrails/quota.d.ts +17 -3
  56. package/esm/src/guardrails/quota.js +18 -4
  57. package/esm/src/guardrails/sanitize.d.ts +36 -6
  58. package/esm/src/guardrails/sanitize.js +152 -60
  59. package/esm/src/guardrails/serialize.d.ts +35 -0
  60. package/esm/src/guardrails/serialize.js +58 -0
  61. package/esm/src/guardrails/testing.d.ts +7 -0
  62. package/esm/src/guardrails/testing.js +4 -0
  63. package/esm/src/guardrails/theorum-error.d.ts +12 -0
  64. package/esm/src/guardrails/theorum-error.js +15 -0
  65. package/esm/src/guardrails/tool-directives.d.ts +48 -0
  66. package/esm/src/guardrails/tool-directives.js +124 -0
  67. package/esm/src/guardrails/tool-result.d.ts +93 -0
  68. package/esm/src/guardrails/tool-result.js +276 -0
  69. package/esm/src/guardrails/types.d.ts +291 -0
  70. package/esm/src/guardrails/types.js +72 -0
  71. package/esm/src/host/client-turn.js +4 -0
  72. package/esm/src/host/mint-trace.d.ts +1 -1
  73. package/esm/src/kernel/auth/crypto.d.ts +42 -0
  74. package/esm/src/kernel/auth/crypto.js +106 -0
  75. package/esm/src/kernel/auth/mod.d.ts +11 -0
  76. package/esm/src/kernel/auth/mod.js +11 -0
  77. package/esm/src/kernel/auth/oauth.d.ts +47 -0
  78. package/esm/src/kernel/auth/oauth.js +278 -0
  79. package/esm/src/kernel/auth/types.d.ts +133 -0
  80. package/esm/src/kernel/auth/types.js +13 -0
  81. package/esm/src/kernel/engine/delta.js +43 -2
  82. package/esm/src/kernel/engine/live-inbound.d.ts +15 -3
  83. package/esm/src/kernel/engine/live-inbound.js +22 -8
  84. package/esm/src/kernel/engine/live-ingress.d.ts +19 -0
  85. package/esm/src/kernel/engine/live-ingress.js +47 -0
  86. package/esm/src/kernel/engine/repair.js +13 -12
  87. package/esm/src/kernel/engine/runner/gates.js +127 -40
  88. package/esm/src/kernel/engine/runner/mod.d.ts +6 -4
  89. package/esm/src/kernel/engine/runner/mod.js +182 -43
  90. package/esm/src/kernel/engine/runner/schema-validation.js +3 -3
  91. package/esm/src/kernel/engine/runner/stages.d.ts +39 -0
  92. package/esm/src/kernel/engine/runner/stages.js +89 -0
  93. package/esm/src/kernel/engine/runner/state.d.ts +24 -0
  94. package/esm/src/kernel/engine/runner/steps.js +128 -61
  95. package/esm/src/kernel/engine/runner/stream.d.ts +9 -2
  96. package/esm/src/kernel/engine/runner/stream.js +114 -55
  97. package/esm/src/kernel/engine/session/mod.d.ts +3 -1
  98. package/esm/src/kernel/engine/session/mod.js +386 -34
  99. package/esm/src/kernel/interaction-parts.d.ts +14 -0
  100. package/esm/src/kernel/interaction-parts.js +23 -0
  101. package/esm/src/kernel/mod.d.ts +19 -7
  102. package/esm/src/kernel/mod.js +10 -5
  103. package/esm/src/kernel/profile-graph.d.ts +159 -0
  104. package/esm/src/kernel/profile-graph.js +156 -0
  105. package/esm/src/kernel/registry/attachments.d.ts +10 -10
  106. package/esm/src/kernel/registry/attachments.js +29 -26
  107. package/esm/src/kernel/registry/catalog.d.ts +25 -11
  108. package/esm/src/kernel/registry/catalog.js +59 -24
  109. package/esm/src/kernel/registry/ingress.js +34 -22
  110. package/esm/src/kernel/registry/profile-outputs.d.ts +4 -0
  111. package/esm/src/kernel/registry/profile-outputs.js +8 -0
  112. package/esm/src/kernel/registry/profiles.d.ts +34 -14
  113. package/esm/src/kernel/registry/profiles.js +306 -59
  114. package/esm/src/kernel/registry/provider-request.js +2 -0
  115. package/esm/src/kernel/registry/resolve.d.ts +7 -6
  116. package/esm/src/kernel/registry/resolve.js +102 -117
  117. package/esm/src/kernel/registry/schemas.js +1 -1
  118. package/esm/src/kernel/registry/sole-model.d.ts +8 -0
  119. package/esm/src/kernel/registry/sole-model.js +10 -0
  120. package/esm/src/kernel/registry/system-prompt.d.ts +10 -0
  121. package/esm/src/kernel/registry/system-prompt.js +40 -0
  122. package/esm/src/kernel/registry/system-role.d.ts +8 -0
  123. package/esm/src/kernel/registry/system-role.js +14 -0
  124. package/esm/src/kernel/registry/vault.d.ts +2 -2
  125. package/esm/src/kernel/registry/vault.js +4 -4
  126. package/esm/src/kernel/schema.d.ts +97 -11
  127. package/esm/src/kernel/schema.js +256 -111
  128. package/esm/src/kernel/stages.d.ts +175 -0
  129. package/esm/src/kernel/stages.js +476 -0
  130. package/esm/src/kernel/stop.d.ts +65 -11
  131. package/esm/src/kernel/stop.js +46 -11
  132. package/esm/src/kernel/tools/events.d.ts +41 -0
  133. package/esm/src/kernel/tools/events.js +71 -0
  134. package/esm/src/kernel/tools/execute.d.ts +63 -24
  135. package/esm/src/kernel/tools/execute.js +475 -224
  136. package/esm/src/kernel/tools/harness.js +16 -14
  137. package/esm/src/kernel/tools/invoke.js +26 -8
  138. package/esm/src/kernel/tools/mod.d.ts +3 -1
  139. package/esm/src/kernel/tools/mod.js +2 -1
  140. package/esm/src/kernel/tools/permission.d.ts +15 -0
  141. package/esm/src/kernel/tools/permission.js +47 -0
  142. package/esm/src/kernel/tools/project.js +5 -3
  143. package/esm/src/kernel/tools/registry.js +20 -10
  144. package/esm/src/kernel/tools/remote.d.ts +94 -0
  145. package/esm/src/kernel/tools/remote.js +577 -0
  146. package/esm/src/kernel/tools/resolve.d.ts +17 -7
  147. package/esm/src/kernel/tools/resolve.js +38 -17
  148. package/esm/src/kernel/tools/schema.d.ts +2 -0
  149. package/esm/src/kernel/tools/schema.js +23 -6
  150. package/esm/src/kernel/tools/stage-run.d.ts +105 -0
  151. package/esm/src/kernel/tools/stage-run.js +155 -0
  152. package/esm/src/kernel/tools/types.d.ts +198 -20
  153. package/esm/src/kernel/types.d.ts +253 -114
  154. package/esm/src/kernel/util/find-last.d.ts +2 -0
  155. package/esm/src/kernel/util/find-last.js +10 -0
  156. package/esm/src/observability/destinations.d.ts +31 -0
  157. package/esm/src/observability/destinations.js +67 -0
  158. package/esm/src/observability/mod.d.ts +10 -3
  159. package/esm/src/observability/mod.js +6 -2
  160. package/esm/src/observability/policy.d.ts +27 -0
  161. package/esm/src/observability/policy.js +80 -0
  162. package/esm/src/observability/resolve-policy.d.ts +16 -0
  163. package/esm/src/observability/resolve-policy.js +64 -0
  164. package/esm/src/observability/trace-attach.d.ts +3 -1
  165. package/esm/src/observability/trace-attach.js +17 -10
  166. package/esm/src/observability/trace-record.d.ts +12 -4
  167. package/esm/src/observability/trace-record.js +63 -19
  168. package/esm/src/observability/trace-sink.d.ts +19 -0
  169. package/esm/src/observability/trace-sink.js +10 -0
  170. package/esm/src/observability/trace.d.ts +14 -11
  171. package/esm/src/observability/trace.js +23 -14
  172. package/esm/src/observability/types.d.ts +113 -0
  173. package/esm/src/observability/types.js +11 -0
  174. package/esm/src/providers/create-provider.d.ts +7 -4
  175. package/esm/src/providers/create-provider.js +21 -7
  176. package/esm/src/providers/google/interactions/framing.d.ts +8 -0
  177. package/esm/src/providers/google/interactions/framing.js +74 -13
  178. package/esm/src/providers/google/live/framing.d.ts +5 -2
  179. package/esm/src/providers/google/live/framing.js +49 -12
  180. package/esm/src/providers/google/live/openapi-schema.js +3 -2
  181. package/esm/src/providers/google/live/stream.d.ts +9 -0
  182. package/esm/src/providers/google/live/stream.js +16 -3
  183. package/esm/src/providers/openrouter/cache-control.d.ts +24 -0
  184. package/esm/src/providers/openrouter/cache-control.js +23 -0
  185. package/esm/src/providers/openrouter/chat.d.ts +21 -1
  186. package/esm/src/providers/openrouter/chat.js +51 -6
  187. package/esm/src/providers/openrouter/image.js +8 -19
  188. package/esm/src/providers/openrouter/openai/chat-payload.js +23 -1
  189. package/esm/src/providers/openrouter/openai/compat.d.ts +7 -1
  190. package/esm/src/providers/openrouter/openai/compat.js +23 -4
  191. package/esm/src/providers/openrouter/openai/image-payload.js +8 -2
  192. package/esm/src/providers/openrouter/openai/sdk-messages.d.ts +1 -1
  193. package/esm/src/providers/openrouter/openai/sdk-messages.js +32 -5
  194. package/package.json +1 -1
  195. package/esm/src/interface/blocks.d.ts +0 -20
  196. package/esm/src/interface/blocks.js +0 -180
  197. package/esm/src/interface/from-profile.d.ts +0 -14
  198. package/esm/src/interface/from-profile.js +0 -104
  199. package/esm/src/interface/inputs.d.ts +0 -8
  200. package/esm/src/interface/inputs.js +0 -127
  201. package/esm/src/interface/mod.d.ts +0 -10
  202. package/esm/src/interface/mod.js +0 -9
  203. package/esm/src/interface/types.d.ts +0 -153
  204. package/esm/src/interface/types.js +0 -13
@@ -1,5 +1,12 @@
1
1
  import { throwIfAborted } from '../../../guardrails/error.js';
2
+ import { resolveGuardrailPolicy } from '../../../guardrails/policy.js';
3
+ import { recordTaint } from '../../../guardrails/tool-result.js';
4
+ import { wireInteractionPart } from '../../interaction-parts.js';
5
+ import { profileTurnOutputs } from '../../registry/profile-outputs.js';
6
+ import { injectWouldExceedMaxSteps } from '../../stages.js';
7
+ import { profileAllowsInject } from '../../stop.js';
2
8
  import { executeRegisteredTool, formatToolFailureForModel, formatToolResult, newCallId, } from '../../tools/execute.js';
9
+ import { applyStageInjects } from './stages.js';
3
10
  import { recordStepEvent } from './state.js';
4
11
  import { yieldProviderEvents } from './stream.js';
5
12
  function isStepLimitReached(step, maxSteps) {
@@ -29,18 +36,20 @@ function captureInteractionId(event, state) {
29
36
  }
30
37
  async function* executeAutonomousStep(args, state, buffer = {
31
38
  holdLate: false,
32
- holdUserVisible: false,
33
39
  }) {
34
40
  const { generation, system, provider, upstream, signal } = args;
35
41
  const genForStep = generationForProviderStep(generation, state);
36
42
  const pendingTools = [];
37
43
  let latestStructured;
44
+ const control = { withholdVisible: false };
38
45
  for await (const event of yieldProviderEvents({
46
+ profile: args.profile,
39
47
  generation: genForStep,
40
48
  system,
41
49
  provider,
42
50
  upstream,
43
51
  signal,
52
+ control,
44
53
  })) {
45
54
  captureInteractionId(event, state);
46
55
  if (event.type === 'structured') {
@@ -57,8 +66,16 @@ async function* executeAutonomousStep(args, state, buffer = {
57
66
  continue;
58
67
  }
59
68
  recordStepEvent(event, state);
60
- const isUserVisible = event.type === 'thought' || event.type === 'text';
61
- const streamNow = !buffer.holdLate || event.type === 'tokens' || (isUserVisible && !buffer.holdUserVisible);
69
+ const isUserVisible = event.type === 'thought' || event.type === 'text' || event.type === 'media';
70
+ if (control.withholdVisible && isUserVisible) {
71
+ // Progressive-yield blocked this attempt — keep events for egress/repair only.
72
+ // Record the decision so the attempt gate knows nothing reached the host.
73
+ state.withheldVisible = true;
74
+ continue;
75
+ }
76
+ // Progressive-yield streams text/thought live under egress; holdLate only
77
+ // buffers non-visible events (e.g. structured) for validation.
78
+ const streamNow = !buffer.holdLate || event.type === 'tokens' || isUserVisible;
62
79
  if (streamNow) {
63
80
  yield event;
64
81
  }
@@ -75,6 +92,7 @@ function appendInteractionsToolResultToHistory(history, toolEv, result) {
75
92
  tool_call_id: tool.id ?? tool.callId ?? `call_${tool.name}`,
76
93
  name: tool.name,
77
94
  content: formatToolResult(result),
95
+ ...(result.parts && result.parts.length > 0 ? { parts: result.parts } : {}),
78
96
  });
79
97
  }
80
98
  function appendToolTurnToHistory(history, toolEv, result) {
@@ -101,6 +119,7 @@ function appendToolTurnToHistory(history, toolEv, result) {
101
119
  tool_call_id: callId,
102
120
  name: tool.name,
103
121
  content: formatToolResult(result),
122
+ ...(result.parts && result.parts.length > 0 ? { parts: result.parts } : {}),
104
123
  });
105
124
  }
106
125
  function queueInteractionsToolContinuation(state, toolEv, result, fallbackInteractionId) {
@@ -116,7 +135,9 @@ function queueInteractionsToolContinuation(state, toolEv, result, fallbackIntera
116
135
  type: 'function_result',
117
136
  name: tool.name,
118
137
  call_id: tool.id ?? tool.callId ?? `call_${tool.name}`,
119
- result: [{ type: 'text', text: formatToolResult(result) }],
138
+ result: result.parts && result.parts.length > 0
139
+ ? result.parts.map(wireInteractionPart)
140
+ : [{ type: 'text', text: formatToolResult(result) }],
120
141
  };
121
142
  if (state.interactionsContinuation &&
122
143
  state.interactionsContinuation.previousInteractionId === previousInteractionId) {
@@ -149,25 +170,79 @@ function enrichToolEvent(tool, callId, patch) {
149
170
  },
150
171
  };
151
172
  }
152
- async function* handlePendingTools(pendingTools, generation, profile, state) {
173
+ async function* drainToolExecEvents(exec, tool, callId, state) {
174
+ let next = await exec.next();
175
+ let sawGate = false;
176
+ while (!next.done) {
177
+ const event = next.value;
178
+ if (event.type === 'tool') {
179
+ const enriched = enrichToolEvent(tool, callId, event.tool);
180
+ state.allEmittedEvents.push(enriched);
181
+ yield enriched;
182
+ if (event.tool?.phase === 'gate')
183
+ sawGate = true;
184
+ }
185
+ else {
186
+ state.allEmittedEvents.push(event);
187
+ yield event;
188
+ }
189
+ next = await exec.next();
190
+ }
191
+ return { settlement: next.value, sawGate };
192
+ }
193
+ function recordProviderToolFailure(state, toolEv, tool, callId, failure, generation, useInteractionsContinuation, patch) {
194
+ const enriched = enrichToolEvent(tool, callId, {
195
+ phase: 'error',
196
+ failure,
197
+ ...patch,
198
+ });
199
+ state.allEmittedEvents.push(enriched);
200
+ recordToolModelResult(state, toolEv, formatToolFailureForModel(failure), generation, useInteractionsContinuation);
201
+ return enriched;
202
+ }
203
+ function applyToolSettlement(settlement, state, toolEv, generation, useInteractionsContinuation) {
204
+ if (settlement.aborted) {
205
+ state.lastStop = {
206
+ kind: 'cancelled',
207
+ ...(typeof settlement.aborted === 'object' && settlement.aborted.reason
208
+ ? { native: settlement.aborted.reason }
209
+ : {}),
210
+ };
211
+ return 'stop_cancelled';
212
+ }
213
+ if (settlement.gated) {
214
+ state.toolSnapshot = generation.tools;
215
+ return 'gated';
216
+ }
217
+ if (settlement.modelResult?.provenance) {
218
+ state.taint = recordTaint(state.taint, settlement.modelResult.provenance, settlement.modelResult.suspicious);
219
+ }
220
+ if (!settlement.modelResult)
221
+ return 'continue';
222
+ recordToolModelResult(state, toolEv, settlement.modelResult, generation, useInteractionsContinuation);
223
+ if (settlement.pendingInject?.length) {
224
+ applyStageInjects(state, settlement.pendingInject);
225
+ }
226
+ return 'continue';
227
+ }
228
+ async function* handlePendingTools(pendingTools, generation, profile, state, safe) {
153
229
  let executed = false;
154
- let sawPause = false;
230
+ let sawGate = false;
155
231
  const useInteractionsContinuation = generation.transport === 'interactions';
156
232
  for (const toolEv of pendingTools) {
233
+ if (sawGate)
234
+ break;
157
235
  const tool = toolEv.tool;
158
- if (!tool) {
236
+ if (!tool)
159
237
  continue;
160
- }
161
238
  executed = true;
162
239
  const callId = tool.id ?? tool.callId ?? newCallId(tool.name || 'unknown');
163
- // Provider cancelled an in-flight call (e.g. live barge-in). Do not execute.
164
240
  if (tool.phase === 'cancel') {
165
241
  const enriched = enrichToolEvent(tool, callId);
166
242
  state.allEmittedEvents.push(enriched);
167
243
  yield enriched;
168
244
  continue;
169
245
  }
170
- // Provider already failed this call (e.g. malformed arguments JSON).
171
246
  if (tool.phase === 'error' && tool.failure) {
172
247
  const enriched = enrichToolEvent(tool, callId);
173
248
  state.allEmittedEvents.push(enriched);
@@ -175,25 +250,24 @@ async function* handlePendingTools(pendingTools, generation, profile, state) {
175
250
  recordToolModelResult(state, toolEv, formatToolFailureForModel(tool.failure), generation, useInteractionsContinuation);
176
251
  continue;
177
252
  }
178
- // Empty name is a protocol defect — never route through the registry as unknown_tool.
179
253
  if (!tool.name) {
180
- const failure = {
254
+ yield recordProviderToolFailure(state, toolEv, tool, callId, {
181
255
  code: 'malformed_arguments',
182
- message: 'Provider tool call is missing a function name',
183
- };
184
- const enriched = enrichToolEvent(tool, callId, {
185
- phase: 'error',
186
- failure,
187
- name: '',
188
- });
189
- state.allEmittedEvents.push(enriched);
190
- yield enriched;
191
- recordToolModelResult(state, toolEv, formatToolFailureForModel(failure), generation, useInteractionsContinuation);
256
+ message: 'Provider tool call is missing a function name', // lexicon-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
257
+ }, generation, useInteractionsContinuation, { name: '' });
192
258
  continue;
193
259
  }
194
- let modelResult;
195
- let paused = false;
196
- const exec = executeRegisteredTool({
260
+ const stages = {
261
+ handlers: safe?.onStage ? [safe.onStage] : [],
262
+ profile,
263
+ step: state.stepCount,
264
+ history: () => state.currentHistory,
265
+ injectAllowed: profileAllowsInject(profile),
266
+ injectWouldExceedMaxSteps: injectWouldExceedMaxSteps(state.stepCount, generation.maxSteps),
267
+ host: generation.host,
268
+ signal: safe?.signal,
269
+ };
270
+ const drained = yield* drainToolExecEvents(executeRegisteredTool({
197
271
  profile,
198
272
  name: tool.name,
199
273
  input: tool.arguments ?? {},
@@ -201,39 +275,26 @@ async function* handlePendingTools(pendingTools, generation, profile, state) {
201
275
  ctx: {
202
276
  sessionPermissions: generation.sessionPermissions,
203
277
  path: generation.tools.path,
204
- turn: { step: state.stepCount },
278
+ turn: { step: state.stepCount, taint: state.taint },
279
+ credentials: safe?.credentials,
280
+ host: generation.host,
281
+ resume: undefined,
282
+ signal: safe?.signal,
205
283
  },
206
284
  snapshot: generation.tools,
207
- });
208
- let next = await exec.next();
209
- while (!next.done) {
210
- const event = next.value;
211
- const enriched = enrichToolEvent(tool, callId, event.tool);
212
- state.allEmittedEvents.push(enriched);
213
- yield enriched;
214
- if (event.tool?.phase === 'error' && event.tool.failure) {
215
- modelResult = formatToolFailureForModel(event.tool.failure);
216
- }
217
- if (event.tool?.phase === 'pause') {
218
- modelResult = undefined;
219
- paused = true;
220
- }
221
- next = await exec.next();
222
- }
223
- if (next.value !== undefined) {
224
- modelResult = next.value;
225
- }
226
- if (paused) {
227
- sawPause = true;
228
- continue;
229
- }
230
- if (!modelResult) {
231
- continue;
285
+ stages,
286
+ }), tool, callId, state);
287
+ if (drained.sawGate)
288
+ sawGate = true;
289
+ const outcome = applyToolSettlement(drained.settlement, state, toolEv, generation, useInteractionsContinuation);
290
+ if (outcome === 'stop_cancelled')
291
+ return false;
292
+ if (outcome === 'gated' || sawGate) {
293
+ sawGate = true;
232
294
  }
233
- recordToolModelResult(state, toolEv, modelResult, generation, useInteractionsContinuation);
234
295
  }
235
- if (sawPause) {
236
- state.lastStop = { kind: 'tool' };
296
+ if (sawGate) {
297
+ state.lastStop = { kind: 'gate' };
237
298
  return false;
238
299
  }
239
300
  return executed;
@@ -242,14 +303,20 @@ async function* executeAttempt(args) {
242
303
  const { profile, generation, system, provider, upstream, state } = args;
243
304
  let latestStructured;
244
305
  let pendingTools = [];
245
- let stepInAttempt = 0;
246
- const holdUserVisible = Boolean(profile.guardrails?.egress?.enforce);
247
- const holdLate = Boolean(profile.outputs?.validation) || holdUserVisible;
248
- while (!isStepLimitReached(stepInAttempt, generation.maxSteps ?? 0)) {
306
+ // Text/thought stream via progressive-yield under egress; validation and egress
307
+ // both hold non-visible events (structured) until the attempt gate, so a policy
308
+ // sees the structured payload before any of it reaches the host.
309
+ const holdLate = Boolean(profileTurnOutputs(profile)?.validation ||
310
+ resolveGuardrailPolicy(profile.guardrails).egress?.enforce);
311
+ // Ceiling is cumulative `state.stepCount` across before_end inject re-entries
312
+ // within this attempt. Validation/egress repair resets stepCount at the start
313
+ // of each attempt cycle (see gates.ts).
314
+ while (!isStepLimitReached(state.stepCount, generation.maxSteps ?? 0)) {
249
315
  throwIfAborted(args.safe.signal);
250
- stepInAttempt++;
251
316
  state.stepCount++;
252
- const stepResult = yield* executeAutonomousStep({ profile, generation, system, provider, upstream, signal: args.safe.signal }, state, { holdLate, holdUserVisible });
317
+ // Mid-loop inject lives on `post_tool` (after tools). Opening inject is `pre_turn`
318
+ // outside this loop.
319
+ const stepResult = yield* executeAutonomousStep({ profile, generation, system, provider, upstream, signal: args.safe.signal }, state, { holdLate });
253
320
  if (stepResult.latestStructured !== undefined) {
254
321
  latestStructured = stepResult.latestStructured;
255
322
  }
@@ -257,7 +324,7 @@ async function* executeAttempt(args) {
257
324
  if (pendingTools.length === 0) {
258
325
  break;
259
326
  }
260
- const executed = yield* handlePendingTools(pendingTools, generation, profile, state);
327
+ const executed = yield* handlePendingTools(pendingTools, generation, profile, state, args.safe);
261
328
  if (!executed) {
262
329
  break;
263
330
  }
@@ -1,11 +1,18 @@
1
1
  import type { ModelProvider, Profile, ResolvedGeneration, TurnEvent } from '../../types.js';
2
- declare function systemFromProfile(profile: Profile, role: string): string;
2
+ /** Mutable control flags shared with the step runner during one provider stream. */
3
+ interface OutboundStreamControl {
4
+ /** Stop releasing text/thought/media to the host; keep recording for egress. */
5
+ withholdVisible: boolean;
6
+ }
3
7
  declare function shouldSkipStreamEvent(event: TurnEvent, profile: Profile): boolean;
4
8
  declare function yieldProviderEvents(args: {
9
+ profile: Profile;
5
10
  generation: ResolvedGeneration;
6
11
  system: string;
7
12
  provider: ModelProvider;
8
13
  upstream: Record<string, unknown>[];
9
14
  signal?: AbortSignal;
15
+ control?: OutboundStreamControl;
10
16
  }): AsyncGenerator<TurnEvent>;
11
- export { shouldSkipStreamEvent, systemFromProfile, yieldProviderEvents };
17
+ export type { OutboundStreamControl };
18
+ export { shouldSkipStreamEvent, yieldProviderEvents };
@@ -1,22 +1,13 @@
1
- import { createCanaryStreamGate, eventHasCanary, isStreamedCanaryEvent, redactCanary, } from '../../../guardrails/canary.js';
1
+ import { eventHasCanary, isStreamedCanaryEvent, redactCanary } from '../../../guardrails/canary.js';
2
+ import { EGRESS_RULES } from '../../../guardrails/egress.js';
2
3
  import { publicError, throwIfAborted, toErrorEvent } from '../../../guardrails/error.js';
4
+ import { guardrailFromHits } from '../../../guardrails/events.js';
5
+ import { resolveGuardrailPolicy } from '../../../guardrails/policy.js';
6
+ import { createOutboundProgressiveGate, } from '../../../guardrails/progressive-yield.js';
7
+ import { profileTurnOutputs } from '../../registry/profile-outputs.js';
3
8
  import { providerCompleteRequest } from '../../registry/provider-request.js';
4
- function systemFromProfile(profile, role) {
5
- const { identity } = profile;
6
- const { systemByRole, system } = identity;
7
- if (systemByRole) {
8
- const byRole = systemByRole[role];
9
- if (byRole) {
10
- return byRole;
11
- }
12
- }
13
- if (system) {
14
- return system;
15
- }
16
- return '';
17
- }
18
9
  function shouldSkipStreamEvent(event, profile) {
19
- return event.type === 'thought' && profile.outputs?.streaming?.streamThoughts === false;
10
+ return (event.type === 'thought' && profileTurnOutputs(profile)?.streaming?.streamThoughts === false);
20
11
  }
21
12
  function* processNormalEvent(event) {
22
13
  if (event.type === 'error') {
@@ -32,38 +23,109 @@ function* processNormalEvent(event) {
32
23
  }
33
24
  }
34
25
  function* yieldCanaryLeak(canary, event) {
26
+ const guardrail = guardrailFromHits('output_delta', 'untrusted', [{ rule: EGRESS_RULES.canary, severity: 'high', match: '[canary]' }], 'block');
27
+ if (guardrail) {
28
+ yield guardrail;
29
+ }
35
30
  yield redactCanary(event, canary);
36
31
  yield toErrorEvent('canary leaked');
37
32
  }
38
- function* yieldGatedStreamEvent(event, gate, canary) {
39
- const fragment = event.text ?? '';
40
- const result = gate.process(fragment);
41
- if (result.leak) {
42
- yield* yieldCanaryLeak(canary, event);
43
- return 'stop';
44
- }
45
- if (result.emit) {
46
- yield* processNormalEvent({ ...event, text: result.emit });
33
+ function* yieldDeltaBlock(hits) {
34
+ const guardrail = guardrailFromHits('output_delta', 'untrusted', hits, 'block');
35
+ if (guardrail) {
36
+ yield guardrail;
47
37
  }
48
- return 'continue';
49
38
  }
50
- function* flushCanaryGate(gate, _canary, lastType) {
51
- const result = gate.flush();
52
- if (result.leak) {
53
- yield toErrorEvent('canary leaked');
54
- return 'stop';
55
- }
56
- if (result.emit) {
57
- const emitType = lastType ?? 'text';
58
- yield* processNormalEvent({ type: emitType, text: result.emit });
59
- }
60
- return 'pass';
39
+ function isHostVisible(event) {
40
+ return event.type === 'text' || event.type === 'thought' || event.type === 'media';
41
+ }
42
+ function canaryOnlyImmediateStop(policy) {
43
+ return !policy.egress?.enforce;
44
+ }
45
+ function hasCanaryHit(hits) {
46
+ return hits.some((hit) => hit.rule === EGRESS_RULES.canary);
61
47
  }
62
48
  async function* yieldProviderEvents(args) {
63
- const { generation, system, provider, upstream, signal } = args;
49
+ const { profile, generation, system, provider, upstream, signal, control } = args;
64
50
  const { canary } = generation;
65
- const gate = canary ? createCanaryStreamGate(canary) : null;
51
+ const policy = resolveGuardrailPolicy(profile.guardrails);
52
+ const context = {
53
+ stage: 'output_final',
54
+ trust: 'untrusted',
55
+ profileId: profile.id,
56
+ ...(canary ? { canary } : {}),
57
+ };
58
+ const gate = createOutboundProgressiveGate(policy, context);
66
59
  let lastStreamType;
60
+ let withholdVisible = false;
61
+ function armWithhold() {
62
+ withholdVisible = true;
63
+ if (control)
64
+ control.withholdVisible = true;
65
+ }
66
+ function withholding() {
67
+ return withholdVisible;
68
+ }
69
+ async function* drainBlockedDelta(hits) {
70
+ yield* yieldDeltaBlock(hits);
71
+ // Arm withhold before recording the unreleased tail so the step runner
72
+ // does not forward that text to the host.
73
+ armWithhold();
74
+ const tail = gate?.drainUnreleased();
75
+ if (tail)
76
+ yield { type: 'text', text: tail };
77
+ }
78
+ async function* flushGate() {
79
+ if (!gate || !lastStreamType) {
80
+ return 'pass';
81
+ }
82
+ const emitType = lastStreamType;
83
+ const result = await gate.flush();
84
+ lastStreamType = undefined;
85
+ if (result.blocked) {
86
+ if (hasCanaryHit(result.hits) && canary && canaryOnlyImmediateStop(policy)) {
87
+ yield* yieldCanaryLeak(canary, { type: emitType, text: gate.accumulated() });
88
+ return 'stop';
89
+ }
90
+ yield* drainBlockedDelta(result.hits);
91
+ return 'pass';
92
+ }
93
+ if (result.emit) {
94
+ yield* processNormalEvent({ type: emitType, text: result.emit });
95
+ }
96
+ return 'pass';
97
+ }
98
+ async function* gateStreamEvent(event) {
99
+ if (!gate) {
100
+ yield* processNormalEvent(event);
101
+ return 'continue';
102
+ }
103
+ if (withholding()) {
104
+ // Keep recording ungated fragments for egress context; host will not see them.
105
+ yield { type: event.type, text: event.text ?? '' };
106
+ return 'continue';
107
+ }
108
+ if (lastStreamType && lastStreamType !== event.type) {
109
+ const flushed = yield* flushGate();
110
+ if (flushed === 'stop') {
111
+ return 'stop';
112
+ }
113
+ }
114
+ lastStreamType = event.type;
115
+ const result = await gate.process(event.text ?? '');
116
+ if (result.blocked) {
117
+ if (hasCanaryHit(result.hits) && canary && canaryOnlyImmediateStop(policy)) {
118
+ yield* yieldCanaryLeak(canary, event);
119
+ return 'stop';
120
+ }
121
+ yield* drainBlockedDelta(result.hits);
122
+ return 'continue';
123
+ }
124
+ if (result.emit) {
125
+ yield* processNormalEvent({ ...event, text: result.emit });
126
+ }
127
+ return 'continue';
128
+ }
67
129
  throwIfAborted(signal);
68
130
  for await (const event of provider.complete({
69
131
  ...providerCompleteRequest(generation, system),
@@ -73,38 +135,35 @@ async function* yieldProviderEvents(args) {
73
135
  },
74
136
  })) {
75
137
  throwIfAborted(signal);
76
- if (gate && isStreamedCanaryEvent(event)) {
77
- if (lastStreamType && lastStreamType !== event.type) {
78
- const status = yield* flushCanaryGate(gate, canary, lastStreamType);
79
- if (status === 'stop') {
80
- return;
81
- }
82
- }
83
- lastStreamType = event.type;
84
- const status = yield* yieldGatedStreamEvent(event, gate, canary);
138
+ if (isStreamedCanaryEvent(event)) {
139
+ const status = yield* gateStreamEvent(event);
85
140
  if (status === 'stop') {
86
141
  return;
87
142
  }
88
143
  continue;
89
144
  }
90
145
  if (gate && lastStreamType) {
91
- const status = yield* flushCanaryGate(gate, canary, lastStreamType);
92
- if (status === 'stop') {
146
+ const flushed = yield* flushGate();
147
+ if (flushed === 'stop') {
93
148
  return;
94
149
  }
95
- lastStreamType = undefined;
96
150
  }
97
151
  if (canary && eventHasCanary(event, canary)) {
98
152
  yield* yieldCanaryLeak(canary, event);
99
153
  return;
100
154
  }
155
+ if (withholding() && isHostVisible(event)) {
156
+ // Record for attempt egress / repair; step runner withholds from host.
157
+ yield event;
158
+ continue;
159
+ }
101
160
  yield* processNormalEvent(event);
102
161
  }
103
- if (gate) {
104
- const status = yield* flushCanaryGate(gate, canary, lastStreamType);
105
- if (status === 'stop') {
162
+ if (gate && lastStreamType) {
163
+ const flushed = yield* flushGate();
164
+ if (flushed === 'stop') {
106
165
  return;
107
166
  }
108
167
  }
109
168
  }
110
- export { shouldSkipStreamEvent, systemFromProfile, yieldProviderEvents };
169
+ export { shouldSkipStreamEvent, yieldProviderEvents };
@@ -4,6 +4,8 @@
4
4
  * Shares resolve / tools / canary / system / outbound gate with `runTurn`.
5
5
  * Does not use `ModelProvider.complete()` — live is a session, not one turn.
6
6
  *
7
+ * Stages: `docs/contracts/stages.md` (cycle map + `executeTool`).
8
+ *
7
9
  * @module
8
10
  */
9
11
  import type { GeminiTransport } from '../../../providers/google/keys.js';
@@ -18,6 +20,6 @@ export interface RunSessionOptions {
18
20
  * Open a gated Gemini Live session for a `type: 'live'` profile.
19
21
  *
20
22
  * Hosts bridge browser sockets and tool dispatch; THEORUM owns Gemini WS,
21
- * framing, inbound prep, and outbound canary/egress gates.
23
+ * framing, inbound prep, outbound canary/egress gates, and live stages.
22
24
  */
23
25
  export declare function runSession(req: SessionRequest, options: RunSessionOptions): Promise<LiveSession>;