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
@@ -0,0 +1,175 @@
1
+ /**
2
+ * Turn stages: frozen shapes, defensive affordance application, and the one
3
+ * spine every stage runs through (`runStage`).
4
+ *
5
+ * Contract: `docs/contracts/stages.md`. Text `runTurn`, live `runSession`, and
6
+ * tool execute differ only in what they do with a stage's output — history
7
+ * append, live text ingress, or a tool gate — never in how the stage runs.
8
+ *
9
+ * @module
10
+ */
11
+ import { AWAITING_USER_INPUT_STATUS, type AwaitingUserInputKind, type TurnStage } from './schema.js';
12
+ import type { TurnStop } from './stop.js';
13
+ import type { ModelToolResult, ToolFailure, ToolGate } from './tools/types.js';
14
+ import type { Profile, TurnEvent, TurnHistoryMessage } from './types.js';
15
+ /** Canonical homes: schema (`TurnStage`, `ToolGateKind`), tools/types (`ToolGate`). */
16
+ export type { AwaitingUserInputKind, ToolGate };
17
+ /** Closed set of kernel-applied stage affordances. */
18
+ export declare const STAGE_AFFORDANCES: readonly ["inject", "abort", "deny", "confirm", "mutate"];
19
+ export type StageAffordance = (typeof STAGE_AFFORDANCES)[number];
20
+ /**
21
+ * Physical affordance matrix from `docs/contracts/stages.md`.
22
+ * Inject still requires `injectAllowed` / `profileAllowsInject` at apply time.
23
+ * Empty list = observe only.
24
+ */
25
+ export declare const STAGE_AFFORDANCE_MATRIX: Readonly<Record<TurnStage, readonly StageAffordance[]>>;
26
+ /** Frozen awaiting completion payload (`docs/contracts/stages.md`). */
27
+ export interface AwaitingUserInput {
28
+ status: typeof AWAITING_USER_INPUT_STATUS;
29
+ kind: AwaitingUserInputKind;
30
+ prompt: string;
31
+ options?: string[];
32
+ }
33
+ /** Tool/stop fields shared by text + live stage apply argument bags. */
34
+ export type StageCallBag = {
35
+ callId?: string;
36
+ tool?: string;
37
+ input?: unknown;
38
+ callNotStarted?: boolean;
39
+ outputRaw?: unknown;
40
+ outputModel?: ModelToolResult;
41
+ failure?: ToolFailure;
42
+ awaiting?: boolean;
43
+ stop?: TurnStop;
44
+ gate?: ToolGate;
45
+ };
46
+ /** Context passed to `onStage`. */
47
+ export interface StageContext extends StageCallBag {
48
+ stage: TurnStage;
49
+ /** 1-based provider step (text) or utterance cycle index (live). */
50
+ step: number;
51
+ history: readonly TurnHistoryMessage[];
52
+ /** Opaque host slot — never traced or client-forwarded by the kernel. */
53
+ host?: unknown;
54
+ }
55
+ /** Host return from `onStage`. */
56
+ export interface StageResult {
57
+ inject?: TurnHistoryMessage[];
58
+ abort?: boolean | {
59
+ reason?: string;
60
+ };
61
+ /** `pre_tool`: refuse the call. `post_tool`: replace the result with this failure. */
62
+ deny?: {
63
+ code?: string;
64
+ message?: string;
65
+ };
66
+ /** `pre_tool` only — request a confirm/permission gate. */
67
+ confirm?: true | {
68
+ summary?: string;
69
+ };
70
+ /** `pre_tool`: replace the call input. `post_tool`: replace the raw output. Both re-validate. */
71
+ mutate?: StageMutate;
72
+ }
73
+ /** What `mutate` replaces: the stage's subject. */
74
+ export type StageMutate = {
75
+ input: unknown;
76
+ } | {
77
+ output: unknown;
78
+ };
79
+ export type StageHandler = (ctx: StageContext) => StageResult | undefined | Promise<StageResult | undefined>;
80
+ export type StageApplyWarningCode = 'affordance_not_allowed' | 'inject_not_allowed' | 'inject_rejected_max_steps' | 'inject_invalid_messages' | 'deny_invalid' | 'confirm_invalid' | 'mutate_invalid' | 'abort_invalid' | 'unknown_field' | 'result_invalid';
81
+ export interface StageApplyWarning {
82
+ code: StageApplyWarningCode;
83
+ message: string;
84
+ field: string;
85
+ }
86
+ export interface StageApplyInput {
87
+ stage: TurnStage;
88
+ /** Host return — treated as untrusted (`unknown` at the boundary). */
89
+ result: unknown;
90
+ /** Profile/session inject gate (`profileAllowsInject` / shipping `allowSteering`). */
91
+ injectAllowed: boolean;
92
+ /** When true, another provider step would exceed `maxSteps`. */
93
+ injectWouldExceedMaxSteps?: boolean;
94
+ /**
95
+ * False when the stage's mutate subject is absent at this fire (a `post_tool`
96
+ * whose body never completed, or a tool whose output the kernel must own).
97
+ * `mutate` is then a `mutate_invalid` warning, never a silent no-op.
98
+ */
99
+ mutable?: boolean;
100
+ }
101
+ export interface StageApplyOutput {
102
+ inject?: TurnHistoryMessage[];
103
+ abort?: boolean | {
104
+ reason?: string;
105
+ };
106
+ deny?: {
107
+ code: string;
108
+ message: string;
109
+ };
110
+ confirm?: {
111
+ summary?: string;
112
+ };
113
+ mutate?: StageMutate;
114
+ warnings: StageApplyWarning[];
115
+ }
116
+ /** True when the affordance is physically allowed at `stage` (ignores inject gate). */
117
+ export declare function stageAllowsAffordance(stage: TurnStage, affordance: StageAffordance): boolean;
118
+ /**
119
+ * Parse tool output as awaiting-user-input. Returns undefined when the shape
120
+ * is absent or invalid (does not throw — callers treat as normal output).
121
+ */
122
+ export declare function parseAwaitingUserInput(output: unknown): AwaitingUserInput | undefined;
123
+ /** True when output is a valid awaiting completion. */
124
+ export declare function isAwaitingUserInput(output: unknown): output is AwaitingUserInput;
125
+ /**
126
+ * Normalize / validate a host `ToolGate`. Returns undefined when invalid
127
+ * (defensive — never throws into the runner).
128
+ */
129
+ export declare function parseToolGate(value: unknown, fallbackTool?: string): ToolGate | undefined;
130
+ /**
131
+ * Defensively apply a host stage return against the affordance matrix.
132
+ * Never throws. Invalid fields become warnings and are dropped.
133
+ */
134
+ export declare function applyStageResult(input: StageApplyInput): StageApplyOutput;
135
+ export type StageEventExtra = {
136
+ callId?: string;
137
+ toolName?: string;
138
+ callNotStarted?: boolean;
139
+ awaiting?: boolean;
140
+ gate?: ToolGate;
141
+ stop?: TurnStop;
142
+ };
143
+ /** Build a stream `stage` event. Unknown extra keys are not copied. */
144
+ export declare function stageEventFields(stage: TurnStage, extra?: Partial<StageEventExtra>): TurnEvent;
145
+ /** True when another provider step would exceed profile/generation maxSteps. */
146
+ export declare function injectWouldExceedMaxSteps(stepCount: number, maxSteps: number | undefined): boolean;
147
+ /** One stage run: the shared call bag plus who handles it and how injects are gated. */
148
+ export interface RunStageArgs extends StageCallBag {
149
+ stage: TurnStage;
150
+ step: number;
151
+ history: readonly TurnHistoryMessage[];
152
+ /**
153
+ * Handlers in call order. Later scalars win, `inject` lists concatenate. A
154
+ * tool-local `preTool` result rides here as the first handler.
155
+ */
156
+ handlers: readonly StageHandler[];
157
+ /** Profile guardrails — every inject site runs the untrusted sanitize path. */
158
+ guardrails: Profile['guardrails'];
159
+ injectAllowed: boolean;
160
+ injectWouldExceedMaxSteps?: boolean;
161
+ /** See `StageApplyInput.mutable`. */
162
+ mutable?: boolean;
163
+ host?: unknown;
164
+ signal?: AbortSignal;
165
+ }
166
+ /** Applied stage output. `inject` is sanitized and always present. */
167
+ export interface RunStageOutput extends Omit<StageApplyOutput, 'inject'> {
168
+ inject: TurnHistoryMessage[];
169
+ }
170
+ /**
171
+ * Run one stage: emit the `stage` event, call the handlers, apply the affordance
172
+ * matrix per handler, emit any warnings, sanitize injects (with a `guardrail`
173
+ * event when redaction fired). Stage events always emit, even with no handlers.
174
+ */
175
+ export declare function runStage(args: RunStageArgs): AsyncGenerator<TurnEvent, RunStageOutput>;
@@ -0,0 +1,476 @@
1
+ /**
2
+ * Turn stages: frozen shapes, defensive affordance application, and the one
3
+ * spine every stage runs through (`runStage`).
4
+ *
5
+ * Contract: `docs/contracts/stages.md`. Text `runTurn`, live `runSession`, and
6
+ * tool execute differ only in what they do with a stage's output — history
7
+ * append, live text ingress, or a tool gate — never in how the stage runs.
8
+ *
9
+ * @module
10
+ */
11
+ import { throwIfAborted } from '../guardrails/error.js';
12
+ import { guardrailFromHits } from '../guardrails/events.js';
13
+ import { detectionForTrust, resolveGuardrailPolicy } from '../guardrails/policy.js';
14
+ import { sanitizeHistory } from '../guardrails/sanitize.js';
15
+ import { AWAITING_USER_INPUT_KINDS, AWAITING_USER_INPUT_STATUS, isToolGateKind, TOOL_PERMISSION, } from './schema.js';
16
+ const AWAITING_KIND_SET = new Set(AWAITING_USER_INPUT_KINDS);
17
+ const PERMISSION_SET = new Set(TOOL_PERMISSION);
18
+ /** Closed set of kernel-applied stage affordances. */
19
+ export const STAGE_AFFORDANCES = ['inject', 'abort', 'deny', 'confirm', 'mutate'];
20
+ /**
21
+ * Physical affordance matrix from `docs/contracts/stages.md`.
22
+ * Inject still requires `injectAllowed` / `profileAllowsInject` at apply time.
23
+ * Empty list = observe only.
24
+ */
25
+ export const STAGE_AFFORDANCE_MATRIX = Object.freeze({
26
+ pre_turn: Object.freeze(['inject', 'abort']),
27
+ pre_tool: Object.freeze(['abort', 'deny', 'confirm', 'mutate']),
28
+ post_tool: Object.freeze(['inject', 'abort', 'deny', 'mutate']),
29
+ before_end: Object.freeze(['inject', 'abort']),
30
+ post_turn: Object.freeze([]),
31
+ });
32
+ const STAGE_RESULT_KEYS = new Set(STAGE_AFFORDANCES);
33
+ /** True when the affordance is physically allowed at `stage` (ignores inject gate). */
34
+ export function stageAllowsAffordance(stage, affordance) {
35
+ return STAGE_AFFORDANCE_MATRIX[stage].includes(affordance);
36
+ }
37
+ function isRecord(value) {
38
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
39
+ }
40
+ function warn(warnings, code, field, message) {
41
+ warnings.push({ code, field, message });
42
+ }
43
+ /**
44
+ * Parse tool output as awaiting-user-input. Returns undefined when the shape
45
+ * is absent or invalid (does not throw — callers treat as normal output).
46
+ */
47
+ export function parseAwaitingUserInput(output) {
48
+ if (!isRecord(output))
49
+ return undefined;
50
+ if (output.status !== AWAITING_USER_INPUT_STATUS)
51
+ return undefined;
52
+ if (typeof output.kind !== 'string' || !AWAITING_KIND_SET.has(output.kind)) {
53
+ return undefined;
54
+ }
55
+ if (typeof output.prompt !== 'string')
56
+ return undefined;
57
+ const prompt = output.prompt.trim();
58
+ if (!prompt)
59
+ return undefined;
60
+ let options;
61
+ if (output.options !== undefined) {
62
+ if (!Array.isArray(output.options))
63
+ return undefined;
64
+ options = [];
65
+ for (const item of output.options) {
66
+ if (typeof item !== 'string')
67
+ return undefined;
68
+ const trimmed = item.trim();
69
+ if (!trimmed)
70
+ return undefined;
71
+ options.push(trimmed);
72
+ }
73
+ if (output.kind === 'choice' && options.length === 0)
74
+ return undefined;
75
+ }
76
+ else if (output.kind === 'choice') {
77
+ return undefined;
78
+ }
79
+ const parsed = {
80
+ status: AWAITING_USER_INPUT_STATUS,
81
+ kind: output.kind,
82
+ prompt,
83
+ };
84
+ if (options)
85
+ parsed.options = options;
86
+ return parsed;
87
+ }
88
+ /** True when output is a valid awaiting completion. */
89
+ export function isAwaitingUserInput(output) {
90
+ return parseAwaitingUserInput(output) !== undefined;
91
+ }
92
+ function parseAuthChallenge(value) {
93
+ if (!isRecord(value))
94
+ return undefined;
95
+ if (typeof value.slot !== 'string' || !value.slot.trim())
96
+ return undefined;
97
+ if (value.authType !== 'bearer' && value.authType !== 'api_key' && value.authType !== 'oauth2') {
98
+ return undefined;
99
+ }
100
+ if (typeof value.message !== 'string' || !value.message.trim())
101
+ return undefined;
102
+ const authChallenge = {
103
+ slot: value.slot.trim(),
104
+ authType: value.authType,
105
+ message: value.message.trim(),
106
+ };
107
+ if (typeof value.authorizationUrl === 'string') {
108
+ authChallenge.authorizationUrl = value.authorizationUrl;
109
+ }
110
+ if (typeof value.state === 'string')
111
+ authChallenge.state = value.state;
112
+ if (typeof value.issuer === 'string')
113
+ authChallenge.issuer = value.issuer;
114
+ if (typeof value.resource === 'string')
115
+ authChallenge.resource = value.resource;
116
+ if (Array.isArray(value.requiredScopes)) {
117
+ const scopes = [];
118
+ for (const s of value.requiredScopes) {
119
+ if (typeof s === 'string' && s.trim())
120
+ scopes.push(s.trim());
121
+ }
122
+ if (scopes.length > 0)
123
+ authChallenge.requiredScopes = scopes;
124
+ }
125
+ return authChallenge;
126
+ }
127
+ /**
128
+ * Normalize / validate a host `ToolGate`. Returns undefined when invalid
129
+ * (defensive — never throws into the runner).
130
+ */
131
+ export function parseToolGate(value, fallbackTool = '') {
132
+ if (!isRecord(value))
133
+ return undefined;
134
+ if (!isToolGateKind(value.kind))
135
+ return undefined;
136
+ const tool = typeof value.tool === 'string' && value.tool.trim() ? value.tool.trim() : fallbackTool.trim();
137
+ if (!tool)
138
+ return undefined;
139
+ const gate = { kind: value.kind, tool };
140
+ if (typeof value.summary === 'string' && value.summary.trim()) {
141
+ gate.summary = value.summary.trim();
142
+ }
143
+ if (typeof value.permission === 'string' && PERMISSION_SET.has(value.permission)) {
144
+ gate.permission = value.permission;
145
+ }
146
+ if (value.kind === 'auth') {
147
+ const authChallenge = parseAuthChallenge(value.authChallenge);
148
+ if (!authChallenge)
149
+ return undefined;
150
+ gate.authChallenge = authChallenge;
151
+ }
152
+ return gate;
153
+ }
154
+ /**
155
+ * Whitelist-copy one inject history message. Drops unknown keys and rejects
156
+ * `role: 'tool'`. Does not run sanitizeHistory — that stays at the inject site.
157
+ */
158
+ function coerceInjectMessage(item, warnings) {
159
+ if (!isRecord(item)) {
160
+ warn(warnings, 'inject_invalid_messages', 'inject', 'inject entry must be an object'); // lexicon-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
161
+ return undefined;
162
+ }
163
+ if (item.role === 'tool') {
164
+ warn(warnings, 'inject_invalid_messages', 'inject', 'inject messages with role "tool" are rejected');
165
+ return undefined;
166
+ }
167
+ if (item.role !== 'user' && item.role !== 'assistant' && item.role !== 'system') {
168
+ warn(warnings, 'inject_invalid_messages', 'inject', `inject role not allowed: ${String(item.role)}`);
169
+ return undefined;
170
+ }
171
+ const msg = { role: item.role };
172
+ if (item.content !== undefined) {
173
+ if (typeof item.content !== 'string') {
174
+ warn(warnings, 'inject_invalid_messages', 'inject', 'inject content must be a string when present');
175
+ return undefined;
176
+ }
177
+ msg.content = item.content;
178
+ }
179
+ if (item.parts !== undefined) {
180
+ if (!Array.isArray(item.parts)) {
181
+ warn(warnings, 'inject_invalid_messages', 'inject', 'inject parts must be an array when present');
182
+ return undefined;
183
+ }
184
+ msg.parts = item.parts;
185
+ }
186
+ if (item.tool_calls !== undefined) {
187
+ if (!Array.isArray(item.tool_calls)) {
188
+ warn(warnings, 'inject_invalid_messages', 'inject', 'inject tool_calls must be an array when present');
189
+ return undefined;
190
+ }
191
+ msg.tool_calls = item.tool_calls;
192
+ }
193
+ if (item.name !== undefined) {
194
+ if (typeof item.name !== 'string') {
195
+ warn(warnings, 'inject_invalid_messages', 'inject', 'inject name must be a string'); // lexicon-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
196
+ return undefined;
197
+ }
198
+ msg.name = item.name;
199
+ }
200
+ if (item.metadata !== undefined) {
201
+ if (!isRecord(item.metadata)) {
202
+ warn(warnings, 'inject_invalid_messages', 'inject', 'inject metadata must be a plain object'); // lexicon-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
203
+ return undefined;
204
+ }
205
+ msg.metadata = { ...item.metadata };
206
+ }
207
+ if (item.tool_call_id !== undefined) {
208
+ warn(warnings, 'inject_invalid_messages', 'inject', 'inject tool_call_id is rejected (not a tool-role message)');
209
+ }
210
+ return msg;
211
+ }
212
+ function coerceHistoryMessages(raw, warnings) {
213
+ if (!Array.isArray(raw)) {
214
+ warn(warnings, 'inject_invalid_messages', 'inject', 'inject must be an array of history messages');
215
+ return undefined;
216
+ }
217
+ const out = [];
218
+ for (const item of raw) {
219
+ const msg = coerceInjectMessage(item, warnings);
220
+ if (msg)
221
+ out.push(msg);
222
+ }
223
+ return out.length > 0 ? out : undefined;
224
+ }
225
+ function applyInjectField(result, input, out) {
226
+ if (!('inject' in result) || result.inject === undefined)
227
+ return;
228
+ const { stage, injectAllowed, injectWouldExceedMaxSteps } = input;
229
+ if (!stageAllowsAffordance(stage, 'inject')) {
230
+ warn(out.warnings, 'affordance_not_allowed', 'inject', `inject is not allowed at stage ${stage}`);
231
+ }
232
+ else if (!injectAllowed) {
233
+ warn(out.warnings, 'inject_not_allowed', 'inject', 'inject gate is closed for this profile/session');
234
+ }
235
+ else if (injectWouldExceedMaxSteps) {
236
+ warn(out.warnings, 'inject_rejected_max_steps', 'inject', 'inject rejected: another model step would exceed maxSteps');
237
+ }
238
+ else {
239
+ const messages = coerceHistoryMessages(result.inject, out.warnings);
240
+ if (messages)
241
+ out.inject = messages;
242
+ }
243
+ }
244
+ function applyAbortField(result, stage, out) {
245
+ if (!('abort' in result) || result.abort === undefined)
246
+ return;
247
+ if (!stageAllowsAffordance(stage, 'abort')) {
248
+ warn(out.warnings, 'affordance_not_allowed', 'abort', 'abort is not allowed at post_turn'); // lexicon-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
249
+ }
250
+ else if (result.abort === true) {
251
+ out.abort = true;
252
+ }
253
+ else if (isRecord(result.abort)) {
254
+ const reason = typeof result.abort.reason === 'string' && result.abort.reason.trim()
255
+ ? result.abort.reason.trim()
256
+ : undefined;
257
+ out.abort = reason ? { reason } : true;
258
+ }
259
+ else if (result.abort !== false) {
260
+ warn(out.warnings, 'abort_invalid', 'abort', 'abort must be boolean or { reason?: string }'); // lexicon-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
261
+ }
262
+ }
263
+ function applyDenyField(result, stage, out) {
264
+ if (!('deny' in result) || result.deny === undefined)
265
+ return;
266
+ if (!stageAllowsAffordance(stage, 'deny')) {
267
+ warn(out.warnings, 'affordance_not_allowed', 'deny', `deny is not allowed at stage ${stage}`); // lexicon-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
268
+ }
269
+ else if (!isRecord(result.deny)) {
270
+ warn(out.warnings, 'deny_invalid', 'deny', 'deny must be an object'); // lexicon-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
271
+ }
272
+ else {
273
+ const code = typeof result.deny.code === 'string' && result.deny.code.trim()
274
+ ? result.deny.code.trim()
275
+ : 'not_authorized';
276
+ const message = typeof result.deny.message === 'string' && result.deny.message.trim()
277
+ ? result.deny.message.trim()
278
+ : 'Tool execution not authorized'; // lexicon-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
279
+ out.deny = { code, message };
280
+ }
281
+ }
282
+ function applyConfirmField(result, stage, out) {
283
+ if (!('confirm' in result) || result.confirm === undefined)
284
+ return;
285
+ if (!stageAllowsAffordance(stage, 'confirm')) {
286
+ warn(out.warnings, 'affordance_not_allowed', 'confirm', `confirm is not allowed at stage ${stage}`);
287
+ }
288
+ else if (result.confirm === true) {
289
+ out.confirm = {};
290
+ }
291
+ else if (isRecord(result.confirm)) {
292
+ const summary = typeof result.confirm.summary === 'string' && result.confirm.summary.trim()
293
+ ? result.confirm.summary.trim()
294
+ : undefined;
295
+ out.confirm = summary ? { summary } : {};
296
+ }
297
+ else {
298
+ warn(out.warnings, 'confirm_invalid', 'confirm', 'confirm must be true or { summary?: string }');
299
+ }
300
+ }
301
+ /** The subject `mutate` replaces at each stage that allows it. */
302
+ const MUTATE_SUBJECT = { pre_tool: 'input', post_tool: 'output' };
303
+ function mutateSubject(stage) {
304
+ return stage in MUTATE_SUBJECT ? MUTATE_SUBJECT[stage] : undefined;
305
+ }
306
+ function applyMutateField(result, input, out) {
307
+ const { stage } = input;
308
+ if (!('mutate' in result) || result.mutate === undefined)
309
+ return;
310
+ if (!stageAllowsAffordance(stage, 'mutate')) {
311
+ warn(out.warnings, 'affordance_not_allowed', 'mutate', `mutate is not allowed at stage ${stage}`);
312
+ return;
313
+ }
314
+ const subject = mutateSubject(stage);
315
+ if (!subject) {
316
+ // Matrix allows mutate but no subject is mapped: a kernel drift, surfaced not swallowed.
317
+ warn(out.warnings, 'mutate_invalid', 'mutate',
318
+ // lexicon-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
319
+ `no mutate subject is defined at stage ${stage}`);
320
+ return;
321
+ }
322
+ if (!isRecord(result.mutate) || result.mutate[subject] === undefined) {
323
+ warn(out.warnings, 'mutate_invalid', 'mutate',
324
+ // lexicon-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
325
+ `mutate must be { ${subject}: unknown } at stage ${stage}`);
326
+ return;
327
+ }
328
+ if (input.mutable === false) {
329
+ warn(out.warnings, 'mutate_invalid', 'mutate',
330
+ // lexicon-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
331
+ `there is no ${subject} to replace at this ${stage} fire`);
332
+ return;
333
+ }
334
+ out.mutate =
335
+ subject === 'input' ? { input: result.mutate.input } : { output: result.mutate.output };
336
+ }
337
+ /**
338
+ * Defensively apply a host stage return against the affordance matrix.
339
+ * Never throws. Invalid fields become warnings and are dropped.
340
+ */
341
+ export function applyStageResult(input) {
342
+ const warnings = [];
343
+ const out = { warnings };
344
+ const { stage, result } = input;
345
+ if (result == null)
346
+ return out;
347
+ if (!isRecord(result)) {
348
+ warn(warnings, 'result_invalid', 'result', 'StageResult must be a plain object'); // lexicon-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
349
+ return out;
350
+ }
351
+ for (const key of Object.keys(result)) {
352
+ if (!STAGE_RESULT_KEYS.has(key)) {
353
+ warn(warnings, 'unknown_field', key, `unknown StageResult field "${key}" ignored`); // lexicon-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
354
+ }
355
+ }
356
+ applyInjectField(result, input, out);
357
+ applyAbortField(result, stage, out);
358
+ applyDenyField(result, stage, out);
359
+ applyConfirmField(result, stage, out);
360
+ applyMutateField(result, input, out);
361
+ // confirm + deny together: deny wins
362
+ if (out.deny && out.confirm) {
363
+ warn(warnings, 'confirm_invalid', 'confirm', 'confirm ignored because deny is set'); // lexicon-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
364
+ delete out.confirm;
365
+ }
366
+ return out;
367
+ }
368
+ /** Build a stream `stage` event. Unknown extra keys are not copied. */
369
+ export function stageEventFields(stage, extra) {
370
+ const event = { type: 'stage', stage };
371
+ if (!extra)
372
+ return event;
373
+ if (extra.callId !== undefined)
374
+ event.callId = extra.callId;
375
+ if (extra.toolName !== undefined)
376
+ event.toolName = extra.toolName;
377
+ if (extra.callNotStarted !== undefined)
378
+ event.callNotStarted = extra.callNotStarted;
379
+ if (extra.awaiting !== undefined)
380
+ event.awaiting = extra.awaiting;
381
+ if (extra.gate !== undefined)
382
+ event.gate = extra.gate;
383
+ if (extra.stop !== undefined)
384
+ event.stop = extra.stop;
385
+ return event;
386
+ }
387
+ /** True when another provider step would exceed profile/generation maxSteps. */
388
+ export function injectWouldExceedMaxSteps(stepCount, maxSteps) {
389
+ if (maxSteps === undefined || maxSteps <= 0)
390
+ return false;
391
+ return stepCount >= maxSteps;
392
+ }
393
+ /**
394
+ * Merge per-handler applied outputs in call order. Later scalars win, `inject`
395
+ * lists concatenate, warnings accumulate. `deny` beats `confirm` across handlers
396
+ * exactly as it does within one return.
397
+ */
398
+ function mergeApplied(parts) {
399
+ const out = { warnings: parts.flatMap((part) => part.warnings) };
400
+ for (const part of parts) {
401
+ if (part.abort !== undefined)
402
+ out.abort = part.abort;
403
+ if (part.deny)
404
+ out.deny = part.deny;
405
+ if (part.confirm)
406
+ out.confirm = part.confirm;
407
+ if (part.mutate)
408
+ out.mutate = part.mutate;
409
+ if (part.inject?.length)
410
+ out.inject = [...(out.inject ?? []), ...part.inject];
411
+ }
412
+ if (out.deny && out.confirm) {
413
+ warn(out.warnings, 'confirm_invalid', 'confirm', 'confirm ignored because deny is set'); // lexicon-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
414
+ delete out.confirm;
415
+ }
416
+ return out;
417
+ }
418
+ /**
419
+ * Await each handler in order and apply its return on its own, so one handler's
420
+ * junk is that handler's warning and cannot erase another's affordance. Each
421
+ * handler gets its own shallow context; abort wins over any handler error.
422
+ */
423
+ async function applyHandlers(handlers, ctx, apply, signal) {
424
+ const parts = [];
425
+ for (const handler of handlers) {
426
+ let raw;
427
+ try {
428
+ raw = await handler({ ...ctx });
429
+ }
430
+ catch (err) {
431
+ throwIfAborted(signal);
432
+ throw err;
433
+ }
434
+ throwIfAborted(signal);
435
+ parts.push(applyStageResult({ ...apply, result: raw }));
436
+ }
437
+ return parts;
438
+ }
439
+ /**
440
+ * Run one stage: emit the `stage` event, call the handlers, apply the affordance
441
+ * matrix per handler, emit any warnings, sanitize injects (with a `guardrail`
442
+ * event when redaction fired). Stage events always emit, even with no handlers.
443
+ */
444
+ export async function* runStage(args) {
445
+ const { stage, step, history, handlers, guardrails, injectAllowed, injectWouldExceedMaxSteps, mutable, host, signal, ...bag } = args;
446
+ throwIfAborted(signal);
447
+ // Stream stage events stay lean (no outputRaw/failure). Hosts read those on
448
+ // StageContext via onStage; tool failures also ride tool events.
449
+ yield stageEventFields(stage, {
450
+ callId: bag.callId,
451
+ toolName: bag.tool,
452
+ callNotStarted: bag.callNotStarted,
453
+ awaiting: bag.awaiting,
454
+ gate: bag.gate,
455
+ stop: bag.stop,
456
+ });
457
+ if (handlers.length === 0)
458
+ return { warnings: [], inject: [] };
459
+ const ctx = { stage, step, history, host, ...bag };
460
+ const applied = mergeApplied(await applyHandlers(handlers, ctx, { stage, injectAllowed, injectWouldExceedMaxSteps, mutable }, signal));
461
+ if (applied.warnings.length > 0) {
462
+ yield {
463
+ ...stageEventFields(stage, { callId: bag.callId, toolName: bag.tool }),
464
+ stageWarnings: applied.warnings,
465
+ };
466
+ }
467
+ const { inject, ...rest } = applied;
468
+ if (!inject?.length)
469
+ return { ...rest, inject: [] };
470
+ const hits = [];
471
+ const sanitized = sanitizeHistory(inject, detectionForTrust(resolveGuardrailPolicy(guardrails), 'untrusted'), hits);
472
+ const redacted = guardrailFromHits('history', 'untrusted', hits, 'redact');
473
+ if (redacted)
474
+ yield redacted;
475
+ return { ...rest, inject: sanitized };
476
+ }