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,22 +2,21 @@
2
2
  * Trace sink primitives for THEORUM.
3
3
  *
4
4
  * Tracing is host-injected: the kernel can write to a provided sink, a memory
5
- * sink, a JSONL directory, or a noop sink. It does not read environment
6
- * variables or own a database destination.
5
+ * sink, a JSONL directory, or a noop sink. Profiles declare policy via
6
+ * `observability`; hosts register named destinations. THEORUM does not read
7
+ * environment variables or own a database destination.
7
8
  *
8
9
  * @module
9
10
  */
10
11
  import * as dntShim from "../../_dnt.shims.js";
11
- const RETAIN_DAYS = 14;
12
+ const DEFAULT_RETAIN_DAYS = 14;
12
13
  const HOURS_PER_DAY = 24;
13
14
  const MIN_PER_HOUR = 60;
14
15
  const SEC_PER_MIN = 60;
15
16
  const MS_PER_SEC = 1000;
16
- const RETAIN_MS = RETAIN_DAYS * HOURS_PER_DAY * MIN_PER_HOUR * SEC_PER_MIN * MS_PER_SEC;
17
17
  const KIB = 1024;
18
18
  const MIB = KIB * KIB;
19
- const ROTATE_MIB = 32;
20
- const ROTATE_BYTES = ROTATE_MIB * MIB;
19
+ const DEFAULT_ROTATE_MIB = 32;
21
20
  const FILE_DAY = /^turns-(\d{4}-\d{2}-\d{2})(?:-\d+)?\.jsonl$/;
