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,133 @@
1
+ /**
2
+ * OAuth 2.1 and Model Context Protocol authorization types.
3
+ *
4
+ * Implements current July 28, 2026 MCP specification requirements:
5
+ * - RFC 9728: Protected Resource Metadata
6
+ * - RFC 8414 & OpenID Connect: Authorization Server Metadata
7
+ * - RFC 9207: Authorization Server Issuer Identification (`iss` validation)
8
+ * - RFC 8707: Resource Indicators
9
+ * - RFC 7636: PKCE with S256
10
+ *
11
+ * @module
12
+ */
13
+ /** RFC 9728 Protected Resource Metadata */
14
+ export interface ProtectedResourceMetadata {
15
+ resource: string;
16
+ authorization_servers: string[];
17
+ scopes_supported?: string[];
18
+ bearer_methods_supported?: string[];
19
+ resource_documentation?: string;
20
+ [key: string]: unknown;
21
+ }
22
+ /** RFC 8414 / OpenID Connect Discovery Metadata */
23
+ export interface AuthorizationServerMetadata {
24
+ issuer: string;
25
+ authorization_endpoint: string;
26
+ token_endpoint: string;
27
+ registration_endpoint?: string;
28
+ scopes_supported?: string[];
29
+ response_types_supported?: string[];
30
+ grant_types_supported?: string[];
31
+ code_challenge_methods_supported?: string[];
32
+ authorization_response_iss_parameter_supported?: boolean;
33
+ client_id_metadata_document_supported?: boolean;
34
+ [key: string]: unknown;
35
+ }
36
+ /** Standard OAuth 2.1 token response */
37
+ export interface OAuthTokens {
38
+ access_token: string;
39
+ token_type?: string;
40
+ expires_in?: number;
41
+ refresh_token?: string;
42
+ scope?: string;
43
+ /** Custom extra parameters returned by AS */
44
+ [key: string]: unknown;
45
+ }
46
+ /** Normalized OAuth credential for storage / pass-through in TurnRequest */
47
+ export interface OAuth2Credential {
48
+ type: 'oauth2';
49
+ issuer: string;
50
+ resource?: string;
51
+ accessToken: string;
52
+ refreshToken?: string;
53
+ expiresAt?: number;
54
+ tokenEndpoint: string;
55
+ clientId: string;
56
+ scope?: string;
57
+ }
58
+ /** Bearer token credential */
59
+ export interface BearerCredential {
60
+ type: 'bearer';
61
+ token: string;
62
+ }
63
+ /** API key credential */
64
+ export interface ApiKeyCredential {
65
+ type: 'api_key';
66
+ key: string;
67
+ headerName?: string;
68
+ headerPrefix?: string;
69
+ }
70
+ /** Universal union of supported tool credentials */
71
+ export type ToolCredential = OAuth2Credential | BearerCredential | ApiKeyCredential;
72
+ /** Options for creating an OAuth 2.1 PKCE authorization flow */
73
+ export interface CreatePkceFlowOptions {
74
+ /** Target resource server URL (e.g. MCP server URL) or direct AS URL */
75
+ resourceServerUrl: string;
76
+ /** The OAuth client id or Client ID Metadata Document HTTPS URL */
77
+ clientId: string;
78
+ /** Redirect URI registered with the OAuth client */
79
+ redirectUri: string;
80
+ /** Desired scopes */
81
+ scopes?: string[];
82
+ /** Secret string used to HMAC-sign the stateless state envelope */
83
+ signingSecret: string;
84
+ /** State envelope TTL in milliseconds (default: 10 minutes) */
85
+ stateTtlMs?: number;
86
+ /** Optional pre-resolved discovery metadata to bypass runtime fetch */
87
+ preResolved?: {
88
+ issuer?: string;
89
+ authorizationEndpoint?: string;
90
+ tokenEndpoint?: string;
91
+ resource?: string;
92
+ };
93
+ /** Optional fetch function for testing or proxying */
94
+ fetchFn?: typeof fetch;
95
+ }
96
+ /** Result of initiating a PKCE flow */
97
+ export interface PkceFlowResult {
98
+ authorizationUrl: string;
99
+ state: string;
100
+ codeVerifier: string;
101
+ codeChallenge: string;
102
+ issuer: string;
103
+ resource?: string;
104
+ }
105
+ /** Options for exchanging an authorization code */
106
+ export interface ExchangePkceCodeOptions {
107
+ code: string;
108
+ state: string;
109
+ /** `iss` parameter returned in the authorization response query (RFC 9207) */
110
+ iss?: string;
111
+ /** Redirect URI matching the authorization request */
112
+ redirectUri: string;
113
+ /** Secret used to sign the state envelope */
114
+ signingSecret: string;
115
+ /** Optional token endpoint override (defaults to discovered AS token_endpoint) */
116
+ tokenEndpoint?: string;
117
+ fetchFn?: typeof fetch;
118
+ }
119
+ /** Result of token exchange */
120
+ export interface ExchangePkceCodeResult {
121
+ tokens: OAuthTokens;
122
+ credential: OAuth2Credential;
123
+ }
124
+ /** Options for refreshing an expired OAuth token */
125
+ export interface RefreshOAuthTokenOptions {
126
+ refreshToken: string;
127
+ tokenEndpoint: string;
128
+ clientId: string;
129
+ resource?: string;
130
+ scope?: string;
131
+ issuer: string;
132
+ fetchFn?: typeof fetch;
133
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * OAuth 2.1 and Model Context Protocol authorization types.
3
+ *
4
+ * Implements current July 28, 2026 MCP specification requirements:
5
+ * - RFC 9728: Protected Resource Metadata
6
+ * - RFC 8414 & OpenID Connect: Authorization Server Metadata
7
+ * - RFC 9207: Authorization Server Issuer Identification (`iss` validation)
8
+ * - RFC 8707: Resource Indicators
9
+ * - RFC 7636: PKCE with S256
10
+ *
11
+ * @module
12
+ */
13
+ export {};
@@ -770,6 +770,20 @@ const INTERMEDIATE_KEYS = [
770
770
  'intermediateTokens',
771
771
  'intermediate',
772
772
  ];
773
+ const CACHED_KEYS = [
774
+ 'total_cached_tokens',
775
+ 'totalCachedTokens',
776
+ 'cached_tokens',
777
+ 'cachedTokens',
778
+ 'cached',
779
+ ];
780
+ const CACHE_WRITE_KEYS = [
781
+ 'cache_write_tokens',
782
+ 'cacheWriteTokens',
783
+ 'cache_creation_input_tokens',
784
+ 'cacheCreationInputTokens',
785
+ 'cacheWrite',
786
+ ];
773
787
  const TOTAL_KEYS = [
774
788
  'total_tokens',
775
789
  'totalTokens',
@@ -789,18 +803,43 @@ function pickNumericField(record, keys) {
789
803
  }
790
804
  return 0;
791
805
  }
806
+ function detailsRecord(raw) {
807
+ for (const key of [
808
+ 'prompt_tokens_details',
809
+ 'promptTokensDetails',
810
+ 'input_tokens_details',
811
+ 'inputTokensDetails',
812
+ ]) {
813
+ const value = raw[key];
814
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
815
+ return value;
816
+ }
817
+ }
818
+ return undefined;
819
+ }
792
820
  function extractUsageTokens(raw) {
793
821
  if (!raw || typeof raw !== 'object') {
794
822
  return undefined;
795
823
  }
796
824
  const r = raw;
825
+ const details = detailsRecord(r);
797
826
  const input = pickNumericField(r, INPUT_KEYS);
798
827
  const output = pickNumericField(r, OUTPUT_KEYS);
799
828
  const thinking = pickNumericField(r, THINKING_KEYS);
800
829
  const toolUse = pickNumericField(r, TOOL_KEYS);
801
830
  const intermediate = pickNumericField(r, INTERMEDIATE_KEYS);
831
+ const cached = pickNumericField(r, CACHED_KEYS) || (details ? pickNumericField(details, CACHED_KEYS) : 0);
832
+ const cacheWrite = pickNumericField(r, CACHE_WRITE_KEYS) ||
833
+ (details ? pickNumericField(details, CACHE_WRITE_KEYS) : 0);
802
834
  const total = pickNumericField(r, TOTAL_KEYS) || input + output + thinking + toolUse + intermediate;
803
- if (input > 0 || output > 0 || thinking > 0 || toolUse > 0 || intermediate > 0 || total > 0) {
835
+ if (input > 0 ||
836
+ output > 0 ||
837
+ thinking > 0 ||
838
+ toolUse > 0 ||
839
+ intermediate > 0 ||
840
+ cached > 0 ||
841
+ cacheWrite > 0 ||
842
+ total > 0) {
804
843
  return {
805
844
  input,
806
845
  output,
@@ -808,6 +847,8 @@ function extractUsageTokens(raw) {
808
847
  toolUse,
809
848
  total,
810
849
  ...(intermediate > 0 ? { intermediate } : {}),
850
+ ...(cached > 0 ? { cached } : {}),
851
+ ...(cacheWrite > 0 ? { cacheWrite } : {}),
811
852
  };
812
853
  }
813
854
  return undefined;
@@ -896,7 +937,7 @@ function parseStructuredOutput(text) {
896
937
  return { ok: true, structured: JSON.parse(text) };
897
938
  }
898
939
  catch {
899
- return { ok: false, error: 'structured output was not valid JSON' };
940
+ return { ok: false, error: 'structured output was not valid JSON' }; // lexicon-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
900
941
  }
901
942
  }
902
943
  export { codeExecutionEvidence, codeExecutionStepKey, eventsFromComplete, eventsFromDelta, eventsFromInteractionSteps, extractTokenEvent, extractUsageTokens, googleBuiltinEvidence, groundingFromEvent, isCodeExecutionType, isGoogleBuiltinStepType, mergeCodeExecutionPayload, parseStructuredOutput, };
@@ -3,7 +3,19 @@
3
3
  *
4
4
  * @module
5
5
  */
6
- import type { Profile } from '../types.js';
7
- /** Sanitize profile-controlled inbound text and wrap with user_data fencing. */
8
- declare function prepareLiveInboundText(profile: Profile, text: string): string;
6
+ import type { Profile, TurnEvent } from '../types.js';
7
+ export interface LiveInboundPrepareResult {
8
+ text: string;
9
+ /** Present when inbound sanitize redacted spans. */
10
+ guardrail?: TurnEvent;
11
+ }
12
+ /**
13
+ * Sanitize profile-controlled inbound text and wrap with user_data fencing.
14
+ *
15
+ * Resolves through the shared policy so Live ingress and the turn engine cannot
16
+ * drift apart on what an unset switch means. Returns an optional guardrail event
17
+ * for the session stream when redaction ran. Match previews follow
18
+ * `observability.include.guardrailMatchPreview`.
19
+ */
20
+ declare function prepareLiveInboundText(profile: Profile, text: string): LiveInboundPrepareResult;
9
21
  export { prepareLiveInboundText };
@@ -4,14 +4,28 @@
4
4
  * @module
5
5
  */
6
6
  import { wrapUserData } from '../../guardrails/canary.js';
7
- import { sanitizeText } from '../../guardrails/sanitize.js';
8
- /** Sanitize profile-controlled inbound text and wrap with user_data fencing. */
7
+ import { guardrailFromHits, projectGuardrailTurnEvent } from '../../guardrails/events.js';
8
+ import { detectionForTrust, resolveGuardrailPolicy } from '../../guardrails/policy.js';
9
+ import { detectText } from '../../guardrails/sanitize.js';
10
+ import { resolveObservabilityPolicy } from '../../observability/resolve-policy.js';
11
+ /**
12
+ * Sanitize profile-controlled inbound text and wrap with user_data fencing.
13
+ *
14
+ * Resolves through the shared policy so Live ingress and the turn engine cannot
15
+ * drift apart on what an unset switch means. Returns an optional guardrail event
16
+ * for the session stream when redaction ran. Match previews follow
17
+ * `observability.include.guardrailMatchPreview`.
18
+ */
9
19
  function prepareLiveInboundText(profile, text) {
10
- const guardrails = profile.guardrails;
11
- const safe = sanitizeText(text, {
12
- sanitizeInput: guardrails?.sanitizeInput === true,
13
- redactSensitive: guardrails?.redactSensitive === true,
14
- });
15
- return wrapUserData(safe);
20
+ const policy = resolveGuardrailPolicy(profile.guardrails);
21
+ const detected = detectText(text, detectionForTrust(policy, 'untrusted'));
22
+ const raw = guardrailFromHits('live_inbound', 'untrusted', detected.hits, 'redact');
23
+ const includeMatch = resolveObservabilityPolicy(profile.observability).include
24
+ .guardrailMatchPreview;
25
+ const guardrail = raw ? projectGuardrailTurnEvent(raw, includeMatch) : undefined;
26
+ return {
27
+ text: wrapUserData(detected.text),
28
+ ...(guardrail ? { guardrail } : {}),
29
+ };
16
30
  }
17
31
  export { prepareLiveInboundText };
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Live realtime ingress — profile gates for sendAudio / sendVideo / sendText.
3
+ *
4
+ * @module
5
+ */
6
+ import type { LiveIngressSpec, Profile } from '../types.js';
7
+ export type LiveIngressChannel = keyof LiveIngressSpec;
8
+ /** Default when `live.ingress.<channel>` is omitted — mic and camera on, typed text off. */
9
+ export declare function liveIngressChannelDefault(channel: LiveIngressChannel): boolean;
10
+ /** Resolve one channel from an ingress spec object (no profile wrapper). */
11
+ export declare function liveIngressEnabledFromSpec(ingress: LiveIngressSpec | undefined, channel: LiveIngressChannel): boolean;
12
+ /** Whether a realtime ingress channel is enabled on the profile. */
13
+ export declare function liveIngressEnabled(profile: Profile, channel: LiveIngressChannel): boolean;
14
+ /** True when at least one realtime ingress channel is enabled. */
15
+ export declare function hasAnyLiveIngress(profile: Profile): boolean;
16
+ /** Reject profiles with every ingress channel disabled. */
17
+ export declare function assertLiveIngressConfigured(profile: Profile): void;
18
+ /** Reject send* calls when the profile disabled that ingress channel. */
19
+ export declare function assertLiveIngress(profile: Profile, channel: LiveIngressChannel): void;
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Live realtime ingress — profile gates for sendAudio / sendVideo / sendText.
3
+ *
4
+ * @module
5
+ */
6
+ import { TheorumError } from '../../guardrails/error.js';
7
+ const LIVE_INGRESS_CHANNELS = ['audio', 'video', 'text'];
8
+ function assertLiveProfile(profile) {
9
+ if (profile.type !== 'live') {
10
+ throw new TheorumError(`Profile '${profile.id}' is not type 'live'`); // lexicon-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
11
+ }
12
+ return profile;
13
+ }
14
+ /** Default when `live.ingress.<channel>` is omitted — mic and camera on, typed text off. */
15
+ export function liveIngressChannelDefault(channel) {
16
+ return channel !== 'text';
17
+ }
18
+ /** Resolve one channel from an ingress spec object (no profile wrapper). */
19
+ export function liveIngressEnabledFromSpec(ingress, channel) {
20
+ const value = ingress?.[channel];
21
+ if (value === undefined)
22
+ return liveIngressChannelDefault(channel);
23
+ return value;
24
+ }
25
+ /** Whether a realtime ingress channel is enabled on the profile. */
26
+ export function liveIngressEnabled(profile, channel) {
27
+ const live = assertLiveProfile(profile);
28
+ return liveIngressEnabledFromSpec(live.live.ingress, channel);
29
+ }
30
+ /** True when at least one realtime ingress channel is enabled. */
31
+ export function hasAnyLiveIngress(profile) {
32
+ const live = assertLiveProfile(profile);
33
+ return LIVE_INGRESS_CHANNELS.some((channel) => liveIngressEnabledFromSpec(live.live.ingress, channel));
34
+ }
35
+ /** Reject profiles with every ingress channel disabled. */
36
+ export function assertLiveIngressConfigured(profile) {
37
+ const live = assertLiveProfile(profile);
38
+ if (hasAnyLiveIngress(live))
39
+ return;
40
+ throw new TheorumError(`Profile '${live.id}': at least one live.ingress channel (audio, video, text) must be enabled`);
41
+ }
42
+ /** Reject send* calls when the profile disabled that ingress channel. */
43
+ export function assertLiveIngress(profile, channel) {
44
+ if (liveIngressEnabled(profile, channel))
45
+ return;
46
+ throw new TheorumError(`Profile '${profile.id}' live.ingress.${channel} is disabled — cannot send on this channel`);
47
+ }
@@ -1,3 +1,5 @@
1
+ import { lexiconText } from '../../guardrails/lexicon.js';
2
+ import { profileTurnOutputs } from '../registry/profile-outputs.js';
1
3
  const MAX_REPAIR_HISTORY_EXCHANGES = 2;
2
4
  const MAX_REPAIR_HISTORY_MESSAGES = MAX_REPAIR_HISTORY_EXCHANGES * 2;
3
5
  function scopeHistory(history) {
@@ -11,28 +13,27 @@ function formatHistoryBlock(messages) {
11
13
  return '';
12
14
  }
13
15
  const lines = messages.map((m) => `${m.role === 'user' ? 'User' : 'Assistant'}: ${m.content}`);
14
- return `### RECENT CONVERSATION CONTEXT (LAST ${messages.length} TURNS)\n${lines.join('\n')}\n\n`;
16
+ const heading = lexiconText('repair.history_heading', { count: messages.length });
17
+ return `${heading}\n${lines.join('\n')}\n\n`;
15
18
  }
16
19
  function synthesizeRepairPrompt(args) {
17
20
  const { profile, repair, history } = args;
18
21
  const guidance = repair.guidance ||
19
- profile.outputs?.validation?.repairGuidance ||
20
- 'Revise the previous output so it satisfies the validator rejection. Preserve the intended user-facing substance unless the guidance says otherwise.';
22
+ profileTurnOutputs(profile)?.validation?.repairGuidance ||
23
+ lexiconText('repair.default_guidance');
21
24
  const historyBlock = formatHistoryBlock(scopeHistory(history));
22
- let prompt = `## OUTPUT REPAIR REQUEST\n\n`;
23
- prompt += `The previous assistant output was rejected by a host validator and must be revised.\n\n`;
24
- prompt += `### PREVIOUS OUTPUT\n\`\`\`\n${repair.previousOutput.trim()}\n\`\`\`\n\n`;
25
- prompt += `### VALIDATOR REJECTION\n${repair.rejection.trim()}\n\n`;
25
+ let prompt = `${lexiconText('repair.prompt_header')}\n\n`;
26
+ prompt += `${lexiconText('repair.prompt_intro')}\n\n`;
27
+ prompt += `${lexiconText('repair.section_previous_output')}\n\`\`\`\n${repair.previousOutput.trim()}\n\`\`\`\n\n`;
28
+ prompt += `${lexiconText('repair.section_validator_rejection')}\n${repair.rejection.trim()}\n\n`;
26
29
  if (guidance.trim()) {
27
- prompt += `### REPAIR GUIDANCE\n${guidance.trim()}\n\n`;
30
+ prompt += `${lexiconText('repair.section_repair_guidance')}\n${guidance.trim()}\n\n`;
28
31
  }
29
32
  if (historyBlock) {
30
33
  prompt += historyBlock;
31
34
  }
32
- prompt += `### INSTRUCTIONS\n`;
33
- prompt += `1. Inspect the previous output and validator rejection.\n`;
34
- prompt += `2. Apply the repair guidance without inventing unsupported facts.\n`;
35
- prompt += `3. Return only the corrected assistant output required by the active profile.`;
35
+ prompt += `${lexiconText('repair.section_instructions')}\n`;
36
+ prompt += lexiconText('repair.prompt_instructions');
36
37
  return prompt;
37
38
  }
38
39
  export { synthesizeRepairPrompt };
@@ -1,14 +1,40 @@
1
+ import { runEnforcer } from '../../../guardrails/egress.js';
1
2
  import { TheorumError, throwIfAborted, toErrorEvent } from '../../../guardrails/error.js';
3
+ import { guardrailFromVerdict } from '../../../guardrails/events.js';
4
+ import { lexiconText } from '../../../guardrails/lexicon.js';
5
+ import { resolveGuardrailPolicy } from '../../../guardrails/policy.js';
2
6
  import { sanitizeTurnRequest } from '../../../guardrails/sanitize.js';
7
+ import { profileTurnOutputs } from '../../registry/profile-outputs.js';
3
8
  import { resolveTurn } from '../../registry/resolve.js';
4
9
  import { getStructured } from '../../registry/schemas.js';
10
+ import { injectWouldExceedMaxSteps } from '../../stages.js';
11
+ import { findLast } from '../../util/find-last.js';
5
12
  import { collectValidationFailures, formatValidationFailures } from './schema-validation.js';
13
+ import { applyTurnStage } from './stages.js';
6
14
  import { executeAttempt } from './steps.js';
15
+ /** Internal reason recorded when a turn is withheld; mapped to public copy on emit. */
16
+ const WITHHELD = 'Turn withheld: egress disclosure violation'; // lexicon-exempt: internal marker mapped by publicError
7
17
  function collectAttemptText(events) {
8
- return events
9
- .filter((e) => e.type === 'text' && e.text)
10
- .map((e) => e.text)
11
- .join('');
18
+ const parts = [];
19
+ for (const event of events) {
20
+ if (event.type === 'text' && event.text) {
21
+ parts.push(event.text);
22
+ }
23
+ }
24
+ return parts.join('');
25
+ }
26
+ /**
27
+ * Project attempt events into the egress payload.
28
+ *
29
+ * Structured output travels alongside text so a profile with `outputs.structured`
30
+ * is covered by its own egress policy rather than passing unexamined.
31
+ */
32
+ function projectOutbound(events) {
33
+ const structured = findLast(events, (e) => e.type === 'structured')?.structured;
34
+ return {
35
+ text: collectAttemptText(events),
36
+ ...(structured !== undefined ? { structured } : {}),
37
+ };
12
38
  }
13
39
  function buildRepairRequest(safe, previousOutput, rejection, repairGuidance) {
14
40
  return {
@@ -25,31 +51,45 @@ function buildRepairRequest(safe, previousOutput, rejection, repairGuidance) {
25
51
  }
26
52
  async function evaluateEgressOutcome(args) {
27
53
  const { egress, attemptEvents, generation, request, profile, canRetry } = args;
28
- const attemptText = collectAttemptText(attemptEvents);
29
- const result = await egress.enforce({
30
- text: attemptText,
31
- canary: generation.canary,
32
- slots: request.input?.slots,
33
- profile,
34
- role: request.input?.role,
35
- });
36
- if (!result.blocked) {
37
- return { action: 'pass' };
54
+ const payload = projectOutbound(attemptEvents);
55
+ const context = {
56
+ stage: 'output_final',
57
+ trust: 'untrusted',
58
+ profileId: profile.id,
59
+ ...(generation.canary ? { canary: generation.canary } : {}),
60
+ ...(request.input?.slots ? { slots: request.input.slots } : {}),
61
+ ...(request.input?.role ? { role: request.input.role } : {}),
62
+ };
63
+ const verdict = await runEnforcer(egress.enforce, payload, context);
64
+ const guardrail = guardrailFromVerdict('output_final', 'untrusted', verdict);
65
+ // `flag` is advisory: the hit is recorded, the turn still releases.
66
+ if (verdict.action === 'allow' || verdict.action === 'flag') {
67
+ return { outcome: { action: 'pass' }, guardrail };
68
+ }
69
+ // The policy supplied safe replacement prose — release that instead.
70
+ if (verdict.action === 'redact') {
71
+ return {
72
+ outcome: { action: 'refusal', event: { type: 'text', text: verdict.text } },
73
+ guardrail,
74
+ };
38
75
  }
39
76
  if (egress.onBlock === 'refuse_to_user') {
40
- return { action: 'refusal', event: { type: 'text', text: result.text } };
77
+ // Only emit a text turn when the policy supplied copy. Without it the kernel
78
+ // has nothing to say — an empty text event reads as a successful empty reply —
79
+ // so fall back to the same withheld error the exhausted-retry path uses.
80
+ return {
81
+ outcome: verdict.refusal
82
+ ? { action: 'refusal', event: { type: 'text', text: verdict.refusal } }
83
+ : { action: 'withhold', event: toErrorEvent(WITHHELD) },
84
+ guardrail,
85
+ };
41
86
  }
42
87
  if (canRetry) {
43
- const rejectionMsg = result.rejectionMessage || 'Egress disclosure violation detected.';
44
- const repairGuidance = egress.repairGuidance ||
45
- 'Rewrite the message as corrected user-visible prose only. Keep the same helpful substance; scrub all internal tool names, leak phrases, and disclosure markers.';
46
- const nextRequest = buildRepairRequest(request, attemptText, rejectionMsg, repairGuidance);
47
- return { action: 'retry', nextRequest };
88
+ const repairGuidance = egress.repairGuidance || lexiconText('egress.default_repair_guidance');
89
+ const nextRequest = buildRepairRequest(request, payload.text, verdict.rejection, repairGuidance);
90
+ return { outcome: { action: 'retry', nextRequest }, guardrail };
48
91
  }
49
- return {
50
- action: 'withhold',
51
- event: toErrorEvent('Turn withheld: egress disclosure violation'),
52
- };
92
+ return { outcome: { action: 'withhold', event: toErrorEvent(WITHHELD) }, guardrail };
53
93
  }
54
94
  async function evaluateValidationOutcome(args) {
55
95
  const { validation, generation, latestStructured, request, canRetry } = args;
@@ -97,7 +137,7 @@ function updateFlowForRetry(flow, nextReq) {
97
137
  }
98
138
  async function* handleEgressGate(egress, flow, state, profile, maxRetries) {
99
139
  const canRetry = flow.currentAttempt < maxRetries;
100
- const outcome = await evaluateEgressOutcome({
140
+ const { outcome, guardrail } = await evaluateEgressOutcome({
101
141
  egress,
102
142
  attemptEvents: state.attemptEvents,
103
143
  generation: flow.currentGen,
@@ -105,6 +145,10 @@ async function* handleEgressGate(egress, flow, state, profile, maxRetries) {
105
145
  profile,
106
146
  canRetry,
107
147
  });
148
+ if (guardrail) {
149
+ state.allEmittedEvents.push(guardrail);
150
+ yield guardrail;
151
+ }
108
152
  if (outcome.action === 'refusal') {
109
153
  state.allEmittedEvents.push(outcome.event);
110
154
  yield outcome.event;
@@ -151,18 +195,59 @@ function gateStatusToAction(status, terminalStatus = 'terminal') {
151
195
  }
152
196
  async function* executeSingleAttemptCycle(args) {
153
197
  const { flow, state, profile, system, provider, upstream, maxRetries } = args;
154
- const validation = profile.outputs?.validation;
155
- const egress = profile.guardrails?.egress;
156
- state.attemptEvents = [];
157
- const { latestStructured } = yield* executeAttempt({
158
- safe: flow.currentReq,
159
- profile,
160
- generation: flow.currentGen,
161
- system,
162
- provider,
163
- upstream,
164
- state,
165
- });
198
+ const validation = profileTurnOutputs(profile)?.validation;
199
+ const egress = resolveGuardrailPolicy(profile.guardrails).egress;
200
+ // Fresh maxSteps budget per validation/egress attempt. before_end inject
201
+ // re-entry inside this cycle still accumulates stepCount (do not reset there).
202
+ state.stepCount = 0;
203
+ let latestStructured;
204
+ // before_end may inject and re-enter the step loop under maxSteps.
205
+ for (;;) {
206
+ state.attemptEvents = [];
207
+ state.withheldVisible = false;
208
+ const attempt = yield* executeAttempt({
209
+ safe: flow.currentReq,
210
+ profile,
211
+ generation: flow.currentGen,
212
+ system,
213
+ provider,
214
+ upstream,
215
+ state,
216
+ });
217
+ latestStructured = attempt.latestStructured;
218
+ // Tool / gate suspension — do not before_end; finalize with that stop.
219
+ if (state.lastStop?.kind === 'tool' || state.lastStop?.kind === 'gate') {
220
+ break;
221
+ }
222
+ if (state.lastStop?.kind === 'cancelled') {
223
+ return { status: 'terminal' };
224
+ }
225
+ const beforeEnd = yield* applyTurnStage({
226
+ profile,
227
+ generation: flow.currentGen,
228
+ state,
229
+ stage: 'before_end',
230
+ step: Math.max(state.stepCount, 1),
231
+ onStage: flow.currentReq.onStage,
232
+ signal: flow.currentReq.signal,
233
+ host: flow.currentGen.host,
234
+ injectWouldExceedMaxSteps: injectWouldExceedMaxSteps(state.stepCount, flow.currentGen.maxSteps),
235
+ });
236
+ if (beforeEnd.abort) {
237
+ state.lastStop = {
238
+ kind: 'cancelled',
239
+ ...(typeof beforeEnd.abort === 'object' && beforeEnd.abort.reason
240
+ ? { native: beforeEnd.abort.reason }
241
+ : {}),
242
+ };
243
+ return { status: 'terminal' };
244
+ }
245
+ if (beforeEnd.injectCount > 0) {
246
+ // Host extended the turn — another provider step under maxSteps.
247
+ continue;
248
+ }
249
+ break;
250
+ }
166
251
  if (egress?.enforce) {
167
252
  const status = yield* handleEgressGate(egress, flow, state, profile, maxRetries);
168
253
  const action = gateStatusToAction(status, 'terminal');
@@ -178,13 +263,15 @@ async function* executeSingleAttemptCycle(args) {
178
263
  }
179
264
  }
180
265
  if (validation || egress?.enforce) {
181
- const alreadyStreamedUserVisible = !egress?.enforce;
182
- yield* yieldBufferedAttemptEvents(state.attemptEvents, alreadyStreamedUserVisible);
266
+ // Progressive-yield already released text/thought live under egress — unless it
267
+ // withheld them mid-stream. A passing final verdict on the full text supersedes
268
+ // that partial-window decision, so the buffer is released instead of dropped.
269
+ yield* yieldBufferedAttemptEvents(state.attemptEvents, !state.withheldVisible);
183
270
  }
184
271
  return { status: 'success' };
185
272
  }
186
273
  async function* runAttemptsWithValidation(safe, profile, generation, system, provider, upstream, state) {
187
- const maxRetries = Math.max(profile.outputs?.validation?.maxRetries ?? 0, profile.guardrails?.egress?.maxRetries ?? 0);
274
+ const maxRetries = Math.max(profileTurnOutputs(profile)?.validation?.maxRetries ?? 0, resolveGuardrailPolicy(profile.guardrails).egress?.maxRetries ?? 0);
188
275
  const flow = {
189
276
  currentAttempt: 0,
190
277
  currentGen: generation,
@@ -7,8 +7,10 @@
7
7
  *
8
8
  * @module
9
9
  */
10
- import { type TraceSink } from '../../../observability/trace.js';
11
- import type { ModelProvider, TurnEvent, TurnRequest } from '../../types.js';
10
+ import type { TraceSink } from '../../../observability/trace-sink.js';
11
+ import type { ModelProvider, TurnEvent, TurnHistoryMessage, TurnRequest } from '../../types.js';
12
+ /** Fold one history message to a compaction transcript line (media → markers). */
13
+ declare function compactionTranscriptLine(m: TurnHistoryMessage): string;
12
14
  /** Execute one host turn against a provider adapter. */
13
- declare function runTurn(req: TurnRequest, provider: ModelProvider, sink?: TraceSink): AsyncGenerator<TurnEvent>;
14
- export { runTurn };
15
+ declare function runTurn(req: TurnRequest, provider: ModelProvider, sinkOverride?: TraceSink): AsyncGenerator<TurnEvent>;
16
+ export { compactionTranscriptLine, runTurn };