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.
- package/CHANGELOG.md +602 -0
- package/dist/agents/admit.d.ts +69 -0
- package/dist/agents/admit.js +129 -0
- package/dist/agents/definition.d.ts +36 -0
- package/dist/agents/definition.js +9 -0
- package/dist/agents/envelope.d.ts +53 -0
- package/dist/agents/envelope.js +68 -0
- package/dist/agents/finding.d.ts +79 -0
- package/dist/agents/finding.js +80 -0
- package/dist/agents/task.d.ts +60 -0
- package/dist/agents/task.js +32 -0
- package/dist/agents/worker.d.ts +68 -0
- package/dist/agents/worker.js +256 -0
- package/dist/capabilities/capability.d.ts +117 -0
- package/dist/capabilities/capability.js +66 -0
- package/dist/capabilities/registry.d.ts +139 -0
- package/dist/capabilities/registry.js +413 -0
- package/dist/capabilities/vocabulary.d.ts +32 -0
- package/dist/capabilities/vocabulary.js +34 -0
- package/dist/cli/cli.js +56 -4
- package/dist/cli/commands/cleanup.js +29 -27
- package/dist/cli/commands/doctor.d.ts +14 -0
- package/dist/cli/commands/doctor.js +38 -8
- package/dist/cli/commands/executions.js +34 -25
- package/dist/cli/commands/info.d.ts +1 -0
- package/dist/cli/commands/info.js +11 -9
- package/dist/cli/commands/init.js +19 -0
- package/dist/cli/commands/inspect.d.ts +40 -1
- package/dist/cli/commands/inspect.js +157 -2
- package/dist/cli/commands/mcp.d.ts +45 -0
- package/dist/cli/commands/mcp.js +148 -0
- package/dist/cli/commands/route.js +21 -0
- package/dist/cli/commands/run.d.ts +2 -0
- package/dist/cli/commands/run.js +36 -4
- package/dist/cli/commands/skills.d.ts +2 -0
- package/dist/cli/commands/skills.js +29 -7
- package/dist/cli/interactive/ansi.d.ts +41 -0
- package/dist/cli/interactive/ansi.js +43 -0
- package/dist/cli/interactive/complete.d.ts +10 -0
- package/dist/cli/interactive/complete.js +19 -0
- package/dist/cli/interactive/repl.d.ts +3 -0
- package/dist/cli/interactive/repl.js +105 -16
- package/dist/cli/interactive/session.d.ts +12 -1
- package/dist/cli/interactive/session.js +83 -5
- package/dist/cli/render.d.ts +13 -0
- package/dist/cli/render.js +18 -0
- package/dist/cli/runtimeSession.d.ts +11 -0
- package/dist/cli/runtimeSession.js +17 -0
- package/dist/config/defaults.d.ts +3 -1
- package/dist/config/defaults.js +2 -0
- package/dist/config/schema.d.ts +1 -0
- package/dist/config/schema.js +2 -2
- package/dist/context/lossVerifier.d.ts +24 -0
- package/dist/context/lossVerifier.js +45 -0
- package/dist/context/summarize.d.ts +19 -0
- package/dist/context/summarize.js +53 -0
- package/dist/core/fallback/fallback.d.ts +8 -0
- package/dist/core/fallback/fallback.js +3 -1
- package/dist/core/router/executor.d.ts +6 -1
- package/dist/core/router/executor.js +9 -2
- package/dist/core/router/normalize.d.ts +2 -0
- package/dist/core/router/request.js +2 -0
- package/dist/core/router/router.d.ts +3 -0
- package/dist/core/router/router.js +7 -0
- package/dist/executions/execution.d.ts +13 -2
- package/dist/generation/generateAdapter.d.ts +14 -0
- package/dist/generation/generateAdapter.js +38 -0
- package/dist/generation/generateSkill.d.ts +26 -0
- package/dist/generation/generateSkill.js +51 -0
- package/dist/index.d.ts +44 -5
- package/dist/index.js +26 -2
- package/dist/mcp/client.d.ts +70 -0
- package/dist/mcp/client.js +221 -0
- package/dist/mcp/manager.d.ts +151 -0
- package/dist/mcp/manager.js +493 -0
- package/dist/mcp/protocol.d.ts +216 -0
- package/dist/mcp/protocol.js +149 -0
- package/dist/mcp/toolAdapter.d.ts +44 -0
- package/dist/mcp/toolAdapter.js +94 -0
- package/dist/mcp/transport.d.ts +109 -0
- package/dist/mcp/transport.js +383 -0
- package/dist/memory/embedders/hash.d.ts +12 -0
- package/dist/memory/embedders/hash.js +31 -0
- package/dist/memory/embedders/http.d.ts +25 -0
- package/dist/memory/embedders/http.js +48 -0
- package/dist/memory/memory.d.ts +19 -2
- package/dist/memory/memory.js +75 -11
- package/dist/memory/semantic.d.ts +17 -0
- package/dist/memory/semantic.js +29 -0
- package/dist/orchestration/budget.d.ts +30 -0
- package/dist/orchestration/budget.js +40 -0
- package/dist/orchestration/executor.d.ts +39 -1
- package/dist/orchestration/executor.js +64 -4
- package/dist/orchestration/orchestrator.d.ts +29 -1
- package/dist/orchestration/orchestrator.js +89 -8
- package/dist/orchestration/plan.d.ts +15 -1
- package/dist/orchestration/plan.js +23 -4
- package/dist/orchestration/planner.d.ts +19 -1
- package/dist/orchestration/planner.js +25 -5
- package/dist/plugin/ai.d.ts +4 -0
- package/dist/plugin/ai.js +9 -0
- package/dist/providers/httpClient.d.ts +25 -1
- package/dist/providers/httpClient.js +93 -0
- package/dist/providers/httpProvider.d.ts +1 -0
- package/dist/providers/httpProvider.js +67 -1
- package/dist/providers/mock/mockProvider.d.ts +3 -0
- package/dist/providers/mock/mockProvider.js +54 -0
- package/dist/providers/mock/scenarios.d.ts +7 -0
- package/dist/providers/provider.d.ts +6 -0
- package/dist/providers/wire/anthropicWire.js +34 -0
- package/dist/providers/wire/openaiWire.js +30 -0
- package/dist/providers/wire/types.d.ts +16 -0
- package/dist/runtime/config.js +50 -6
- package/dist/runtime/intent/aiClassifier.d.ts +19 -0
- package/dist/runtime/intent/aiClassifier.js +74 -0
- package/dist/runtime/models/modelProfile.d.ts +61 -0
- package/dist/runtime/models/modelProfile.js +139 -0
- package/dist/runtime/planning/deriveCapabilities.d.ts +95 -0
- package/dist/runtime/planning/deriveCapabilities.js +146 -0
- package/dist/runtime/policy.d.ts +10 -0
- package/dist/runtime/policy.js +9 -2
- package/dist/runtime/runtime.d.ts +173 -0
- package/dist/runtime/runtime.js +723 -50
- package/dist/runtime/types.d.ts +94 -2
- package/dist/skills/manifest.d.ts +3 -0
- package/dist/skills/manifest.js +24 -0
- package/dist/skills/registry.d.ts +16 -1
- package/dist/skills/registry.js +21 -1
- package/dist/skills/skill.d.ts +6 -1
- package/dist/store/area.d.ts +15 -1
- package/dist/store/area.js +19 -8
- package/dist/store/crypto.d.ts +21 -0
- package/dist/store/crypto.js +49 -0
- package/dist/store/paths.d.ts +5 -1
- package/dist/store/paths.js +6 -0
- package/dist/store/store.d.ts +15 -3
- package/dist/store/store.js +28 -7
- package/dist/telemetry/sinks/otlp.d.ts +31 -0
- package/dist/telemetry/sinks/otlp.js +76 -0
- package/dist/tools/builtins/filesystem.js +1 -0
- package/dist/tools/builtins/git.js +1 -0
- package/dist/tools/builtins/shell.js +1 -0
- package/dist/tools/permissions.d.ts +28 -0
- package/dist/tools/permissions.js +72 -0
- package/dist/tools/registry.d.ts +18 -2
- package/dist/tools/registry.js +22 -2
- package/dist/tools/tool.d.ts +4 -0
- package/dist/types.d.ts +11 -1
- package/dist/util/flatten.d.ts +11 -0
- package/dist/util/flatten.js +18 -0
- package/dist/util/semaphore.d.ts +19 -0
- package/dist/util/semaphore.js +60 -0
- package/package.json +24 -9
package/dist/memory/memory.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
|
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,40 @@
|
|
|
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';
|
|
10
14
|
export interface StepObservation {
|
|
11
15
|
stepId: string;
|
|
12
16
|
skill?: string;
|
|
13
17
|
tool?: string;
|
|
18
|
+
/** Phase 3.4: the agent DEFINITION id this step delegated to. */
|
|
19
|
+
agent?: string;
|
|
20
|
+
/** Phase 3.4: `agentTaskId`, NEVER `taskId` — that name belongs to the router. */
|
|
21
|
+
agentTaskId?: string;
|
|
14
22
|
ok: boolean;
|
|
15
23
|
output?: string;
|
|
16
24
|
error?: string;
|
|
25
|
+
code?: StepObservationCode;
|
|
26
|
+
/** Phase 3.4, AGENT STEPS ONLY: `{ findings }`, bounded and clamped. Observations are persisted
|
|
27
|
+
* verbatim, so copying every skill's `data` here would be unbounded durable growth. */
|
|
28
|
+
data?: unknown;
|
|
29
|
+
/** Phase 3.4, agent steps only. */
|
|
30
|
+
artifacts?: ArtifactRef[];
|
|
31
|
+
/** Phase 3.4, agent steps only: METERED inner model calls. Charging the actual is the refund. */
|
|
32
|
+
callsUsed?: number;
|
|
17
33
|
}
|
|
18
34
|
export interface ExecuteResult {
|
|
19
35
|
ok: boolean;
|
|
20
36
|
plan: ExecutionPlan;
|
|
21
37
|
observations: StepObservation[];
|
|
38
|
+
/** Phase 22: true when execution paused BEFORE a wave because running it would exceed `callBudget`.
|
|
39
|
+
* Not a failure — the completed steps stand and the rest can resume with a raised budget. */
|
|
40
|
+
stoppedForBudget?: boolean;
|
|
41
|
+
/** Model calls (skill steps) actually executed this run. */
|
|
42
|
+
callsUsed?: number;
|
|
22
43
|
}
|
|
23
44
|
export interface ExecuteDeps {
|
|
24
45
|
runSkill: (id: string, input: unknown) => Promise<{
|
|
@@ -30,6 +51,23 @@ export interface ExecuteDeps {
|
|
|
30
51
|
signal?: AbortSignal;
|
|
31
52
|
/** Step ids already completed in a prior run — marked succeeded and NOT re-executed (resume). */
|
|
32
53
|
skip?: ReadonlySet<string>;
|
|
54
|
+
/** Phase 22: max model calls (skill steps) this run may make. When the NEXT wave's skill steps would
|
|
55
|
+
* exceed it, execution stops before that wave (phase granularity) with `stoppedForBudget:true`. */
|
|
56
|
+
callBudget?: number;
|
|
57
|
+
/** Phase 19: per-tool / per-skill concurrency caps (id → max concurrent). Bounds how many steps using a
|
|
58
|
+
* given tool/skill run at once, WITHIN the `maxParallelSteps` batch. Absent ⇒ no per-key limit. */
|
|
59
|
+
limits?: {
|
|
60
|
+
perTool?: Record<string, number>;
|
|
61
|
+
perSkill?: Record<string, number>;
|
|
62
|
+
perAgent?: Record<string, number>;
|
|
63
|
+
};
|
|
64
|
+
/** Phase 3.4: run one agent step. Absent ⇒ an agent step fails with code 'agent-not-enabled'. */
|
|
65
|
+
runAgent?: (step: PlanStep, ctx: {
|
|
66
|
+
reservation: number;
|
|
67
|
+
signal?: AbortSignal;
|
|
68
|
+
}) => Promise<StepObservation>;
|
|
69
|
+
/** Phase 3.4: what an agent step reserves. The SAME function the pre-flight estimate uses. */
|
|
70
|
+
reserve?: ReserveAgentCalls;
|
|
33
71
|
}
|
|
34
72
|
/** Execute a plan. Runs DAG waves; within a wave, batches of at most maxParallelSteps run concurrently. */
|
|
35
73
|
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
|
-
|
|
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,18 @@ 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
|
+
let callsUsed = 0; // model calls = executed skill steps (tool steps are free)
|
|
59
|
+
let stoppedForBudget = false;
|
|
30
60
|
for (const wave of executionWaves(plan.steps)) {
|
|
31
61
|
// A step runs only if all its dependencies succeeded; otherwise it is skipped.
|
|
32
62
|
const runnable = [];
|
|
@@ -38,28 +68,58 @@ export async function executePlan(plan, deps) {
|
|
|
38
68
|
else if (deps.signal?.aborted) {
|
|
39
69
|
step.status = 'skipped';
|
|
40
70
|
failedOrSkipped.add(step.id);
|
|
71
|
+
if (step.agent) {
|
|
72
|
+
// An agent step that never started because the run was aborted says so, rather than vanishing.
|
|
73
|
+
cancelled.add(step.id);
|
|
74
|
+
observations.push({ stepId: step.id, agent: step.agent, ok: false, code: 'cancelled', error: 'cancelled: the run was aborted' });
|
|
75
|
+
}
|
|
41
76
|
}
|
|
42
77
|
else if (depsFailed) {
|
|
43
78
|
step.status = 'skipped';
|
|
44
79
|
failedOrSkipped.add(step.id);
|
|
45
|
-
|
|
80
|
+
if ((step.dependsOn ?? []).some((d) => cancelled.has(d))) {
|
|
81
|
+
cancelled.add(step.id);
|
|
82
|
+
observations.push({ stepId: step.id, ok: false, code: 'cancelled', error: 'cancelled: an agent branch was cancelled' });
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
observations.push({ stepId: step.id, ok: false, error: 'skipped: a dependency failed' });
|
|
86
|
+
}
|
|
46
87
|
}
|
|
47
88
|
else {
|
|
48
89
|
runnable.push(step);
|
|
49
90
|
}
|
|
50
91
|
}
|
|
92
|
+
// Phase 22 budget gate: if running this wave's skill steps would exceed the call budget, stop BEFORE
|
|
93
|
+
// it (phase granularity). Remaining steps keep status 'pending' and can resume with a raised budget.
|
|
94
|
+
if (deps.callBudget !== undefined) {
|
|
95
|
+
const waveCalls = foldCalls(runnable, deps.callBudget - callsUsed, deps.reserve);
|
|
96
|
+
if (waveCalls > 0 && callsUsed + waveCalls > deps.callBudget) {
|
|
97
|
+
stoppedForBudget = true;
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
51
101
|
for (let i = 0; i < runnable.length; i += max) {
|
|
52
102
|
const batch = runnable.slice(i, i + max);
|
|
53
103
|
batch.forEach((s) => (s.status = 'running'));
|
|
54
|
-
const results = await Promise.all(batch.map((s) => runStep(s, deps)));
|
|
104
|
+
const results = await Promise.all(batch.map((s) => runStep(s, deps, limiters)));
|
|
55
105
|
results.forEach((obs, j) => {
|
|
56
106
|
const step = batch[j];
|
|
57
107
|
step.status = obs.ok ? 'succeeded' : 'failed';
|
|
108
|
+
if (step.skill)
|
|
109
|
+
callsUsed += 1;
|
|
110
|
+
// Charging what an agent actually SPENT is the refund: an aborted or cancelled task reports its
|
|
111
|
+
// metered total and the unspent reservation is simply never charged. No ledger, no refund path.
|
|
112
|
+
else if (step.agent)
|
|
113
|
+
callsUsed += obs.callsUsed ?? stepCalls(step, Number.POSITIVE_INFINITY, deps.reserve);
|
|
114
|
+
if (!obs.ok && obs.code === 'cancelled')
|
|
115
|
+
cancelled.add(step.id);
|
|
58
116
|
if (!obs.ok)
|
|
59
117
|
failedOrSkipped.add(step.id);
|
|
60
118
|
observations.push(obs);
|
|
61
119
|
});
|
|
62
120
|
}
|
|
63
121
|
}
|
|
64
|
-
|
|
122
|
+
// Success only if every step succeeded AND we didn't stop early for budget.
|
|
123
|
+
const ok = !stoppedForBudget && plan.steps.every((s) => s.status === 'succeeded');
|
|
124
|
+
return { ok, plan, observations, callsUsed, ...(stoppedForBudget ? { stoppedForBudget: true } : {}) };
|
|
65
125
|
}
|
|
@@ -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,19 @@ 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'];
|
|
33
57
|
}
|
|
34
58
|
export interface OrchestrateOutcome {
|
|
35
59
|
status: OrchestrationStatus;
|
|
@@ -38,5 +62,9 @@ export interface OrchestrateOutcome {
|
|
|
38
62
|
observations: StepObservation[];
|
|
39
63
|
clarification?: string;
|
|
40
64
|
summary: string;
|
|
65
|
+
/** Phase 3.1: structured capability gaps behind a planning failure (additive metadata). */
|
|
66
|
+
gaps?: CapabilityGap[];
|
|
67
|
+
/** Set on `waiting_for_budget` — the estimate/progress the caller surfaces and persists. */
|
|
68
|
+
budget?: PlanBudget;
|
|
41
69
|
}
|
|
42
70
|
export declare function orchestrate(input: OrchestrateInput): Promise<OrchestrateOutcome>;
|