22
21
  /**
23
22
  * Write a trace record without allowing trace failures to fail the turn.
@@ -55,8 +54,9 @@ function dayStamp(ms) {
55
54
  function fileDay(name) {
56
55
  return FILE_DAY.exec(name)?.[1];
57
56
  }
58
- async function pruneTraces(dir, now) {
59
- const cutoff = now - RETAIN_MS;
57
+ async function pruneTraces(dir, now, retainForDays) {
58
+ const retainMs = retainForDays * HOURS_PER_DAY * MIN_PER_HOUR * SEC_PER_MIN * MS_PER_SEC;
59
+ const cutoff = now - retainMs;
60
60
  for await (const entry of dntShim.Deno.readDir(dir)) {
61
61
  const day = fileDay(entry.name);
62
62
  if (day && Date.parse(`${day}T00:00:00.000Z`) < cutoff) {
@@ -64,12 +64,12 @@ async function pruneTraces(dir, now) {
64
64
  }
65
65
  }
66
66
  }
67
- async function pickFile(dir, now) {
67
+ async function pickFile(dir, now, rotateBytes) {
68
68
  const day = dayStamp(now);
69
69
  const base = `${dir}/turns-${day}.jsonl`;
70
70
  try {
71
71
  const info = await dntShim.Deno.stat(base);
72
- if ((info.size ?? 0) < ROTATE_BYTES) {
72
+ if ((info.size ?? 0) < rotateBytes) {
73
73
  return base;
74
74
  }
75
75
  }
@@ -78,14 +78,23 @@ async function pickFile(dir, now) {
78
78
  }
79
79
  return `${dir}/turns-${day}-${now}.jsonl`;
80
80
  }
81
- /** Trace sink that writes daily rotating JSONL files under a host-selected directory. */
82
- function jsonlSink(dir, now = Date.now) {
81
+ /**
82
+ * Trace sink that writes daily rotating JSONL files under a host-selected directory.
83
+ *
84
+ * @param dir - Absolute host-chosen directory
85
+ * @param optionsOrNow - Retention/rotate options, or a `now` clock (legacy)
86
+ */
87
+ function jsonlSink(dir, optionsOrNow) {
88
+ const options = typeof optionsOrNow === 'function' ? { now: optionsOrNow } : (optionsOrNow ?? {});
89
+ const now = options.now ?? Date.now;
90
+ const retainForDays = options.retainForDays ?? DEFAULT_RETAIN_DAYS;
91
+ const rotateBytes = (options.rotateAfterMiB ?? DEFAULT_ROTATE_MIB) * MIB;
83
92
  return {
84
93
  write: async (record) => {
85
94
  const at = now();
86
95
  await dntShim.Deno.mkdir(dir, { recursive: true });
87
- await pruneTraces(dir, at);
88
- const path = await pickFile(dir, at);
96
+ await pruneTraces(dir, at, retainForDays);
97
+ const path = await pickFile(dir, at, rotateBytes);
89
98
  await dntShim.Deno.writeTextFile(path, `${JSON.stringify(record)}\n`, { append: true });
90
99
  },
91
100
  };
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Profile observability vocabulary — destination, scrub, and include policy.
3
+ *
4
+ * This module is the single source of truth for observability profile types. It
5
+ * must not import from `src/kernel/`: the kernel type-imports
6
+ * `ProfileObservabilitySpec` for `ProfileCommon.observability`, and that edge
7
+ * stays one-directional.
8
+ *
9
+ * @module
10
+ */
11
+ import type { TraceSink } from './trace-sink.js';
12
+ /** Which payloads land in each TraceRecord. Omitted keys use resolved defaults. */
13
+ export interface TraceIncludeSpec {
14
+ /** Scrubbed provider HTTP/SSE rows. Default: true. */
15
+ upstreamLog?: boolean;
16
+ /** Scrubbed outbound request body. Default: false. */
17
+ outboundWire?: boolean;
18
+ /** Verbatim provider step JSON (`events[].evidence.raw`). Default: false. */
19
+ evidenceRaw?: boolean;
20
+ /** Token / usage fields. Default: true. */
21
+ usage?: boolean;
22
+ /**
23
+ * Persist `{ type: 'guardrail' }` decisions into the TraceRecord. Default: true.
24
+ */
25
+ guardrailDecisions?: boolean;
26
+ /**
27
+ * Keep `GuardrailHit.match` (exact matched substring, capped) on guardrail
28
+ * events in the live stream and TraceRecord. Default: false — debugging only;
29
+ * treat like server logs when enabled.
30
+ */
31
+ guardrailMatchPreview?: boolean;
32
+ }
33
+ /**
34
+ * Scrubbing of what is written — independent of turn-path `profile.guardrails`.
35
+ * Defaults are safe for a host-confidential store.
36
+ */
37
+ export interface TraceScrubSpec {
38
+ /** Strip credentials / PII spans in stored text. Default: true. */
39
+ sensitive?: boolean;
40
+ /** Strip injection spans in the stored request copy. Default: true. */
41
+ injection?: boolean;
42
+ /** Never persist the canary token. Default: true. */
43
+ canary?: boolean;
44
+ }
45
+ /**
46
+ * Profile observability — what to record, and where.
47
+ *
48
+ * Omit the whole block → no tracing (noop). Prefer `writeTo: '<registered-id>'`
49
+ * in shared profiles; pass a TraceSink only for tests / custom exporters.
50
+ */
51
+ export interface ProfileObservabilitySpec {
52
+ /**
53
+ * Destination for completed turn records.
54
+ *
55
+ * - `false` — explicitly off
56
+ * - `string` — host-registered destination id (`registerTraceDestination`)
57
+ * - `TraceSink` — inline writer (tests, OTEL bridge, etc.)
58
+ *
59
+ * `runTurn(..., sink)` still overrides this for one call.
60
+ */
61
+ writeTo?: false | string | TraceSink;
62
+ /**
63
+ * Fraction of turns to record, 0–1 inclusive.
64
+ * Omitted → 1 (every turn). `0` means record none.
65
+ */
66
+ sampleRate?: number;
67
+ /** Which payloads land in each TraceRecord. */
68
+ include?: TraceIncludeSpec;
69
+ /**
70
+ * Scrubbing of stored records — independent of turn-path guardrails.
71
+ * Defaults stay on even when `guardrails.redactSensitive` is false.
72
+ */
73
+ scrub?: TraceScrubSpec;
74
+ /** JSONL retention days when the resolved destination is JSONL. Default: 14. */
75
+ retainForDays?: number;
76
+ /** JSONL rotate threshold in MiB when the resolved destination is JSONL. Default: 32. */
77
+ rotateAfterMiB?: number;
78
+ /**
79
+ * Called when record build or destination write fails.
80
+ * Must not throw; tracing never fails the turn.
81
+ */
82
+ onWriteError?: (err: unknown) => void;
83
+ }
84
+ /** Resolved include flags after defaults. */
85
+ export interface ResolvedTraceInclude {
86
+ upstreamLog: boolean;
87
+ outboundWire: boolean;
88
+ evidenceRaw: boolean;
89
+ usage: boolean;
90
+ guardrailDecisions: boolean;
91
+ guardrailMatchPreview: boolean;
92
+ }
93
+ /** Resolved scrub flags after defaults. */
94
+ export interface ResolvedTraceScrub {
95
+ sensitive: boolean;
96
+ injection: boolean;
97
+ canary: boolean;
98
+ }
99
+ /**
100
+ * Observability policy with defaults applied.
101
+ * Every path resolves through `resolveObservabilityPolicy`.
102
+ */
103
+ export interface ResolvedObservabilityPolicy {
104
+ /** False when omitted, `writeTo: false`, or sampleRate drops the turn. */
105
+ record: boolean;
106
+ writeTo: false | string | TraceSink | undefined;
107
+ sampleRate: number;
108
+ include: ResolvedTraceInclude;
109
+ scrub: ResolvedTraceScrub;
110
+ retainForDays: number;
111
+ rotateAfterMiB: number;
112
+ onWriteError?: (err: unknown) => void;
113
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Profile observability vocabulary — destination, scrub, and include policy.
3
+ *
4
+ * This module is the single source of truth for observability profile types. It
5
+ * must not import from `src/kernel/`: the kernel type-imports
6
+ * `ProfileObservabilitySpec` for `ProfileCommon.observability`, and that edge
7
+ * stays one-directional.
8
+ *
9
+ * @module
10
+ */
11
+ export {};
@@ -1,15 +1,15 @@
1
1
  /**
2
2
  * Host provider factory — the single public door for binding a profile to a transport.
3
3
  *
4
- * Routes from `profile.model.protocol` / `provider` (and whether the profile is
5
- * a speech or image role). Adapters under this folder are internal implementation.
4
+ * Routes from the selected model binding's `protocol` / `provider` (and whether the
5
+ * profile is a speech or image role). Adapters under this folder are internal implementation.
6
6
  *
7
7
  * Every adapter graph is loaded only when that transport's first `complete` runs —
8
8
  * not when this module is imported.
9
9
  *
10
10
  * @module
11
11
  */
12
- import type { ModelProvider, Profile } from '../kernel/types.js';
12
+ import type { ModelId, ModelProvider, Profile } from '../kernel/types.js';
13
13
  import type { GeminiTransport } from './google/keys.js';
14
14
  import type { LocalProviderConfig, OpenAiGatewayConfig } from './types.js';
15
15
  /** Credentials supplied by the host when creating a provider. */
@@ -32,5 +32,8 @@ export declare function isImageRole(profile: Profile): boolean;
32
32
  /**
33
33
  * Create a `ModelProvider` for a turn-based profile (text / image / speech).
34
34
  * Live profiles use `runSession` — `createProvider` rejects geminiLive.
35
+ *
36
+ * When a profile declares multiple models, pass `modelId` to pick the binding used
37
+ * for adapter selection (defaults to `defaultModel` or the sole model key).
35
38
  */
36
- export declare function createProvider(profile: Profile, options?: CreateProviderOptions): ModelProvider;
39
+ export declare function createProvider(profile: Profile, options?: CreateProviderOptions, modelId?: ModelId): ModelProvider;
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * Host provider factory — the single public door for binding a profile to a transport.
3
3
  *
4
- * Routes from `profile.model.protocol` / `provider` (and whether the profile is
5
- * a speech or image role). Adapters under this folder are internal implementation.
4
+ * Routes from the selected model binding's `protocol` / `provider` (and whether the
5
+ * profile is a speech or image role). Adapters under this folder are internal implementation.
6
6
  *
7
7
  * Every adapter graph is loaded only when that transport's first `complete` runs —
8
8
  * not when this module is imported.
@@ -10,6 +10,8 @@
10
10
  * @module
11
11
  */
12
12
  import { TheorumError } from '../guardrails/error.js';
13
+ import { requireModelProfile } from '../kernel/registry/resolve.js';
14
+ import { soleModelId } from '../kernel/registry/sole-model.js';
13
15
  import { isValidPair } from '../kernel/schema.js';
14
16
  import { markModuleLoad } from './probe.js';
15
17
  export function isSpeechRole(profile) {
@@ -18,6 +20,18 @@ export function isSpeechRole(profile) {
18
20
  export function isImageRole(profile) {
19
21
  return profile.type === 'image';
20
22
  }
23
+ function bindingForProvider(input, modelId) {
24
+ const profile = requireModelProfile(input, 'createProvider');
25
+ const id = modelId ?? profile.defaultModel ?? soleModelId(profile.models);
26
+ if (!id) {
27
+ throw new TheorumError(`createProvider: profile '${profile.id}' must set defaultModel when multiple models are declared`);
28
+ }
29
+ const binding = profile.models[id];
30
+ if (!binding) {
31
+ throw new TheorumError(`createProvider: profile '${profile.id}' has no model '${id}'`);
32
+ }
33
+ return binding;
34
+ }
21
35
  /**
22
36
  * Lazy-load an adapter on first `complete`. When `THEORUM_IMPORT_PROBE=1`,
23
37
  * emits `LOADED:<label>` exactly once at load time (import-isolation tests).
@@ -52,9 +66,12 @@ function lazyLocal(config) {
52
66
  /**
53
67
  * Create a `ModelProvider` for a turn-based profile (text / image / speech).
54
68
  * Live profiles use `runSession` — `createProvider` rejects geminiLive.
69
+ *
70
+ * When a profile declares multiple models, pass `modelId` to pick the binding used
71
+ * for adapter selection (defaults to `defaultModel` or the sole model key).
55
72
  */
56
- export function createProvider(profile, options = {}) {
57
- const { protocol, provider } = profile.model;
73
+ export function createProvider(profile, options = {}, modelId) {
74
+ const { protocol, provider } = bindingForProvider(profile, modelId);
58
75
  if (!isValidPair(protocol, provider)) {
59
76
  throw new TheorumError(`createProvider: unsupported protocol/provider pair '${protocol}'/'${provider}'`);
60
77
  }
@@ -85,8 +102,5 @@ export function createProvider(profile, options = {}) {
85
102
  }
86
103
  return lazyLocal(options.local);
87
104
  }
88
- // Exhaustiveness guard: isValidPair above already rejects unknown pairs, so
89
- // this throw is unreachable at runtime. It exists so TypeScript errors if a
90
- // new protocol/provider is added to PROTOCOL_PROVIDERS without a branch here.
91
105
  throw new TheorumError(`createProvider: unsupported protocol/provider pair '${protocol}'/'${provider}'`);
92
106
  }
@@ -3,6 +3,14 @@ export declare function camelToSnake(key: string): string;
3
3
  export declare function toGoogleValue(value: unknown): unknown;
4
4
  export declare function wirePart(part: InteractionPart): Record<string, string>;
5
5
  export declare function userInputStep(parts: InteractionPart[]): Record<string, unknown>;
6
+ /**
7
+ * Map one host history message to Interactions input step(s).
8
+ *
9
+ * OpenAI-shaped assistant `tool_calls` (often with no `content`) become
10
+ * `function_call` steps — never empty `model_output` text.
11
+ */
12
+ export declare function historySteps(msg: TurnHistoryMessage): Record<string, unknown>[];
13
+ /** Single-step helper for simple messages (first of {@link historySteps}). */
6
14
  export declare function historyStep(msg: TurnHistoryMessage): Record<string, unknown>;
7
15
  export declare function systemHoldsUserInput(system: string, parts: InteractionPart[]): boolean;
8
16
  export declare function jsonResponseFormat(schema: Record<string, unknown>): unknown[];
@@ -1,4 +1,5 @@
1
1
  import { TheorumError } from '../../../guardrails/error.js';
2
+ import { wireInteractionPart } from '../../../kernel/interaction-parts.js';
2
3
  import { getStructured } from '../../../kernel/registry/schemas.js';
3
4
  import { getTool } from '../../../kernel/tools/registry.js';
4
5
  export function camelToSnake(key) {
@@ -24,35 +25,91 @@ export function toGoogleValue(value) {
24
25
  return value;
25
26
  }
26
27
  export function wirePart(part) {
27
- if (part.type === 'text') {
28
- return { type: 'text', text: part.text };
29
- }
30
- return { type: part.type, mimeType: part.mimeType, data: part.data };
28
+ return wireInteractionPart(part);
31
29
  }
32
30
  const USER_INPUT = 'user_input';
33
31
  export function userInputStep(parts) {
34
32
  return { type: USER_INPUT, content: parts.map(wirePart) };
35
33
  }
36
34
  function functionResultStep(msg) {
35
+ const result = msg.parts && msg.parts.length > 0
36
+ ? msg.parts.map(wirePart)
37
+ : [{ type: 'text', text: msg.content ?? '' }];
37
38
  return {
38
39
  type: 'function_result',
39
40
  name: msg.name ?? '',
40
41
  call_id: msg.tool_call_id ?? '',
41
- result: [{ type: 'text', text: msg.content ?? '' }],
42
+ result,
42
43
  };
43
44
  }
44
- export function historyStep(msg) {
45
- if (msg.role === 'tool') {
46
- return functionResultStep(msg);
45
+ /** Parse OpenAI-style tool-call `arguments` JSON into an Interactions object. */
46
+ function functionCallArguments(raw) {
47
+ const trimmed = raw.trim();
48
+ if (!trimmed)
49
+ return {};
50
+ try {
51
+ const parsed = JSON.parse(trimmed);
52
+ if (parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)) {
53
+ return parsed;
54
+ }
55
+ return { value: parsed };
56
+ }
57
+ catch {
58
+ return { value: raw };
47
59
  }
48
- const isAssistant = msg.role === 'assistant';
60
+ }
61
+ function functionCallStep(call) {
62
+ const step = {
63
+ type: 'function_call',
64
+ id: call.id,
65
+ name: call.function.name,
66
+ arguments: functionCallArguments(call.function.arguments),
67
+ };
68
+ if (call.thoughtSignature) {
69
+ step.thoughtSignature = call.thoughtSignature;
70
+ }
71
+ return step;
72
+ }
73
+ function textOrPartsStep(role, msg) {
49
74
  // Google Interactions input steps: assistant history is `model_output` (not `model_turn`).
50
- const type = isAssistant ? 'model_output' : 'user_input';
75
+ const type = role === 'assistant' ? 'model_output' : 'user_input';
51
76
  if (msg.parts && msg.parts.length > 0) {
52
77
  return { type, content: msg.parts.map(wirePart) };
53
78
  }
54
79
  return { type, content: [{ type: 'text', text: msg.content ?? '' }] };
55
80
  }
81
+ /**
82
+ * Map one host history message to Interactions input step(s).
83
+ *
84
+ * OpenAI-shaped assistant `tool_calls` (often with no `content`) become
85
+ * `function_call` steps — never empty `model_output` text.
86
+ */
87
+ export function historySteps(msg) {
88
+ if (msg.role === 'tool') {
89
+ return [functionResultStep(msg)];
90
+ }
91
+ if (msg.role === 'assistant' && msg.tool_calls && msg.tool_calls.length > 0) {
92
+ const steps = [];
93
+ const hasParts = Boolean(msg.parts && msg.parts.length > 0);
94
+ const hasText = Boolean(msg.content?.trim());
95
+ if (hasParts || hasText) {
96
+ steps.push(textOrPartsStep('assistant', msg));
97
+ }
98
+ for (const call of msg.tool_calls) {
99
+ steps.push(functionCallStep(call));
100
+ }
101
+ return steps;
102
+ }
103
+ if (msg.role === 'assistant') {
104
+ return [textOrPartsStep('assistant', msg)];
105
+ }
106
+ return [textOrPartsStep('user', msg)];
107
+ }
108
+ /** Single-step helper for simple messages (first of {@link historySteps}). */
109
+ export function historyStep(msg) {
110
+ const steps = historySteps(msg);
111
+ return steps[0] ?? { type: 'user_input', content: [{ type: 'text', text: '' }] };
112
+ }
56
113
  export function systemHoldsUserInput(system, parts) {
57
114
  for (const part of parts) {
58
115
  if (part.type === 'text' && part.text && system.includes(part.text)) {
@@ -154,7 +211,7 @@ export function inputStepsFromRequest(req) {
154
211
  }
155
212
  const inputSteps = [];
156
213
  for (const h of req.history ?? []) {
157
- inputSteps.push(historyStep(h));
214
+ inputSteps.push(...historySteps(h));
158
215
  }
159
216
  if (req.input.length > 0 || inputSteps.length === 0) {
160
217
  inputSteps.push(userInputStep(req.input));
@@ -189,8 +246,12 @@ export function baseInteractionsBody(req) {
189
246
  attachSpeechConfig(req, generationConfig);
190
247
  }
191
248
  else {
192
- generationConfig.thinkingLevel = req.thinking;
193
- generationConfig.thinkingSummaries = req.summaries;
249
+ if (req.thinking) {
250
+ generationConfig.thinkingLevel = req.thinking;
251
+ }
252
+ if (req.summaries) {
253
+ generationConfig.thinkingSummaries = req.summaries;
254
+ }
194
255
  }
195
256
  return {
196
257
  model: req.apiId,
@@ -16,7 +16,7 @@ export declare function buildGeminiLiveSetupMessage(req: ProviderCompleteRequest
16
16
  /** Build the `clientContent` message used for seeding conversation history before realtime streaming. */
17
17
  export declare function buildGeminiLiveClientContent(history: TurnHistoryMessage[]): Record<string, unknown> | null;
18
18
  /** Build a `realtimeInput` message for streaming audio, video, or text chunks. */
19
- export declare function buildGeminiLiveRealtimeInput(part: InteractionPart): Record<string, unknown>;
19
+ export declare function buildGeminiLiveRealtimeInput(input: InteractionPart): Record<string, unknown>;
20
20
  /** Build a `realtimeInput` message with text. */
21
21
  export declare function buildGeminiLiveRealtimeText(text: string): Record<string, unknown>;
22
22
  /** Build a `toolResponse` message returning the execution result of a tool call. */
@@ -37,9 +37,12 @@ export type ParsedLiveMessage = {
37
37
  };
38
38
  export declare function parseGeminiLiveMessage(raw: unknown): ParsedLiveMessage;
39
39
  export declare function parseFunctionArguments(raw: unknown): ReturnType<typeof parseToolArgumentsObject>;
40
- export declare function extractUsageTokens(metadata: Record<string, unknown>): TurnTokens | undefined;
40
+ export declare function extractLiveUsageTokens(metadata: Record<string, unknown>): TurnTokens | undefined;
41
41
  /** Parse Gemini goAway.timeLeft (seconds number, "10s", duration string) → ms when known. */
42
42
  export declare function parseGoAwayTimeLeftMs(timeLeft: unknown): number | undefined;
43
+ export type LiveInteractionStatus = 'IN_PROGRESS' | 'IDLE';
44
+ /** Read the server-side `interactionStatus` when the message carries one. */
45
+ export declare function readLiveInteractionStatus(message: Record<string, unknown>): LiveInteractionStatus | undefined;
43
46
  /**
44
47
  * Fold a raw `BidiGenerateContentServerMessage` into normalized `TurnEvent` items.
45
48
  */
@@ -5,8 +5,10 @@
5
5
  *
6
6
  * @module
7
7
  */
8
- import { getTool } from '../../../kernel/tools/registry.js';
8
+ import { TheorumError } from '../../../guardrails/error.js';
9
9
  import { groundingFromEvent } from '../../../kernel/engine/delta.js';
10
+ import { isMediaRefPart } from '../../../kernel/interaction-parts.js';
11
+ import { getTool } from '../../../kernel/tools/registry.js';
10
12
  import { base64ToBytes, bytesToBase64, wrapPcmAsWav } from '../../shared/pcm.js';
11
13
  import { parseToolArgumentsObject } from '../../shared/tool-args.js';
12
14
  import { GEMINI_LIVE_WS_URL } from '../urls.js';
@@ -15,11 +17,18 @@ import { toGeminiOpenApiSchema } from './openapi-schema.js';
15
17
  export function buildGeminiLiveWebSocketUrl(apiKey) {
16
18
  return `${GEMINI_LIVE_WS_URL}?key=${encodeURIComponent(apiKey)}`;
17
19
  }
20
+ /**
21
+ * Live tools are always declared non-blocking: the kernel session already runs
22
+ * tool execution asynchronously and honours `toolCallCancellation`, so the model
23
+ * is free to keep speaking while a call is in flight.
24
+ */
25
+ const LIVE_FUNCTION_BEHAVIOR = 'NON_BLOCKING';
18
26
  export function wireFunctionDeclaration(decl) {
19
27
  const parameters = toGeminiOpenApiSchema(decl.parameters);
20
28
  return {
21
29
  name: decl.name,
22
30
  description: decl.description,
31
+ behavior: LIVE_FUNCTION_BEHAVIOR,
23
32
  parameters: parameters && typeof parameters === 'object'
24
33
  ? parameters
25
34
  : { type: 'OBJECT', properties: {} },
@@ -33,6 +42,7 @@ export function wireLiveTools(req) {
33
42
  functionDeclarations.push({
34
43
  name: id,
35
44
  description: entry.description,
45
+ behavior: LIVE_FUNCTION_BEHAVIOR,
36
46
  });
37
47
  }
38
48
  }
@@ -146,6 +156,17 @@ export function buildGeminiLiveSetupMessage(req) {
146
156
  applyLiveOptionalFeatures(live, setup);
147
157
  return { setup };
148
158
  }
159
+ /** Live carries inline bytes only — provider file references are rejected until support is verified. */
160
+ function inlineMediaPart(part) {
161
+ if (isMediaRefPart(part)) {
162
+ throw new TheorumError('media references are not supported on geminiLive');
163
+ }
164
+ return part;
165
+ }
166
+ function inlineData(part) {
167
+ const inline = inlineMediaPart(part);
168
+ return { mimeType: inline.mimeType, data: inline.data };
169
+ }
149
170
  /** Format a single history message into a Google turn object. */
150
171
  function historyTurnToGoogleTurn(msg) {
151
172
  const role = msg.role === 'assistant' ? 'model' : 'user';
@@ -158,12 +179,7 @@ function historyTurnToGoogleTurn(msg) {
158
179
  parts.push({ text: part.text });
159
180
  }
160
181
  else {
161
- parts.push({
162
- inlineData: {
163
- mimeType: part.mimeType,
164
- data: part.data,
165
- },
166
- });
182
+ parts.push({ inlineData: inlineData(part) });
167
183
  }
168
184
  }
169
185
  return { role, parts };
@@ -181,14 +197,15 @@ export function buildGeminiLiveClientContent(history) {
181
197
  };
182
198
  }
183
199
  /** Build a `realtimeInput` message for streaming audio, video, or text chunks. */
184
- export function buildGeminiLiveRealtimeInput(part) {
185
- if (part.type === 'text') {
200
+ export function buildGeminiLiveRealtimeInput(input) {
201
+ if (input.type === 'text') {
186
202
  return {
187
203
  realtimeInput: {
188
- text: part.text,
204
+ text: input.text,
189
205
  },
190
206
  };
191
207
  }
208
+ const part = inlineMediaPart(input);
192
209
  if (part.type === 'audio') {
193
210
  return {
194
211
  realtimeInput: {
@@ -271,7 +288,7 @@ function readTokenCount(metadata, camelKey, snakeKey) {
271
288
  const val = metadata[camelKey] ?? metadata[snakeKey];
272
289
  return typeof val === 'number' ? val : 0;
273
290
  }
274
- export function extractUsageTokens(metadata) {
291
+ export function extractLiveUsageTokens(metadata) {
275
292
  const prompt = readTokenCount(metadata, 'promptTokenCount', 'prompt_token_count');
276
293
  const output = readTokenCount(metadata, 'responseTokenCount', 'response_token_count');
277
294
  const thinking = readTokenCount(metadata, 'thoughtsTokenCount', 'thoughts_token_count');
@@ -454,6 +471,22 @@ function foldLiveGrounding(serverContent, events) {
454
471
  });
455
472
  }
456
473
  }
474
+ /** Read the server-side `interactionStatus` when the message carries one. */
475
+ export function readLiveInteractionStatus(message) {
476
+ const raw = message.interactionStatus ?? message.interaction_status;
477
+ if (raw === 'IN_PROGRESS' || raw === 'IDLE')
478
+ return raw;
479
+ return undefined;
480
+ }
481
+ function foldInteractionStatus(message, events) {
482
+ const status = readLiveInteractionStatus(message);
483
+ if (!status)
484
+ return;
485
+ events.push({
486
+ type: 'session',
487
+ session: { kind: status === 'IDLE' ? 'idle' : 'working' },
488
+ });
489
+ }
457
490
  function foldServerContent(message, events) {
458
491
  const serverContent = message.serverContent;
459
492
  if (!serverContent || typeof serverContent !== 'object')
@@ -488,11 +521,14 @@ function foldServerContent(message, events) {
488
521
  foldModelPart(part, events);
489
522
  }
490
523
  foldLiveGrounding(serverContent, events);
524
+ if (serverContent.turnComplete === true || serverContent.turn_complete === true) {
525
+ events.push({ type: 'session', session: { kind: 'turn_complete' } });
526
+ }
491
527
  }
492
528
  function foldUsageMetadata(message, events) {
493
529
  const usageMetadata = message.usageMetadata;
494
530
  if (usageMetadata) {
495
- const tokens = extractUsageTokens(usageMetadata);
531
+ const tokens = extractLiveUsageTokens(usageMetadata);
496
532
  if (tokens) {
497
533
  events.push({ type: 'tokens', tokens });
498
534
  }
@@ -510,6 +546,7 @@ export function foldGeminiLiveServerMessage(message) {
510
546
  foldToolCalls(message, events);
511
547
  foldToolCancellations(message, events);
512
548
  foldServerContent(message, events);
549
+ foldInteractionStatus(message, events);
513
550
  foldUsageMetadata(message, events);
514
551
  return events;
515
552
  }
@@ -12,8 +12,9 @@ function convertGeminiSchemaType(value) {
12
12
  const converted = {};
13
13
  if (schemaTypes.includes('null'))
14
14
  converted.nullable = true;
15
- if (nonNullTypes.length === 1)
16
- converted.type = nonNullTypes[0].toUpperCase();
15
+ const [onlyType] = nonNullTypes;
16
+ if (onlyType !== undefined && nonNullTypes.length === 1)
17
+ converted.type = onlyType.toUpperCase();
17
18
  if (nonNullTypes.length > 1) {
18
19
  converted.anyOf = nonNullTypes.map((item) => ({ type: item.toUpperCase() }));
19
20
  }
@@ -32,5 +32,14 @@ export interface LiveQueue {
32
32
  isClosed: () => boolean;
33
33
  }
34
34
  export declare function createLiveQueue(): LiveQueue;
35
+ /**
36
+ * Conversational cycle boundary.
37
+ *
38
+ * When the server reports `interactionStatus`, that is authoritative: `IDLE`
39
+ * closes the cycle and `IN_PROGRESS` keeps it open even across `turnComplete`
40
+ * (background reasoning / async tools may still produce output). Without the
41
+ * field, `turnComplete` is the boundary as before.
42
+ */
43
+ export declare function turnPhaseFromMessage(message: Record<string, unknown>, events: TurnEvent[]): LiveTurnPhase;
35
44
  /** Attach handlers that keep the socket open across conversational turns. */
36
45
  export declare function attachLiveSessionHandlers(ws: WebSocket, liveQueue: LiveQueue): void;