ai-runtime-engine 1.3.0 → 2.8.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 +638 -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/roles.d.ts +36 -0
  11. package/dist/agents/roles.js +44 -0
  12. package/dist/agents/synthesize.d.ts +44 -0
  13. package/dist/agents/synthesize.js +60 -0
  14. package/dist/agents/task.d.ts +112 -0
  15. package/dist/agents/task.js +48 -0
  16. package/dist/agents/worker.d.ts +91 -0
  17. package/dist/agents/worker.js +377 -0
  18. package/dist/capabilities/capability.d.ts +117 -0
  19. package/dist/capabilities/capability.js +66 -0
  20. package/dist/capabilities/registry.d.ts +139 -0
  21. package/dist/capabilities/registry.js +413 -0
  22. package/dist/capabilities/vocabulary.d.ts +32 -0
  23. package/dist/capabilities/vocabulary.js +34 -0
  24. package/dist/cli/cli.js +55 -4
  25. package/dist/cli/commands/cleanup.js +29 -27
  26. package/dist/cli/commands/doctor.d.ts +14 -0
  27. package/dist/cli/commands/doctor.js +38 -8
  28. package/dist/cli/commands/executions.js +34 -25
  29. package/dist/cli/commands/info.d.ts +1 -0
  30. package/dist/cli/commands/info.js +11 -9
  31. package/dist/cli/commands/init.js +19 -0
  32. package/dist/cli/commands/inspect.d.ts +40 -1
  33. package/dist/cli/commands/inspect.js +157 -2
  34. package/dist/cli/commands/mcp.d.ts +45 -0
  35. package/dist/cli/commands/mcp.js +148 -0
  36. package/dist/cli/commands/route.js +21 -0
  37. package/dist/cli/commands/run.d.ts +1 -0
  38. package/dist/cli/commands/run.js +21 -2
  39. package/dist/cli/commands/skills.d.ts +2 -0
  40. package/dist/cli/commands/skills.js +29 -7
  41. package/dist/cli/interactive/ansi.d.ts +41 -0
  42. package/dist/cli/interactive/ansi.js +43 -0
  43. package/dist/cli/interactive/complete.d.ts +10 -0
  44. package/dist/cli/interactive/complete.js +19 -0
  45. package/dist/cli/interactive/lanes.d.ts +69 -0
  46. package/dist/cli/interactive/lanes.js +181 -0
  47. package/dist/cli/interactive/repl.d.ts +3 -0
  48. package/dist/cli/interactive/repl.js +91 -13
  49. package/dist/cli/interactive/session.d.ts +8 -0
  50. package/dist/cli/interactive/session.js +73 -2
  51. package/dist/cli/render.d.ts +7 -0
  52. package/dist/cli/render.js +10 -0
  53. package/dist/cli/runtimeSession.d.ts +11 -0
  54. package/dist/cli/runtimeSession.js +17 -0
  55. package/dist/config/defaults.d.ts +3 -1
  56. package/dist/config/defaults.js +2 -0
  57. package/dist/config/schema.d.ts +1 -0
  58. package/dist/config/schema.js +2 -2
  59. package/dist/context/lossVerifier.d.ts +24 -0
  60. package/dist/context/lossVerifier.js +45 -0
  61. package/dist/context/summarize.d.ts +19 -0
  62. package/dist/context/summarize.js +53 -0
  63. package/dist/core/fallback/fallback.d.ts +5 -0
  64. package/dist/core/fallback/fallback.js +3 -1
  65. package/dist/core/router/router.d.ts +3 -0
  66. package/dist/core/router/router.js +1 -0
  67. package/dist/executions/agentTasks.d.ts +627 -0
  68. package/dist/executions/agentTasks.js +149 -0
  69. package/dist/executions/checkpoint.d.ts +5 -1
  70. package/dist/executions/checkpoint.js +13 -1
  71. package/dist/executions/execution.d.ts +36 -2
  72. package/dist/executions/store.d.ts +37 -0
  73. package/dist/executions/store.js +33 -0
  74. package/dist/generation/generateAdapter.d.ts +14 -0
  75. package/dist/generation/generateAdapter.js +38 -0
  76. package/dist/generation/generateSkill.d.ts +26 -0
  77. package/dist/generation/generateSkill.js +51 -0
  78. package/dist/index.d.ts +47 -4
  79. package/dist/index.js +33 -2
  80. package/dist/mcp/client.d.ts +70 -0
  81. package/dist/mcp/client.js +221 -0
  82. package/dist/mcp/manager.d.ts +151 -0
  83. package/dist/mcp/manager.js +493 -0
  84. package/dist/mcp/protocol.d.ts +216 -0
  85. package/dist/mcp/protocol.js +149 -0
  86. package/dist/mcp/toolAdapter.d.ts +44 -0
  87. package/dist/mcp/toolAdapter.js +94 -0
  88. package/dist/mcp/transport.d.ts +109 -0
  89. package/dist/mcp/transport.js +383 -0
  90. package/dist/memory/embedders/hash.d.ts +12 -0
  91. package/dist/memory/embedders/hash.js +31 -0
  92. package/dist/memory/embedders/http.d.ts +25 -0
  93. package/dist/memory/embedders/http.js +48 -0
  94. package/dist/memory/memory.d.ts +19 -2
  95. package/dist/memory/memory.js +75 -11
  96. package/dist/memory/semantic.d.ts +17 -0
  97. package/dist/memory/semantic.js +29 -0
  98. package/dist/orchestration/budget.d.ts +30 -0
  99. package/dist/orchestration/budget.js +40 -0
  100. package/dist/orchestration/executor.d.ts +64 -1
  101. package/dist/orchestration/executor.js +104 -7
  102. package/dist/orchestration/orchestrator.d.ts +35 -1
  103. package/dist/orchestration/orchestrator.js +106 -8
  104. package/dist/orchestration/plan.d.ts +15 -1
  105. package/dist/orchestration/plan.js +23 -4
  106. package/dist/orchestration/planner.d.ts +19 -1
  107. package/dist/orchestration/planner.js +25 -5
  108. package/dist/plugin/ai.d.ts +4 -0
  109. package/dist/plugin/ai.js +9 -0
  110. package/dist/runtime/config.js +50 -6
  111. package/dist/runtime/intent/aiClassifier.d.ts +19 -0
  112. package/dist/runtime/intent/aiClassifier.js +74 -0
  113. package/dist/runtime/models/modelProfile.d.ts +61 -0
  114. package/dist/runtime/models/modelProfile.js +139 -0
  115. package/dist/runtime/planning/deriveCapabilities.d.ts +95 -0
  116. package/dist/runtime/planning/deriveCapabilities.js +146 -0
  117. package/dist/runtime/policy.d.ts +10 -0
  118. package/dist/runtime/policy.js +9 -2
  119. package/dist/runtime/runtime.d.ts +233 -0
  120. package/dist/runtime/runtime.js +1042 -60
  121. package/dist/runtime/types.d.ts +88 -2
  122. package/dist/security/redact.js +22 -10
  123. package/dist/skills/manifest.d.ts +3 -0
  124. package/dist/skills/manifest.js +24 -0
  125. package/dist/skills/registry.d.ts +16 -1
  126. package/dist/skills/registry.js +21 -1
  127. package/dist/skills/skill.d.ts +6 -1
  128. package/dist/store/area.d.ts +15 -1
  129. package/dist/store/area.js +19 -8
  130. package/dist/store/crypto.d.ts +21 -0
  131. package/dist/store/crypto.js +49 -0
  132. package/dist/store/paths.d.ts +5 -1
  133. package/dist/store/paths.js +6 -0
  134. package/dist/store/store.d.ts +15 -3
  135. package/dist/store/store.js +28 -7
  136. package/dist/telemetry/sinks/otlp.d.ts +31 -0
  137. package/dist/telemetry/sinks/otlp.js +76 -0
  138. package/dist/tools/builtins/filesystem.js +1 -0
  139. package/dist/tools/builtins/git.js +1 -0
  140. package/dist/tools/builtins/shell.js +1 -0
  141. package/dist/tools/permissions.d.ts +28 -0
  142. package/dist/tools/permissions.js +72 -0
  143. package/dist/tools/registry.d.ts +18 -2
  144. package/dist/tools/registry.js +22 -2
  145. package/dist/tools/tool.d.ts +4 -0
  146. package/dist/types.d.ts +5 -1
  147. package/dist/util/flatten.d.ts +11 -0
  148. package/dist/util/flatten.js +18 -0
  149. package/dist/util/hash.d.ts +19 -0
  150. package/dist/util/hash.js +39 -0
  151. package/dist/util/semaphore.d.ts +19 -0
  152. package/dist/util/semaphore.js +60 -0
  153. package/package.json +24 -9
@@ -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;
@@ -28,6 +29,10 @@ export interface FallbackInput {
28
29
  budget?: BudgetTracker;
29
30
  /** Estimated USD cost of a candidate call, consulted before spending (used with `budget`). */
30
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;
31
36
  }
32
37
  export type FallbackResult = {
33
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, input.onDelta);
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) {
@@ -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;
@@ -144,6 +144,7 @@ export class Router {
144
144
  ...(template.stream && req.onDelta ? { onDelta: req.onDelta } : {}),
145
145
  validate: (response) => validateResponse({ response, ...(template.output ? { output: template.output } : {}), ...(template.tools ? { tools: template.tools } : {}) }),
146
146
  ...(budget ? { budget, costOf } : {}),
147
+ ...(this.deps.providerLimiter ? { providerLimiter: this.deps.providerLimiter } : {}),
147
148
  });
148
149
  baseReport.attempts = fb.attempts;
149
150
  const fallbackCount = Math.max(0, fb.attempts.length - 1);