ai-runtime-engine 1.2.0 → 2.7.0

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 (153) hide show
  1. package/CHANGELOG.md +602 -0
  2. package/dist/agents/admit.d.ts +69 -0
  3. package/dist/agents/admit.js +129 -0
  4. package/dist/agents/definition.d.ts +36 -0
  5. package/dist/agents/definition.js +9 -0
  6. package/dist/agents/envelope.d.ts +53 -0
  7. package/dist/agents/envelope.js +68 -0
  8. package/dist/agents/finding.d.ts +79 -0
  9. package/dist/agents/finding.js +80 -0
  10. package/dist/agents/task.d.ts +60 -0
  11. package/dist/agents/task.js +32 -0
  12. package/dist/agents/worker.d.ts +68 -0
  13. package/dist/agents/worker.js +256 -0
  14. package/dist/capabilities/capability.d.ts +117 -0
  15. package/dist/capabilities/capability.js +66 -0
  16. package/dist/capabilities/registry.d.ts +139 -0
  17. package/dist/capabilities/registry.js +413 -0
  18. package/dist/capabilities/vocabulary.d.ts +32 -0
  19. package/dist/capabilities/vocabulary.js +34 -0
  20. package/dist/cli/cli.js +56 -4
  21. package/dist/cli/commands/cleanup.js +29 -27
  22. package/dist/cli/commands/doctor.d.ts +14 -0
  23. package/dist/cli/commands/doctor.js +38 -8
  24. package/dist/cli/commands/executions.js +34 -25
  25. package/dist/cli/commands/info.d.ts +1 -0
  26. package/dist/cli/commands/info.js +11 -9
  27. package/dist/cli/commands/init.js +19 -0
  28. package/dist/cli/commands/inspect.d.ts +40 -1
  29. package/dist/cli/commands/inspect.js +157 -2
  30. package/dist/cli/commands/mcp.d.ts +45 -0
  31. package/dist/cli/commands/mcp.js +148 -0
  32. package/dist/cli/commands/route.js +21 -0
  33. package/dist/cli/commands/run.d.ts +2 -0
  34. package/dist/cli/commands/run.js +36 -4
  35. package/dist/cli/commands/skills.d.ts +2 -0
  36. package/dist/cli/commands/skills.js +29 -7
  37. package/dist/cli/interactive/ansi.d.ts +41 -0
  38. package/dist/cli/interactive/ansi.js +43 -0
  39. package/dist/cli/interactive/complete.d.ts +10 -0
  40. package/dist/cli/interactive/complete.js +19 -0
  41. package/dist/cli/interactive/repl.d.ts +3 -0
  42. package/dist/cli/interactive/repl.js +105 -16
  43. package/dist/cli/interactive/session.d.ts +12 -1
  44. package/dist/cli/interactive/session.js +83 -5
  45. package/dist/cli/render.d.ts +13 -0
  46. package/dist/cli/render.js +18 -0
  47. package/dist/cli/runtimeSession.d.ts +11 -0
  48. package/dist/cli/runtimeSession.js +17 -0
  49. package/dist/config/defaults.d.ts +3 -1
  50. package/dist/config/defaults.js +2 -0
  51. package/dist/config/schema.d.ts +1 -0
  52. package/dist/config/schema.js +2 -2
  53. package/dist/context/lossVerifier.d.ts +24 -0
  54. package/dist/context/lossVerifier.js +45 -0
  55. package/dist/context/summarize.d.ts +19 -0
  56. package/dist/context/summarize.js +53 -0
  57. package/dist/core/fallback/fallback.d.ts +8 -0
  58. package/dist/core/fallback/fallback.js +3 -1
  59. package/dist/core/router/executor.d.ts +6 -1
  60. package/dist/core/router/executor.js +9 -2
  61. package/dist/core/router/normalize.d.ts +2 -0
  62. package/dist/core/router/request.js +2 -0
  63. package/dist/core/router/router.d.ts +3 -0
  64. package/dist/core/router/router.js +7 -0
  65. package/dist/executions/execution.d.ts +13 -2
  66. package/dist/generation/generateAdapter.d.ts +14 -0
  67. package/dist/generation/generateAdapter.js +38 -0
  68. package/dist/generation/generateSkill.d.ts +26 -0
  69. package/dist/generation/generateSkill.js +51 -0
  70. package/dist/index.d.ts +44 -5
  71. package/dist/index.js +26 -2
  72. package/dist/mcp/client.d.ts +70 -0
  73. package/dist/mcp/client.js +221 -0
  74. package/dist/mcp/manager.d.ts +151 -0
  75. package/dist/mcp/manager.js +493 -0
  76. package/dist/mcp/protocol.d.ts +216 -0
  77. package/dist/mcp/protocol.js +149 -0
  78. package/dist/mcp/toolAdapter.d.ts +44 -0
  79. package/dist/mcp/toolAdapter.js +94 -0
  80. package/dist/mcp/transport.d.ts +109 -0
  81. package/dist/mcp/transport.js +383 -0
  82. package/dist/memory/embedders/hash.d.ts +12 -0
  83. package/dist/memory/embedders/hash.js +31 -0
  84. package/dist/memory/embedders/http.d.ts +25 -0
  85. package/dist/memory/embedders/http.js +48 -0
  86. package/dist/memory/memory.d.ts +19 -2
  87. package/dist/memory/memory.js +75 -11
  88. package/dist/memory/semantic.d.ts +17 -0
  89. package/dist/memory/semantic.js +29 -0
  90. package/dist/orchestration/budget.d.ts +30 -0
  91. package/dist/orchestration/budget.js +40 -0
  92. package/dist/orchestration/executor.d.ts +39 -1
  93. package/dist/orchestration/executor.js +64 -4
  94. package/dist/orchestration/orchestrator.d.ts +29 -1
  95. package/dist/orchestration/orchestrator.js +89 -8
  96. package/dist/orchestration/plan.d.ts +15 -1
  97. package/dist/orchestration/plan.js +23 -4
  98. package/dist/orchestration/planner.d.ts +19 -1
  99. package/dist/orchestration/planner.js +25 -5
  100. package/dist/plugin/ai.d.ts +4 -0
  101. package/dist/plugin/ai.js +9 -0
  102. package/dist/providers/httpClient.d.ts +25 -1
  103. package/dist/providers/httpClient.js +93 -0
  104. package/dist/providers/httpProvider.d.ts +1 -0
  105. package/dist/providers/httpProvider.js +67 -1
  106. package/dist/providers/mock/mockProvider.d.ts +3 -0
  107. package/dist/providers/mock/mockProvider.js +54 -0
  108. package/dist/providers/mock/scenarios.d.ts +7 -0
  109. package/dist/providers/provider.d.ts +6 -0
  110. package/dist/providers/wire/anthropicWire.js +34 -0
  111. package/dist/providers/wire/openaiWire.js +30 -0
  112. package/dist/providers/wire/types.d.ts +16 -0
  113. package/dist/runtime/config.js +50 -6
  114. package/dist/runtime/intent/aiClassifier.d.ts +19 -0
  115. package/dist/runtime/intent/aiClassifier.js +74 -0
  116. package/dist/runtime/models/modelProfile.d.ts +61 -0
  117. package/dist/runtime/models/modelProfile.js +139 -0
  118. package/dist/runtime/planning/deriveCapabilities.d.ts +95 -0
  119. package/dist/runtime/planning/deriveCapabilities.js +146 -0
  120. package/dist/runtime/policy.d.ts +10 -0
  121. package/dist/runtime/policy.js +9 -2
  122. package/dist/runtime/runtime.d.ts +173 -0
  123. package/dist/runtime/runtime.js +723 -50
  124. package/dist/runtime/types.d.ts +94 -2
  125. package/dist/skills/manifest.d.ts +3 -0
  126. package/dist/skills/manifest.js +24 -0
  127. package/dist/skills/registry.d.ts +16 -1
  128. package/dist/skills/registry.js +21 -1
  129. package/dist/skills/skill.d.ts +6 -1
  130. package/dist/store/area.d.ts +15 -1
  131. package/dist/store/area.js +19 -8
  132. package/dist/store/crypto.d.ts +21 -0
  133. package/dist/store/crypto.js +49 -0
  134. package/dist/store/paths.d.ts +5 -1
  135. package/dist/store/paths.js +6 -0
  136. package/dist/store/store.d.ts +15 -3
  137. package/dist/store/store.js +28 -7
  138. package/dist/telemetry/sinks/otlp.d.ts +31 -0
  139. package/dist/telemetry/sinks/otlp.js +76 -0
  140. package/dist/tools/builtins/filesystem.js +1 -0
  141. package/dist/tools/builtins/git.js +1 -0
  142. package/dist/tools/builtins/shell.js +1 -0
  143. package/dist/tools/permissions.d.ts +28 -0
  144. package/dist/tools/permissions.js +72 -0
  145. package/dist/tools/registry.d.ts +18 -2
  146. package/dist/tools/registry.js +22 -2
  147. package/dist/tools/tool.d.ts +4 -0
  148. package/dist/types.d.ts +11 -1
  149. package/dist/util/flatten.d.ts +11 -0
  150. package/dist/util/flatten.js +18 -0
  151. package/dist/util/semaphore.d.ts +19 -0
  152. package/dist/util/semaphore.js +60 -0
  153. package/package.json +24 -9
