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
@@ -5,9 +5,53 @@
5
5
  * the iteration limit. Dry-run performs ZERO mutations. Missing information surfaces as clarification.
6
6
  */
7
7
  import { generatePlan } from './planner.js';
8
+ import { foldCalls } from './budget.js';
9
+ import { flattenClamp } from '../util/flatten.js';
8
10
  import { executePlan } from './executor.js';
9
- function planSummary(plan) {
10
- const lines = plan.steps.map((s) => ` ${s.id}. ${s.description} [${s.skill ? 'skill:' + s.skill : 'tool:' + s.tool}]`);
11
+ /** Model calls a plan needs = its skill steps (tool steps make no model call). */
12
+ /** Pre-flight model-call estimate. One rule, shared with the executor's wave gate (see ./budget.ts). */
13
+ function estimateCalls(plan, maxCalls, reserve) {
14
+ return foldCalls(plan.steps, maxCalls, reserve);
15
+ }
16
+ /** Render the granted dimensions of a clamped policy compactly, for the approval disclosure. */
17
+ function permissionSummary(p) {
18
+ const on = [];
19
+ if (p.fsRead)
20
+ on.push('fsRead');
21
+ if (p.fsWrite)
22
+ on.push('fsWrite');
23
+ if (p.shell)
24
+ on.push(`shell${p.shellAllowedCommands?.length ? `(${p.shellAllowedCommands.join(',')})` : ''}`);
25
+ if (p.gitCommit)
26
+ on.push('gitCommit');
27
+ if (p.gitPush)
28
+ on.push('gitPush');
29
+ if (p.network)
30
+ on.push('network');
31
+ const mcp = Object.entries(p.mcp?.servers ?? {}).filter(([, g]) => g !== 'off' && g !== false);
32
+ if (mcp.length)
33
+ on.push(`mcp(${mcp.map(([id, g]) => `${id}:${String(g)}`).join(',')})`);
34
+ return on.length ? on.join(', ') : 'none';
35
+ }
36
+ /**
37
+ * The plan a human is asked to approve. An AGENT step discloses its ENVELOPE — what it may spend, what
38
+ * it may touch, and what it is allowed to do — because approving a delegation blind is approving a
39
+ * blank cheque. The skill/tool line is unchanged.
40
+ *
41
+ * Every source-controlled segment on the agent line is clamped: a long tool list or permission summary
42
+ * must not be able to forge a second envelope line in the very text a human is reading to decide.
43
+ */
44
+ function planSummary(plan, envelopes) {
45
+ const lines = plan.steps.map((s) => {
46
+ if (s.agent) {
47
+ const e = envelopes?.find((x) => x.agentId === s.agent);
48
+ const env = e
49
+ ? ` (reserves ${e.reservation} model call(s), max ${e.maxToolCalls} tool call(s), ${e.maxDurationMs}ms; tools: ${flattenClamp(e.tools.join(', '), 160) || 'none'}; permissions: ${flattenClamp(permissionSummary(e.permissions), 120)})`
50
+ : ' (envelope unavailable)';
51
+ return ` ${s.id}. ${s.description} [agent:${flattenClamp(s.agent, 40)}]${env}`;
52
+ }
53
+ return ` ${s.id}. ${s.description} [${s.skill ? 'skill:' + s.skill : 'tool:' + s.tool}]`;
54
+ });
11
55
  return `Plan v${plan.version} for "${plan.goal}" (${plan.steps.length} steps):\n${lines.join('\n')}`;
12
56
  }
13
57
  export async function orchestrate(input) {
@@ -15,6 +59,12 @@ export async function orchestrate(input) {
15
59
  const loops = input.mode === 'orchestrate' || input.mode === 'agent' || input.mode === 'debug';
16
60
  const maxIterations = loops ? Math.max(1, input.policy.maxIterations ?? (input.mode === 'agent' ? 10 : 5)) : 1;
17
61
  const needsApproval = input.policy.approval !== 'none' && input.policy.autonomy !== 'chat';
62
+ // ONE reserve function per run, shared by the pre-flight estimate and the executor's wave gate — so
63
+ // the two can never disagree about what an agent step costs. It deliberately IGNORES `remaining`:
64
+ // clamping a reservation to what is left would make `callsUsed + waveCalls` equal the budget instead
65
+ // of exceeding it, so the gate could never fire and the agent would overrun at run time.
66
+ const envelopeById = new Map((input.agents ?? []).map((e) => [e.agentId, e]));
67
+ const reserve = input.agents ? (step) => (step.agent ? envelopeById.get(step.agent)?.reservation ?? 1 : 0) : undefined;
18
68
  let priorObservations = [];
19
69
  // Accumulate observations across replan iterations so an early return never discards the trace of
20
70
  // prior iterations that already executed (and may have mutated the workspace).
@@ -27,30 +77,61 @@ export async function orchestrate(input) {
27
77
  tools: input.tools,
28
78
  version,
29
79
  ...(input.routing ? { routing: input.routing } : {}),
80
+ ...(input.capabilityCatalog ? { capabilityCatalog: input.capabilityCatalog } : {}),
81
+ ...(input.requiredCapabilities ? { requiredCapabilities: input.requiredCapabilities } : {}),
82
+ ...(input.agents ? { agents: input.agents } : {}),
83
+ ...(input.resolveGaps ? { resolveGaps: input.resolveGaps } : {}),
30
84
  ...(version > 1 ? { reason: 'previous attempt did not complete', priorObservations } : {}),
31
85
  });
32
86
  if (planResult.clarification)
33
87
  return { status: 'waiting_for_clarification', planHistory, observations: [...allObservations], clarification: planResult.clarification, summary: planResult.clarification };
34
88
  if (!planResult.plan)
35
- return { status: 'failed', planHistory, observations: [...allObservations], summary: planResult.error ?? 'planning failed' };
89
+ return { status: 'failed', planHistory, observations: [...allObservations], summary: planResult.error ?? 'planning failed', ...(planResult.gaps?.length ? { gaps: planResult.gaps } : {}) };
36
90
  const plan = planResult.plan;
37
91
  planHistory.push(plan);
38
92
  if (input.mode === 'plan')
39
- return { status: 'completed', plan, planHistory, observations: [...allObservations], summary: `${planSummary(plan)}\n(plan only — not executed)` };
93
+ return { status: 'completed', plan, planHistory, observations: [...allObservations], summary: `${planSummary(plan, input.agents)}\n(plan only — not executed)` };
40
94
  // Dry-run: report, never execute. Checked BEFORE approval — a dry run changes nothing, so it never
41
95
  // needs approval.
42
96
  if (input.policy.dryRun)
43
- return { status: 'dry-run', plan, planHistory, observations: [...allObservations], summary: `${planSummary(plan)}\n(dry run — no changes were made)` };
97
+ return { status: 'dry-run', plan, planHistory, observations: [...allObservations], summary: `${planSummary(plan, input.agents)}\n(dry run — no changes were made)` };
44
98
  // Approval gate (assisted/autonomous with an approval level).
45
99
  if (needsApproval) {
46
100
  if (!input.approval)
47
- return { status: 'waiting_for_approval', plan, planHistory, observations: [...allObservations], summary: `${planSummary(plan)}\nApproval required before execution.` };
48
- const approved = await input.approval.requestApproval({ action: `execute plan v${plan.version} for "${plan.goal}"`, reason: planSummary(plan), risk: 'medium' });
101
+ return { status: 'waiting_for_approval', plan, planHistory, observations: [...allObservations], summary: `${planSummary(plan, input.agents)}\nApproval required before execution.` };
102
+ const approved = await input.approval.requestApproval({ action: `execute plan v${plan.version} for "${plan.goal}"`, reason: planSummary(plan, input.agents), risk: 'medium' });
49
103
  if (!approved)
50
104
  return { status: 'failed', plan, planHistory, observations: [...allObservations], summary: 'plan was not approved' };
51
105
  }
52
- const exec = await executePlan(plan, { runSkill: input.runSkill, runTool: input.runTool, ...(input.policy.maxParallelSteps ? { maxParallelSteps: input.policy.maxParallelSteps } : {}), ...(input.signal ? { signal: input.signal } : {}) });
106
+ // Phase 22 budget check. Model calls needed = skill steps. When a call budget is set and the plan
107
+ // won't fit, either NOTIFY (default: refuse to start, report the estimate) or run PARTIAL (execute the
108
+ // phases that fit, then pause resumably). `input.partial` (the --partial flag / req.partial) opts in.
109
+ const maxCalls = input.policy.maxCalls;
110
+ const partial = input.partial === true;
111
+ const estCalls = estimateCalls(plan, maxCalls, reserve);
112
+ if (maxCalls !== undefined && !partial && estCalls > maxCalls) {
113
+ return {
114
+ status: 'waiting_for_budget',
115
+ plan,
116
+ planHistory,
117
+ observations: [...allObservations],
118
+ budget: { estCalls, maxCalls, completedSteps: 0, totalSteps: plan.steps.length },
119
+ summary: `This task looks like ~${estCalls} model call(s), but the budget is ${maxCalls}. Raise the budget (AI_MAX_CALLS or maxCalls) and re-run, or run the phases that fit with --partial (then resume as you raise it).`,
120
+ };
121
+ }
122
+ const exec = await executePlan(plan, { runSkill: input.runSkill, runTool: input.runTool, ...(input.runAgent ? { runAgent: input.runAgent } : {}), ...(reserve ? { reserve } : {}), ...(input.policy.maxParallelSteps ? { maxParallelSteps: input.policy.maxParallelSteps } : {}), ...(input.policy.limits ? { limits: input.policy.limits } : {}), ...(input.signal ? { signal: input.signal } : {}), ...(maxCalls !== undefined ? { callBudget: maxCalls } : {}) });
53
123
  allObservations.push(...exec.observations);
124
+ if (exec.stoppedForBudget) {
125
+ const done = plan.steps.filter((s) => s.status === 'succeeded').length;
126
+ return {
127
+ status: 'waiting_for_budget',
128
+ plan,
129
+ planHistory,
130
+ observations: [...allObservations],
131
+ budget: { estCalls, maxCalls: maxCalls, completedSteps: done, totalSteps: plan.steps.length },
132
+ summary: `Ran ${done} of ${plan.steps.length} step(s) within the ${maxCalls}-call budget. Raise the budget (AI_MAX_CALLS) and resume to continue.`,
133
+ };
134
+ }
54
135
  if (exec.ok)
55
136
  return { status: 'completed', plan, planHistory, observations: [...allObservations], summary: `completed "${plan.goal}" in ${plan.steps.length} step(s)` };
56
137
  // Failed: gather evidence and replan (orchestrate only) or stop.
@@ -3,7 +3,9 @@
3
3
  * REGISTERED skill or tool; a plan referencing anything unregistered is invalid (never executed). Plans
4
4
  * are data: the model proposes them, the Runtime owns execution and permission enforcement.
5
5
  */
6
- export type PlanStepStatus = 'pending' | 'running' | 'succeeded' | 'failed' | 'skipped';
6
+ /** The five step statuses. A const array as well as a type, so a projection table can be checked. */
7
+ export declare const PLAN_STEP_STATUSES: readonly ["pending", "running", "succeeded", "failed", "skipped"];
8
+ export type PlanStepStatus = (typeof PLAN_STEP_STATUSES)[number];
7
9
  export interface PlanStep {
8
10
  id: string;
9
11
  description: string;
@@ -11,6 +13,10 @@ export interface PlanStep {
11
13
  skill?: string;
12
14
  /** A registered tool id. */
13
15
  tool?: string;
16
+ /** A registered AGENT DEFINITION id (Phase 3.4). skill/tool/agent are mutually exclusive per step. */
17
+ agent?: string;
18
+ /** Finding ids this step was created to address (Phase 3.4). Additive; persisted with the plan. */
19
+ motivatedBy?: string[];
14
20
  input?: unknown;
15
21
  /** Ids of steps that must succeed first. */
16
22
  dependsOn?: string[];
@@ -27,11 +33,19 @@ export interface ExecutionPlan {
27
33
  export interface PlanValidation {
28
34
  ok: boolean;
29
35
  errors: string[];
36
+ /** Phase 3.1: the unregistered references behind the `unknown skill/tool` errors, as structured data.
37
+ * Additive — `errors` is unchanged and remains the source of truth for messages. */
38
+ missing?: Array<{
39
+ stepId: string;
40
+ kind: 'skill' | 'tool';
41
+ id: string;
42
+ }>;
30
43
  }
31
44
  /** Validate that every step references a registered skill/tool and dependencies/DAG are sound. */
32
45
  export declare function validatePlan(plan: ExecutionPlan, available: {
33
46
  skills: string[];
34
47
  tools: string[];
48
+ agents?: string[];
35
49
  }): PlanValidation;
36
50
  /** A topological execution order → array of "waves" (each wave's steps can run in parallel). */
37
51
  export declare function executionWaves(steps: PlanStep[]): PlanStep[][];
@@ -3,25 +3,44 @@
3
3
  * REGISTERED skill or tool; a plan referencing anything unregistered is invalid (never executed). Plans
4
4
  * are data: the model proposes them, the Runtime owns execution and permission enforcement.
5
5
  */
6
+ /** The five step statuses. A const array as well as a type, so a projection table can be checked. */
7
+ export const PLAN_STEP_STATUSES = ['pending', 'running', 'succeeded', 'failed', 'skipped'];
6
8
  /** Validate that every step references a registered skill/tool and dependencies/DAG are sound. */
7
9
  export function validatePlan(plan, available) {
8
10
  const errors = [];
11
+ const missing = [];
9
12
  const skills = new Set(available.skills);
10
13
  const tools = new Set(available.tools);
14
+ // Phase 3.4: `agents` is OPTIONAL. Absent ⇒ agent execution is not enabled, and every check below
15
+ // that mentions an agent is unreachable, so validation is byte-identical to 2.6.0.
16
+ const agents = new Set(available.agents ?? []);
11
17
  const ids = new Set(plan.steps.map((s) => s.id));
12
18
  if (plan.steps.length === 0)
13
19
  errors.push('plan has no steps');
14
20
  if (ids.size !== plan.steps.length)
15
21
  errors.push('plan has duplicate step ids'); // dupes would silently drop a step
16
22
  for (const step of plan.steps) {
17
- if (!step.skill && !step.tool)
23
+ // With no step naming an agent, `named === 0` is exactly `!step.skill && !step.tool`, so this is the
24
+ // 2.6.0 condition and the 2.6.0 message, verbatim.
25
+ const named = [step.skill, step.tool, step.agent].filter((v) => v !== undefined && v !== '').length;
26
+ if (named === 0)
18
27
  errors.push(`step ${step.id}: names neither a skill nor a tool`);
19
28
  if (step.skill && step.tool)
20
29
  errors.push(`step ${step.id}: names both a skill and a tool`);
21
- if (step.skill && !skills.has(step.skill))
30
+ if (step.agent && (step.skill || step.tool))
31
+ errors.push(`step ${step.id}: names both an agent and a skill/tool`);
32
+ if (step.agent && !available.agents)
33
+ errors.push(`step ${step.id}: agent execution is not enabled`);
34
+ else if (step.agent && !agents.has(step.agent))
35
+ errors.push(`step ${step.id}: unknown agent '${step.agent}'`);
36
+ if (step.skill && !skills.has(step.skill)) {
22
37
  errors.push(`step ${step.id}: unknown skill '${step.skill}'`);
23
- if (step.tool && !tools.has(step.tool))
38
+ missing.push({ stepId: step.id, kind: 'skill', id: step.skill });
39
+ }
40
+ if (step.tool && !tools.has(step.tool)) {
24
41
  errors.push(`step ${step.id}: unknown tool '${step.tool}'`);
42
+ missing.push({ stepId: step.id, kind: 'tool', id: step.tool });
43
+ }
25
44
  for (const dep of step.dependsOn ?? []) {
26
45
  if (!ids.has(dep))
27
46
  errors.push(`step ${step.id}: depends on unknown step '${dep}'`);
@@ -31,7 +50,7 @@ export function validatePlan(plan, available) {
31
50
  }
32
51
  if (hasCycle(plan.steps))
33
52
  errors.push('plan has a dependency cycle');
34
- return { ok: errors.length === 0, errors };
53
+ return { ok: errors.length === 0, errors, ...(missing.length ? { missing } : {}) };
35
54
  }
36
55
  function hasCycle(steps) {
37
56
  const deps = new Map(steps.map((s) => [s.id, s.dependsOn ?? []]));
@@ -6,7 +6,9 @@
6
6
  import type { AI } from '../plugin/ai.js';
7
7
  import type { RoutingPreferences } from '../types.js';
8
8
  import type { Skill } from '../skills/skill.js';
9
- import type { ExecutionPlan } from './plan.js';
9
+ import type { ExecutionPlan, PlanValidation } from './plan.js';
10
+ import type { CapabilityGap } from '../capabilities/capability.js';
11
+ import type { AgentEnvelope } from '../agents/envelope.js';
10
12
  export interface PlannerInput {
11
13
  goal: string;
12
14
  ai: AI;
@@ -16,6 +18,19 @@ export interface PlannerInput {
16
18
  reason?: string;
17
19
  /** Observations from a prior attempt, to inform a replan. */
18
20
  priorObservations?: string[];
21
+ /** Phase 3.1: a pre-rendered, capped, fenced action-capability snapshot. Absent ⇒ the prompt is
22
+ * byte-identical to 2.3.0 (the catalog flag is off by default). */
23
+ capabilityCatalog?: string;
24
+ /** Phase 3.3: a pre-rendered, clamped block naming the capabilities the goal was derived to need and
25
+ * the provider chosen for each. Absent ⇒ the prompt is byte-identical (the flag is off by default). */
26
+ requiredCapabilities?: string;
27
+ /** Phase 3.4: the narrowed agent envelopes available to this plan. Absent ⇒ the prompt carries no
28
+ * agent rows and no agent schema line, and `validatePlan` rejects any agent step. */
29
+ agents?: AgentEnvelope[];
30
+ /** Phase 3.4: a pre-rendered, FENCED, bounded block of active findings from completed agent steps. */
31
+ findings?: string;
32
+ /** Phase 3.1: resolves unregistered references into structured gaps for the failure result. */
33
+ resolveGaps?: (missing: NonNullable<PlanValidation['missing']>) => CapabilityGap[];
19
34
  /** User exclude/prefer routing applied to the planning model call. */
20
35
  routing?: RoutingPreferences;
21
36
  }
@@ -24,6 +39,9 @@ export interface PlannerResult {
24
39
  /** Set when the planner needs the user to disambiguate or enable a capability. */
25
40
  clarification?: string;
26
41
  error?: string;
42
+ /** Phase 3.1: structured capability gaps behind an `unknown skill/tool` failure (additive metadata —
43
+ * `error` and `clarification` strings are unchanged). */
44
+ gaps?: CapabilityGap[];
27
45
  }
28
46
  /** Generate and validate a plan for a goal. */
29
47
  export declare function generatePlan(input: PlannerInput): Promise<PlannerResult>;
@@ -4,10 +4,16 @@
4
4
  * execute. When nothing maps to the goal, the planner surfaces a clarification rather than guessing.
5
5
  */
6
6
  import { validatePlan } from './plan.js';
7
- function catalog(skills, tools) {
7
+ function catalog(skills, tools, agents) {
8
8
  const skillLines = skills.map((s) => ` - skill "${s.id}": ${s.description}`).join('\n') || ' (none)';
9
9
  const toolLines = tools.map((t) => ` - tool "${t}"`).join('\n') || ' (none)';
10
- return `Available skills:\n${skillLines}\nAvailable tools:\n${toolLines}`;
10
+ const base = `Available skills:\n${skillLines}\nAvailable tools:\n${toolLines}`;
11
+ // Phase 3.4: agent rows appear ONLY when envelopes were supplied, so the block above is byte-identical
12
+ // whenever agents are disabled. The objective is already clamped by `narrowEnvelope`.
13
+ if (!agents?.length)
14
+ return base;
15
+ const agentLines = agents.map((a) => ` - agent "${a.agentId}": ${a.objective}`).join('\n');
16
+ return `${base}\nAvailable agents (delegate a bounded sub-task):\n${agentLines}`;
11
17
  }
12
18
  /** Generate and validate a plan for a goal. */
13
19
  export async function generatePlan(input) {
@@ -17,8 +23,19 @@ export async function generatePlan(input) {
17
23
  const prompt = [
18
24
  `Goal: ${input.goal}`,
19
25
  input.priorObservations?.length ? `Prior attempt observations:\n${input.priorObservations.map((o) => ` - ${o}`).join('\n')}` : '',
20
- catalog(input.skills, input.tools),
26
+ catalog(input.skills, input.tools, input.agents),
27
+ // Phase 3.1: opt-in capability snapshot (pre-rendered, capped, fenced by the caller). Absent by
28
+ // default, so the prompt above stays byte-identical to 2.3.0.
29
+ input.capabilityCatalog ? input.capabilityCatalog : '',
30
+ // Phase 3.3: opt-in derived-requirement block (pre-rendered + clamped by the caller). Falsy when
31
+ // absent, so `.filter(Boolean)` leaves the prompt above byte-identical.
32
+ input.requiredCapabilities ? input.requiredCapabilities : '',
33
+ // Phase 3.4: the findings block is pre-rendered, fenced, and bounded by the caller. Falsy when
34
+ // absent, so `.filter(Boolean)` leaves the prompt above unchanged.
35
+ input.findings ? input.findings : '',
21
36
  'Produce a minimal JSON plan: {"steps":[{"id","description","skill" OR "tool","input","dependsOn":[ids]}]}.',
37
+ // Phase 3.4: only mentioned when agents are actually available.
38
+ input.agents?.length ? 'A step may instead delegate to an agent: {"id","description","agent":"<id>","input","dependsOn":[ids]}. Use an agent for a bounded sub-task that needs its own plan.' : '',
22
39
  'Use ONLY the skills/tools listed above. If nothing fits, return {"steps":[],"clarification":"<question>"}.',
23
40
  ]
24
41
  .filter(Boolean)
@@ -47,6 +64,8 @@ export async function generatePlan(input) {
47
64
  description: s.description ?? '',
48
65
  ...(s.skill ? { skill: s.skill } : {}),
49
66
  ...(s.tool ? { tool: s.tool } : {}),
67
+ ...(s.agent ? { agent: s.agent } : {}),
68
+ ...(Array.isArray(s.motivatedBy) ? { motivatedBy: s.motivatedBy.filter((x) => typeof x === 'string') } : {}),
50
69
  ...(s.input !== undefined ? { input: s.input } : {}),
51
70
  ...(Array.isArray(s.dependsOn) ? { dependsOn: s.dependsOn } : {}),
52
71
  status: 'pending',
@@ -58,12 +77,13 @@ export async function generatePlan(input) {
58
77
  steps,
59
78
  ...(input.reason ? { reason: input.reason } : {}),
60
79
  };
61
- const validation = validatePlan(plan, { skills: input.skills.map((s) => s.id), tools: input.tools });
80
+ const validation = validatePlan(plan, { skills: input.skills.map((s) => s.id), tools: input.tools, ...(input.agents ? { agents: input.agents.map((a) => a.agentId) } : {}) });
62
81
  if (!validation.ok) {
63
82
  // Empty plan with no steps → treat as "nothing to do / needs clarification" rather than a hard error.
64
83
  if (steps.length === 0)
65
84
  return { clarification: 'I could not form a plan for this goal. Can you clarify what you want done?' };
66
- return { error: `invalid plan: ${validation.errors.join('; ')}` };
85
+ const gaps = validation.missing && input.resolveGaps ? input.resolveGaps(validation.missing) : undefined;
86
+ return { error: `invalid plan: ${validation.errors.join('; ')}`, ...(gaps && gaps.length ? { gaps } : {}) };
67
87
  }
68
88
  return { plan };
69
89
  }
@@ -23,6 +23,10 @@ export interface AIOptions {
23
23
  env?: NodeJS.ProcessEnv;
24
24
  /** Extra telemetry sinks for centralized observability (e.g. a collector exporter). */
25
25
  sinks?: TelemetrySink[];
26
+ /** Phase 19: per-provider in-flight concurrency caps (id → max concurrent calls). Shared across all runs. */
27
+ concurrency?: {
28
+ perProvider?: Record<string, number>;
29
+ };
26
30
  }
27
31
  /** A key-free view of a registered provider — safe to return from the public API. */
28
32
  export interface ProviderInfo {
package/dist/plugin/ai.js CHANGED
@@ -10,6 +10,7 @@ import { resolveConfig } from '../config/defaults.js';
10
10
  import { loadConfigAsync } from '../config/load.js';
11
11
  import { MemorySink, MultiSink } from '../telemetry/telemetry.js';
12
12
  import { FileSink } from '../telemetry/sinks/file.js';
13
+ import { OtlpSink } from '../telemetry/sinks/otlp.js';
13
14
  import { HealthMonitor } from '../core/health/monitor.js';
14
15
  import { PerformanceStore } from '../learning/performanceStore.js';
15
16
  import { CapabilityOverlay } from '../core/capabilities/overlay.js';
@@ -18,6 +19,7 @@ import { buildProvider } from '../providers/factory.js';
18
19
  import { McpRegistry } from '../mcp/mcp.js';
19
20
  import { presetToConfig } from '../marketplace/presets.js';
20
21
  import { generateProviderConfig, generateProviderConfigFromFile } from '../generation/generateAdapter.js';
22
+ import { KeyedSemaphore } from '../util/semaphore.js';
21
23
  export class AI {
22
24
  config;
23
25
  registry = new ProviderRegistry();
@@ -36,12 +38,18 @@ export class AI {
36
38
  const sinks = [memory];
37
39
  if (this.config.telemetry.enabled && this.config.telemetry.sink === 'file' && this.config.telemetry.path)
38
40
  sinks.push(new FileSink(this.config.telemetry.path));
41
+ if (this.config.telemetry.enabled && this.config.telemetry.sink === 'otlp' && this.config.telemetry.endpoint) {
42
+ const authEnv = this.config.telemetry.headersEnv;
43
+ const authHeader = authEnv ? (options.env ?? process.env)[authEnv] : undefined;
44
+ sinks.push(new OtlpSink({ endpoint: this.config.telemetry.endpoint, ...(options.fetchImpl ? { fetchImpl: options.fetchImpl } : {}), ...(authHeader ? { authHeader } : {}) }));
45
+ }
39
46
  if (options.sinks)
40
47
  sinks.push(...options.sinks);
41
48
  this.telemetry = sinks.length > 1 ? new MultiSink(sinks) : memory;
42
49
  this.health = new HealthMonitor(this.telemetry, options.clock);
43
50
  if (this.config.learning.enabled)
44
51
  this.performance = new PerformanceStore();
52
+ const providerLimiter = options.concurrency?.perProvider ? new KeyedSemaphore(options.concurrency.perProvider) : undefined;
45
53
  this.router = new Router({
46
54
  registry: this.registry,
47
55
  tasks: this.tasks,
@@ -51,6 +59,7 @@ export class AI {
51
59
  overlay: this.overlay,
52
60
  ...(this.performance ? { performance: this.performance } : {}),
53
61
  ...(options.clock ? { clock: options.clock } : {}),
62
+ ...(providerLimiter?.active ? { providerLimiter } : {}),
54
63
  });
55
64
  // Build concrete adapters from config (credential resolved from the env-var NAME). Providers of
56
65
  // kind 'mock' are registered programmatically instead.
@@ -11,7 +11,8 @@
11
11
  * caller built, never in anything this function stores or throws.
12
12
  */
13
13
  import type { Clock } from '../util/clock.js';
14
- import type { WireRequest } from './wire/types.js';
14
+ import type { FinishReason } from '../types.js';
15
+ import type { WireDelta, WireRequest } from './wire/types.js';
15
16
  export type FetchLike = typeof fetch;
16
17
  export interface CallHttpInput {
17
18
  build: (jsonMode: boolean) => WireRequest;
@@ -32,3 +33,26 @@ export interface CallHttpResult {
32
33
  jsonModeUsed: boolean;
33
34
  }
34
35
  export declare function callHttp(input: CallHttpInput): Promise<CallHttpResult>;
36
+ export interface CallHttpStreamInput {
37
+ build: (jsonMode: boolean) => WireRequest;
38
+ readDelta: (data: string) => WireDelta;
39
+ onDelta: (text: string) => void;
40
+ providerId: string;
41
+ model: string;
42
+ timeoutMs: number;
43
+ jsonMode: boolean;
44
+ fetchImpl?: FetchLike;
45
+ clock?: Clock;
46
+ signal?: AbortSignal;
47
+ }
48
+ export interface CallHttpStreamResult {
49
+ status: number;
50
+ text: string;
51
+ usage?: {
52
+ inputTokens?: number;
53
+ outputTokens?: number;
54
+ };
55
+ finishReason: FinishReason;
56
+ durationMs: number;
57
+ }
58
+ export declare function callHttpStream(input: CallHttpStreamInput): Promise<CallHttpStreamResult>;
@@ -78,3 +78,96 @@ export async function callHttp(input) {
78
78
  }
79
79
  throw lastError ?? new AIError(`${providerId} request failed`, { category: 'NETWORK', retryable: true, providerId, model });
80
80
  }
81
+ export async function callHttpStream(input) {
82
+ const fetchImpl = input.fetchImpl ?? fetch;
83
+ const clock = input.clock ?? systemClock;
84
+ const { providerId, model } = input;
85
+ const started = clock.now();
86
+ const req = input.build(input.jsonMode);
87
+ // Combine the per-request timeout with any caller abort signal (cancellation).
88
+ const signals = [AbortSignal.timeout(input.timeoutMs)];
89
+ if (input.signal)
90
+ signals.push(input.signal);
91
+ const signal = signals.length > 1 ? AbortSignal.any(signals) : signals[0];
92
+ let res;
93
+ try {
94
+ res = await fetchImpl(req.url, { method: 'POST', headers: req.headers, body: JSON.stringify(req.body), signal });
95
+ }
96
+ catch (e) {
97
+ // undici embeds the full URL in the message — use only the error NAME, never the message.
98
+ const name = e instanceof Error ? e.name : 'Error';
99
+ const isTimeout = name === 'TimeoutError' || name === 'AbortError';
100
+ throw new AIError(`${providerId} stream request failed (${name})`, { category: isTimeout ? 'TIMEOUT' : 'NETWORK', retryable: true, providerId, model });
101
+ }
102
+ if (!res.ok || !res.body) {
103
+ const text = await res.text().catch(() => '');
104
+ const excerpt = redactString(text.slice(0, 300));
105
+ const category = statusToCategory(res.status);
106
+ throw new AIError(`${providerId} stream returned HTTP ${res.status}: ${excerpt}`, { category, status: res.status, retryable: isRetryable(category), providerId, model });
107
+ }
108
+ const reader = res.body.getReader();
109
+ const decoder = new TextDecoder();
110
+ let buffer = '';
111
+ let text = '';
112
+ let usage;
113
+ let finishReason = 'stop';
114
+ let done = false;
115
+ const handleData = (payload) => {
116
+ if (!payload)
117
+ return;
118
+ const delta = input.readDelta(payload);
119
+ if (delta.text) {
120
+ text += delta.text;
121
+ input.onDelta(delta.text);
122
+ }
123
+ if (delta.usage)
124
+ usage = { ...usage, ...delta.usage };
125
+ if (delta.finishReason)
126
+ finishReason = delta.finishReason;
127
+ if (delta.done)
128
+ done = true;
129
+ };
130
+ try {
131
+ try {
132
+ while (!done) {
133
+ const { value, done: streamDone } = await reader.read();
134
+ if (streamDone)
135
+ break;
136
+ buffer += decoder.decode(value, { stream: true });
137
+ // Process complete lines; keep the trailing partial in the buffer.
138
+ let nl;
139
+ while ((nl = buffer.indexOf('\n')) >= 0) {
140
+ const line = buffer.slice(0, nl).replace(/\r$/, '');
141
+ buffer = buffer.slice(nl + 1);
142
+ if (line.startsWith('data:'))
143
+ handleData(line.slice(5).trim());
144
+ // `event:` / blank / comment (`:`) lines are ignored — the type is in the data JSON.
145
+ if (done)
146
+ break;
147
+ }
148
+ }
149
+ // Flush a final line that had no trailing newline.
150
+ if (!done && buffer.startsWith('data:'))
151
+ handleData(buffer.slice(5).trim());
152
+ }
153
+ finally {
154
+ try {
155
+ await reader.cancel();
156
+ }
157
+ catch {
158
+ /* nothing to do */
159
+ }
160
+ }
161
+ }
162
+ catch (e) {
163
+ // A mid-stream drop/timeout after 200 OK: categorize and sanitize like the initial-fetch path (undici
164
+ // embeds the full URL in the message — use only the error NAME, never the message).
165
+ const name = e instanceof Error ? e.name : 'Error';
166
+ const isTimeout = name === 'TimeoutError' || name === 'AbortError';
167
+ throw new AIError(`${providerId} stream interrupted (${name})`, { category: isTimeout ? 'TIMEOUT' : 'NETWORK', retryable: true, providerId, model });
168
+ }
169
+ const result = { status: res.status, text, finishReason, durationMs: clock.now() - started };
170
+ if (usage)
171
+ result.usage = usage;
172
+ return result;
173
+ }
@@ -46,4 +46,5 @@ export declare class HttpProvider implements AIProvider {
46
46
  getCapabilities(model: string): Promise<CapabilityProfile>;
47
47
  estimate(request: AIRequest): Promise<ExecutionEstimate>;
48
48
  execute(request: AIRequest): Promise<AIResponse>;
49
+ executeStream(request: AIRequest, onDelta: (chunk: string) => void): Promise<AIResponse>;
49
50
  }
@@ -12,7 +12,7 @@ import { AIError } from '../core/fallback/errors.js';
12
12
  import { emptyProfile } from '../core/capabilities/evidence.js';
13
13
  import { extractJson } from '../util/extractJson.js';
14
14
  import { getWire } from './wire/registry.js';
15
- import { callHttp } from './httpClient.js';
15
+ import { callHttp, callHttpStream } from './httpClient.js';
16
16
  function originOf(url) {
17
17
  try {
18
18
  return new URL(url).origin;
@@ -132,4 +132,70 @@ export class HttpProvider {
132
132
  }
133
133
  return response;
134
134
  }
135
+ async executeStream(request, onDelta) {
136
+ const wire = getWire(this.cfg.wireShape);
137
+ const wantsJson = request.output?.format === 'json' || request.output?.format === 'structured_output';
138
+ // JSON output, or a wire without streaming support, degrades to the normal single-shot path.
139
+ if (wantsJson || !wire.buildStreamRequest || !wire.readDelta)
140
+ return this.execute(request);
141
+ const needsKey = this.cfg.requiresKey !== false;
142
+ const apiKey = this.cfg.credential.use();
143
+ if (needsKey && !apiKey) {
144
+ throw new AIError(`no ${this.cfg.credential.envName ?? 'API key'} in the environment`, { category: 'AUTHENTICATION', retryable: false, providerId: this.id, model: request.model });
145
+ }
146
+ const maxTokens = request.params?.maxTokens ?? 2000;
147
+ const ctx = {
148
+ baseUrl: this.cfg.baseUrl,
149
+ model: request.model,
150
+ maxTokens,
151
+ ...(apiKey ? { apiKey } : {}),
152
+ ...(this.cfg.headers ? { headers: this.cfg.headers } : {}),
153
+ };
154
+ // Graceful degrade: if the stream fails BEFORE any token (a gateway that doesn't do SSE, or 400s on
155
+ // the streaming body), fall back to buffered execute() so a non-streaming endpoint still works — the
156
+ // caller just doesn't get live tokens. Once tokens have flowed we can't degrade (would double-emit),
157
+ // so we rethrow and let the router fall back to a different candidate.
158
+ let emitted = 0;
159
+ let result;
160
+ try {
161
+ result = await callHttpStream({
162
+ build: (jsonMode) => wire.buildStreamRequest(request, ctx, jsonMode),
163
+ readDelta: (data) => wire.readDelta(data),
164
+ onDelta: (chunk) => {
165
+ emitted += 1;
166
+ onDelta(chunk);
167
+ },
168
+ providerId: this.id,
169
+ model: request.model,
170
+ timeoutMs: request.timeoutMs || this.cfg.timeoutMs || 90_000,
171
+ jsonMode: this.jsonMode,
172
+ ...(this.cfg.fetchImpl ? { fetchImpl: this.cfg.fetchImpl } : {}),
173
+ ...(this.cfg.clock ? { clock: this.cfg.clock } : {}),
174
+ ...(request.signal ? { signal: request.signal } : {}),
175
+ });
176
+ }
177
+ catch (e) {
178
+ if (emitted === 0)
179
+ return this.execute(request);
180
+ throw e;
181
+ }
182
+ const response = {
183
+ finishReason: result.finishReason,
184
+ providerId: this.id,
185
+ model: request.model,
186
+ latencyMs: result.durationMs,
187
+ };
188
+ if (result.text)
189
+ response.text = result.text;
190
+ if (result.usage) {
191
+ const inTok = result.usage.inputTokens;
192
+ const outTok = result.usage.outputTokens;
193
+ response.usage = {
194
+ ...(inTok !== undefined ? { inputTokens: inTok } : {}),
195
+ ...(outTok !== undefined ? { outputTokens: outTok } : {}),
196
+ ...(inTok !== undefined && outTok !== undefined ? { totalTokens: inTok + outTok } : {}),
197
+ };
198
+ }
199
+ return response;
200
+ }
135
201
  }
@@ -32,4 +32,7 @@ export declare class MockProvider implements AIProvider {
32
32
  getCapabilities(model: string): Promise<CapabilityProfile>;
33
33
  estimate(request: AIRequest): Promise<ExecutionEstimate>;
34
34
  execute(request: AIRequest): Promise<AIResponse>;
35
+ /** Stream text chunks then resolve with the full aggregate (Phase 13). Failure behaviors still throw. */
36
+ executeStream(request: AIRequest, onDelta: (chunk: string) => void): Promise<AIResponse>;
37
+ private respond;
35
38
  }