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
@@ -7,9 +7,12 @@
7
7
  *
8
8
  * @module
9
9
  */
10
+ /** lexicon-exempt-file: authoring field-meta / closed unions — not runtime user or model copy (P2) */
11
+ import { EGRESS_ON_BLOCK } from '../guardrails/types.js';
10
12
  import { GOOGLE_SPEECH_VOICES } from '../presets/google/speech-voices.js';
13
+ export { EGRESS_ON_BLOCK };
11
14
  /** Primary profile archetype. Discriminated union key for `ProfileDefinition` and `Profile`. */
12
- export const PROFILE_TYPES = ['text', 'image', 'speech', 'live'];
15
+ export const PROFILE_TYPES = ['text', 'image', 'speech', 'live', 'host'];
13
16
  /** Model reasoning effort level normalized across provider adapters. */
14
17
  export const THINKING_LEVELS = [
15
18
  'none',
@@ -35,13 +38,15 @@ export const PROTOCOL_PROVIDERS = {
35
38
  };
36
39
  /**
37
40
  * Legal wire protocols for each profile archetype.
38
- * 'live' profiles require 'geminiLive'; turn-based archetypes require turn protocols.
41
+ * 'live' profiles require 'geminiLive'; turn-based archetypes require turn protocols;
42
+ * 'host' never runs a model and binds no protocol.
39
43
  */
40
44
  export const PROFILE_TYPE_PROTOCOLS = {
41
45
  text: ['geminiInteractions', 'openAi'],
42
46
  image: ['geminiInteractions', 'openAi'],
43
47
  speech: ['geminiInteractions', 'openAi'],
44
48
  live: ['geminiLive'],
49
+ host: [],
45
50
  };
46
51
  /** Protocols allowed for a profile archetype (`type`). */
47
52
  export function protocolsForProfileType(type) {
@@ -56,7 +61,6 @@ export const KEY_SLOTS = ['slotA', 'slotB', 'slotC', 'paid'];
56
61
  /** Key slots that may overflow to `paid` after quota backoff. */
57
62
  export const OVERFLOW_KEY_SLOTS = ['slotA', 'slotB', 'slotC'];
58
63
  /** Profile-level control a caller may toggle at turn time. */
59
- export const CONTROL_IDS = ['thinking'];
60
64
  /** Normalized multimodal part category. */
61
65
  export const MEDIA_INPUT_KIND_VALUES = ['image', 'audio', 'video', 'document'];
62
66
  /** Provider thinking-summary behavior. */
@@ -65,6 +69,20 @@ export const SUMMARY_MODES = ['auto', 'none'];
65
69
  export const STREAM_MODES = ['sse', 'buffered'];
66
70
  /** Audio container for speech generation output. */
67
71
  export const SPEECH_AUDIO_FORMATS = ['pcm', 'mp3'];
72
+ /** Speech `format` values legal for a wire protocol (`assertSpeechRole` / UI). */
73
+ export function speechFormatsForProtocol(protocol) {
74
+ return protocol === 'openAi' ? SPEECH_AUDIO_FORMATS : ['pcm'];
75
+ }
76
+ export function isSpeechFormatAllowedForProtocol(protocol, format) {
77
+ return speechFormatsForProtocol(protocol).includes(format);
78
+ }
79
+ /** Snap an illegal or omitted format to the first legal value for the protocol. */
80
+ export function coerceSpeechFormat(protocol, format) {
81
+ const allowed = speechFormatsForProtocol(protocol);
82
+ if (format && allowed.includes(format))
83
+ return format;
84
+ return allowed[0];
85
+ }
68
86
  /** Live session activity handling (barge-in behavior). */
69
87
  export const LIVE_ACTIVITY_HANDLINGS = ['START_OF_ACTIVITY_INTERRUPTS', 'NO_INTERRUPTION'];
70
88
  /** Live session voice activity detection sensitivity. */
@@ -82,13 +100,25 @@ export const SCHEMA_ENFORCEMENTS = ['responseFormat', 'prompt'];
82
100
  export const COMPACTION_METERS = ['history', 'input'];
83
101
  /** When compaction runs relative to the primary turn. */
84
102
  export const COMPACTION_TIMINGS = ['before', 'after'];
85
- /** Egress block handling. */
86
- export const EGRESS_ON_BLOCK = ['reject_to_agent', 'refuse_to_user'];
103
+ /** OpenRouter prompt-cache mode (models.*.cache.mode). */
104
+ export const CACHE_MODES = ['automatic', 'system'];
105
+ /** OpenRouter ephemeral cache TTL (models.*.cache.ttl). */
106
+ export const CACHE_TTLS = ['5m', '1h'];
87
107
  /** Why a turn ended (provider-neutral). */
88
108
  export const TURN_STOP_KINDS = [
89
109
  'completed',
90
110
  'length',
111
+ /**
112
+ * @deprecated Shipping pause fiction (`tool.phase: 'pause'`). Target: use `gate`
113
+ * for confirm/permission/auth suspension; awaiting is a completed tool result.
114
+ * Removed when stages slices release.
115
+ */
91
116
  'tool',
117
+ /**
118
+ * Honest suspension: `pre_tool` confirm / permission / auth blocked the body.
119
+ * Host resumes via invokeTool/executeTool; not continueFrom.
120
+ */
121
+ 'gate',
92
122
  'filtered',
93
123
  'provider_error',
94
124
  'cancelled',
@@ -97,15 +127,91 @@ export const TURN_STOP_KINDS = [
97
127
  /** Live: model finished generating audio/text for this utterance; turn may still be open. */
98
128
  'generation_complete',
99
129
  ];
130
+ /**
131
+ * Stop kinds eligible for `continueFrom` / resumption allowlists.
132
+ * Excludes terminal-success, user abort, tool/gate suspension, filter, and live-only
133
+ * boundaries — those use other host paths (or are not resumeable).
134
+ */
135
+ export const CONTINUE_STOP_KINDS = ['length', 'stream_incomplete', 'provider_error'];
136
+ /**
137
+ * Turn / utterance-cycle timeline stages (`docs/contracts/stages.md`).
138
+ * Replaces the former steer barriers (`pre_llm` / `pre_tool_followup`).
139
+ */
140
+ export const TURN_STAGES = [
141
+ 'pre_turn',
142
+ 'pre_tool',
143
+ 'post_tool',
144
+ 'before_end',
145
+ 'post_turn',
146
+ ];
147
+ const TURN_STAGE_SET = new Set(TURN_STAGES);
148
+ /** True when `value` is a known `TurnStage`. */
149
+ export function isTurnStage(value) {
150
+ return typeof value === 'string' && TURN_STAGE_SET.has(value);
151
+ }
152
+ /** Stages where inject is physically meaningful (still requires inject gate). */
153
+ export const TURN_INJECT_STAGES = ['pre_turn', 'post_tool', 'before_end'];
154
+ const TURN_INJECT_STAGE_SET = new Set(TURN_INJECT_STAGES);
155
+ /** True when inject is physically meaningful at this stage (gate still required). */
156
+ export function isTurnInjectStage(value) {
157
+ return typeof value === 'string' && TURN_INJECT_STAGE_SET.has(value);
158
+ }
159
+ /** `pre_tool` gate kinds — confirm-to-run / permission / auth. Not awaiting. */
160
+ export const TOOL_GATE_KINDS = ['confirmation', 'permission', 'auth'];
161
+ const TOOL_GATE_KIND_SET = new Set(TOOL_GATE_KINDS);
162
+ /** True when `value` is a known tool-gate kind. */
163
+ export function isToolGateKind(value) {
164
+ return typeof value === 'string' && TOOL_GATE_KIND_SET.has(value);
165
+ }
166
+ /** `awaiting_user_input.kind` — harness ask_user / human-as-product completions. */
167
+ export const AWAITING_USER_INPUT_KINDS = ['confirm', 'choice', 'text'];
168
+ /** Discriminator on tool output for awaiting completions. */
169
+ export const AWAITING_USER_INPUT_STATUS = 'awaiting_user_input';
100
170
  /** Per-tool visibility tier — enforced by the kernel at resolve time. */
101
171
  export const TOOL_LOAD_TIERS = ['T0', 'T1', 'T2'];
172
+ /** HTTP verbs supported by declarative HTTP tools. */
173
+ export const HTTP_METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'];
174
+ /** When remote tool auth is missing or expired. */
175
+ export const AUTH_UNAUTHENTICATED_POLICIES = ['pause', 'report_to_model'];
102
176
  /** Registered tool discriminant (`registerTool`). */
103
- export const TOOL_TYPES = ['builtin', 'function'];
177
+ export const TOOL_TYPES = ['builtin', 'function', 'http', 'mcp'];
104
178
  /** Semantic access level — host policy / UI; not enforced by execute. */
105
179
  export const TOOL_ACCESS = ['read-only', 'read-write', 'destructive'];
106
180
  /** Execution authorization tier for registered tools. */
107
181
  export const TOOL_PERMISSION = ['auto', 'session_consent', 'always_confirm'];
108
- /** MIME essence normalized media part category (shared ingress map). */
182
+ /** Credential attachment modes for HTTP and MCP tools (`auth.type`). */
183
+ export const TOOL_AUTH_TYPES = ['bearer', 'api_key', 'oauth2'];
184
+ /** Playground auth select — includes UI-only `none` (omits auth at compile time). */
185
+ export const PLAYGROUND_AUTH_TYPES = ['none', ...TOOL_AUTH_TYPES];
186
+ /**
187
+ * MIME essence → normalized media part category.
188
+ *
189
+ * This table is the complete media-input vocabulary of the package: every MIME
190
+ * any supported transport may carry on a turn appears here exactly once, and
191
+ * nothing else is a media input type. `assertMediaMime` (`registry/ingress.ts`)
192
+ * refuses anything absent from it, so hosts declare what they accept in
193
+ * `inputs.attachments.accept` / `inputs.voice.accept` and keep no second table.
194
+ *
195
+ * Contents are the union of the documented provider input lists:
196
+ * - Google Interactions / Live (image, audio, video, document lists verified
197
+ * 2026-09-12) — the widest of the three and therefore the table's shape. The
198
+ * document row is Google's document-understanding list in full: PDF, plain
199
+ * text, HTML, CSS, Markdown (`text/md`), CSV, XML, RTF, JavaScript
200
+ * (`text/javascript`, `application/x-javascript`) and Python
201
+ * (`text/x-python`, `application/x-python`); `application/json` is an
202
+ * established row alongside it. TypeScript, `application/xml` and
203
+ * `application/rtf` are NOT on Google's list and are therefore not rows.
204
+ * - OpenRouter / OpenAI-compat (`providers/openrouter/openai/compat.ts`,
205
+ * `sdk-messages.ts`): the adapters wire every `MediaInputKind` (image →
206
+ * `image_url`/`image`, audio → `input_audio`, video and document → `file`)
207
+ * and forward the part's MIME verbatim, so their accepted set is open-ended
208
+ * and adds no rows. What they cannot carry — a `uri` reference part — is
209
+ * refused at request time with `TheorumError`, not by a second MIME list.
210
+ *
211
+ * Alias essences that providers also emit (`image/jpg`, `video/mov`,
212
+ * `audio/x-wav`, `video/x-ms-wmv`, …) are rows here; `resolveInputParts`
213
+ * canonicalizes `image/jpg` to `image/jpeg` on the wire.
214
+ */
109
215
  export const MEDIA_INPUT_KINDS = {
110
216
  'image/png': 'image',
111
217
  'image/jpeg': 'image',
@@ -124,10 +230,17 @@ export const MEDIA_INPUT_KINDS = {
124
230
  'audio/webm': 'audio',
125
231
  'audio/mp4': 'audio',
126
232
  'audio/pcm': 'audio',
233
+ 'audio/m4a': 'audio',
234
+ 'audio/opus': 'audio',
235
+ 'audio/l16': 'audio',
236
+ 'audio/alaw': 'audio',
237
+ 'audio/mulaw': 'audio',
127
238
  'video/mp4': 'video',
128
239
  'video/mpeg': 'video',
129
240
  'video/quicktime': 'video',
241
+ 'video/mov': 'video',
130
242
  'video/x-msvideo': 'video',
243
+ 'video/avi': 'video',
131
244
  'video/x-flv': 'video',
132
245
  'video/mpg': 'video',
133
246
  'video/webm': 'video',
@@ -138,7 +251,15 @@ export const MEDIA_INPUT_KINDS = {
138
251
  'text/plain': 'document',
139
252
  'text/csv': 'document',
140
253
  'text/markdown': 'document',
254
+ 'text/md': 'document',
141
255
  'text/html': 'document',
256
+ 'text/css': 'document',
257
+ 'text/xml': 'document',
258
+ 'text/rtf': 'document',
259
+ 'text/javascript': 'document',
260
+ 'application/x-javascript': 'document',
261
+ 'text/x-python': 'document',
262
+ 'application/x-python': 'document',
142
263
  'application/json': 'document',
143
264
  };
144
265
  /** Type-prefix wildcards accepted by `mimeAllowed`. */
@@ -205,12 +326,11 @@ function field(type, doc, options, optionDescriptionsOrNote, optionNote) {
205
326
  }
206
327
  /**
207
328
  * Parents whose next key is a host-owned map key (model id, slot name, …).
208
- * The annotator substitutes `*` so `model.config.flash.apiId` → `model.config.*.apiId`.
329
+ * The annotator substitutes `*` so `models.flash.apiId` → `models.*.apiId`.
209
330
  */
210
331
  export const DYNAMIC_FIELD_PARENTS = new Set([
211
- 'model.config',
212
- 'model.select',
213
- 'model.thinking',
332
+ 'models',
333
+ 'models.*.efforts',
214
334
  'identity.systemByRole',
215
335
  'inputs.slots',
216
336
  'inputs.limitsByMime',
@@ -236,46 +356,27 @@ export function catalogPathFor(keys) {
236
356
  */
237
357
  export const PROFILE_FIELDS = {
238
358
  id: field('string', 'Host-owned profile identifier.'),
239
- type: field("'text' | 'image' | 'speech' | 'live'", 'Required profile archetype.'),
359
+ type: field("'text' | 'image' | 'speech' | 'live' | 'host'", 'Required profile archetype. host = tool-execution ceiling for invokeTool; never runs a model.'),
240
360
  identity: field('{ handle, system?, systemByRole? }', 'Display handle and system instruction the model receives each turn.'),
241
361
  'identity.handle': field('string', 'Public handle for this agent.'),
242
362
  'identity.system': field('string', 'System instruction merged into every turn.'),
243
363
  'identity.systemByRole': field('Record<string, string>', 'Optional system instruction keyed by turn role.'),
244
364
  'identity.systemByRole.*': field('string', 'System instruction merged when this turn role is active.'),
245
- model: field('ProfileModelSpec', 'Protocol, provider, allowlist, wire config, and step bounds.'),
246
- 'model.protocol': field(unionType(PROTOCOLS), 'Wire protocol. Pairs with provider via createProvider.', PROTOCOLS, {
365
+ models: field('Record<ModelId, ModelBinding>', 'Host-named model bindings. Keys are model ids; each entry carries protocol, provider, and wire config.'),
366
+ 'models.*': field('ModelBinding', 'Wire binding for one host-named model id.'),
367
+ 'models.*.protocol': field(unionType(PROTOCOLS), 'Wire protocol for this model. Must be valid for profile type.', PROTOCOLS, {
247
368
  geminiInteractions: 'Google Gemini Interactions wire protocol (Gemini 2.5 / 3+).',
369
+ geminiLive: 'Gemini Live bidirectional WebSocket protocol.',
248
370
  openAi: 'OpenAI-compatible chat completions and streaming protocol.',
249
371
  }),
250
- 'model.provider': field(unionType(PROVIDERS), 'Transport. Must form a legal pair with protocol.', PROVIDERS, {
372
+ 'models.*.provider': field(unionType(PROVIDERS), 'Transport for this model. Must form a legal pair with protocol.', PROVIDERS, {
251
373
  google: 'Direct Google Gemini API transport.',
252
374
  openrouter: 'OpenRouter multi-provider API proxy.',
253
375
  local: 'Local OpenAI-compatible server (Ollama, llama.cpp, vLLM).',
254
376
  }),
255
- 'model.allow': field('ModelId[]', 'Ids this profile may select. Each id must exist in config.'),
256
- 'model.config': field('Record<ModelId, ModelSpec>', 'Host-owned wire config keyed by the same ids used in allow / select.'),
257
- 'model.config.*': field('ModelSpec', 'Host-owned wire config for this model id (apiId, thinking, token limits, compaction).'),
258
- 'model.config.*.apiId': field('string', 'Provider wire model id.'),
259
- 'model.config.*.thinking': field('{ on, off }', 'Thinking levels used when a boolean thinking control is on or off.'),
260
- 'model.config.*.thinking.on': field(unionType(THINKING_LEVELS), 'Level when thinking is on.', THINKING_LEVELS, {
261
- none: 'Disable reasoning tokens completely.',
262
- minimal: 'Minimal reasoning tokens for fastest response.',
263
- low: 'Low reasoning budget for basic structured tasks.',
264
- medium: 'Balanced reasoning for multi-step agent actions.',
265
- high: 'Deep reasoning for complex planning and code.',
266
- xhigh: 'Extended reasoning budget for hard problems.',
267
- max: 'Maximum reasoning tokens supported by model.',
268
- }),
269
- 'model.config.*.thinking.off': field(unionType(THINKING_LEVELS), 'Level when thinking is off.', THINKING_LEVELS, {
270
- none: 'Disable reasoning tokens completely.',
271
- minimal: 'Minimal reasoning tokens for fastest response.',
272
- low: 'Low reasoning budget for basic structured tasks.',
273
- medium: 'Balanced reasoning for multi-step agent actions.',
274
- high: 'Deep reasoning for complex planning and code.',
275
- xhigh: 'Extended reasoning budget for hard problems.',
276
- max: 'Maximum reasoning tokens supported by model.',
277
- }),
278
- 'model.config.*.thinkingLevels': field('ThinkingLevel[]', 'Levels this model accepts. Illegal values are clamped.', THINKING_LEVELS, {
377
+ 'models.*.apiId': field('string', 'Provider wire model id.'),
378
+ 'models.*.efforts': field('Record<string, ThinkingLevel>', 'Alias thinking level. One entry = fixed; two+ may be selectable at turn time.'),
379
+ 'models.*.efforts.*': field(unionType(THINKING_LEVELS), 'Wire thinking level for this effort alias.', THINKING_LEVELS, {
279
380
  none: 'Disable reasoning tokens completely.',
280
381
  minimal: 'Minimal reasoning tokens for fastest response.',
281
382
  low: 'Low reasoning budget for basic structured tasks.',
@@ -284,59 +385,45 @@ export const PROFILE_FIELDS = {
284
385
  xhigh: 'Extended reasoning budget for hard problems.',
285
386
  max: 'Maximum reasoning tokens supported by model.',
286
387
  }),
287
- 'model.config.*.summaries': field('{ on, off }', 'Summary behavior for the thinking control.'),
288
- 'model.config.*.summaries.on': field(unionType(SUMMARY_MODES), 'Summaries when thinking is on.', SUMMARY_MODES, {
289
- auto: 'Emit thinking summaries when available.',
290
- none: 'Suppress thinking summaries from the stream.',
291
- }),
292
- 'model.config.*.summaries.off': field(unionType(SUMMARY_MODES), 'Summaries when thinking is off.', SUMMARY_MODES, {
293
- auto: 'Emit thinking summaries when available.',
294
- none: 'Suppress thinking summaries from the stream.',
295
- }),
296
- 'model.config.*.maxOutputTokens': field('number', 'Maximum tokens the model may emit.'),
297
- 'model.config.*.temperature': field('number', 'Sampling temperature.'),
298
- 'model.config.*.builtInTools': field('BuiltinToolId[]', 'Provider-native builtins enabled whenever this model is selected.'),
299
- 'model.config.*.key': field(unionType(KEY_SLOTS), 'Optional vault slot for this model. Overrides profile.model.key.', KEY_SLOTS),
300
- 'model.config.*.compaction': field('CompactionSpec', 'Optional compaction policy for this model.'),
301
- 'model.config.*.compaction.maxTokens': field('number', 'Token budget compared by the trigger (compactAt * maxTokens).'),
302
- 'model.config.*.compaction.compactAt': field('number', 'Fraction of maxTokens at which compaction fires. Must be in (0, 1).'),
303
- 'model.config.*.compaction.previousExchanges': field('number', '≥ 1 = exchange count, (0, 1) = fraction of maxTokens, 0 = compact all.'),
304
- 'model.config.*.compaction.profile': field('ProfileId', 'Compaction agent profile id. Must be registered before the owning profile.'),
305
- 'model.config.*.compaction.timing': field(unionType(COMPACTION_TIMINGS), 'When compaction runs relative to the primary turn.', COMPACTION_TIMINGS, {
388
+ 'models.*.defaultEffort': field('string', 'Effort alias when the turn omits effort.'),
389
+ 'models.*.allowEffortSelect': field('boolean', 'Turn may pass effort. Requires two or more efforts keys.'),
390
+ 'models.*.summaries': field('boolean', 'Emit thinking summaries on the stream.'),
391
+ 'models.*.maxOutputTokens': field('number', 'Maximum tokens the model may emit.'),
392
+ 'models.*.temperature': field('number', 'Sampling temperature.'),
393
+ 'models.*.builtInTools': field('BuiltinToolId[]', 'Provider-native builtins enabled whenever this model is selected.'),
394
+ 'models.*.key': field(unionType(KEY_SLOTS), 'Optional vault slot for this model. Overrides profile.key.', KEY_SLOTS),
395
+ 'models.*.compaction': field('CompactionSpec', 'Optional compaction policy for this model.'),
396
+ 'models.*.compaction.maxTokens': field('number', 'Token budget compared by the trigger (compactAt * maxTokens).'),
397
+ 'models.*.compaction.compactAt': field('number', 'Fraction of maxTokens at which compaction fires. Must be in (0, 1).'),
398
+ 'models.*.compaction.previousExchanges': field('number', ' 1 = exchange count, (0, 1) = fraction of maxTokens, 0 = compact all.'),
399
+ 'models.*.compaction.profile': field('ProfileId', 'Compaction agent profile id. Must be registered before the owning profile.'),
400
+ 'models.*.compaction.timing': field(unionType(COMPACTION_TIMINGS), 'When compaction runs relative to the primary turn.', COMPACTION_TIMINGS, {
306
401
  before: 'Compact synchronously before running the turn request.',
307
402
  after: 'Signal on done for host async background compaction.',
308
403
  }),
309
- 'model.config.*.compaction.meter': field(unionType(COMPACTION_METERS), 'What the threshold meters. Defaults to history.', COMPACTION_METERS, {
404
+ 'models.*.compaction.meter': field(unionType(COMPACTION_METERS), 'What the threshold meters. Defaults to history.', COMPACTION_METERS, {
310
405
  history: 'Meters token count across prior conversation turns.',
311
406
  input: 'Meters full turn input token count (system + history + attachments).',
312
407
  }),
313
- 'model.select': field('Record<string, ModelId>', 'Named aliases (fast / smart) allowlisted ids.'),
314
- 'model.select.*': field('ModelId', 'Allowlisted model id for this select key.'),
315
- 'model.thinking': field('ThinkingLevel | Record<string, ThinkingLevel>', 'Default thinking pin, or a map keyed by select labels.', THINKING_LEVELS, {
316
- none: 'Disable reasoning tokens completely.',
317
- minimal: 'Minimal reasoning tokens for fastest response.',
318
- low: 'Low reasoning budget for basic structured tasks.',
319
- medium: 'Balanced reasoning for multi-step agent actions.',
320
- high: 'Deep reasoning for complex planning and code.',
321
- xhigh: 'Extended reasoning budget for hard problems.',
322
- max: 'Maximum reasoning tokens supported by model.',
408
+ 'models.*.cache': field('CacheSpec', 'OpenRouter prompt-cache policy. Only valid when provider is openrouter.'),
409
+ 'models.*.cache.mode': field(unionType(CACHE_MODES), 'How to place cache_control on OpenRouter requests.', CACHE_MODES, {
410
+ automatic: 'Top-level cache_control; breakpoint advances with the conversation.',
411
+ system: 'Explicit cache_control breakpoint on the system instruction only.',
323
412
  }),
324
- 'model.thinking.*': field(unionType(THINKING_LEVELS), 'Thinking pin for this select key.', THINKING_LEVELS, {
325
- none: 'Disable reasoning tokens completely.',
326
- minimal: 'Minimal reasoning tokens for fastest response.',
327
- low: 'Low reasoning budget for basic structured tasks.',
328
- medium: 'Balanced reasoning for multi-step agent actions.',
329
- high: 'Deep reasoning for complex planning and code.',
330
- xhigh: 'Extended reasoning budget for hard problems.',
331
- max: 'Maximum reasoning tokens supported by model.',
413
+ 'models.*.cache.ttl': field(unionType(CACHE_TTLS), 'Ephemeral cache TTL. Omit provider default (typically 5m on Anthropic).', CACHE_TTLS, {
414
+ '5m': 'Five-minute ephemeral cache (default when ttl is omitted).',
415
+ '1h': 'One-hour ephemeral cache (higher write cost; better for long sessions).',
332
416
  }),
333
- 'model.controls': field('ControlId[]', 'Turn-time toggles this profile exposes.', CONTROL_IDS),
334
- 'model.maxSteps': field('number', 'Tool-loop ceiling. <=0 unbounded, 1 one-shot, >1 ceiling. Omit → unbounded.'),
335
- 'model.key': field(unionType(OVERFLOW_KEY_SLOTS), 'Vault key slot (slotA/B/C). Paid is overflow-only via model.config.*.key or forcePaidKey builtins.', OVERFLOW_KEY_SLOTS),
336
- tools: field('{ allow: ToolId[]; t1Policy?; t2Loader? }', 'Custom tools (allow), optional T1 policy, optional T2 loader function id. Builtins belong on model.config.*.builtInTools.'),
337
- 'tools.allow': field('ToolId[]', 'Custom tools the agent may call. Builtins are declared per model, not here.'),
338
- 'tools.t1Policy': field('(ctx) => ToolId[] | Promise<ToolId[]>', 'Optional T1 policy which eligible loadTier:T1 tools to wire at turn start.'),
339
- 'tools.t2Loader': field('ToolId', 'Optional function tool id for T2 promotion. Must be in tools.allow; handler returns { loaded: string[] }.'),
417
+ 'models.*.store': field('boolean', 'Gemini Interactions: whether the provider stores the interaction. Omit → provider default.'),
418
+ 'models.*.persistViaInteractionId': field('boolean', 'Gemini Interactions: prefer previous_interaction_id over client-owned history. Omit → host/turn decides.'),
419
+ defaultModel: field('ModelId', 'Default model id when the turn omits model.'),
420
+ allowModelSelect: field('boolean', 'Turn may pass model. Requires two or more models keys.'),
421
+ maxSteps: field('number', 'Tool-loop ceiling. <=0 unbounded, 1 one-shot, >1 ceiling. Omit unbounded.'),
422
+ key: field(unionType(OVERFLOW_KEY_SLOTS), 'Vault key slot (slotA/B/C). Paid is overflow-only via models.*.key or forcePaidKey builtins.', OVERFLOW_KEY_SLOTS),
423
+ tools: field('{ allow: ToolId[]; t1Policy?; t2Loader? }', 'Custom tools (allow), optional T1 policy, optional T2 loader function id. Builtins belong on models.*.builtInTools. Live and host profiles use `{ allow }` only — live wires every allowed tool at session setup; host executes every allowed tool.'),
424
+ 'tools.allow': field('ToolId[]', 'Custom tools the agent may call. Builtins are declared per model, not here. On type live every listed id is wired at session setup regardless of loadTier; on type host every listed id is executable.'),
425
+ 'tools.t1Policy': field('(ctx) => ToolId[] | Promise<ToolId[]>', 'Optional T1 policy — which eligible loadTier:T1 tools to wire at turn start. Not supported on type live.'),
426
+ 'tools.t2Loader': field('ToolId', 'Optional function tool id for T2 promotion. Must be in tools.allow; handler returns { loaded: string[] }. Not supported on type live.'),
340
427
  inputs: field('ProfileInputsSpec', 'Text, attachment, voice, slot, and size rules.'),
341
428
  'inputs.text': field('boolean', 'Whether the profile accepts text on a turn. False rejects text.'),
342
429
  'inputs.attachments': field('{ accept: string[] }', 'File upload allowlist.'),
@@ -365,6 +452,10 @@ export const PROFILE_FIELDS = {
365
452
  mp3: 'MP3 encoded stream. Requires openAi protocol.',
366
453
  }),
367
454
  live: field('ProfileLiveSpec', 'Bidirectional live audio/video streaming session pins.'),
455
+ 'live.ingress': field('LiveIngressSpec', 'Realtime ingress toggles for sendAudio / sendVideo / sendText — not turn file attachments.'),
456
+ 'live.ingress.audio': field('boolean', 'Microphone PCM via LiveSession.sendAudio. Omit → enabled.'),
457
+ 'live.ingress.video': field('boolean', 'Webcam JPEG frames via LiveSession.sendVideo. Omit → enabled.'),
458
+ 'live.ingress.text': field('boolean', 'Typed text via LiveSession.sendText. Omit → disabled.'),
368
459
  'live.voice': field('string', 'TTS voice name for live audio output (e.g. Puck, Aoede, Charon).', GOOGLE_SPEECH_VOICES, 'Google preset vocabulary; kernel accepts any string.'),
369
460
  'live.vad': field('LiveVadSpec', 'Voice activity detection, barge-in, and endpointing sensitivity.'),
370
461
  'live.vad.activityHandling': field(unionType(LIVE_ACTIVITY_HANDLINGS), 'Barge-in handling when user speaks.', LIVE_ACTIVITY_HANDLINGS),
@@ -389,33 +480,27 @@ export const PROFILE_FIELDS = {
389
480
  buffered: 'Buffers response into a single completed turn event.',
390
481
  }),
391
482
  'outputs.streaming.streamThoughts': field('boolean', 'Emit model thinking on the turn stream.'),
392
- turnResumption: field('ProfileTurnResumptionSpec', 'Continue after a non-user stop.'),
393
- 'turnResumption.allowContinue': field('TurnStopKind[]', 'Kinds eligible for a Continue / continueFrom turn.', TURN_STOP_KINDS, {
483
+ turnBehaviour: field('ProfileTurnBehaviourSpec', 'Resume after a non-user stop (text/image/speech); text and live may also allow mid-turn inject via allowSteering.'),
484
+ 'turnBehaviour.resumption': field('ProfileTurnResumptionSpec', 'Continue after a non-user stop (continueFrom). Not valid on live — use live.sessionResumption.'),
485
+ 'turnBehaviour.resumption.allowContinue': field('ContinueStopKind[]', 'Kinds eligible for a Continue / continueFrom turn. Not tool/cancelled/completed/filtered/live boundaries.', CONTINUE_STOP_KINDS, {
394
486
  length: 'Model hit maximum output token ceiling.',
395
487
  stream_incomplete: 'Network connection or stream dropped prematurely.',
396
488
  provider_error: 'Upstream provider returned an error code or timeout.',
397
- tool: 'Turn paused at tool execution boundary.',
398
- filtered: 'Content safety filter intercepted output.',
399
- cancelled: 'Turn aborted via AbortSignal.',
400
- completed: 'Turn finished normally.',
401
- interrupted: 'Live barge-in interrupted the in-flight response.',
402
- generation_complete: 'Live model finished generating this utterance; turn may still be open.',
403
489
  }),
404
- 'turnResumption.autoContinue': field('TurnStopKind[]', 'Kinds the host may auto-continue once without a CTA.', TURN_STOP_KINDS, {
490
+ 'turnBehaviour.resumption.autoContinue': field('ContinueStopKind[]', 'Kinds the host may auto-continue once without a CTA. Subset of ContinueStopKind.', CONTINUE_STOP_KINDS, {
405
491
  length: 'Model hit maximum output token ceiling.',
406
492
  stream_incomplete: 'Network connection or stream dropped prematurely.',
407
493
  provider_error: 'Upstream provider returned an error code or timeout.',
408
- tool: 'Turn paused at tool execution boundary.',
409
- filtered: 'Content safety filter intercepted output.',
410
- cancelled: 'Turn aborted via AbortSignal.',
411
- completed: 'Turn finished normally.',
412
- interrupted: 'Live barge-in interrupted the in-flight response.',
413
- generation_complete: 'Live model finished generating this utterance; turn may still be open.',
414
494
  }),
415
- guardrails: field('ProfileGuardrailsSpec', 'Quota, canary, sanitize, redact, and egress switches.'),
416
- 'guardrails.quota': field('{ perDay: number }', 'Host HTTP helper not enforced inside runTurn.'),
495
+ 'turnBehaviour.resumption.maxContinues': field('number', 'Max continueFrom rounds the kernel accepts (compared to TurnRequest.continuation).'),
496
+ 'turnBehaviour.resumption.continueInstruction': field('string', 'Host replacement for the continue instruction appended on continueFrom turns. Omitted: registered default.'),
497
+ 'turnBehaviour.allowSteering': field('boolean', 'Text and live. When true (default), host onStage inject affordances are applied. Image/speech must omit.'),
498
+ guardrails: field('ProfileGuardrailsSpec', 'Quota, canary, sanitize, redact, egress, network, and taint switches. On type host only the invokeTool-path guards are accepted (HostGuardrailsSpec: sanitizeInput, redactSensitive, network, taint) — quota, canary, and egress guard a model turn and are refused.'),
499
+ 'guardrails.quota': field('QuotaGuardrailSpec', 'Host HTTP helper — not enforced inside runTurn.'),
417
500
  'guardrails.quota.perDay': field('number', 'Daily turn cap used by host quota middleware.'),
418
- 'guardrails.canary': field('boolean', 'Enable per-turn canary token bound to system prompt.'),
501
+ 'guardrails.quota.message': field('string', 'Host copy surfaced by quotaExhausted when the quota trips. The kernel ships no fallback.'),
502
+ 'guardrails.canary': field('boolean | CanaryGuardrailSpec', 'Per-turn canary token bound to system prompt. Default true; set false to opt out; object form supplies bindNote.'),
503
+ 'guardrails.canary.bindNote': field('string', 'Host template appended to the system prompt; must contain the {canary} placeholder. Omitted: registered default.'),
419
504
  'guardrails.sanitizeInput': field('boolean', 'Strip inbound injection spans.'),
420
505
  'guardrails.redactSensitive': field('boolean', 'Redact sensitive spans.'),
421
506
  'guardrails.egress': field('ProfileEgressSpec', 'Host check before user-visible text is released.'),
@@ -426,27 +511,86 @@ export const PROFILE_FIELDS = {
426
511
  }),
427
512
  'guardrails.egress.maxRetries': field('number', 'Repair-turn ceiling after an egress block.'),
428
513
  'guardrails.egress.repairGuidance': field('string', 'Instruction appended on an egress repair turn.'),
514
+ 'guardrails.network': field('NetworkGuardrailSpec', 'SSRF guardrails for declarative HTTP and MCP tools.'),
515
+ 'guardrails.network.allowPrivateNetworks': field('boolean', 'Allow loopback and private-network targets when resolving tool URLs.'),
516
+ 'guardrails.network.allowedHosts': field('string[]', 'Explicit hostname allowlist for declarative HTTP and MCP egress.'),
517
+ observability: field('ProfileObservabilitySpec', 'Trace destination, scrub, include, and sampling policy for this profile.'),
518
+ 'observability.writeTo': field('false | string | TraceSink', 'false = off; string = registerTraceDestination id; TraceSink = inline writer. runTurn third arg overrides.'),
519
+ 'observability.sampleRate': field('number', 'Fraction of turns to record (0–1). Default 1. Ignored when runTurn passes an explicit sink.'),
520
+ 'observability.include': field('TraceIncludeSpec', 'Which TraceRecord payloads to keep (upstreamLog, outboundWire, evidenceRaw, usage, guardrailDecisions, guardrailMatchPreview).'),
521
+ 'observability.include.upstreamLog': field('boolean', 'Scrubbed provider HTTP/SSE rows. Default true.'),
522
+ 'observability.include.outboundWire': field('boolean', 'Scrubbed outbound request body. Default false.'),
523
+ 'observability.include.evidenceRaw': field('boolean', 'Verbatim provider step JSON on events. Default false.'),
524
+ 'observability.include.usage': field('boolean', 'Token / usage fields. Default true.'),
525
+ 'observability.include.guardrailDecisions': field('boolean', 'Persist { type: "guardrail" } decisions in the TraceRecord. Default true.'),
526
+ 'observability.include.guardrailMatchPreview': field('boolean', 'Keep GuardrailHit.match (capped matched substring) on stream + TraceRecord. Default false — debugging only.'),
527
+ 'observability.scrub': field('TraceScrubSpec', 'Scrubbing of stored records — independent of profile.guardrails. Defaults on.'),
528
+ 'observability.scrub.sensitive': field('boolean', 'Strip credentials / PII spans in stored text. Default true.'),
529
+ 'observability.scrub.injection': field('boolean', 'Strip injection spans in the stored request copy. Default true.'),
530
+ 'observability.scrub.canary': field('boolean', 'Never persist the canary token. Default true.'),
531
+ 'observability.retainForDays': field('number', 'JSONL retention days when writeTo resolves to a jsonl destination. Default 14.'),
532
+ 'observability.rotateAfterMiB': field('number', 'JSONL rotate threshold in MiB when writeTo resolves to a jsonl destination. Default 32.'),
533
+ 'observability.onWriteError': field('(err: unknown) => void', 'Host hook when record build or destination write fails. Must not throw.'),
429
534
  };
535
+ const TOOL_TYPE_FIELD = field(unionType(TOOL_TYPES), 'Discriminator: builtin (provider-native), function (host handler), http (declarative HTTP), or mcp (remote MCP).', TOOL_TYPES, {
536
+ builtin: 'Provider-native capability; wire maps to the provider adapter.',
537
+ function: 'Host-owned tool with Zod input/output and a handler. Profile tools.t2Loader may promote T2 tools when output includes { loaded }.',
538
+ http: 'Declarative HTTP tool calling remote REST/JSON endpoint with optional auth/PKCE.',
539
+ mcp: 'Remote Model Context Protocol tool calling remote MCP server with JSON-RPC.',
540
+ });
430
541
  /** Adjacent tool catalog fields that appear next to profile examples. */
431
542
  export const EXTRA_FIELDS = {
432
- type: field(unionType(TOOL_TYPES), 'Discriminator: builtin (provider-native) or function (host handler).', TOOL_TYPES, {
433
- builtin: 'Provider-native capability; wire maps to the provider adapter.',
434
- function: 'Host-owned tool with Zod input/output and a handler. Profile tools.t2Loader may promote T2 tools when output includes { loaded }.',
435
- }),
436
- name: field('string', 'Wire tool id — custom: tools.allow; provider builtin: model.config.*.builtInTools. Visibility via loadTier (T0/T1/T2).'),
543
+ /** Playground / UI path avoids collision with profile `type` in fieldMeta(). */
544
+ 'registerTool.type': TOOL_TYPE_FIELD,
545
+ name: field('string', 'Wire tool id custom: tools.allow; provider builtin: models.*.builtInTools. Visibility via loadTier (T0/T1/T2).'),
437
546
  description: field('string', 'Model-facing description included in function declarations.'),
438
547
  input: field('ZodSchema', 'Zod input schema for function tools; converted to JSON Schema at registration.'),
439
548
  output: field('ZodSchema', 'Zod output schema for function tools; validates handler results.'),
440
549
  handler: field('ToolHandler', 'Host function or async generator run on model tool calls and invokeTool resumes.'),
550
+ endpoint: field('string', 'HTTP URL template for declarative tools. Use {param} placeholders for path segments.'),
551
+ method: field(unionType(HTTP_METHODS), 'HTTP verb for declarative tools.', HTTP_METHODS),
552
+ headers: field('Record<string, string>', 'Optional static headers merged on every HTTP or MCP request.'),
553
+ mapping: field('{ pathParams?, queryParams?, bodyParam? }', 'Maps tool input fields to URL path segments, query string, or JSON body.'),
554
+ 'mapping.pathParams': field('string[]', 'Input keys substituted into {name} path segments on the endpoint template.'),
555
+ 'mapping.queryParams': field('string[]', 'Input keys appended as query-string parameters.'),
556
+ 'mapping.bodyParam': field('string', 'Single input key sent as the JSON request body (POST/PUT/PATCH).'),
557
+ serverUrl: field('string', 'Streamable HTTP MCP server endpoint (JSON-RPC tools/call).'),
558
+ mcpToolName: field('string', 'Remote tool name on the MCP server (tools/list → tools/call).'),
559
+ auth: field('HttpToolAuthConfig', 'Optional credential slot and header wiring for HTTP and MCP tools.'),
560
+ 'auth.type': field(unionType(TOOL_AUTH_TYPES), 'How credentials from the slot are attached to outbound requests.', TOOL_AUTH_TYPES, {
561
+ bearer: 'Authorization header with optional prefix (default Bearer).',
562
+ api_key: 'Named header carries the raw key or token.',
563
+ oauth2: 'OAuth2 access token with optional refresh via the credential slot.',
564
+ }),
565
+ 'auth.slot': field('string', 'Credential slot id resolved from ToolContext.credentials at execution time.'),
566
+ 'auth.headerName': field('string', "Request header for bearer/api_key auth (default 'Authorization')."),
567
+ 'auth.headerPrefix': field('string', "Prefix before the secret (default 'Bearer ' for bearer auth)."),
568
+ 'auth.onUnauthenticated': field(unionType(AUTH_UNAUTHENTICATED_POLICIES), 'Whether a missing/expired credential gates the turn or reports to the model.', AUTH_UNAUTHENTICATED_POLICIES, {
569
+ pause: 'Emit ToolGate { kind: auth } (tool.phase gate + stop.kind gate) and wait for host credential injection. Schema id remains `pause`.',
570
+ report_to_model: 'Return a model-visible finding without gating the turn.',
571
+ }),
572
+ 'auth.scopes': field('string[]', 'OAuth2 scopes requested during authorization.'),
573
+ 'auth.clientId': field('string', 'OAuth2 client id for the authorization code flow.'),
574
+ 'auth.redirectUri': field('string', 'OAuth2 redirect URI registered for this client.'),
575
+ 'playground.authType': field(unionType(PLAYGROUND_AUTH_TYPES), 'Playground auth select — `none` omits auth when compiling registerTool.', PLAYGROUND_AUTH_TYPES, {
576
+ none: 'No credential slot — tool runs without Authorization headers.',
577
+ bearer: 'Authorization header with optional prefix (default Bearer).',
578
+ api_key: 'Named header carries the raw key or token.',
579
+ oauth2: 'OAuth2 access token with optional refresh via the credential slot.',
580
+ }),
581
+ 'playground.testCredential': field('string', 'Playground-only: one-shot credential for the pre-run connectivity check (not compiled into registerTool).'),
582
+ 'playground.stubOutput': field('Record<string, unknown>', 'Playground-only: fixed JSON object returned by function tool stubs when no demo handler exists.'),
583
+ 'registerStructured.enforced': field(unionType(SCHEMA_ENFORCEMENTS), 'How structured output is enforced on the wire.', SCHEMA_ENFORCEMENTS),
584
+ 'registerStructured.jsonSchema': field('Record<string, unknown>', 'JSON Schema body registered under outputs.structured id.'),
441
585
  access: field(unionType(TOOL_ACCESS), 'Semantic access level for policy and UI.', TOOL_ACCESS, {
442
586
  'read-only': 'Reads host or remote state; no lasting mutation.',
443
587
  'read-write': 'May create or update host state.',
444
588
  destructive: 'May delete, charge, or otherwise hard-to-undo actions.',
445
589
  }),
446
- loadTier: field(unionType(TOOL_LOAD_TIERS), 'When this tool is wired to the model (profile allow / builtInTools is still required).', TOOL_LOAD_TIERS, {
447
- T0: 'Wired at turn start when allowed (custom on allow / builtin on the model).',
448
- T1: 'Wired when profile.tools.t1Policy selects it.',
449
- T2: 'Deferred until profile.tools.t2Loader returns { loaded } and the kernel promotes those ids.',
590
+ loadTier: field(unionType(TOOL_LOAD_TIERS), 'When this tool is wired to the model (profile allow / builtInTools is still required). Live sessions wire every allowed tool at setup; host profiles execute every allowed tool.', TOOL_LOAD_TIERS, {
591
+ T0: 'Wired at turn/session start when allowed (custom on allow / builtin on the model).',
592
+ T1: 'Wired when profile.tools.t1Policy selects it (text/image turns; live wires it at setup).',
593
+ T2: 'Deferred until profile.tools.t2Loader returns { loaded } and the kernel promotes those ids (text/image turns; live wires it at setup).',
450
594
  }),
451
595
  permission: field(unionType(TOOL_PERMISSION), 'Default permission tier for this tool before the handler runs.', TOOL_PERMISSION, {
452
596
  auto: 'Run without an extra host consent step.',
@@ -460,3 +604,4 @@ export const EXTRA_FIELDS = {
460
604
  export function fieldMeta(path) {
461
605
  return PROFILE_FIELDS[path] ?? EXTRA_FIELDS[path];
462
606
  }
607
+ export { PROFILE_GRAPH, profileGraphFacet, spineFacetsForProfileType, } from './profile-graph.js';