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
@@ -8,20 +8,28 @@
8
8
  import { systemClock } from '../util/clock.js';
9
9
  import { redactString } from '../security/redact.js';
10
10
  import { bm25Search } from './bm25.js';
11
+ import { semanticRank } from './semantic.js';
11
12
  function physical(scope) {
12
13
  if (scope === 'user')
13
14
  return 'user';
14
15
  if (scope === 'repository')
15
16
  return 'repository';
17
+ // The organization branch MUST precede the project catch-all, or org facts silently land in project.
18
+ if (scope === 'organization')
19
+ return 'organization';
16
20
  return 'project';
17
21
  }
18
- const PHYSICAL = ['user', 'project', 'repository'];
22
+ const PHYSICAL_BASE = ['user', 'project', 'repository'];
19
23
  export class MemoryStore {
20
24
  store;
25
+ embedder;
21
26
  clock;
22
27
  counter = 0;
23
- constructor(store, clock = systemClock) {
28
+ /** Per-instance cache of fact-text → vector, so a stable fact isn't re-embedded on every query. */
29
+ vecCache = new Map();
30
+ constructor(store, clock = systemClock, embedder) {
24
31
  this.store = store;
32
+ this.embedder = embedder;
25
33
  this.clock = clock;
26
34
  }
27
35
  get enabled() {
@@ -30,6 +38,11 @@ export class MemoryStore {
30
38
  area(scope) {
31
39
  return this.store.memory(physical(scope));
32
40
  }
41
+ /** Physical scopes to traverse for reads/cascades — includes `organization` only when one is configured,
42
+ * so search/delete/supersession/purge cover it for free without leaking it into org-less stores. */
43
+ physicalScopes() {
44
+ return this.store.organizationId ? [...PHYSICAL_BASE, 'organization'] : PHYSICAL_BASE;
45
+ }
33
46
  nextId() {
34
47
  // clock + pid + per-instance counter + entropy → collision-safe across instances and processes.
35
48
  return `mem_${this.clock.now().toString(36)}_${process.pid.toString(36)}_${(this.counter += 1).toString(36)}_${Math.random().toString(36).slice(2, 6)}`;
@@ -38,6 +51,11 @@ export class MemoryStore {
38
51
  remember(input) {
39
52
  const now = this.clock.now();
40
53
  const scope = input.scope ?? 'project';
54
+ // The org scope is only available when an organization is configured — a clear error beats silently
55
+ // writing an org-intended fact into the project store (where physical() would otherwise route it).
56
+ if (scope === 'organization' && !this.store.organizationId) {
57
+ throw new Error("organization memory scope is not available; set `runtime.organization` in your config");
58
+ }
41
59
  const record = {
42
60
  id: this.nextId(),
43
61
  scope,
@@ -62,7 +80,7 @@ export class MemoryStore {
62
80
  /** Mark existing records in the same conflict group as superseded by the new record. */
63
81
  supersedeConflicts(incoming) {
64
82
  const superseded = [];
65
- for (const scope of PHYSICAL) {
83
+ for (const scope of this.physicalScopes()) {
66
84
  const area = this.store.memory(scope);
67
85
  for (const id of area.listIds()) {
68
86
  const rec = area.tryReadJson(id);
@@ -80,7 +98,7 @@ export class MemoryStore {
80
98
  incoming.supersedes = superseded;
81
99
  }
82
100
  get(id) {
83
- for (const scope of PHYSICAL) {
101
+ for (const scope of this.physicalScopes()) {
84
102
  const rec = this.store.memory(scope).tryReadJson(id);
85
103
  if (rec)
86
104
  return rec;
@@ -90,7 +108,7 @@ export class MemoryStore {
90
108
  /** Every stored record across scopes (including superseded/expired). A corrupt file is skipped. */
91
109
  all() {
92
110
  const out = [];
93
- for (const scope of PHYSICAL) {
111
+ for (const scope of this.physicalScopes()) {
94
112
  const area = this.store.memory(scope);
95
113
  for (const id of area.listIds()) {
96
114
  const rec = area.tryReadJson(id);
@@ -101,7 +119,8 @@ export class MemoryStore {
101
119
  return out;
102
120
  }
103
121
  /** Retrieval: filter (scope/domain/confidence/temporal/superseded) → BM25 rank → top-N. */
104
- search(query, opts = {}) {
122
+ /** The filter stage shared by BM25 and semantic search: expiry, active-supersession, confidence, scope, domain. */
123
+ filteredCandidates(opts) {
105
124
  const now = opts.now ?? this.clock.now();
106
125
  const minConfidence = opts.minConfidence ?? 0;
107
126
  const scopeFilter = opts.scopes ? new Set(opts.scopes) : undefined;
@@ -113,7 +132,7 @@ export class MemoryStore {
113
132
  const s = byId.get(sid);
114
133
  return !!s && (s.validUntil === undefined || s.validUntil >= now);
115
134
  };
116
- const candidates = all.filter((r) => {
135
+ return all.filter((r) => {
117
136
  if (r.validUntil !== undefined && r.validUntil < now)
118
137
  return false;
119
138
  if (r.supersededBy && supersederActive(r.supersededBy))
@@ -126,9 +145,49 @@ export class MemoryStore {
126
145
  return false;
127
146
  return true;
128
147
  });
129
- const candById = new Map(candidates.map((r) => [r.id, r]));
130
- const hits = bm25Search(query, candidates.map((r) => ({ id: r.id, text: r.text })), opts.limit ?? 10);
131
- return hits.map((h) => candById.get(h.id)).filter((r) => r !== undefined);
148
+ }
149
+ rankBm25(query, candidates, limit) {
150
+ const byId = new Map(candidates.map((r) => [r.id, r]));
151
+ const hits = bm25Search(query, candidates.map((r) => ({ id: r.id, text: r.text })), limit ?? 10);
152
+ return hits.map((h) => byId.get(h.id)).filter((r) => r !== undefined);
153
+ }
154
+ /** Retrieve facts by BM25 keyword relevance (the offline default; sync, unchanged since 1.0). */
155
+ search(query, opts = {}) {
156
+ return this.rankBm25(query, this.filteredCandidates(opts), opts.limit);
157
+ }
158
+ /**
159
+ * Retrieve facts by SEMANTIC (vector) relevance when an embedder is configured, else exactly BM25
160
+ * (Phase 14). Same filter stage, same `{scope/confidence/temporal}` semantics, same return shape. Any
161
+ * embedder failure falls back to BM25 — retrieval must never break the run. Candidate vectors are cached
162
+ * per fact text so a stable fact is embedded at most once per session.
163
+ */
164
+ async searchSemantic(query, opts = {}) {
165
+ const candidates = this.filteredCandidates(opts);
166
+ if (!this.embedder || candidates.length === 0)
167
+ return this.rankBm25(query, candidates, opts.limit);
168
+ try {
169
+ const need = [...new Set(candidates.map((c) => c.text))].filter((t) => !this.vecCache.has(t));
170
+ if (need.length) {
171
+ const vecs = await this.embedder.embed(need);
172
+ // Cache only USABLE vectors — a degenerate ([] or non-finite) embedding must never be treated as
173
+ // valid nor poison the cache (a poisoned entry silently excludes that fact for the rest of the session).
174
+ need.forEach((t, i) => { if (isUsableVec(vecs[i]))
175
+ this.vecCache.set(t, vecs[i]); });
176
+ }
177
+ const [queryVec] = await this.embedder.embed([query]);
178
+ const docs = candidates.filter((c) => this.vecCache.has(c.text)).map((c) => ({ id: c.id, vec: this.vecCache.get(c.text) }));
179
+ // A degenerate query vector, or no candidate that embedded usably, degrades to BM25 — retrieval must
180
+ // never silently return nothing because the embedder misbehaved (invariant: an empty vector → fallback).
181
+ if (!isUsableVec(queryVec) || docs.length === 0)
182
+ return this.rankBm25(query, candidates, opts.limit);
183
+ const byId = new Map(candidates.map((r) => [r.id, r]));
184
+ const hits = semanticRank(queryVec, docs, opts.limit ?? 10);
185
+ return hits.map((h) => byId.get(h.id)).filter((r) => r !== undefined);
186
+ }
187
+ catch {
188
+ // An embedder problem (network, bad response) must never break retrieval — fall back to BM25.
189
+ return this.rankBm25(query, candidates, opts.limit);
190
+ }
132
191
  }
133
192
  /** Delete a record and cascade (strip references, heal supersession chains), under the lock. */
134
193
  delete(id) {
@@ -140,7 +199,7 @@ export class MemoryStore {
140
199
  const inheritor = target?.supersededBy; // if the removed record was itself superseded, heirs re-link to its head
141
200
  let primary = false;
142
201
  let relationships = 0;
143
- for (const scope of PHYSICAL) {
202
+ for (const scope of this.physicalScopes()) {
144
203
  const area = this.store.memory(scope);
145
204
  for (const other of area.listIds()) {
146
205
  if (other === id) {
@@ -189,3 +248,8 @@ export class MemoryStore {
189
248
  function clamp(n) {
190
249
  return Math.max(0, Math.min(1, n));
191
250
  }
251
+ /** A vector is usable for ranking only if it is a non-empty array of finite numbers. A degenerate vector
252
+ * (empty, or containing NaN/Infinity) must degrade to BM25, never yield a silently-empty semantic result. */
253
+ function isUsableVec(v) {
254
+ return Array.isArray(v) && v.length > 0 && v.every(Number.isFinite);
255
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Semantic ranking — the vector counterpart to BM25 (Phase 14). Given a query vector and candidate
3
+ * vectors, rank by cosine similarity and return the same `{ id, score }` shape as `bm25Search`, so the
4
+ * retrieval API is unchanged whether ranking is lexical or semantic. Pure and dependency-free.
5
+ */
6
+ export interface VecDoc {
7
+ id: string;
8
+ vec: number[];
9
+ }
10
+ export interface RankedHit {
11
+ id: string;
12
+ score: number;
13
+ }
14
+ /** Cosine similarity of two equal-length vectors; 0 when either has no magnitude (or lengths differ). */
15
+ export declare function cosineSimilarity(a: number[], b: number[]): number;
16
+ /** Rank candidates by cosine similarity to the query vector; top-`limit`, descending, deterministic ties. */
17
+ export declare function semanticRank(queryVec: number[], docs: VecDoc[], limit?: number): RankedHit[];
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Semantic ranking — the vector counterpart to BM25 (Phase 14). Given a query vector and candidate
3
+ * vectors, rank by cosine similarity and return the same `{ id, score }` shape as `bm25Search`, so the
4
+ * retrieval API is unchanged whether ranking is lexical or semantic. Pure and dependency-free.
5
+ */
6
+ /** Cosine similarity of two equal-length vectors; 0 when either has no magnitude (or lengths differ). */
7
+ export function cosineSimilarity(a, b) {
8
+ if (a.length !== b.length)
9
+ return 0;
10
+ let dot = 0;
11
+ let na = 0;
12
+ let nb = 0;
13
+ for (let i = 0; i < a.length; i += 1) {
14
+ dot += a[i] * b[i];
15
+ na += a[i] * a[i];
16
+ nb += b[i] * b[i];
17
+ }
18
+ if (na === 0 || nb === 0)
19
+ return 0;
20
+ return dot / (Math.sqrt(na) * Math.sqrt(nb));
21
+ }
22
+ /** Rank candidates by cosine similarity to the query vector; top-`limit`, descending, deterministic ties. */
23
+ export function semanticRank(queryVec, docs, limit = 10) {
24
+ return docs
25
+ .map((d) => ({ id: d.id, score: cosineSimilarity(queryVec, d.vec) }))
26
+ .filter((h) => h.score > 0)
27
+ .sort((a, b) => b.score - a.score || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))
28
+ .slice(0, limit);
29
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * The single source of per-step model-call cost (Phase 3.4).
3
+ *
4
+ * Before this, the rule "a model call is a skill step" was written out three times — the pre-flight
5
+ * estimate, the executor's wave gate, and the resume recomputation. A third step kind with a non-unit
6
+ * cost makes three copies untenable, so every call site now folds through here and they cannot disagree
7
+ * about what a step costs.
8
+ *
9
+ * The invariant is PER-STEP COST, not the total: the estimate folds the whole plan while the wave gate
10
+ * folds a wave's runnable steps, exactly as before. Sharing one `reserve` is what keeps them consistent.
11
+ */
12
+ import type { PlanStep } from './plan.js';
13
+ /** What an agent step reserves, given the pool still available to it when it is reached. */
14
+ export type ReserveAgentCalls = (step: PlanStep, remaining: number) => number;
15
+ /**
16
+ * Model calls ONE step needs: a skill step is 1, an agent step is its reservation, a tool step is free.
17
+ * With no `reserve` supplied an agent step costs 0 — which is only reachable when agents are disabled,
18
+ * in which case no step names one.
19
+ */
20
+ export declare function stepCalls(step: PlanStep, remaining: number, reserve?: ReserveAgentCalls): number;
21
+ /**
22
+ * Fold the cost of a step list SEQUENTIALLY, against a remainder that declines as it goes — so the
23
+ * number is order-dependent and a reserve function can see what is left.
24
+ *
25
+ * Returns exactly the integer `steps.filter((s) => s.skill).length` returns when no step names an agent,
26
+ * which is what makes the flag-off path byte-identical. Traversal is ARRAY order, not wave order:
27
+ * `executionWaves` drops steps on a cycle or a dangling dependency, which would silently under-count.
28
+ * `validatePlan` rejects such plans before they reach here, but array order removes the hazard entirely.
29
+ */
30
+ export declare function foldCalls(steps: PlanStep[], budgetRemaining: number | undefined, reserve?: ReserveAgentCalls): number;
@@ -0,0 +1,40 @@
1
+ /**
2
+ * The single source of per-step model-call cost (Phase 3.4).
3
+ *
4
+ * Before this, the rule "a model call is a skill step" was written out three times — the pre-flight
5
+ * estimate, the executor's wave gate, and the resume recomputation. A third step kind with a non-unit
6
+ * cost makes three copies untenable, so every call site now folds through here and they cannot disagree
7
+ * about what a step costs.
8
+ *
9
+ * The invariant is PER-STEP COST, not the total: the estimate folds the whole plan while the wave gate
10
+ * folds a wave's runnable steps, exactly as before. Sharing one `reserve` is what keeps them consistent.
11
+ */
12
+ /**
13
+ * Model calls ONE step needs: a skill step is 1, an agent step is its reservation, a tool step is free.
14
+ * With no `reserve` supplied an agent step costs 0 — which is only reachable when agents are disabled,
15
+ * in which case no step names one.
16
+ */
17
+ export function stepCalls(step, remaining, reserve) {
18
+ if (step.skill)
19
+ return 1;
20
+ if (step.agent)
21
+ return Math.max(0, Math.trunc(reserve ? reserve(step, remaining) : 0));
22
+ return 0;
23
+ }
24
+ /**
25
+ * Fold the cost of a step list SEQUENTIALLY, against a remainder that declines as it goes — so the
26
+ * number is order-dependent and a reserve function can see what is left.
27
+ *
28
+ * Returns exactly the integer `steps.filter((s) => s.skill).length` returns when no step names an agent,
29
+ * which is what makes the flag-off path byte-identical. Traversal is ARRAY order, not wave order:
30
+ * `executionWaves` drops steps on a cycle or a dangling dependency, which would silently under-count.
31
+ * `validatePlan` rejects such plans before they reach here, but array order removes the hazard entirely.
32
+ */
33
+ export function foldCalls(steps, budgetRemaining, reserve) {
34
+ let used = 0;
35
+ for (const step of steps) {
36
+ const remaining = budgetRemaining === undefined ? Number.POSITIVE_INFINITY : budgetRemaining - used;
37
+ used += stepCalls(step, remaining, reserve);
38
+ }
39
+ return used;
40
+ }
@@ -6,19 +6,61 @@
6
6
  */
7
7
  import type { SkillResult, SkillValidation } from '../skills/skill.js';
8
8
  import type { ToolResult } from '../tools/tool.js';
9
- import type { ExecutionPlan } from './plan.js';
9
+ import type { ExecutionPlan, PlanStep } from './plan.js';
10
+ import type { ReserveAgentCalls } from './budget.js';
11
+ import type { ArtifactRef } from '../runtime/types.js';
12
+ /** Why a step ended the way it did, when the reason is not simply "the skill/tool said so". */
13
+ export type StepObservationCode = 'cancelled' | 'agent-failed' | 'agent-timeout' | 'agent-call-budget' | 'agent-tool-budget' | 'finding-contract' | 'agent-not-enabled'
14
+ /** Phase 3.5: the agent's inner run needs an answer. NOT a failure — the step stays `pending` so a
15
+ * resume re-runs it rather than skipping its dependents. */
16
+ | 'agent-waiting';
10
17
  export interface StepObservation {
11
18
  stepId: string;
12
19
  skill?: string;
13
20
  tool?: string;
21
+ /** Phase 3.4: the agent DEFINITION id this step delegated to. */
22
+ agent?: string;
23
+ /** Phase 3.4: `agentTaskId`, NEVER `taskId` — that name belongs to the router. */
24
+ agentTaskId?: string;
14
25
  ok: boolean;
15
26
  output?: string;
16
27
  error?: string;
28
+ code?: StepObservationCode;
29
+ /** Phase 3.4, AGENT STEPS ONLY: `{ findings }`, bounded and clamped. Observations are persisted
30
+ * verbatim, so copying every skill's `data` here would be unbounded durable growth. */
31
+ data?: unknown;
32
+ /** Phase 3.4, agent steps only. */
33
+ artifacts?: ArtifactRef[];
34
+ /** Phase 3.4, agent steps only: METERED inner model calls. Charging the actual is the refund. */
35
+ callsUsed?: number;
36
+ }
37
+ /**
38
+ * A quiescent moment in `executePlan`, handed to `ExecuteDeps.onProgress` (Phase 3.5).
39
+ *
40
+ * Every fire point is between awaits — no `Promise.all` is in flight and every semaphore slot has been
41
+ * released — so the plan's step statuses are consistent and the snapshot describes a state the run could
42
+ * legitimately be resumed from. `observations` is a DELTA (what appeared since the previous fire), so a
43
+ * sink appends rather than de-duplicating; `callsUsed` is cumulative for this `executePlan` call.
44
+ */
45
+ export interface ProgressSnapshot {
46
+ at: 'wave-partition' | 'batch' | 'budget-stop' | 'plan-end';
47
+ plan: ExecutionPlan;
48
+ observations: StepObservation[];
49
+ callsUsed: number;
50
+ /** Step ids whose agent is waiting for input. Non-empty ⇒ the run is resumable, NOT failed. */
51
+ waiting: string[];
17
52
  }
18
53
  export interface ExecuteResult {
19
54
  ok: boolean;
20
55
  plan: ExecutionPlan;
21
56
  observations: StepObservation[];
57
+ /** Phase 22: true when execution paused BEFORE a wave because running it would exceed `callBudget`.
58
+ * Not a failure — the completed steps stand and the rest can resume with a raised budget. */
59
+ stoppedForBudget?: boolean;
60
+ /** Phase 3.5: steps left waiting for input. Non-empty ⇒ resumable; callers must not report `failed`. */
61
+ waiting?: string[];
62
+ /** Model calls (skill steps) actually executed this run. */
63
+ callsUsed?: number;
22
64
  }
23
65
  export interface ExecuteDeps {
24
66
  runSkill: (id: string, input: unknown) => Promise<{
@@ -30,6 +72,27 @@ export interface ExecuteDeps {
30
72
  signal?: AbortSignal;
31
73
  /** Step ids already completed in a prior run — marked succeeded and NOT re-executed (resume). */
32
74
  skip?: ReadonlySet<string>;
75
+ /** Phase 22: max model calls (skill steps) this run may make. When the NEXT wave's skill steps would
76
+ * exceed it, execution stops before that wave (phase granularity) with `stoppedForBudget:true`. */
77
+ callBudget?: number;
78
+ /** Phase 19: per-tool / per-skill concurrency caps (id → max concurrent). Bounds how many steps using a
79
+ * given tool/skill run at once, WITHIN the `maxParallelSteps` batch. Absent ⇒ no per-key limit. */
80
+ limits?: {
81
+ perTool?: Record<string, number>;
82
+ perSkill?: Record<string, number>;
83
+ perAgent?: Record<string, number>;
84
+ };
85
+ /** Phase 3.4: run one agent step. Absent ⇒ an agent step fails with code 'agent-not-enabled'. */
86
+ runAgent?: (step: PlanStep, ctx: {
87
+ reservation: number;
88
+ signal?: AbortSignal;
89
+ }) => Promise<StepObservation>;
90
+ /** Phase 3.4: what an agent step reserves. The SAME function the pre-flight estimate uses. */
91
+ reserve?: ReserveAgentCalls;
92
+ /** Phase 3.5: called at each quiescent point so progress reaches disk BEFORE the next wave starts
93
+ * (invariant 18). Synchronous and must not throw — the executor does not own persistence policy and
94
+ * will not try to recover from a sink that fails. */
95
+ onProgress?: (snapshot: ProgressSnapshot) => void;
33
96
  }
34
97
  /** Execute a plan. Runs DAG waves; within a wave, batches of at most maxParallelSteps run concurrently. */
35
98
  export declare function executePlan(plan: ExecutionPlan, deps: ExecuteDeps): Promise<ExecuteResult>;
@@ -5,7 +5,22 @@
5
5
  * never mutates anything itself; all side effects go through the permission-gated tools.
6
6
  */
7
7
  import { executionWaves } from './plan.js';
8
- async function runStep(step, deps) {
8
+ import { KeyedSemaphore } from '../util/semaphore.js';
9
+ import { foldCalls, stepCalls } from './budget.js';
10
+ /** Run a step, bounded by the per-tool/per-skill limiter when one is configured (else a direct call). */
11
+ async function runStep(step, deps, limiters) {
12
+ const exec = () => runStepInner(step, deps);
13
+ if (limiters) {
14
+ if (step.skill)
15
+ return limiters.skills.run(step.skill, exec);
16
+ if (step.tool)
17
+ return limiters.tools.run(step.tool, exec);
18
+ if (step.agent)
19
+ return limiters.agents.run(step.agent, exec);
20
+ }
21
+ return exec();
22
+ }
23
+ async function runStepInner(step, deps) {
9
24
  try {
10
25
  if (step.skill) {
11
26
  const { result, validation } = await deps.runSkill(step.skill, step.input);
@@ -16,6 +31,11 @@ async function runStep(step, deps) {
16
31
  const result = await deps.runTool(step.tool, step.input);
17
32
  return { stepId: step.id, tool: step.tool, ok: result.ok, ...(result.output !== undefined ? { output: result.output } : {}), ...(result.ok ? {} : { error: result.error?.message ?? 'tool failed' }) };
18
33
  }
34
+ if (step.agent) {
35
+ if (!deps.runAgent)
36
+ return { stepId: step.id, agent: step.agent, ok: false, code: 'agent-not-enabled', error: 'agent execution is not enabled' };
37
+ return deps.runAgent(step, { reservation: stepCalls(step, Number.POSITIVE_INFINITY, deps.reserve), ...(deps.signal ? { signal: deps.signal } : {}) });
38
+ }
19
39
  return { stepId: step.id, ok: false, error: 'step names neither a skill nor a tool' };
20
40
  }
21
41
  catch (err) {
@@ -25,8 +45,30 @@ async function runStep(step, deps) {
25
45
  /** Execute a plan. Runs DAG waves; within a wave, batches of at most maxParallelSteps run concurrently. */
26
46
  export async function executePlan(plan, deps) {
27
47
  const max = Math.max(1, deps.maxParallelSteps ?? 2);
48
+ // Per-tool/per-skill limiters are built once (persist across waves). Absent config ⇒ direct calls.
49
+ const limiters = deps.limits
50
+ ? { tools: new KeyedSemaphore(deps.limits.perTool ?? {}), skills: new KeyedSemaphore(deps.limits.perSkill ?? {}), agents: new KeyedSemaphore(deps.limits.perAgent ?? {}) }
51
+ : undefined;
28
52
  const observations = [];
29
53
  const failedOrSkipped = new Set(); // read for dependency-skipping; success is tracked via step.status
54
+ // Cancellation is distinguished from failure so a cancelled BRANCH never reads as a silent skip. Only
55
+ // a step carrying `agent` can enter this set, so with agents disabled it is provably empty and both
56
+ // branches below render exactly as 2.6.0.
57
+ const cancelled = new Set();
58
+ // Phase 3.5: steps whose agent asked for input. They are NOT failures — the step stays `pending`, its
59
+ // dependents are neither run nor skipped, and the run stops scheduling so it can be resumed.
60
+ const waiting = new Set();
61
+ let callsUsed = 0; // model calls = executed skill steps (tool steps are free)
62
+ let stoppedForBudget = false;
63
+ // Phase 3.5: observations are handed to the sink as a DELTA, so it appends instead of de-duplicating.
64
+ let emitted = 0;
65
+ const fire = (at) => {
66
+ if (!deps.onProgress)
67
+ return;
68
+ const delta = observations.slice(emitted);
69
+ emitted = observations.length;
70
+ deps.onProgress({ at, plan, observations: delta, callsUsed, waiting: [...waiting] });
71
+ };
30
72
  for (const wave of executionWaves(plan.steps)) {
31
73
  // A step runs only if all its dependencies succeeded; otherwise it is skipped.
32
74
  const runnable = [];
@@ -38,28 +80,83 @@ export async function executePlan(plan, deps) {
38
80
  else if (deps.signal?.aborted) {
39
81
  step.status = 'skipped';
40
82
  failedOrSkipped.add(step.id);
83
+ if (step.agent) {
84
+ // An agent step that never started because the run was aborted says so, rather than vanishing.
85
+ cancelled.add(step.id);
86
+ observations.push({ stepId: step.id, agent: step.agent, ok: false, code: 'cancelled', error: 'cancelled: the run was aborted' });
87
+ }
41
88
  }
42
89
  else if (depsFailed) {
43
90
  step.status = 'skipped';
44
91
  failedOrSkipped.add(step.id);
45
- observations.push({ stepId: step.id, ok: false, error: 'skipped: a dependency failed' });
92
+ if ((step.dependsOn ?? []).some((d) => cancelled.has(d))) {
93
+ cancelled.add(step.id);
94
+ observations.push({ stepId: step.id, ok: false, code: 'cancelled', error: 'cancelled: an agent branch was cancelled' });
95
+ }
96
+ else {
97
+ observations.push({ stepId: step.id, ok: false, error: 'skipped: a dependency failed' });
98
+ }
46
99
  }
47
100
  else {
48
101
  runnable.push(step);
49
102
  }
50
103
  }
104
+ // Every skipped/cancelled verdict for this wave is now decided and nothing is in flight: the
105
+ // cheapest honest moment to get the wave's shape on disk, and it precedes the budget stop below.
106
+ fire('wave-partition');
107
+ // Phase 22 budget gate: if running this wave's skill steps would exceed the call budget, stop BEFORE
108
+ // it (phase granularity). Remaining steps keep status 'pending' and can resume with a raised budget.
109
+ if (deps.callBudget !== undefined) {
110
+ const waveCalls = foldCalls(runnable, deps.callBudget - callsUsed, deps.reserve);
111
+ if (waveCalls > 0 && callsUsed + waveCalls > deps.callBudget) {
112
+ stoppedForBudget = true;
113
+ fire('budget-stop');
114
+ break;
115
+ }
116
+ }
51
117
  for (let i = 0; i < runnable.length; i += max) {
52
118
  const batch = runnable.slice(i, i + max);
53
119
  batch.forEach((s) => (s.status = 'running'));
54
- const results = await Promise.all(batch.map((s) => runStep(s, deps)));
120
+ const results = await Promise.all(batch.map((s) => runStep(s, deps, limiters)));
55
121
  results.forEach((obs, j) => {
56
122
  const step = batch[j];
57
- step.status = obs.ok ? 'succeeded' : 'failed';
58
- if (!obs.ok)
59
- failedOrSkipped.add(step.id);
123
+ // A waiting agent is the one non-terminal step outcome: it is neither a success nor a failure, so
124
+ // it must not mark the step `failed` — that would skip every dependent and make the branch
125
+ // unrecoverable on resume, which is the opposite of what waiting means.
126
+ const isWaiting = !obs.ok && obs.code === 'agent-waiting';
127
+ step.status = isWaiting ? 'pending' : obs.ok ? 'succeeded' : 'failed';
128
+ if (step.skill)
129
+ callsUsed += 1;
130
+ // Charging what an agent actually SPENT is the refund: an aborted or cancelled task reports its
131
+ // metered total and the unspent reservation is simply never charged. No ledger, no refund path.
132
+ else if (step.agent)
133
+ callsUsed += obs.callsUsed ?? stepCalls(step, Number.POSITIVE_INFINITY, deps.reserve);
134
+ if (isWaiting)
135
+ waiting.add(step.id);
136
+ else {
137
+ if (!obs.ok && obs.code === 'cancelled')
138
+ cancelled.add(step.id);
139
+ if (!obs.ok)
140
+ failedOrSkipped.add(step.id);
141
+ }
60
142
  observations.push(obs);
61
143
  });
144
+ fire('batch');
62
145
  }
146
+ // Something is waiting for a human. Scheduling further waves would run work whose inputs may change
147
+ // once the answer arrives, so the run stops here and resumes when it is answered.
148
+ if (waiting.size > 0)
149
+ break;
63
150
  }
64
- return { ok: plan.steps.every((s) => s.status === 'succeeded'), plan, observations };
151
+ // Success only if every step succeeded AND we didn't stop early for budget or for an answer.
152
+ const ok = !stoppedForBudget && waiting.size === 0 && plan.steps.every((s) => s.status === 'succeeded');
153
+ fire('plan-end');
154
+ return {
155
+ ok,
156
+ plan,
157
+ observations,
158
+ callsUsed,
159
+ ...(stoppedForBudget ? { stoppedForBudget: true } : {}),
160
+ ...(waiting.size > 0 ? { waiting: [...waiting] } : {}),
161
+ };
65
162
  }
@@ -10,9 +10,20 @@ import type { Skill, SkillResult, SkillValidation } from '../skills/skill.js';
10
10
  import type { ApprovalProvider } from '../runtime/host.js';
11
11
  import type { ExecutionPolicy } from '../runtime/policy.js';
12
12
  import type { ToolResult } from '../tools/tool.js';
13
+ import type { PlannerInput } from './planner.js';
14
+ import type { AgentEnvelope } from '../agents/envelope.js';
15
+ import type { ExecuteDeps } from './executor.js';
16
+ import type { CapabilityGap } from '../capabilities/capability.js';
13
17
  import type { StepObservation } from './executor.js';
14
18
  import type { ExecutionPlan } from './plan.js';
15
- export type OrchestrationStatus = 'completed' | 'failed' | 'waiting_for_clarification' | 'waiting_for_approval' | 'dry-run';
19
+ export type OrchestrationStatus = 'completed' | 'failed' | 'waiting_for_clarification' | 'waiting_for_approval' | 'waiting_for_budget' | 'dry-run';
20
+ /** Estimated vs available model-call budget for a plan (Phase 22). Structurally matches executions' BudgetInfo. */
21
+ export interface PlanBudget {
22
+ estCalls: number;
23
+ maxCalls: number;
24
+ completedSteps: number;
25
+ totalSteps: number;
26
+ }
16
27
  export interface OrchestrateInput {
17
28
  goal: string;
18
29
  /** plan = plan only; execute = single pass; orchestrate/agent/debug = replan loop. */
@@ -30,6 +41,25 @@ export interface OrchestrateInput {
30
41
  signal?: AbortSignal;
31
42
  /** User exclude/prefer routing, applied to planning-model calls. */
32
43
  routing?: RoutingPreferences;
44
+ /** Phase 22: run the phases that fit the call budget and pause resumably (vs. the default notify-and-wait). */
45
+ partial?: boolean;
46
+ /** Phase 3.1: pre-rendered action-capability snapshot for the planner prompt (opt-in). */
47
+ capabilityCatalog?: string;
48
+ /** Phase 3.3: pre-rendered "Required capabilities" block for the planner prompt (opt-in). */
49
+ requiredCapabilities?: string;
50
+ /** Phase 3.4: the narrowed envelopes for this run's agent definitions. Absent ⇒ agents are inert:
51
+ * the planner is told nothing about agents and `validatePlan` rejects any agent step. */
52
+ agents?: AgentEnvelope[];
53
+ /** Phase 3.4: runs one agent step. Supplied together with `agents` by the Runtime. */
54
+ runAgent?: ExecuteDeps['runAgent'];
55
+ /** Phase 3.1: maps unregistered plan references to structured gaps. */
56
+ resolveGaps?: PlannerInput['resolveGaps'];
57
+ /** Phase 3.5: the plan is settled — approved if approval was required, and inside the call budget.
58
+ * Fired BEFORE the first step runs, so a crash during wave 1 still resumes against a real plan
59
+ * instead of replanning from the bare goal. */
60
+ onPlan?: (plan: ExecutionPlan) => void;
61
+ /** Phase 3.5: forwarded verbatim to `executePlan` — the per-wave/per-batch commit points. */
62
+ onProgress?: ExecuteDeps['onProgress'];
33
63
  }
34
64
  export interface OrchestrateOutcome {
35
65
  status: OrchestrationStatus;
@@ -38,5 +68,9 @@ export interface OrchestrateOutcome {
38
68
  observations: StepObservation[];
39
69
  clarification?: string;
40
70
  summary: string;
71
+ /** Phase 3.1: structured capability gaps behind a planning failure (additive metadata). */
72
+ gaps?: CapabilityGap[];
73
+ /** Set on `waiting_for_budget` — the estimate/progress the caller surfaces and persists. */
74
+ budget?: PlanBudget;
41
75
  }
42
76
  export declare function orchestrate(input: OrchestrateInput): Promise<OrchestrateOutcome>;