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
@@ -2,50 +2,225 @@
2
2
  * Runtime profile registry for host-owned THEORUM profiles.
3
3
  *
4
4
  * THEORUM ships profile types — not application profiles and not invented defaults.
5
- * Hosts must pass required fields explicitly (`type`, `model.protocol`, `model.provider`, …).
5
+ * Hosts must pass required fields explicitly (`type`, `models`, …).
6
6
  *
7
7
  * @module
8
8
  */
9
9
  import { TheorumError } from '../../guardrails/error.js';
10
- import { isValidPair, isValidProfileProtocol, protocolsForProfileType } from '../schema.js';
10
+ import { HOST_GUARDRAIL_FIELDS, } from '../../guardrails/types.js';
11
+ import { resolveObservabilityPolicy } from '../../observability/resolve-policy.js';
12
+ import { assertLiveIngressConfigured } from '../engine/live-ingress.js';
13
+ import { CACHE_MODES, CACHE_TTLS, isValidPair, isValidProfileProtocol, protocolsForProfileType, } from '../schema.js';
14
+ import { isContinueStopKind } from '../stop.js';
11
15
  import { getTool } from '../tools/registry.js';
16
+ import { soleModelId } from './sole-model.js';
12
17
  const profiles = new Map();
13
- function assertModelSpecs(profileId, allow, config) {
14
- for (const id of allow) {
15
- if (!config[id]) {
16
- throw new TheorumError(`Profile ${profileId} allowlists '${id}' without a model spec`);
18
+ /** Fields a `host` profile must not carry — rejected when supplied. */
19
+ const HOST_ABSENT_FIELDS = [
20
+ 'models',
21
+ 'defaultModel',
22
+ 'allowModelSelect',
23
+ 'identity',
24
+ 'inputs',
25
+ 'outputs',
26
+ 'turnBehaviour',
27
+ 'key',
28
+ 'maxSteps',
29
+ ];
30
+ /**
31
+ * Guardrails that only a model turn can run, so a host profile must not declare
32
+ * them: `egress` gates user-visible model text in the turn runner, `canary` is
33
+ * minted into a system prompt, and `quota` counts turns. None is reachable from
34
+ * `invokeTool`, so accepting them would register config that guards nothing.
35
+ */
36
+ const HOST_ABSENT_GUARDRAILS = ['quota', 'canary', 'egress'];
37
+ const HOST_GUARDRAIL_REASON = {
38
+ quota: 'quota counts model turns', // lexicon-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
39
+ canary: 'canary is minted into a system prompt', // lexicon-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
40
+ egress: 'egress gates user-visible model text in the turn runner', // lexicon-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
41
+ };
42
+ function assertHostGuardrails(profileId, guardrails) {
43
+ if (!guardrails)
44
+ return;
45
+ const extra = guardrails;
46
+ for (const key of HOST_ABSENT_GUARDRAILS) {
47
+ if (extra[key] !== undefined) {
48
+ throw new TheorumError(`Profile ${profileId}: type 'host' must not set guardrails.${key} — a host profile runs no model and ${HOST_GUARDRAIL_REASON[key]}. Host profiles accept ${HOST_GUARDRAIL_FIELDS.join(', ')}.`);
17
49
  }
18
50
  }
19
51
  }
20
- function assertProtocolProvider(profileId, model) {
21
- if (!model.protocol) {
22
- throw new TheorumError(`Profile ${profileId} must set model.protocol`);
52
+ function defineHostProfile(input) {
53
+ const extra = input;
54
+ for (const key of HOST_ABSENT_FIELDS) {
55
+ if (extra[key] !== undefined) {
56
+ throw new TheorumError(`Profile ${input.id}: type 'host' must not set ${key}`); // lexicon-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
57
+ }
23
58
  }
24
- if (!model.provider) {
25
- throw new TheorumError(`Profile ${profileId} must set model.provider`);
59
+ assertHostGuardrails(input.id, input.guardrails);
60
+ assertHostTools(input.id, input.tools);
61
+ assertObservability(input.id, input.observability);
62
+ return {
63
+ type: 'host',
64
+ id: input.id,
65
+ tools: { allow: input.tools.allow },
66
+ guardrails: input.guardrails,
67
+ observability: input.observability,
68
+ };
69
+ }
70
+ function assertHostTools(profileId, tools) {
71
+ if (!Array.isArray(tools?.allow)) {
72
+ throw new TheorumError(`Profile ${profileId}: type 'host' must set tools.allow`); // lexicon-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
26
73
  }
27
- if (!isValidPair(model.protocol, model.provider)) {
28
- throw new TheorumError(`Profile ${profileId}: protocol '${model.protocol}' is not valid for provider '${model.provider}'`);
74
+ const extra = tools;
75
+ if (extra.t1Policy !== undefined || extra.t2Loader !== undefined) {
76
+ throw new TheorumError(`Profile ${profileId}: tools.t1Policy / tools.t2Loader are not supported on type 'host' — every allowed tool is executable`);
29
77
  }
30
78
  }
31
- function assertTypeProtocol(profile) {
32
- const protocol = profile.model.protocol;
33
- if (isValidProfileProtocol(profile.type, protocol))
79
+ function assertModelsNonEmpty(profileId, models) {
80
+ if (Object.keys(models).length === 0) {
81
+ throw new TheorumError(`Profile ${profileId} must declare at least one model`); // lexicon-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
82
+ }
83
+ }
84
+ function assertDefaultModel(profileId, input) {
85
+ const ids = Object.keys(input.models);
86
+ const inferred = input.defaultModel ?? soleModelId(input.models);
87
+ if (!inferred) {
88
+ throw new TheorumError(`Profile ${profileId} must set defaultModel when more than one model is declared`);
89
+ }
90
+ if (!input.models[inferred]) {
91
+ throw new TheorumError(`Profile ${profileId} defaultModel '${inferred}' is not declared`); // lexicon-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
92
+ }
93
+ if (input.allowModelSelect && ids.length < 2) {
94
+ throw new TheorumError(`Profile ${profileId} allowModelSelect requires at least two models`); // lexicon-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
95
+ }
96
+ }
97
+ function assertModelBinding(profileId, modelId, binding) {
98
+ if (!binding.protocol) {
99
+ throw new TheorumError(`Profile ${profileId} model '${modelId}' must set protocol`); // lexicon-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
100
+ }
101
+ if (!binding.provider) {
102
+ throw new TheorumError(`Profile ${profileId} model '${modelId}' must set provider`); // lexicon-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
103
+ }
104
+ if (!binding.apiId) {
105
+ throw new TheorumError(`Profile ${profileId} model '${modelId}' must set apiId`); // lexicon-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
106
+ }
107
+ if (!isValidPair(binding.protocol, binding.provider)) {
108
+ throw new TheorumError(`Profile ${profileId} model '${modelId}': protocol '${binding.protocol}' is not valid for provider '${binding.provider}'`);
109
+ }
110
+ assertModelEfforts(profileId, modelId, binding);
111
+ if (binding.cache) {
112
+ assertCacheSpec(profileId, modelId, binding);
113
+ }
114
+ assertInteractionsPersistence(profileId, modelId, binding);
115
+ }
116
+ function assertModelEfforts(profileId, modelId, binding) {
117
+ const efforts = binding.efforts;
118
+ if (!efforts || Object.keys(efforts).length === 0) {
119
+ if (binding.defaultEffort || binding.allowEffortSelect) {
120
+ throw new TheorumError(`Profile ${profileId} model '${modelId}': defaultEffort and allowEffortSelect require efforts`);
121
+ }
34
122
  return;
35
- const valid = protocolsForProfileType(profile.type).join(', ');
36
- throw new TheorumError(`Profile ${profile.id}: type '${profile.type}' cannot use protocol '${protocol}'. Supported: ${valid}`);
123
+ }
124
+ const keys = Object.keys(efforts);
125
+ const defaultAlias = binding.defaultEffort ?? (keys.length === 1 ? keys[0] : undefined);
126
+ if (!defaultAlias) {
127
+ throw new TheorumError(`Profile ${profileId} model '${modelId}' must set defaultEffort when more than one effort is declared`);
128
+ }
129
+ if (!efforts[defaultAlias]) {
130
+ throw new TheorumError(`Profile ${profileId} model '${modelId}' defaultEffort '${defaultAlias}' is not declared`);
131
+ }
132
+ if (binding.allowEffortSelect && keys.length < 2) {
133
+ throw new TheorumError(`Profile ${profileId} model '${modelId}' allowEffortSelect requires at least two efforts`);
134
+ }
135
+ }
136
+ function assertTypeProtocols(profile) {
137
+ for (const [modelId, binding] of Object.entries(profile.models)) {
138
+ if (!isValidProfileProtocol(profile.type, binding.protocol)) {
139
+ const valid = protocolsForProfileType(profile.type).join(', ');
140
+ throw new TheorumError(`Profile ${profile.id} model '${modelId}': type '${profile.type}' cannot use protocol '${binding.protocol}'. Supported: ${valid}`);
141
+ }
142
+ }
143
+ }
144
+ function profileModelFields(input) {
145
+ return {
146
+ models: input.models,
147
+ defaultModel: input.defaultModel ?? soleModelId(input.models),
148
+ allowModelSelect: input.allowModelSelect,
149
+ maxSteps: input.maxSteps,
150
+ key: input.key,
151
+ };
152
+ }
153
+ function assertContinueKindList(profileId, path, kinds) {
154
+ if (!kinds?.length)
155
+ return;
156
+ for (const kind of kinds) {
157
+ if (!isContinueStopKind(kind)) {
158
+ throw new TheorumError(`Profile ${profileId}: turnBehaviour.resumption.${path} may only include ContinueStopKind ` + // lexicon-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
159
+ `(length | stream_incomplete | provider_error); got '${kind}'`);
160
+ }
161
+ }
162
+ }
163
+ function assertResumption(profileId, resumption) {
164
+ if (!resumption)
165
+ return;
166
+ assertContinueKindList(profileId, 'allowContinue', resumption.allowContinue);
167
+ assertContinueKindList(profileId, 'autoContinue', resumption.autoContinue);
168
+ }
169
+ function assertTurnBehaviour(profileId, input) {
170
+ if (input.type === 'host')
171
+ return;
172
+ if (input.type === 'live') {
173
+ const tb = input.turnBehaviour;
174
+ if (tb?.resumption !== undefined) {
175
+ throw new TheorumError(`Profile ${profileId}: type 'live' uses live.sessionResumption, not turnBehaviour.resumption`);
176
+ }
177
+ return;
178
+ }
179
+ if (input.type !== 'text' && input.turnBehaviour?.allowSteering !== undefined) {
180
+ throw new TheorumError(`Profile ${profileId}: turnBehaviour.allowSteering is only valid on type 'text' or 'live'`);
181
+ }
182
+ assertResumption(profileId, input.turnBehaviour?.resumption);
183
+ }
184
+ function assertObservability(profileId, spec) {
185
+ if (!spec) {
186
+ return;
187
+ }
188
+ try {
189
+ const policy = resolveObservabilityPolicy(spec);
190
+ if (policy.retainForDays <= 0 || !Number.isFinite(policy.retainForDays)) {
191
+ throw new TheorumError(`Profile ${profileId}: observability.retainForDays must be a positive number`);
192
+ }
193
+ if (policy.rotateAfterMiB <= 0 || !Number.isFinite(policy.rotateAfterMiB)) {
194
+ throw new TheorumError(`Profile ${profileId}: observability.rotateAfterMiB must be a positive number`);
195
+ }
196
+ }
197
+ catch (err) {
198
+ if (err instanceof TheorumError && err.message.startsWith('Profile ')) {
199
+ throw err;
200
+ }
201
+ const message = err instanceof Error ? err.message : String(err);
202
+ throw new TheorumError(`Profile ${profileId}: ${message}`);
203
+ }
37
204
  }
38
- /** Define a typed profile. Required fields must be set explicitly; optional fields stay optional. */
39
205
  function defineProfile(input) {
40
- assertProtocolProvider(input.id, input.model);
41
- assertModelSpecs(input.id, input.model.allow, input.model.config);
206
+ if (input.type === 'host') {
207
+ return defineHostProfile(input);
208
+ }
209
+ assertModelsNonEmpty(input.id, input.models);
210
+ assertDefaultModel(input.id, input);
211
+ assertTurnBehaviour(input.id, input);
212
+ assertObservability(input.id, input.observability);
213
+ for (const [modelId, binding] of Object.entries(input.models)) {
214
+ assertModelBinding(input.id, modelId, binding);
215
+ }
42
216
  const identity = {
43
217
  handle: input.identity.handle,
44
218
  system: input.identity.system,
45
219
  systemByRole: input.identity.systemByRole,
46
220
  };
47
- const outputs = input.outputs;
48
221
  const guardrails = input.guardrails;
222
+ const observability = input.observability;
223
+ const modelFields = profileModelFields(input);
49
224
  let profile;
50
225
  switch (input.type) {
51
226
  case 'text':
@@ -53,12 +228,13 @@ function defineProfile(input) {
53
228
  type: 'text',
54
229
  id: input.id,
55
230
  identity,
56
- model: input.model,
231
+ ...modelFields,
57
232
  tools: input.tools,
58
233
  inputs: input.inputs,
59
- outputs,
60
- turnResumption: input.turnResumption,
234
+ outputs: input.outputs,
235
+ turnBehaviour: input.turnBehaviour,
61
236
  guardrails,
237
+ observability,
62
238
  };
63
239
  break;
64
240
  case 'image':
@@ -66,13 +242,14 @@ function defineProfile(input) {
66
242
  type: 'image',
67
243
  id: input.id,
68
244
  identity,
69
- model: input.model,
245
+ ...modelFields,
70
246
  image: input.image,
71
247
  tools: input.tools,
72
248
  inputs: input.inputs,
73
- outputs,
74
- turnResumption: input.turnResumption,
249
+ outputs: input.outputs,
250
+ turnBehaviour: input.turnBehaviour,
75
251
  guardrails,
252
+ observability,
76
253
  };
77
254
  break;
78
255
  case 'speech':
@@ -80,56 +257,96 @@ function defineProfile(input) {
80
257
  type: 'speech',
81
258
  id: input.id,
82
259
  identity,
83
- model: input.model,
260
+ ...modelFields,
84
261
  speech: input.speech,
85
- outputs,
86
- turnResumption: input.turnResumption,
262
+ outputs: input.outputs,
263
+ turnBehaviour: input.turnBehaviour,
87
264
  guardrails,
265
+ observability,
88
266
  };
89
267
  break;
90
- case 'live':
268
+ case 'live': {
269
+ const liveInput = input;
270
+ if (liveInput.inputs !== undefined) {
271
+ throw new TheorumError(`Profile ${input.id}: type 'live' must not set inputs`); // lexicon-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
272
+ }
273
+ if (liveInput.outputs !== undefined) {
274
+ throw new TheorumError(`Profile ${input.id}: type 'live' must not set outputs`); // lexicon-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
275
+ }
91
276
  profile = {
92
277
  type: 'live',
93
278
  id: input.id,
94
279
  identity,
95
- model: input.model,
280
+ ...modelFields,
96
281
  live: input.live,
97
- tools: input.tools,
98
- inputs: input.inputs,
99
- outputs,
282
+ tools: assertLiveTools(input.id, input.tools),
283
+ turnBehaviour: liveInput.turnBehaviour,
100
284
  guardrails,
285
+ observability,
101
286
  };
287
+ assertLiveIngressConfigured(profile);
102
288
  break;
289
+ }
103
290
  default: {
104
291
  const _exhaustive = input;
105
- throw new TheorumError(`Unknown profile type '${String(_exhaustive)}'`);
292
+ throw new TheorumError(`Unknown profile type '${String(_exhaustive)}'`); // lexicon-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
106
293
  }
107
294
  }
108
- assertTypeProtocol(profile);
295
+ assertTypeProtocols(profile);
109
296
  return profile;
110
297
  }
111
298
  function assertCompactionSpec(profileId, modelId, spec) {
112
- const tag = `Profile ${profileId} model ${modelId} compaction`;
299
+ const tag = `Profile ${profileId} model ${modelId} compaction`; // lexicon-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
113
300
  assertCompactionBudget(tag, spec);
114
301
  assertCompactionRetain(tag, spec);
115
302
  if (spec.meter != null && spec.meter !== 'history' && spec.meter !== 'input') {
116
- throw new TheorumError(`${tag}: meter must be 'history' or 'input'`);
303
+ throw new TheorumError(`${tag}: meter must be 'history' or 'input'`); // lexicon-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
117
304
  }
118
305
  if (!profiles.has(spec.profile)) {
119
306
  throw new TheorumError(`${tag}: compaction profile '${spec.profile}' must be registered before '${profileId}'`);
120
307
  }
121
308
  }
309
+ function assertCacheSpec(profileId, modelId, binding) {
310
+ const spec = binding.cache;
311
+ if (!spec) {
312
+ return;
313
+ }
314
+ const tag = `Profile ${profileId} model '${modelId}'`;
315
+ if (binding.provider !== 'openrouter' || binding.protocol !== 'openAi') {
316
+ throw new TheorumError(`${tag}: cache is only valid when protocol is 'openAi' and provider is 'openrouter'`);
317
+ }
318
+ if (!CACHE_MODES.includes(spec.mode)) {
319
+ throw new TheorumError(`${tag}: cache.mode must be one of ${CACHE_MODES.join(' | ')}`); // lexicon-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
320
+ }
321
+ if (spec.ttl != null && !CACHE_TTLS.includes(spec.ttl)) {
322
+ throw new TheorumError(`${tag}: cache.ttl must be one of ${CACHE_TTLS.join(' | ')}`); // lexicon-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
323
+ }
324
+ }
325
+ function assertInteractionsPersistence(profileId, modelId, binding) {
326
+ if (binding.store === undefined && binding.persistViaInteractionId === undefined) {
327
+ return;
328
+ }
329
+ if (binding.protocol === 'geminiInteractions' && binding.provider === 'google') {
330
+ return;
331
+ }
332
+ const which = binding.store !== undefined && binding.persistViaInteractionId !== undefined
333
+ ? 'store and persistViaInteractionId' // lexicon-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
334
+ : binding.store !== undefined
335
+ ? 'store'
336
+ : 'persistViaInteractionId';
337
+ throw new TheorumError(`Profile ${profileId} model '${modelId}': ${which} is only valid when protocol is 'geminiInteractions' and provider is 'google'`);
338
+ }
122
339
  function assertCompactionBudget(tag, spec) {
123
340
  if (spec.maxTokens <= 0) {
124
- throw new TheorumError(`${tag}: maxTokens must be > 0`);
341
+ throw new TheorumError(`${tag}: maxTokens must be > 0`); // lexicon-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
125
342
  }
126
343
  if (spec.compactAt <= 0 || spec.compactAt >= 1) {
127
- throw new TheorumError(`${tag}: compactAt must be in (0, 1)`);
344
+ throw new TheorumError(`${tag}: compactAt must be in (0, 1)`); // lexicon-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
128
345
  }
129
346
  }
130
347
  function assertCompactionRetain(tag, spec) {
131
348
  if (spec.previousExchanges < 0) {
132
- throw new TheorumError(`${tag}: previousExchanges must be >= 0`);
349
+ throw new TheorumError(`${tag}: previousExchanges must be >= 0`); // lexicon-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
133
350
  }
134
351
  if (spec.previousExchanges > 0 && spec.previousExchanges < 1) {
135
352
  if (spec.previousExchanges >= spec.compactAt) {
@@ -137,7 +354,7 @@ function assertCompactionRetain(tag, spec) {
137
354
  }
138
355
  }
139
356
  if (spec.previousExchanges >= 1 && !Number.isInteger(spec.previousExchanges)) {
140
- throw new TheorumError(`${tag}: previousExchanges >= 1 must be an integer`);
357
+ throw new TheorumError(`${tag}: previousExchanges >= 1 must be an integer`); // lexicon-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
141
358
  }
142
359
  }
143
360
  function profileToolsAllow(profile) {
@@ -150,14 +367,34 @@ function assertCustomToolsOnly(profile) {
150
367
  for (const id of profileToolsAllow(profile)) {
151
368
  const tool = getTool(id);
152
369
  if (tool?.type === 'builtin') {
153
- throw new TheorumError(`Profile ${profile.id} lists builtin '${id}' in tools.allow — declare it on model.config.*.builtInTools instead`);
370
+ throw new TheorumError(profile.type === 'host'
371
+ ? `Profile ${profile.id} lists builtin '${id}' in tools.allow — type 'host' never runs a model` // lexicon-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
372
+ : `Profile ${profile.id} lists builtin '${id}' in tools.allow — declare it on models.*.builtInTools instead`);
154
373
  }
155
374
  }
156
375
  }
376
+ function assertLiveTools(profileId, tools) {
377
+ const extra = tools;
378
+ if (extra.t1Policy !== undefined) {
379
+ throw new TheorumError(`Profile ${profileId}: tools.t1Policy is not supported on type 'live' — wire T0 tools in tools.allow for session setup`);
380
+ }
381
+ if (extra.t2Loader !== undefined) {
382
+ throw new TheorumError(`Profile ${profileId}: tools.t2Loader is not supported on type 'live' — Gemini Live function declarations are fixed at session setup`);
383
+ }
384
+ return { allow: tools.allow };
385
+ }
157
386
  function assertProfileToolLoader(profile) {
158
387
  if (profile.type === 'speech') {
159
388
  return;
160
389
  }
390
+ if (profile.type === 'live') {
391
+ assertLiveTools(profile.id, profile.tools);
392
+ return;
393
+ }
394
+ if (profile.type === 'host') {
395
+ assertHostTools(profile.id, profile.tools);
396
+ return;
397
+ }
161
398
  const loaderId = profile.tools.t2Loader;
162
399
  if (!loaderId) {
163
400
  return;
@@ -170,13 +407,19 @@ function assertProfileToolLoader(profile) {
170
407
  throw new TheorumError(`Profile ${profile.id} tools.t2Loader '${loaderId}' must be a registered type: 'function' tool`);
171
408
  }
172
409
  }
410
+ /** Every builtin id declared across a profile's model bindings. */
411
+ function* modelBuiltinIds(profile) {
412
+ for (const [modelId, binding] of Object.entries(profile.models)) {
413
+ for (const id of binding.builtInTools ?? []) {
414
+ yield { modelId, id };
415
+ }
416
+ }
417
+ }
173
418
  function assertModelBuiltInTools(profile) {
174
- for (const [modelId, spec] of Object.entries(profile.model.config)) {
175
- for (const id of spec.builtInTools ?? []) {
176
- const tool = getTool(id);
177
- if (tool?.type !== 'builtin') {
178
- throw new TheorumError(`Profile ${profile.id} model '${modelId}' lists '${id}' in builtInTools — not a registered builtin`);
179
- }
419
+ for (const { modelId, id } of modelBuiltinIds(profile)) {
420
+ const tool = getTool(id);
421
+ if (tool?.type !== 'builtin') {
422
+ throw new TheorumError(`Profile ${profile.id} model '${modelId}' lists '${id}' in builtInTools — not a registered builtin`);
180
423
  }
181
424
  }
182
425
  }
@@ -184,14 +427,14 @@ function assertCompactionOnlyOnText(profile) {
184
427
  if (profile.type === 'text') {
185
428
  return;
186
429
  }
187
- for (const [modelId, spec] of Object.entries(profile.model.config)) {
188
- if (spec.compaction) {
430
+ for (const [modelId, binding] of Object.entries(profile.models)) {
431
+ if (binding.compaction) {
189
432
  throw new TheorumError(`Profile ${profile.id} model '${modelId}': compaction is only valid on type 'text'`);
190
433
  }
191
434
  }
192
435
  }
193
436
  function assertMediaLimits(profile) {
194
- if (profile.type === 'speech') {
437
+ if (profile.type === 'speech' || profile.type === 'live') {
195
438
  return;
196
439
  }
197
440
  const inputs = profile.inputs;
@@ -201,22 +444,26 @@ function assertMediaLimits(profile) {
201
444
  const { attachments, voice, maxFiles, maxBytes, maxTurnBytes } = inputs;
202
445
  if (attachments || voice) {
203
446
  if (!(maxFiles && maxBytes && maxTurnBytes)) {
204
- throw new TheorumError(`Profile ${profile.id} must set maxFiles, maxBytes, and maxTurnBytes`);
447
+ throw new TheorumError(`Profile ${profile.id} must set maxFiles, maxBytes, and maxTurnBytes`); // lexicon-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
205
448
  }
206
449
  }
207
450
  }
208
451
  /** Register one host-owned profile in the process-local registry. */
209
452
  function registerProfile(profileInput) {
210
453
  const profile = defineProfile(profileInput);
211
- assertModelSpecs(profile.id, profile.model.allow, profile.model.config);
212
454
  assertCustomToolsOnly(profile);
213
455
  assertProfileToolLoader(profile);
456
+ if (profile.type === 'host') {
457
+ // No models, ingress, media limits, or compaction to validate — the tool ceiling is the whole contract.
458
+ profiles.set(profile.id, profile);
459
+ return;
460
+ }
214
461
  assertModelBuiltInTools(profile);
215
462
  assertCompactionOnlyOnText(profile);
216
463
  assertMediaLimits(profile);
217
- for (const [modelId, spec] of Object.entries(profile.model.config)) {
218
- if (spec.compaction) {
219
- assertCompactionSpec(profile.id, modelId, spec.compaction);
464
+ for (const [modelId, binding] of Object.entries(profile.models)) {
465
+ if (binding.compaction) {
466
+ assertCompactionSpec(profile.id, modelId, binding.compaction);
220
467
  }
221
468
  }
222
469
  profiles.set(profile.id, profile);
@@ -13,6 +13,8 @@ function providerCompleteRequest(generation, system) {
13
13
  temperature: generation.temperature,
14
14
  builtins: generation.builtins,
15
15
  googleMapsLocation: isInteractions ? generation.googleMapsLocation : undefined,
16
+ cache: generation.cache,
17
+ sessionId: generation.sessionId,
16
18
  system,
17
19
  input: generation.input,
18
20
  history: generation.history,
@@ -3,16 +3,17 @@
3
3
  *
4
4
  * @module
5
5
  */
6
- import type { ModelId, Profile, ProjectedProfile, ResolvedGeneration, TurnRequest } from '../types.js';
7
- declare function pickModel(profile: Profile, select?: string): ModelId;
6
+ import type { ModelId, ModelProfile, Profile, ProjectedProfile, ResolvedGeneration, TurnRequest } from '../types.js';
7
+ /** Narrow to a model-binding profile; `host` never runs a model. */
8
+ declare function requireModelProfile(profile: Profile, door: string): ModelProfile;
9
+ declare function pickModel(profile: ModelProfile, requested?: string): ModelId;
8
10
  /** Resolve a host `TurnRequest` into provider-ready generation state. */
9
11
  declare function resolveTurn(req: TurnRequest): {
10
- profile: Profile;
12
+ profile: ModelProfile;
11
13
  generation: ResolvedGeneration;
12
14
  };
13
15
  /** Project a profile object into a safe host/UI inspection object. */
14
- declare function projectProfileObject(profile: Profile): ProjectedProfile;
16
+ declare function projectProfileObject(input: Profile): ProjectedProfile;
15
17
  /** Project a registered profile into a safe host/UI inspection object. */
16
18
  declare function projectProfile(id: Profile['id']): ProjectedProfile;
17
- declare function pickSystemRole(profile: Profile, requested?: string): string;
18
- export { pickModel, pickSystemRole, projectProfile, projectProfileObject, resolveTurn };
19
+ export { pickModel, projectProfile, projectProfileObject, requireModelProfile, resolveTurn };