@@ -9,6 +9,16 @@ import { SETUP_CANDIDATES } from '../commands/setup.js';
9
9
  import { gatherInfo, renderInfo } from '../commands/info.js';
10
10
  import { gatherDoctor, renderDoctor } from '../commands/doctor.js';
11
11
  import { planCleanup } from '../commands/cleanup.js';
12
+ import { actionCapabilityRows, renderActionCapabilities, renderCapabilityGaps } from '../commands/inspect.js';
13
+ import { renderMcpList, renderMcpDetail } from '../commands/mcp.js';
14
+ import { candidatesFrom, deriveCapabilitiesOffline } from '../../runtime/planning/deriveCapabilities.js';
15
+ import { displaySafe } from '../render.js';
16
+ /** Top-level slash commands, for REPL tab-completion (Phase 21b). Kept in sync with the `handle` dispatch. */
17
+ export const SLASH_COMMANDS = [
18
+ 'help', 'status', 'info', 'doctor', 'cleanup', 'mode', 'compare', 'models', 'config', 'providers', 'tools', 'capabilities', 'mcp',
19
+ 'skills', 'memory', 'conversations', 'executions', 'resume', 'resume-execution', 'pause', 'cancel', 'approve',
20
+ 'deny', 'learning', 'feedback', 'permissions', 'budget', 'stream', 'dry-run', 'clear', 'exit', 'quit',
21
+ ];
12
22
  const HELP = [
13
23
  'Commands:',
14
24
  ' /help show this help',
@@ -24,6 +34,10 @@ const HELP = [
24
34
  ' /providers/<id> [models|capabilities|health] inspect one provider',
25
35
  ' /models list all known models',
26
36
  ' /tools list registered tools',
37
+ ' /capabilities list action capabilities (what the runtime can DO) + providers',
38
+ ' /capabilities <goal> which capabilities a goal needs, and what is missing (offline)',
39
+ ' /mcp list MCP servers (state, tools, grant)',
40
+ ' /mcp/<id> inspect one MCP server (protocol, tools, diagnostics)',
27
41
  ' /skills [discover] list available skills; discover scans the workspace',
28
42
  ' /permissions show the current tool permissions',
29
43
  ' /feedback <text> record feedback on the last result (e.g. "that worked")',
@@ -42,6 +56,8 @@ const HELP = [
42
56
  ' /cancel <id> cancel an execution',
43
57
  ' /config show the resolved configuration',
44
58
  ' /dry-run toggle dry-run (plan only, no changes)',
59
+ ' /stream toggle token-by-token streaming of answers',
60
+ ' /budget show the call/cost budget (AI_MAX_CALLS / AI_MAX_COST_USD)',
45
61
  ' /clear clear the screen',
46
62
  ' /exit leave the session',
47
63
  '',
@@ -53,8 +69,10 @@ export class ReplSession {
53
69
  viewCache;
54
70
  conversationId;
55
71
  dryRunMode = false;
56
- constructor(runtime) {
72
+ streaming;
73
+ constructor(runtime, opts = {}) {
57
74
  this.runtime = runtime;
75
+ this.streaming = opts.streaming ?? false;
58
76
  }
59
77
  currentMode() {
60
78
  return this.mode;
@@ -64,6 +82,27 @@ export class ReplSession {
64
82
  this.viewCache = new ProviderViewCache(this.runtime.ai);
65
83
  return this.viewCache;
66
84
  }
85
+ /**
86
+ * MCP servers in the REPL. The session's Runtime is already connected (the REPL connects eagerly at
87
+ * start), so this NEVER opens a second Runtime — that would spawn duplicate stdio children and give
88
+ * `close()` the wrong one to tear down. It reuses the exact formatters the `mcp` command prints.
89
+ */
90
+ mcpView(target) {
91
+ if (!this.runtime.hasMcpServers())
92
+ return { lines: renderMcpList({}, []) };
93
+ const grants = this.runtime.permissions().mcp.servers;
94
+ if (!target) {
95
+ const lines = renderMcpList(grants, this.runtime.mcp().list());
96
+ const warnings = this.runtime.mcpWarningsList();
97
+ if (warnings.length)
98
+ lines.push('', 'Warnings:', ...warnings.map((w) => ` ! ${w}`));
99
+ return { lines };
100
+ }
101
+ const status = this.runtime.mcp().status(target);
102
+ if (!status)
103
+ return { lines: [`no MCP server '${displaySafe(target, 40)}' — /mcp lists them`] };
104
+ return { lines: renderMcpDetail(grants, status, this.runtime.mcp().diagnostics(target), this.runtime.mcp().tools(target)) };
105
+ }
67
106
  async handle(raw) {
68
107
  const line = raw.trim();
69
108
  if (!line)
@@ -106,6 +145,24 @@ export class ReplSession {
106
145
  return this.config();
107
146
  case 'providers':
108
147
  return this.providers(parts[1], args[0]);
148
+ case 'capabilities': {
149
+ // `capabilities --actions` connects (inspect.ts); without this the same question reported
150
+ // different MCP availability on the two surfaces. connectMcp is idempotent.
151
+ if (this.runtime.hasMcpServers())
152
+ await this.runtime.connectMcp();
153
+ // Both forms, and never a truncated goal: `/capabilities run the tests` and
154
+ // `/capabilities/run the tests` must mean the same thing.
155
+ const goal = [...parts.slice(1), ...args].join(' ').trim();
156
+ if (!goal)
157
+ return { lines: renderActionCapabilities(actionCapabilityRows(this.runtime)) };
158
+ const required = deriveCapabilitiesOffline(goal, candidatesFrom(this.runtime.capabilities().list()));
159
+ if (!required.length)
160
+ return { lines: [`no action capability matched "${displaySafe(goal, 80)}" — /capabilities lists everything this runtime can do.`] };
161
+ return { lines: renderCapabilityGaps(this.runtime.capabilityReport(required)) };
162
+ }
163
+ case 'mcp':
164
+ // `/mcp/<id>` and `/mcp <id>` both address one server.
165
+ return this.mcpView(parts[1] ?? args[0]);
109
166
  case 'tools':
110
167
  return { lines: ['Tools:', ...this.runtime.tools().map((t) => ` ${t.id.padEnd(14)} ${t.description}`)] };
111
168
  case 'skills': {
@@ -133,6 +190,17 @@ export class ReplSession {
133
190
  case 'dryrun':
134
191
  this.dryRunMode = !this.dryRunMode;
135
192
  return { lines: [`dry-run ${this.dryRunMode ? 'ON — plans will be shown, nothing executed' : 'OFF'}`] };
193
+ case 'stream':
194
+ this.streaming = !this.streaming;
195
+ return { lines: [`streaming ${this.streaming ? 'ON — answers render token-by-token' : 'OFF'}`] };
196
+ case 'budget':
197
+ return {
198
+ lines: [
199
+ `call budget (AI_MAX_CALLS): ${process.env.AI_MAX_CALLS ?? '(unset — no limit)'}`,
200
+ `cost budget (AI_MAX_COST_USD): ${process.env.AI_MAX_COST_USD ?? '(unset — no limit)'}`,
201
+ 'over budget: notify-and-wait by default; add --partial (one-shot) to run the phases that fit and pause.',
202
+ ],
203
+ };
136
204
  case 'memory':
137
205
  return this.memory(args);
138
206
  case 'conversations':
@@ -161,9 +229,10 @@ export class ReplSession {
161
229
  this.conversationId = this.runtime.conversations.start();
162
230
  this.runtime.conversations.append(this.conversationId, 'user', input);
163
231
  }
164
- const result = await this.runtime.run({ input, mode: forceMode ?? this.mode, ...(this.dryRunMode ? { dryRun: true } : {}) });
232
+ const result = await this.runtime.run({ input, mode: forceMode ?? this.mode, ...(this.dryRunMode ? { dryRun: true } : {}), ...(this.streaming ? { stream: true } : {}) });
165
233
  const lines = [];
166
- if (result.response?.text)
234
+ // When the answer already streamed live (response.streamed), don't reprint it.
235
+ if (result.response?.text && !result.response.streamed)
167
236
  lines.push(result.response.text);
168
237
  else if (result.response?.json !== undefined)
169
238
  lines.push(JSON.stringify(result.response.json, null, 2));
@@ -175,17 +244,25 @@ export class ReplSession {
175
244
  const execId = result.execution?.id;
176
245
  lines.push(execId ? `(approval required — /approve ${execId} to proceed, or /deny ${execId})` : '(approval required — approve the execution via /executions then /approve <id>)');
177
246
  }
247
+ if (result.status === 'waiting_for_budget') {
248
+ const execId = result.execution?.id;
249
+ lines.push(execId ? `(over budget — raise AI_MAX_CALLS and /resume-execution ${execId}, or re-run with more budget)` : '(over budget — raise the call budget and re-run, or add --partial)');
250
+ }
178
251
  if (result.status === 'failed')
179
252
  lines.push('(did not complete)');
180
253
  if (result.clarification)
181
254
  lines.push(`? ${result.clarification.question}`);
182
255
  if (result.memory?.captured)
183
256
  lines.push('(remembered)');
257
+ // Phase 3.3: the ONE gap renderer, shared with `ai-runtime run` and both capabilities surfaces.
258
+ // Appended AFTER the existing advisory tail so every positional assertion above is untouched.
259
+ if (result.capabilityGaps)
260
+ lines.push(...renderCapabilityGaps(result.capabilityGaps));
184
261
  if (this.conversationId && result.response?.text)
185
262
  this.runtime.conversations.append(this.conversationId, 'assistant', result.response.text, result.runId);
186
263
  return { lines };
187
264
  }
188
- memory(args) {
265
+ async memory(args) {
189
266
  if (!this.runtime.memory.enabled)
190
267
  return { lines: ['memory is disabled (stateless mode).'] };
191
268
  const sub = args[0];
@@ -193,7 +270,8 @@ export class ReplSession {
193
270
  const q = args.slice(1).join(' ');
194
271
  if (!q)
195
272
  return { lines: ['usage: /memory search <query>'] };
196
- const hits = this.runtime.memory.search(q, { limit: 10 });
273
+ // Semantic ranking when an embedder is configured; degrades to BM25 internally.
274
+ const hits = await this.runtime.memory.searchSemantic(q, { limit: 10 });
197
275
  return { lines: hits.length ? hits.map((r) => ` ${r.id} [${r.scope}] ${r.text}`) : ['no matches.'] };
198
276
  }
199
277
  if (sub === 'delete') {
@@ -4,4 +4,17 @@
4
4
  * directly from a command.
5
5
  */
6
6
  export declare function print(line: string): void;
7
+ /**
8
+ * Write a streamed chunk WITHOUT a trailing newline, redacted like every other egress. (Redaction is
9
+ * per-chunk, matching the lifecycle emitter; a secret split across chunk boundaries is the known limit of
10
+ * any token stream — but model output never contains the env-var secrets the redactor tracks.)
11
+ */
12
+ export declare function printChunk(chunk: string): void;
7
13
  export declare function printError(line: string): void;
14
+ /**
15
+ * Clamp a source- or MODEL-controlled string before it reaches the terminal — the terminal twin of
16
+ * `promptSafe`. LOAD-BEARING: `print` redacts secrets but strips no control characters and bounds no
17
+ * length, and a gap's `capabilityId` originates in the model's plan JSON — `sanitizeSegment` restricts
18
+ * its charset but sets no maximum, and the `register` gap hint interpolates it.
19
+ */
20
+ export declare function displaySafe(raw: string, max?: number): string;
@@ -4,11 +4,29 @@
4
4
  * directly from a command.
5
5
  */
6
6
  import { redactString } from '../security/redact.js';
7
+ import { flattenClamp } from '../util/flatten.js';
7
8
  export function print(line) {
8
9
  // eslint-disable-next-line no-console
9
10
  console.log(redactString(line));
10
11
  }
12
+ /**
13
+ * Write a streamed chunk WITHOUT a trailing newline, redacted like every other egress. (Redaction is
14
+ * per-chunk, matching the lifecycle emitter; a secret split across chunk boundaries is the known limit of
15
+ * any token stream — but model output never contains the env-var secrets the redactor tracks.)
16
+ */
17
+ export function printChunk(chunk) {
18
+ process.stdout.write(redactString(chunk));
19
+ }
11
20
  export function printError(line) {
12
21
  // eslint-disable-next-line no-console
13
22
  console.error(redactString(line));
14
23
  }
24
+ /**
25
+ * Clamp a source- or MODEL-controlled string before it reaches the terminal — the terminal twin of
26
+ * `promptSafe`. LOAD-BEARING: `print` redacts secrets but strips no control characters and bounds no
27
+ * length, and a gap's `capabilityId` originates in the model's plan JSON — `sanitizeSegment` restricts
28
+ * its charset but sets no maximum, and the `register` gap hint interpolates it.
29
+ */
30
+ export function displaySafe(raw, max = 80) {
31
+ return flattenClamp(raw, max);
32
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * One place where a CLI command borrows a Runtime and gives it back.
3
+ *
4
+ * A `Runtime` can own OS resources — today MCP stdio child processes. A one-shot command that returns
5
+ * without releasing them does not merely leak: a live child keeps the event loop alive, so the CLI never
6
+ * exits. `withRuntime` guarantees the release on every path, including a thrown error.
7
+ */
8
+ import { Runtime } from '../runtime/runtime.js';
9
+ export declare function withRuntime<T>(opts: {
10
+ config?: string;
11
+ }, fn: (rt: Runtime) => Promise<T>): Promise<T>;
@@ -0,0 +1,17 @@
1
+ /**
2
+ * One place where a CLI command borrows a Runtime and gives it back.
3
+ *
4
+ * A `Runtime` can own OS resources — today MCP stdio child processes. A one-shot command that returns
5
+ * without releasing them does not merely leak: a live child keeps the event loop alive, so the CLI never
6
+ * exits. `withRuntime` guarantees the release on every path, including a thrown error.
7
+ */
8
+ import { Runtime } from '../runtime/runtime.js';
9
+ export async function withRuntime(opts, fn) {
10
+ const rt = await Runtime.load({ ...(opts.config ? { config: opts.config } : {}) });
11
+ try {
12
+ return await fn(rt);
13
+ }
14
+ finally {
15
+ await rt.close();
16
+ }
17
+ }
@@ -24,9 +24,11 @@ export interface ResolvedConfig {
24
24
  privacy: ResolvedPrivacy;
25
25
  telemetry: {
26
26
  enabled: boolean;
27
- sink: 'memory' | 'file';
27
+ sink: 'memory' | 'file' | 'otlp';
28
28
  storePrompts: false;
29
29
  path?: string;
30
+ endpoint?: string;
31
+ headersEnv?: string;
30
32
  };
31
33
  learning: {
32
34
  enabled: boolean;
@@ -33,6 +33,8 @@ export function resolveConfig(cfg) {
33
33
  sink: cfg.telemetry?.sink ?? 'memory',
34
34
  storePrompts: false,
35
35
  ...(cfg.telemetry?.path !== undefined ? { path: cfg.telemetry.path } : {}),
36
+ ...(cfg.telemetry?.endpoint !== undefined ? { endpoint: cfg.telemetry.endpoint } : {}),
37
+ ...(cfg.telemetry?.headersEnv !== undefined ? { headersEnv: cfg.telemetry.headersEnv } : {}),
36
38
  };
37
39
  return {
38
40
  providers: cfg.providers,
@@ -6,4 +6,5 @@
6
6
  import type { RouterConfig } from '../types.js';
7
7
  /** Canonical strategy names. Exported for reuse by the runtime-config layer (no duplication). */
8
8
  export declare const STRATEGIES: readonly ["best", "fastest", "cheapest", "highest-quality", "local-only", "cloud-only", "privacy-first", "provider-specific", "fallback-only"];
9
+ export declare const KEY_LIKE: RegExp;
9
10
  export declare function parseConfig(raw: unknown): RouterConfig;
@@ -10,7 +10,7 @@ export const STRATEGIES = ['best', 'fastest', 'cheapest', 'highest-quality', 'lo
10
10
  const EVIDENCE = ['unsupported', 'unknown', 'inferred', 'documented', 'verified'];
11
11
  const GROUPS = ['input', 'output', 'intelligence', 'agent'];
12
12
  const KINDS = ['openai-compatible', 'gemini', 'groq', 'anthropic', 'ollama', 'custom', 'mock'];
13
- const KEY_LIKE = /^(sk-|gsk_|Bearer\s|[A-Za-z0-9_-]{40,}$)/;
13
+ export const KEY_LIKE = /^(sk-|gsk_|Bearer\s|[A-Za-z0-9_-]{40,}$)/;
14
14
  const capabilityRequirement = z.object({
15
15
  group: z.enum(GROUPS),
16
16
  key: z.string().min(1),
@@ -64,7 +64,7 @@ const routerConfig = z
64
64
  .object({ allowCloud: z.boolean().optional(), allowLocal: z.boolean().optional(), sensitiveDataAllowedOnCloud: z.boolean().optional(), treatUnknownAsSensitive: z.boolean().optional(), defaultSensitivity: z.enum(['low', 'high', 'unknown']).optional() })
65
65
  .strict()
66
66
  .optional(),
67
- telemetry: z.object({ enabled: z.boolean().optional(), sink: z.enum(['memory', 'file']).optional(), storePrompts: z.literal(false).optional(), path: z.string().optional() }).strict().optional(),
67
+ telemetry: z.object({ enabled: z.boolean().optional(), sink: z.enum(['memory', 'file', 'otlp']).optional(), storePrompts: z.literal(false).optional(), path: z.string().optional(), endpoint: z.string().optional(), headersEnv: z.string().refine((v) => v === undefined || !KEY_LIKE.test(v), { message: 'headersEnv must be an env-var NAME, not a header/token value' }).optional() }).strict().optional(),
68
68
  tasks: z.array(taskDefinition).optional(),
69
69
  })
70
70
  .strict();
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Model-based context-loss verification (Phase 16) — an OPT-IN rung ON TOP OF the deterministic
3
+ * `validate()` in the compiler. After the sync `compileContext` runs, this asks a model "does the compiled
4
+ * context drop anything task-critical that was in the originals?" and returns a single check that the caller
5
+ * APPENDS to `validation.checks`. It NEVER blocks: any budget/parse/model problem yields an `ok:true`
6
+ * `inconclusive` check, and the deterministic `validation.ok` is untouched. Both the originals and the
7
+ * compiled text are `wrapUntrusted`-fenced — they are data to inspect, never instructions.
8
+ */
9
+ import type { AI } from '../plugin/ai.js';
10
+ import type { ContextBlock, CompiledContext } from './compiler.js';
11
+ export interface VerifyLossInput {
12
+ ai: AI;
13
+ /** The ORIGINAL blocks handed to the compiler (pre-compression). */
14
+ blocks: ContextBlock[];
15
+ /** The compiler's output (its `system` string is what will actually be sent). */
16
+ compiled: CompiledContext;
17
+ }
18
+ /** The check appended to `ContextValidation.checks`. `ok:false` means the model flagged task-critical loss. */
19
+ export type LossCheck = {
20
+ name: 'model-loss-verify';
21
+ ok: boolean;
22
+ why?: string;
23
+ };
24
+ export declare function verifyContextLoss(input: VerifyLossInput): Promise<LossCheck>;
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Model-based context-loss verification (Phase 16) — an OPT-IN rung ON TOP OF the deterministic
3
+ * `validate()` in the compiler. After the sync `compileContext` runs, this asks a model "does the compiled
4
+ * context drop anything task-critical that was in the originals?" and returns a single check that the caller
5
+ * APPENDS to `validation.checks`. It NEVER blocks: any budget/parse/model problem yields an `ok:true`
6
+ * `inconclusive` check, and the deterministic `validation.ok` is untouched. Both the originals and the
7
+ * compiled text are `wrapUntrusted`-fenced — they are data to inspect, never instructions.
8
+ */
9
+ import { wrapUntrusted } from '../tools/untrusted.js';
10
+ import { extractJson } from '../util/extractJson.js';
11
+ export async function verifyContextLoss(input) {
12
+ const inconclusive = (why) => ({ name: 'model-loss-verify', ok: true, why: `inconclusive: ${why}` });
13
+ try {
14
+ // Only critical/high blocks carry task-critical information worth verifying.
15
+ const important = input.blocks.filter((b) => b.retention === 'critical' || b.retention === 'high');
16
+ if (important.length === 0)
17
+ return inconclusive('no task-critical blocks to verify');
18
+ const originals = important.map((b) => `[${b.retention}:${b.id}] ${b.resolveContent ? b.resolveContent() : b.text}`).join('\n');
19
+ const prompt = `You verify whether a COMPILED model context dropped anything task-critical from the ORIGINAL blocks.\n\n` +
20
+ `ORIGINAL task-critical blocks:\n${wrapUntrusted('context-originals', originals)}\n\n` +
21
+ `COMPILED context that will be sent:\n${wrapUntrusted('context-compiled', input.compiled.system)}\n\n` +
22
+ `Reply ONLY with JSON: {"lost": true|false, "items": ["<short phrase>", ...]}. Set "lost" true ONLY if ` +
23
+ `something task-critical present in the originals is ABSENT or corrupted in the compiled context.`;
24
+ const result = await input.ai.run({
25
+ task: 'chat',
26
+ input: { text: prompt },
27
+ system: 'You are a context-loss verifier. Respond only with the requested JSON.',
28
+ output: { format: 'json' },
29
+ requirements: { intelligence: ['reasoning'], output: ['json'] },
30
+ });
31
+ if (!result.ok || !result.response)
32
+ return inconclusive('verifier run did not complete');
33
+ const verdict = (result.response.json ?? extractJson(result.response.text ?? ''));
34
+ if (typeof verdict?.lost !== 'boolean')
35
+ return inconclusive('verifier response was not parseable JSON');
36
+ if (!verdict.lost)
37
+ return { name: 'model-loss-verify', ok: true, why: 'model found no task-critical loss' };
38
+ const items = Array.isArray(verdict.items) ? verdict.items.filter((x) => typeof x === 'string').slice(0, 5) : [];
39
+ return { name: 'model-loss-verify', ok: false, why: items.length ? `model flagged omissions: ${items.join('; ')}` : 'model flagged task-critical loss' };
40
+ }
41
+ catch {
42
+ // A verifier problem must never break a run — report it as inconclusive, leave validation.ok alone.
43
+ return inconclusive('verifier error');
44
+ }
45
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Opt-in abstractive summarization pre-pass (Phase 16) — runs BEFORE `compileContext`. For non-critical
3
+ * blocks (`high`/`normal`) whose estimate exceeds their fair share of the budget, it asks a model for a
4
+ * shorter text and swaps it in. `critical` and `discardable` blocks are NEVER summarized (critical must stay
5
+ * verbatim; discardable is dropped by the compiler anyway). Originals are never mutated — a new block array
6
+ * is returned. Any model failure, or a "summary" that does not actually shrink the block, keeps the original
7
+ * (the compiler's deterministic truncation then handles it, exactly as before). Content is `wrapUntrusted`-
8
+ * fenced. Absent config ⇒ this never runs and the compiler sees the original blocks.
9
+ */
10
+ import type { AI } from '../plugin/ai.js';
11
+ import type { ContextBlock } from './compiler.js';
12
+ import { TokenEstimator } from './tokens.js';
13
+ export interface SummarizeInput {
14
+ ai: AI;
15
+ blocks: ContextBlock[];
16
+ budgetTokens: number;
17
+ estimator?: TokenEstimator;
18
+ }
19
+ export declare function summarizeOverBudget(input: SummarizeInput): Promise<ContextBlock[]>;
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Opt-in abstractive summarization pre-pass (Phase 16) — runs BEFORE `compileContext`. For non-critical
3
+ * blocks (`high`/`normal`) whose estimate exceeds their fair share of the budget, it asks a model for a
4
+ * shorter text and swaps it in. `critical` and `discardable` blocks are NEVER summarized (critical must stay
5
+ * verbatim; discardable is dropped by the compiler anyway). Originals are never mutated — a new block array
6
+ * is returned. Any model failure, or a "summary" that does not actually shrink the block, keeps the original
7
+ * (the compiler's deterministic truncation then handles it, exactly as before). Content is `wrapUntrusted`-
8
+ * fenced. Absent config ⇒ this never runs and the compiler sees the original blocks.
9
+ */
10
+ import { TokenEstimator } from './tokens.js';
11
+ import { wrapUntrusted } from '../tools/untrusted.js';
12
+ export async function summarizeOverBudget(input) {
13
+ const estimator = input.estimator ?? new TokenEstimator();
14
+ const compressible = input.blocks.filter((b) => b.retention === 'high' || b.retention === 'normal');
15
+ if (compressible.length === 0)
16
+ return input.blocks;
17
+ // Fair share: the budget split across the compressible blocks. Only those exceeding it are summarized.
18
+ const share = Math.max(1, Math.floor(Math.max(0, input.budgetTokens) / compressible.length));
19
+ const out = [];
20
+ for (const b of input.blocks) {
21
+ if (b.retention !== 'high' && b.retention !== 'normal') {
22
+ out.push(b); // critical stays verbatim; discardable is the compiler's to drop
23
+ continue;
24
+ }
25
+ const full = b.resolveContent ? b.resolveContent() : b.text;
26
+ if (estimator.estimate(full) <= share) {
27
+ out.push(b); // already within its share — nothing to gain
28
+ continue;
29
+ }
30
+ try {
31
+ const prompt = `Summarize the following content to about ${share} tokens or fewer, preserving EVERY identifier, ` +
32
+ `file path, number, and task-critical fact. Output ONLY the summary text.\n\n${wrapUntrusted(`block:${b.id}`, full)}`;
33
+ const result = await input.ai.run({
34
+ task: 'chat',
35
+ input: { text: prompt },
36
+ system: 'You compress context while preserving task-critical facts and identifiers. Output only the summary text.',
37
+ requirements: { intelligence: ['reasoning'], output: ['text'] },
38
+ });
39
+ const summary = result.ok ? (result.response?.text ?? '').trim() : '';
40
+ // Accept only a summary that genuinely shrinks the block; otherwise keep the original.
41
+ if (summary && estimator.estimate(summary) < estimator.estimate(full)) {
42
+ out.push({ ...b, text: summary, ...(b.resolveContent ? { resolveContent: () => summary } : {}) });
43
+ }
44
+ else {
45
+ out.push(b);
46
+ }
47
+ }
48
+ catch {
49
+ out.push(b); // model failure → keep the original; the compiler truncates deterministically as before
50
+ }
51
+ }
52
+ return out;
53
+ }
@@ -11,6 +11,7 @@ import type { RequestTemplate } from '../router/normalize.js';
11
11
  import { AIError } from './errors.js';
12
12
  import type { BudgetTracker } from '../policies/budget.js';
13
13
  import type { Clock } from '../../util/clock.js';
14
+ import type { KeyedSemaphore } from '../../util/semaphore.js';
14
15
  export interface FallbackInput {
15
16
  ranked: ScoredCandidate[];
16
17
  template: RequestTemplate;
@@ -19,12 +20,19 @@ export interface FallbackInput {
19
20
  signal?: AbortSignal;
20
21
  clock?: Clock;
21
22
  onAttempt?: (record: AttemptRecord) => void;
23
+ /** Phase-13 streaming: forwarded to each attempt's `executeOnce`; deltas ride this callback, the final
24
+ * aggregate rides the return value. Only meaningful when `template.stream` is set. */
25
+ onDelta?: (chunk: string) => void;
22
26
  /** Optional Phase-6 validation. A failing report drops this candidate and continues (no poisoning). */
23
27
  validate?: (response: AIResponse, model: string) => ValidationReport;
24
28
  /** Optional spend guardrail. When it cannot afford the next call, the run STOPS with BUDGET. */
25
29
  budget?: BudgetTracker;
26
30
  /** Estimated USD cost of a candidate call, consulted before spending (used with `budget`). */
27
31
  costOf?: (providerId: string, model: string) => Promise<number>;
32
+ /** Phase 19: optional per-provider in-flight limiter, SHARED across concurrent router calls. Bounds how
33
+ * many calls to a given provider run at once (this fallback walks candidates sequentially, so it only
34
+ * bites under fan-out). Absent / unlimited ⇒ a zero-overhead pass-through. */
35
+ providerLimiter?: KeyedSemaphore;
28
36
  }
29
37
  export type FallbackResult = {
30
38
  ok: true;
@@ -34,7 +34,9 @@ export async function runWithFallback(input) {
34
34
  tried += 1;
35
35
  const started = clock.now();
36
36
  const request = buildRequest(input.template, model.id, input.signal);
37
- const outcome = await executeOnce(provider, request);
37
+ const outcome = input.providerLimiter
38
+ ? await input.providerLimiter.run(providerId, () => executeOnce(provider, request, input.onDelta))
39
+ : await executeOnce(provider, request, input.onDelta);
38
40
  const latencyMs = clock.now() - started;
39
41
  input.budget?.recordCall(estCost);
40
42
  if (outcome.ok) {
@@ -13,4 +13,9 @@ export type ExecOutcome = {
13
13
  ok: false;
14
14
  error: AIError;
15
15
  };
16
- export declare function executeOnce(provider: AIProvider, request: AIRequest): Promise<ExecOutcome>;
16
+ /**
17
+ * Run one attempt. When `onDelta` is supplied AND the request asked to stream AND the provider supports
18
+ * `executeStream`, the answer streams token-by-token; otherwise the normal single-shot `execute()` runs.
19
+ * Either way the resolved `AIResponse` is the full aggregate.
20
+ */
21
+ export declare function executeOnce(provider: AIProvider, request: AIRequest, onDelta?: (chunk: string) => void): Promise<ExecOutcome>;
@@ -4,9 +4,16 @@
4
4
  * raw vendor error.
5
5
  */
6
6
  import { AIError, toAIError } from '../fallback/errors.js';
7
- export async function executeOnce(provider, request) {
7
+ /**
8
+ * Run one attempt. When `onDelta` is supplied AND the request asked to stream AND the provider supports
9
+ * `executeStream`, the answer streams token-by-token; otherwise the normal single-shot `execute()` runs.
10
+ * Either way the resolved `AIResponse` is the full aggregate.
11
+ */
12
+ export async function executeOnce(provider, request, onDelta) {
8
13
  try {
9
- const response = await provider.execute(request);
14
+ const response = onDelta && request.stream && provider.executeStream
15
+ ? await provider.executeStream(request, onDelta)
16
+ : await provider.execute(request);
10
17
  return { ok: true, response };
11
18
  }
12
19
  catch (e) {
@@ -22,6 +22,8 @@ export interface RequestTemplate {
22
22
  params?: AIRequest['params'];
23
23
  timeoutMs: number;
24
24
  sensitivity: Sensitivity;
25
+ /** Stream the answer token-by-token (Phase 13). Set only for text output; never for JSON. */
26
+ stream?: boolean;
25
27
  }
26
28
  export interface NormalizeResult {
27
29
  task: NormalizedTask;
@@ -15,6 +15,8 @@ export function buildRequest(template, model, signal) {
15
15
  req.tools = template.tools;
16
16
  if (template.params !== undefined)
17
17
  req.params = template.params;
18
+ if (template.stream)
19
+ req.stream = true;
18
20
  if (signal !== undefined)
19
21
  req.signal = signal;
20
22
  return req;
@@ -14,6 +14,7 @@ import type { PerformanceStore } from '../../learning/performanceStore.js';
14
14
  import type { CapabilityOverlay } from '../capabilities/overlay.js';
15
15
  import type { Clock } from '../../util/clock.js';
16
16
  import type { RunRequest, RunResult } from '../../types.js';
17
+ import type { KeyedSemaphore } from '../../util/semaphore.js';
17
18
  export interface RouterDeps {
18
19
  registry: ProviderRegistry;
19
20
  tasks: TaskRegistry;
@@ -23,6 +24,8 @@ export interface RouterDeps {
23
24
  performance?: PerformanceStore;
24
25
  overlay?: CapabilityOverlay;
25
26
  clock?: Clock;
27
+ /** Phase 19: shared per-provider in-flight limiter, forwarded to fallback (bounds concurrent calls to a provider). */
28
+ providerLimiter?: KeyedSemaphore;
26
29
  }
27
30
  export declare class Router {
28
31
  private readonly deps;
@@ -31,6 +31,11 @@ export class Router {
31
31
  // Phase 1 — normalize
32
32
  const { task, template } = normalize(req, tasks, config);
33
33
  const pin = { provider: req.provider, model: req.model };
34
+ // Phase 13 — streaming is opt-in and TEXT-ONLY: a JSON/structured request never streams (partial
35
+ // JSON is useless). The delta callback rides `req.onDelta`; the final aggregate rides the return value.
36
+ const wantsJsonOut = template.output?.format === 'json' || template.output?.format === 'structured_output';
37
+ if (req.stream && req.onDelta && !wantsJsonOut)
38
+ template.stream = true;
34
39
  // Team policy (org-level guardrails) merged over per-run constraints.
35
40
  const policy = config.policy;
36
41
  if (policy.requireLocal)
@@ -136,8 +141,10 @@ export class Router {
136
141
  maxFallbacks: config.maxFallbacks,
137
142
  clock: this.clock,
138
143
  onAttempt,
144
+ ...(template.stream && req.onDelta ? { onDelta: req.onDelta } : {}),
139
145
  validate: (response) => validateResponse({ response, ...(template.output ? { output: template.output } : {}), ...(template.tools ? { tools: template.tools } : {}) }),
140
146
  ...(budget ? { budget, costOf } : {}),
147
+ ...(this.deps.providerLimiter ? { providerLimiter: this.deps.providerLimiter } : {}),
141
148
  });
142
149
  baseReport.attempts = fb.attempts;
143
150
  const fallbackCount = Math.max(0, fb.attempts.length - 1);
@@ -24,11 +24,22 @@ export interface Checkpoint {
24
24
  skillVersions: Record<string, string>;
25
25
  completedSteps: string[];
26
26
  }
27
- /** What an execution is waiting for (approval or clarification), persisted so a restart can resume it. */
27
+ /** A plan's estimated vs available model-call budget (Phase 22), carried on a budget-paused execution. */
28
+ export interface BudgetInfo {
29
+ /** Estimated model calls the plan needs (its skill steps). */
30
+ estCalls: number;
31
+ /** The call budget in force (`policy.maxCalls`). */
32
+ maxCalls: number;
33
+ /** Steps already completed (0 on a notify-before-start pause). */
34
+ completedSteps: number;
35
+ totalSteps: number;
36
+ }
37
+ /** What an execution is waiting for (approval / clarification / budget), persisted so a restart resumes it. */
28
38
  export interface PendingInput {
29
- kind: 'approval' | 'clarification';
39
+ kind: 'approval' | 'clarification' | 'budget';
30
40
  question?: string;
31
41
  action?: string;
42
+ budget?: BudgetInfo;
32
43
  }
33
44
  export interface Execution {
34
45
  id: string;
@@ -6,6 +6,7 @@
6
6
  */
7
7
  import { type OpenApiAnalysis } from '../discovery/openapi.js';
8
8
  import type { ProviderConfig } from '../types.js';
9
+ import type { AI } from '../plugin/ai.js';
9
10
  export interface GenerateOptions {
10
11
  id?: string;
11
12
  apiKeyEnv?: string;
@@ -15,3 +16,16 @@ export interface GenerateOptions {
15
16
  /** Turn an OpenAPI analysis into a ProviderConfig. Throws if the spec yielded no usable mapping. */
16
17
  export declare function generateProviderConfig(analysis: OpenApiAnalysis, overrides?: GenerateOptions): ProviderConfig;
17
18
  export declare function generateProviderConfigFromFile(specPath: string, overrides?: GenerateOptions): ProviderConfig;
19
+ export interface SuggestResult {
20
+ ok: boolean;
21
+ config?: ProviderConfig;
22
+ error?: string;
23
+ }
24
+ /**
25
+ * AI-assisted adapter rung (Phase 21a) — an opt-in gap-filler on top of the DETERMINISTIC
26
+ * `generateProviderConfig`. When the spec doesn't enumerate models, a model proposes a likely model-id
27
+ * list; the OUTPUT is STILL produced by `generateProviderConfig` (a validated `ProviderConfig`, never code),
28
+ * so a bad suggestion can only propose model-id strings the operator then confirms — it can never inject an
29
+ * unvalidated config. Any model/parse failure falls back to the deterministic result (or its error).
30
+ */
31
+ export declare function suggestProviderConfig(analysis: OpenApiAnalysis, ai: AI, overrides?: GenerateOptions): Promise<SuggestResult>;