@wrongstack/core 0.6.3 → 0.6.5
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/dist/{agent-bridge-eb7qnNrd.d.ts → agent-bridge-BBXK_ppx.d.ts} +1 -1
- package/dist/agent-subagent-runner-DsSm9lKN.d.ts +174 -0
- package/dist/{compactor-RIPuTtWK.d.ts → compactor-C8NhpSt5.d.ts} +1 -1
- package/dist/{config-BGGuP_Ar.d.ts → config-DfC6g6KV.d.ts} +1 -1
- package/dist/{context-CDRyrkKQ.d.ts → context-DN5v-uQX.d.ts} +11 -0
- package/dist/coordination/index.d.ts +10 -9
- package/dist/coordination/index.js +113 -20
- package/dist/coordination/index.js.map +1 -1
- package/dist/defaults/index.d.ts +21 -20
- package/dist/defaults/index.js +1329 -925
- package/dist/defaults/index.js.map +1 -1
- package/dist/{events-BHuIHekD.d.ts → events-CJqwQl8G.d.ts} +17 -1
- package/dist/execution/index.d.ts +82 -13
- package/dist/execution/index.js +1366 -74
- package/dist/execution/index.js.map +1 -1
- package/dist/extension/index.d.ts +6 -6
- package/dist/{goal-store-DVCfj7Ff.d.ts → goal-store-_Er467ya.d.ts} +2 -2
- package/dist/{index-CPcDqvZh.d.ts → index-CXnWsGBp.d.ts} +11 -175
- package/dist/{index-BOn9NK7D.d.ts → index-DcnXDPdY.d.ts} +6 -6
- package/dist/index.d.ts +28 -27
- package/dist/index.js +1068 -664
- package/dist/index.js.map +1 -1
- package/dist/infrastructure/index.d.ts +6 -6
- package/dist/kernel/index.d.ts +9 -9
- package/dist/kernel/index.js.map +1 -1
- package/dist/{mcp-servers-DBdh3cee.d.ts → mcp-servers-CevFHHM1.d.ts} +3 -3
- package/dist/models/index.d.ts +2 -2
- package/dist/models/index.js +5 -16
- package/dist/models/index.js.map +1 -1
- package/dist/{multi-agent-CxSb-9dQ.d.ts → multi-agent-D5IbASk_.d.ts} +16 -4
- package/dist/observability/index.d.ts +2 -2
- package/dist/{path-resolver-CMGNadvq.d.ts → path-resolver-CBx_q1HA.d.ts} +2 -2
- package/dist/{plan-templates-BJflQY2i.d.ts → plan-templates-BEOllUJV.d.ts} +5 -4
- package/dist/{provider-runner-BFgNXpaP.d.ts → provider-runner-Byh5TcJs.d.ts} +3 -3
- package/dist/{retry-policy-LKS8MHsB.d.ts → retry-policy-BZSIMxrJ.d.ts} +1 -1
- package/dist/sdd/index.d.ts +3 -3
- package/dist/{secret-scrubber-CfMdAJ_l.d.ts → secret-scrubber-CT7wefiO.d.ts} +1 -1
- package/dist/{secret-scrubber-BzQR5BiL.d.ts → secret-scrubber-I0QHY_ob.d.ts} +1 -1
- package/dist/security/index.d.ts +3 -3
- package/dist/{selector-C7HqnZJU.d.ts → selector-DDb_mq9X.d.ts} +1 -1
- package/dist/{session-reader-CzfRA6Vk.d.ts → session-reader-B9nVkziM.d.ts} +1 -1
- package/dist/storage/index.d.ts +6 -6
- package/dist/storage/index.js +27 -2
- package/dist/storage/index.js.map +1 -1
- package/dist/{system-prompt-Dl2QY1_B.d.ts → system-prompt-gL06H9P4.d.ts} +1 -1
- package/dist/{tool-executor-FoxBjULX.d.ts → tool-executor-BF7QfYVE.d.ts} +4 -4
- package/dist/types/index.d.ts +15 -15
- package/dist/types/index.js +5 -16
- package/dist/types/index.js.map +1 -1
- package/dist/utils/index.d.ts +1 -1
- package/package.json +1 -1
- package/skills/audit-log/SKILL.md +57 -28
- package/skills/bug-hunter/SKILL.md +85 -61
- package/skills/git-flow/SKILL.md +73 -18
- package/skills/multi-agent/SKILL.md +69 -40
- package/skills/node-modern/SKILL.md +111 -19
- package/skills/prompt-engineering/SKILL.md +97 -16
- package/skills/react-modern/SKILL.md +104 -18
- package/skills/refactor-planner/SKILL.md +73 -43
- package/skills/sdd/SKILL.md +54 -112
- package/skills/security-scanner/SKILL.md +95 -93
- package/skills/skill-creator/SKILL.md +58 -25
- package/skills/typescript-strict/SKILL.md +107 -15
package/dist/execution/index.js
CHANGED
|
@@ -2,7 +2,8 @@ import { execFile } from 'child_process';
|
|
|
2
2
|
import { promisify } from 'util';
|
|
3
3
|
import * as fs from 'fs/promises';
|
|
4
4
|
import * as path from 'path';
|
|
5
|
-
import { randomBytes } from 'crypto';
|
|
5
|
+
import { randomUUID, randomBytes } from 'crypto';
|
|
6
|
+
import { EventEmitter } from 'events';
|
|
6
7
|
|
|
7
8
|
// src/utils/token-estimate.ts
|
|
8
9
|
var RoughTokenEstimate = (text, charsPerToken = 3.5) => Math.max(1, Math.ceil(text.length / charsPerToken));
|
|
@@ -2231,6 +2232,1369 @@ function sleep(ms) {
|
|
|
2231
2232
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
2232
2233
|
}
|
|
2233
2234
|
|
|
2235
|
+
// src/coordination/subagent-budget.ts
|
|
2236
|
+
var BudgetExceededError = class extends Error {
|
|
2237
|
+
kind;
|
|
2238
|
+
limit;
|
|
2239
|
+
observed;
|
|
2240
|
+
constructor(kind, limit, observed) {
|
|
2241
|
+
super(`Budget exceeded: ${kind} (limit=${limit}, observed=${observed})`);
|
|
2242
|
+
this.name = "BudgetExceededError";
|
|
2243
|
+
this.kind = kind;
|
|
2244
|
+
this.limit = limit;
|
|
2245
|
+
this.observed = observed;
|
|
2246
|
+
}
|
|
2247
|
+
};
|
|
2248
|
+
var BudgetThresholdSignal = class extends Error {
|
|
2249
|
+
kind;
|
|
2250
|
+
limit;
|
|
2251
|
+
used;
|
|
2252
|
+
/** Resolves to 'extend' (with optional new limits) or 'stop' */
|
|
2253
|
+
decision;
|
|
2254
|
+
constructor(kind, limit, used, decision) {
|
|
2255
|
+
super(`Budget soft limit: ${kind} (limit=${limit}, used=${used})`);
|
|
2256
|
+
this.name = "BudgetThresholdSignal";
|
|
2257
|
+
this.kind = kind;
|
|
2258
|
+
this.limit = limit;
|
|
2259
|
+
this.used = used;
|
|
2260
|
+
this.decision = decision;
|
|
2261
|
+
}
|
|
2262
|
+
};
|
|
2263
|
+
var SubagentBudget = class _SubagentBudget {
|
|
2264
|
+
limits;
|
|
2265
|
+
iterations = 0;
|
|
2266
|
+
toolCalls = 0;
|
|
2267
|
+
tokenInput = 0;
|
|
2268
|
+
tokenOutput = 0;
|
|
2269
|
+
costUsd = 0;
|
|
2270
|
+
startTime = null;
|
|
2271
|
+
_onThreshold;
|
|
2272
|
+
/**
|
|
2273
|
+
* Tracks which budget kinds currently have an extension request
|
|
2274
|
+
* in flight. While a kind is here, further `checkLimit` calls for the
|
|
2275
|
+
* same kind are no-ops — without this dedup, every `recordIteration`
|
|
2276
|
+
* after the limit is reached spawns a fresh decision Promise (until
|
|
2277
|
+
* the first one lands and patches limits), flooding the FleetBus
|
|
2278
|
+
* with redundant threshold events. Cleared in `checkLimitAsync`'s
|
|
2279
|
+
* `finally`.
|
|
2280
|
+
*/
|
|
2281
|
+
pendingExtensions = /* @__PURE__ */ new Set();
|
|
2282
|
+
/**
|
|
2283
|
+
* Hard cap on how long `checkLimitAsync` waits for the coordinator to
|
|
2284
|
+
* respond before defaulting to 'stop'. Without this fallback an absent
|
|
2285
|
+
* or hung listener (Director not built / event filter detached mid-run)
|
|
2286
|
+
* leaves the budget over-limit and never enforces anything, since
|
|
2287
|
+
* `checkLimit` returns synchronously via `void this.checkLimitAsync`.
|
|
2288
|
+
* Hardcoded for now — most fleets set their own per-task timeout that
|
|
2289
|
+
* dwarfs this anyway. 30 s matches the existing event-emit timeoutMs.
|
|
2290
|
+
*/
|
|
2291
|
+
static DECISION_TIMEOUT_MS = 3e4;
|
|
2292
|
+
/**
|
|
2293
|
+
* Injected by the runner when wiring the budget to its EventBus.
|
|
2294
|
+
* Used by `checkLimitAsync` to emit `budget.threshold_reached` events.
|
|
2295
|
+
*/
|
|
2296
|
+
_events;
|
|
2297
|
+
/**
|
|
2298
|
+
* Optional callback for soft-limit handling. When set, the budget will
|
|
2299
|
+
* call this instead of throwing when a limit is first reached. The
|
|
2300
|
+
* handler decides whether to throw, continue, or ask the coordinator.
|
|
2301
|
+
*/
|
|
2302
|
+
get onThreshold() {
|
|
2303
|
+
return this._onThreshold;
|
|
2304
|
+
}
|
|
2305
|
+
set onThreshold(fn) {
|
|
2306
|
+
this._onThreshold = fn;
|
|
2307
|
+
}
|
|
2308
|
+
constructor(limits = {}) {
|
|
2309
|
+
this.limits = Object.freeze({ ...limits });
|
|
2310
|
+
}
|
|
2311
|
+
start() {
|
|
2312
|
+
this.startTime = Date.now();
|
|
2313
|
+
}
|
|
2314
|
+
/** Returns true if we're within 10% of any limit — useful for pre-flight checks. */
|
|
2315
|
+
isNearLimit() {
|
|
2316
|
+
const { maxIterations, maxToolCalls, maxTokens, maxCostUsd } = this.limits;
|
|
2317
|
+
if (maxIterations && this.iterations >= maxIterations * 0.9) return true;
|
|
2318
|
+
if (maxToolCalls && this.toolCalls >= maxToolCalls * 0.9) return true;
|
|
2319
|
+
if (maxTokens && this.tokenInput + this.tokenOutput >= maxTokens * 0.9) return true;
|
|
2320
|
+
if (maxCostUsd && this.costUsd >= maxCostUsd * 0.9) return true;
|
|
2321
|
+
return false;
|
|
2322
|
+
}
|
|
2323
|
+
/**
|
|
2324
|
+
* Synchronous budget check — always throws synchronously so callers
|
|
2325
|
+
* (especially test event handlers using `expect().toThrow()`) get an
|
|
2326
|
+
* unhandled rejection when the budget is exceeded without a handler.
|
|
2327
|
+
* When `onThreshold` IS configured, the actual async threshold-handling
|
|
2328
|
+
* is dispatched as a fire-and-forget promise.
|
|
2329
|
+
*/
|
|
2330
|
+
checkLimit(kind, used, limit) {
|
|
2331
|
+
if (!this._onThreshold) {
|
|
2332
|
+
throw new BudgetExceededError(kind, limit, used);
|
|
2333
|
+
}
|
|
2334
|
+
const bus = this._events;
|
|
2335
|
+
if (!bus || bus.listenerCount("budget.threshold_reached") === 0) {
|
|
2336
|
+
throw new BudgetExceededError(kind, limit, used);
|
|
2337
|
+
}
|
|
2338
|
+
if (this.pendingExtensions.has(kind)) return;
|
|
2339
|
+
this.pendingExtensions.add(kind);
|
|
2340
|
+
const decision = this.negotiateExtension(kind, used, limit);
|
|
2341
|
+
throw new BudgetThresholdSignal(kind, limit, used, decision);
|
|
2342
|
+
}
|
|
2343
|
+
/**
|
|
2344
|
+
* Drive the threshold handler to a decision. Resolves with `'stop'`
|
|
2345
|
+
* (signal the runner to abort) or `{ extend: ... }` (limits already
|
|
2346
|
+
* patched in-place; the runner should not abort). Always releases the
|
|
2347
|
+
* `pendingExtensions` slot in `finally`.
|
|
2348
|
+
*
|
|
2349
|
+
* The 'continue' return from a sync handler is treated as
|
|
2350
|
+
* `{ extend: {} }` — keep going without patching, next overrun will
|
|
2351
|
+
* fire a fresh signal.
|
|
2352
|
+
*/
|
|
2353
|
+
async negotiateExtension(kind, used, limit) {
|
|
2354
|
+
try {
|
|
2355
|
+
const result = this._onThreshold({
|
|
2356
|
+
kind,
|
|
2357
|
+
used,
|
|
2358
|
+
limit,
|
|
2359
|
+
// Inject a requestDecision helper the handler can call to emit the
|
|
2360
|
+
// budget.threshold_reached event and wait for the coordinator's verdict.
|
|
2361
|
+
// A hard fallback timer guarantees the promise eventually resolves
|
|
2362
|
+
// even if no listener responds — without it, an absent/detached
|
|
2363
|
+
// Director would leave the budget permanently in "asking" state.
|
|
2364
|
+
requestDecision: () => {
|
|
2365
|
+
const bus = this._events;
|
|
2366
|
+
if (!bus || bus.listenerCount("budget.threshold_reached") === 0) {
|
|
2367
|
+
return Promise.resolve("stop");
|
|
2368
|
+
}
|
|
2369
|
+
return new Promise((resolve) => {
|
|
2370
|
+
let resolved = false;
|
|
2371
|
+
const respond = (d) => {
|
|
2372
|
+
if (resolved) return;
|
|
2373
|
+
resolved = true;
|
|
2374
|
+
resolve(d);
|
|
2375
|
+
};
|
|
2376
|
+
const fallback = setTimeout(
|
|
2377
|
+
() => respond("stop"),
|
|
2378
|
+
_SubagentBudget.DECISION_TIMEOUT_MS
|
|
2379
|
+
);
|
|
2380
|
+
bus.emit("budget.threshold_reached", {
|
|
2381
|
+
kind,
|
|
2382
|
+
used,
|
|
2383
|
+
limit,
|
|
2384
|
+
timeoutMs: _SubagentBudget.DECISION_TIMEOUT_MS,
|
|
2385
|
+
extend: (extra) => {
|
|
2386
|
+
clearTimeout(fallback);
|
|
2387
|
+
respond({ extend: extra });
|
|
2388
|
+
},
|
|
2389
|
+
deny: () => {
|
|
2390
|
+
clearTimeout(fallback);
|
|
2391
|
+
respond("stop");
|
|
2392
|
+
}
|
|
2393
|
+
});
|
|
2394
|
+
});
|
|
2395
|
+
}
|
|
2396
|
+
});
|
|
2397
|
+
if (result === "throw") return "stop";
|
|
2398
|
+
if (result === "continue") return { extend: {} };
|
|
2399
|
+
const decision = await result;
|
|
2400
|
+
if (decision === "stop") return "stop";
|
|
2401
|
+
const ext = decision.extend;
|
|
2402
|
+
if (ext.maxIterations !== void 0) {
|
|
2403
|
+
this.limits.maxIterations = ext.maxIterations;
|
|
2404
|
+
}
|
|
2405
|
+
if (ext.maxToolCalls !== void 0) {
|
|
2406
|
+
this.limits.maxToolCalls = ext.maxToolCalls;
|
|
2407
|
+
}
|
|
2408
|
+
if (ext.maxTokens !== void 0) {
|
|
2409
|
+
this.limits.maxTokens = ext.maxTokens;
|
|
2410
|
+
}
|
|
2411
|
+
if (ext.maxCostUsd !== void 0) {
|
|
2412
|
+
this.limits.maxCostUsd = ext.maxCostUsd;
|
|
2413
|
+
}
|
|
2414
|
+
if (ext.timeoutMs !== void 0) {
|
|
2415
|
+
this.limits.timeoutMs = ext.timeoutMs;
|
|
2416
|
+
}
|
|
2417
|
+
return decision;
|
|
2418
|
+
} finally {
|
|
2419
|
+
this.pendingExtensions.delete(kind);
|
|
2420
|
+
}
|
|
2421
|
+
}
|
|
2422
|
+
recordIteration() {
|
|
2423
|
+
this.iterations++;
|
|
2424
|
+
if (this.limits.maxIterations !== void 0 && this.iterations > this.limits.maxIterations) {
|
|
2425
|
+
void this.checkLimit("iterations", this.iterations, this.limits.maxIterations);
|
|
2426
|
+
}
|
|
2427
|
+
}
|
|
2428
|
+
recordToolCall() {
|
|
2429
|
+
this.toolCalls++;
|
|
2430
|
+
if (this.limits.maxToolCalls !== void 0 && this.toolCalls > this.limits.maxToolCalls) {
|
|
2431
|
+
void this.checkLimit("tool_calls", this.toolCalls, this.limits.maxToolCalls);
|
|
2432
|
+
}
|
|
2433
|
+
}
|
|
2434
|
+
recordUsage(usage, costUsd = 0) {
|
|
2435
|
+
this.tokenInput += usage.input;
|
|
2436
|
+
this.tokenOutput += usage.output;
|
|
2437
|
+
this.costUsd += costUsd;
|
|
2438
|
+
const totalTokens = this.tokenInput + this.tokenOutput;
|
|
2439
|
+
if (this.limits.maxTokens !== void 0 && totalTokens > this.limits.maxTokens) {
|
|
2440
|
+
void this.checkLimit("tokens", totalTokens, this.limits.maxTokens);
|
|
2441
|
+
}
|
|
2442
|
+
if (this.limits.maxCostUsd !== void 0 && this.costUsd > this.limits.maxCostUsd) {
|
|
2443
|
+
void this.checkLimit("cost", this.costUsd, this.limits.maxCostUsd);
|
|
2444
|
+
}
|
|
2445
|
+
}
|
|
2446
|
+
/**
|
|
2447
|
+
* Wall-clock budget check. Unlike other limits, timeout is treated as a
|
|
2448
|
+
* warning-only event — it NEVER hard-stops the subagent. When the
|
|
2449
|
+
* elapsed time exceeds timeoutMs, emits `budget.threshold_reached` with
|
|
2450
|
+
* kind='timeout' so the Director can decide whether to extend or warn.
|
|
2451
|
+
* Call this from the iteration loop so a hung tool gets a chance to
|
|
2452
|
+
* negotiate more time before the coordinator's Promise.race kills it.
|
|
2453
|
+
*/
|
|
2454
|
+
checkTimeout() {
|
|
2455
|
+
if (this.startTime === null || this.limits.timeoutMs === void 0) return;
|
|
2456
|
+
const elapsed = Date.now() - this.startTime;
|
|
2457
|
+
if (elapsed > this.limits.timeoutMs) {
|
|
2458
|
+
void this.checkLimit("timeout", elapsed, this.limits.timeoutMs);
|
|
2459
|
+
}
|
|
2460
|
+
}
|
|
2461
|
+
/** Returns true if a timeout has occurred without throwing. Useful for races. */
|
|
2462
|
+
isTimedOut() {
|
|
2463
|
+
if (this.startTime === null || this.limits.timeoutMs === void 0) return false;
|
|
2464
|
+
return Date.now() - this.startTime > this.limits.timeoutMs;
|
|
2465
|
+
}
|
|
2466
|
+
usage() {
|
|
2467
|
+
return {
|
|
2468
|
+
iterations: this.iterations,
|
|
2469
|
+
toolCalls: this.toolCalls,
|
|
2470
|
+
tokens: {
|
|
2471
|
+
input: this.tokenInput,
|
|
2472
|
+
output: this.tokenOutput,
|
|
2473
|
+
total: this.tokenInput + this.tokenOutput
|
|
2474
|
+
},
|
|
2475
|
+
costUsd: this.costUsd,
|
|
2476
|
+
elapsedMs: this.startTime === null ? 0 : Date.now() - this.startTime
|
|
2477
|
+
};
|
|
2478
|
+
}
|
|
2479
|
+
};
|
|
2480
|
+
|
|
2481
|
+
// src/coordination/agent-subagent-runner.ts
|
|
2482
|
+
function makeAgentSubagentRunner(opts) {
|
|
2483
|
+
const format = opts.formatTaskInput ?? defaultFormatTaskInput;
|
|
2484
|
+
return async (task, ctx) => {
|
|
2485
|
+
const factoryResult = await opts.factory(ctx.config);
|
|
2486
|
+
const { agent, events } = factoryResult;
|
|
2487
|
+
const detachFleet = opts.fleetBus?.attach(ctx.subagentId, events, task.id);
|
|
2488
|
+
const aborter = new AbortController();
|
|
2489
|
+
ctx.budget._events = events;
|
|
2490
|
+
ctx.budget.onThreshold = ({ requestDecision }) => requestDecision();
|
|
2491
|
+
let budgetError = null;
|
|
2492
|
+
const onBudgetError = (err) => {
|
|
2493
|
+
if (err instanceof BudgetThresholdSignal) {
|
|
2494
|
+
err.decision.then((decision) => {
|
|
2495
|
+
if (decision === "stop") {
|
|
2496
|
+
budgetError = new BudgetExceededError(err.kind, err.limit, err.used);
|
|
2497
|
+
aborter.abort();
|
|
2498
|
+
}
|
|
2499
|
+
}).catch(() => {
|
|
2500
|
+
budgetError = new BudgetExceededError(err.kind, err.limit, err.used);
|
|
2501
|
+
aborter.abort();
|
|
2502
|
+
});
|
|
2503
|
+
return;
|
|
2504
|
+
}
|
|
2505
|
+
aborter.abort();
|
|
2506
|
+
budgetError = err instanceof BudgetExceededError ? err : new BudgetExceededError(
|
|
2507
|
+
"tool_calls",
|
|
2508
|
+
0,
|
|
2509
|
+
0
|
|
2510
|
+
);
|
|
2511
|
+
if (budgetError !== err && err instanceof Error) {
|
|
2512
|
+
budgetError.message += ` (caused by: ${err.message})`;
|
|
2513
|
+
}
|
|
2514
|
+
};
|
|
2515
|
+
let lastToolFailed = null;
|
|
2516
|
+
const unsub = [];
|
|
2517
|
+
unsub.push(
|
|
2518
|
+
events.on("tool.executed", (e) => {
|
|
2519
|
+
try {
|
|
2520
|
+
ctx.budget.recordToolCall();
|
|
2521
|
+
} catch (eb) {
|
|
2522
|
+
onBudgetError(eb);
|
|
2523
|
+
}
|
|
2524
|
+
if (e.ok === false) {
|
|
2525
|
+
lastToolFailed = e.name;
|
|
2526
|
+
} else if (e.ok === true) {
|
|
2527
|
+
lastToolFailed = null;
|
|
2528
|
+
}
|
|
2529
|
+
}),
|
|
2530
|
+
events.on("provider.response", (e) => {
|
|
2531
|
+
try {
|
|
2532
|
+
ctx.budget.recordUsage(e.usage);
|
|
2533
|
+
} catch (e2) {
|
|
2534
|
+
void onBudgetError(e2);
|
|
2535
|
+
}
|
|
2536
|
+
}),
|
|
2537
|
+
events.on("iteration.started", () => {
|
|
2538
|
+
try {
|
|
2539
|
+
ctx.budget.recordIteration();
|
|
2540
|
+
} catch (e) {
|
|
2541
|
+
void onBudgetError(e);
|
|
2542
|
+
}
|
|
2543
|
+
const u = ctx.budget.usage();
|
|
2544
|
+
const since = u.iterations - lastSummaryAtIteration;
|
|
2545
|
+
if (since >= SUMMARY_INTERVAL) {
|
|
2546
|
+
lastSummaryAtIteration = u.iterations;
|
|
2547
|
+
events.emit("subagent.iteration_summary", {
|
|
2548
|
+
subagentId: ctx.subagentId,
|
|
2549
|
+
iteration: u.iterations,
|
|
2550
|
+
toolCalls: u.toolCalls,
|
|
2551
|
+
costUsd: u.costUsd,
|
|
2552
|
+
currentTool: currentToolName,
|
|
2553
|
+
partialText: streamingTextAcc.trim() || void 0
|
|
2554
|
+
});
|
|
2555
|
+
}
|
|
2556
|
+
}),
|
|
2557
|
+
// D3: cooperative timeout enforcement DURING a long tool call.
|
|
2558
|
+
// The iteration-loop checkTimeout() only fires between agent
|
|
2559
|
+
// iterations — a single `bash sleep 3600` call would otherwise
|
|
2560
|
+
// park inside one tool execution while the timeout silently
|
|
2561
|
+
// passes, relying solely on the coordinator's hard Promise.race
|
|
2562
|
+
// to interrupt. Tools that emit `tool.progress` (bash chunks,
|
|
2563
|
+
// fetch byte progress, spawn-stream stdout) give us a heartbeat
|
|
2564
|
+
// we can hang the check on. When the budget trips here:
|
|
2565
|
+
// 1. onBudgetError sets budgetError + aborter.abort()
|
|
2566
|
+
// 2. aborter signal propagates to agent.run → tool executor
|
|
2567
|
+
// 3. tool's own signal listener kills the child process
|
|
2568
|
+
// Cheap: O(1) per progress event, and the budget short-circuits
|
|
2569
|
+
// when timeoutMs is unset (most subagents have one set anyway).
|
|
2570
|
+
events.on("tool.progress", () => {
|
|
2571
|
+
try {
|
|
2572
|
+
ctx.budget.checkTimeout();
|
|
2573
|
+
} catch (e) {
|
|
2574
|
+
void onBudgetError(e);
|
|
2575
|
+
}
|
|
2576
|
+
})
|
|
2577
|
+
);
|
|
2578
|
+
let currentToolName;
|
|
2579
|
+
let streamingTextAcc = "";
|
|
2580
|
+
let lastSummaryAtIteration = 0;
|
|
2581
|
+
const SUMMARY_INTERVAL = 25;
|
|
2582
|
+
unsub.push(
|
|
2583
|
+
events.on("tool.started", (e) => {
|
|
2584
|
+
currentToolName = e.name;
|
|
2585
|
+
}),
|
|
2586
|
+
events.on("provider.text_delta", (e) => {
|
|
2587
|
+
streamingTextAcc = (streamingTextAcc + e.text).slice(-200);
|
|
2588
|
+
})
|
|
2589
|
+
);
|
|
2590
|
+
const onParentAbort = () => aborter.abort();
|
|
2591
|
+
ctx.signal.addEventListener("abort", onParentAbort);
|
|
2592
|
+
let result;
|
|
2593
|
+
try {
|
|
2594
|
+
result = await agent.run(format(task, ctx.config), { signal: aborter.signal });
|
|
2595
|
+
} finally {
|
|
2596
|
+
detachFleet?.();
|
|
2597
|
+
ctx.signal.removeEventListener("abort", onParentAbort);
|
|
2598
|
+
for (const u of unsub) u();
|
|
2599
|
+
if (factoryResult.dispose) {
|
|
2600
|
+
try {
|
|
2601
|
+
await factoryResult.dispose();
|
|
2602
|
+
} catch {
|
|
2603
|
+
}
|
|
2604
|
+
}
|
|
2605
|
+
}
|
|
2606
|
+
if (budgetError) {
|
|
2607
|
+
if ("decision" in budgetError) {
|
|
2608
|
+
const decision = await budgetError.decision;
|
|
2609
|
+
if (decision === "stop") {
|
|
2610
|
+
budgetError = new BudgetExceededError(
|
|
2611
|
+
budgetError.kind,
|
|
2612
|
+
budgetError.limit,
|
|
2613
|
+
budgetError.used
|
|
2614
|
+
);
|
|
2615
|
+
} else {
|
|
2616
|
+
budgetError = null;
|
|
2617
|
+
}
|
|
2618
|
+
}
|
|
2619
|
+
if (budgetError) throw budgetError;
|
|
2620
|
+
}
|
|
2621
|
+
if (result.status === "failed") {
|
|
2622
|
+
throw result.error instanceof Error ? result.error : new Error(String(result.error ?? "agent failed"));
|
|
2623
|
+
}
|
|
2624
|
+
if (result.status === "aborted") {
|
|
2625
|
+
throw new Error("agent aborted");
|
|
2626
|
+
}
|
|
2627
|
+
if (result.status === "max_iterations") {
|
|
2628
|
+
throw new Error("agent exhausted iteration limit");
|
|
2629
|
+
}
|
|
2630
|
+
const usage = ctx.budget.usage();
|
|
2631
|
+
const finalText = (result.finalText ?? "").trim();
|
|
2632
|
+
if (finalText.length === 0 && usage.toolCalls === 0) {
|
|
2633
|
+
throw new Error("empty response");
|
|
2634
|
+
}
|
|
2635
|
+
if (finalText.length === 0 && lastToolFailed !== null) {
|
|
2636
|
+
throw new Error(`tool failed: ${lastToolFailed}`);
|
|
2637
|
+
}
|
|
2638
|
+
return {
|
|
2639
|
+
result: result.finalText,
|
|
2640
|
+
iterations: result.iterations,
|
|
2641
|
+
toolCalls: usage.toolCalls
|
|
2642
|
+
};
|
|
2643
|
+
};
|
|
2644
|
+
}
|
|
2645
|
+
function defaultFormatTaskInput(task) {
|
|
2646
|
+
return task.description ?? "";
|
|
2647
|
+
}
|
|
2648
|
+
|
|
2649
|
+
// src/types/provider.ts
|
|
2650
|
+
var ProviderError = class extends WrongStackError {
|
|
2651
|
+
status;
|
|
2652
|
+
retryable;
|
|
2653
|
+
providerId;
|
|
2654
|
+
body;
|
|
2655
|
+
constructor(message, status, retryable, providerId, opts = {}) {
|
|
2656
|
+
super({
|
|
2657
|
+
message,
|
|
2658
|
+
code: providerStatusToCode(status, opts.body?.type),
|
|
2659
|
+
subsystem: "provider",
|
|
2660
|
+
severity: status >= 500 ? "error" : "warning",
|
|
2661
|
+
recoverable: retryable,
|
|
2662
|
+
context: { providerId, status },
|
|
2663
|
+
cause: opts.cause
|
|
2664
|
+
});
|
|
2665
|
+
this.name = "ProviderError";
|
|
2666
|
+
this.status = status;
|
|
2667
|
+
this.retryable = retryable;
|
|
2668
|
+
this.providerId = providerId;
|
|
2669
|
+
this.body = opts.body;
|
|
2670
|
+
}
|
|
2671
|
+
/**
|
|
2672
|
+
* Render a one-line, user-facing description. Designed for the CLI/TUI
|
|
2673
|
+
* status line and the agent's retry warning. Avoids dumping raw JSON
|
|
2674
|
+
* (which is what users see today when a 529 lands and the log message
|
|
2675
|
+
* includes the full `{"type":"error",...}` body).
|
|
2676
|
+
*
|
|
2677
|
+
* Examples:
|
|
2678
|
+
* "minimax-coding-plan overloaded (529): High traffic detected. Upgrade for highspeed model. [req 06534785201de9c0…]"
|
|
2679
|
+
* "openai rate limited (429): Retry after 12s"
|
|
2680
|
+
* "anthropic invalid request (400): messages.0.role must be one of 'user'|'assistant'"
|
|
2681
|
+
* "groq HTTP 500 (server error)"
|
|
2682
|
+
*/
|
|
2683
|
+
describe() {
|
|
2684
|
+
const kind = describeStatus(this.status, this.body?.type);
|
|
2685
|
+
const head = `${this.providerId} ${kind}`;
|
|
2686
|
+
const detail = this.body?.message?.trim();
|
|
2687
|
+
const reqId = this.body?.requestId ? ` [req ${this.body.requestId.slice(0, 16)}${this.body.requestId.length > 16 ? "\u2026" : ""}]` : "";
|
|
2688
|
+
if (detail && detail.length > 0) {
|
|
2689
|
+
return `${head}: ${truncate(detail, 240)}${reqId}`;
|
|
2690
|
+
}
|
|
2691
|
+
return `${head}${reqId}`;
|
|
2692
|
+
}
|
|
2693
|
+
};
|
|
2694
|
+
function describeStatus(status, type) {
|
|
2695
|
+
if (status === 0) return "network error";
|
|
2696
|
+
if (type === "overloaded_error" || status === 529) return `overloaded (${status})`;
|
|
2697
|
+
if (type === "rate_limit_error" || status === 429) return `rate limited (${status})`;
|
|
2698
|
+
if (type === "authentication_error" || status === 401) return `auth failed (${status})`;
|
|
2699
|
+
if (type === "permission_error" || status === 403) return `forbidden (${status})`;
|
|
2700
|
+
if (type === "not_found_error" || status === 404) return `not found (${status})`;
|
|
2701
|
+
if (type === "invalid_request_error" || status === 400) return `invalid request (${status})`;
|
|
2702
|
+
if (status === 408) return `timeout (${status})`;
|
|
2703
|
+
if (status >= 500 && status < 600) return `HTTP ${status} (server error)`;
|
|
2704
|
+
if (type) return `${type} (${status})`;
|
|
2705
|
+
return `HTTP ${status}`;
|
|
2706
|
+
}
|
|
2707
|
+
function truncate(s, n) {
|
|
2708
|
+
return s.length <= n ? s : `${s.slice(0, n - 1)}\u2026`;
|
|
2709
|
+
}
|
|
2710
|
+
function providerStatusToCode(status, type) {
|
|
2711
|
+
if (status === 0) return "PROVIDER_NETWORK_ERROR";
|
|
2712
|
+
if (type === "rate_limit_error" || status === 429) return "PROVIDER_RATE_LIMITED";
|
|
2713
|
+
if (type === "authentication_error" || status === 401) return "PROVIDER_AUTH_FAILED";
|
|
2714
|
+
if (type === "overloaded_error" || status === 529) return "PROVIDER_OVERLOADED";
|
|
2715
|
+
if (type === "invalid_request_error" || status === 400) return "PROVIDER_INVALID_REQUEST";
|
|
2716
|
+
if (status === 408) return "PROVIDER_NETWORK_ERROR";
|
|
2717
|
+
if (status >= 500) return "PROVIDER_SERVER_ERROR";
|
|
2718
|
+
return "PROVIDER_INVALID_REQUEST";
|
|
2719
|
+
}
|
|
2720
|
+
var FLEET_ROSTER_BUDGETS = {
|
|
2721
|
+
"audit-log": { timeoutMs: 8 * 60 * 1e3, maxIterations: 150, maxToolCalls: 500 },
|
|
2722
|
+
"bug-hunter": { timeoutMs: 15 * 60 * 1e3, maxIterations: 200, maxToolCalls: 600 },
|
|
2723
|
+
"refactor-planner": { timeoutMs: 12 * 60 * 1e3, maxIterations: 180, maxToolCalls: 550 },
|
|
2724
|
+
"security-scanner": { timeoutMs: 15 * 60 * 1e3, maxIterations: 200, maxToolCalls: 600 }
|
|
2725
|
+
};
|
|
2726
|
+
function applyRosterBudget(cfg) {
|
|
2727
|
+
const defaultBudget = FLEET_ROSTER_BUDGETS[cfg.role ?? ""];
|
|
2728
|
+
if (!defaultBudget) return cfg;
|
|
2729
|
+
return {
|
|
2730
|
+
...cfg,
|
|
2731
|
+
timeoutMs: cfg.timeoutMs ?? defaultBudget.timeoutMs,
|
|
2732
|
+
maxIterations: cfg.maxIterations ?? defaultBudget.maxIterations,
|
|
2733
|
+
maxToolCalls: cfg.maxToolCalls ?? defaultBudget.maxToolCalls,
|
|
2734
|
+
maxTokens: cfg.maxTokens ?? defaultBudget.maxTokens,
|
|
2735
|
+
maxCostUsd: cfg.maxCostUsd ?? defaultBudget.maxCostUsd
|
|
2736
|
+
};
|
|
2737
|
+
}
|
|
2738
|
+
|
|
2739
|
+
// src/coordination/multi-agent-coordinator.ts
|
|
2740
|
+
var DefaultMultiAgentCoordinator = class extends EventEmitter {
|
|
2741
|
+
coordinatorId;
|
|
2742
|
+
config;
|
|
2743
|
+
runner;
|
|
2744
|
+
subagents = /* @__PURE__ */ new Map();
|
|
2745
|
+
pendingTasks = [];
|
|
2746
|
+
completedResults = [];
|
|
2747
|
+
totalIterations = 0;
|
|
2748
|
+
inFlight = 0;
|
|
2749
|
+
/**
|
|
2750
|
+
* Subagents currently being stopped. Set on entry to `stop()`, cleared
|
|
2751
|
+
* once `recordCompletion` lands the terminal TaskResult. Used by
|
|
2752
|
+
* `runDispatched` and `findIdleSubagent` to refuse mid-flight dispatch
|
|
2753
|
+
* to a subagent the caller has already asked to terminate — closes the
|
|
2754
|
+
* assign+terminate race where a fresh task could land on a worker that
|
|
2755
|
+
* was about to be killed.
|
|
2756
|
+
*/
|
|
2757
|
+
terminating = /* @__PURE__ */ new Set();
|
|
2758
|
+
constructor(config, options = {}) {
|
|
2759
|
+
super();
|
|
2760
|
+
this.coordinatorId = config.coordinatorId;
|
|
2761
|
+
this.config = config;
|
|
2762
|
+
this.runner = options.runner;
|
|
2763
|
+
}
|
|
2764
|
+
/**
|
|
2765
|
+
* Replace the runner after construction. Used when the runner depends
|
|
2766
|
+
* on infrastructure (e.g. FleetBus) that isn't available until after
|
|
2767
|
+
* the coordinator's owning Director is built.
|
|
2768
|
+
*/
|
|
2769
|
+
setRunner(runner) {
|
|
2770
|
+
this.runner = runner;
|
|
2771
|
+
}
|
|
2772
|
+
/**
|
|
2773
|
+
* Change the in-flight dispatch ceiling at runtime. Lowering does NOT
|
|
2774
|
+
* preempt running tasks — already-dispatched subagents finish their
|
|
2775
|
+
* current task; only future dispatches respect the new cap. Raising
|
|
2776
|
+
* immediately tries to fill the freed slots from the pending queue.
|
|
2777
|
+
*/
|
|
2778
|
+
setMaxConcurrent(n) {
|
|
2779
|
+
if (!Number.isFinite(n) || n < 1) {
|
|
2780
|
+
throw new Error(`maxConcurrent must be a finite integer >= 1, got ${n}`);
|
|
2781
|
+
}
|
|
2782
|
+
this.config.maxConcurrent = Math.floor(n);
|
|
2783
|
+
this.tryDispatchNext();
|
|
2784
|
+
}
|
|
2785
|
+
async spawn(subagent) {
|
|
2786
|
+
const id = subagent.id || randomUUID();
|
|
2787
|
+
if (this.subagents.has(id)) {
|
|
2788
|
+
throw new Error(`Subagent id "${id}" already exists \u2014 refusing to overwrite`);
|
|
2789
|
+
}
|
|
2790
|
+
const context = {
|
|
2791
|
+
subagentId: id,
|
|
2792
|
+
tasks: [],
|
|
2793
|
+
// Wired later by the caller via setSubagentBridge() once the
|
|
2794
|
+
// bidirectional bridge is created. Readers must null-check / use
|
|
2795
|
+
// hasParentBridge() — the type now reflects this.
|
|
2796
|
+
parentBridge: null,
|
|
2797
|
+
doneCondition: this.config.doneCondition,
|
|
2798
|
+
maxConcurrent: this.config.maxConcurrent ?? 4
|
|
2799
|
+
};
|
|
2800
|
+
this.subagents.set(id, {
|
|
2801
|
+
config: { ...subagent, id },
|
|
2802
|
+
context,
|
|
2803
|
+
status: "idle",
|
|
2804
|
+
abortController: new AbortController()
|
|
2805
|
+
});
|
|
2806
|
+
this.emit("subagent.started", { subagent: { ...subagent, id } });
|
|
2807
|
+
return { subagentId: id, agentId: id };
|
|
2808
|
+
}
|
|
2809
|
+
async assign(task) {
|
|
2810
|
+
this.pendingTasks.push(task);
|
|
2811
|
+
this.tryDispatchNext();
|
|
2812
|
+
}
|
|
2813
|
+
async delegate(to, msg) {
|
|
2814
|
+
const subagent = this.subagents.get(to);
|
|
2815
|
+
if (!subagent) throw new Error(`Subagent "${to}" not found`);
|
|
2816
|
+
if (!subagent.context.parentBridge) {
|
|
2817
|
+
throw new Error(`Subagent "${to}" has no parentBridge \u2014 call setSubagentBridge() first`);
|
|
2818
|
+
}
|
|
2819
|
+
await subagent.context.parentBridge.send(msg);
|
|
2820
|
+
}
|
|
2821
|
+
/**
|
|
2822
|
+
* Wire up the communication bridge for a subagent. Call after spawn() once
|
|
2823
|
+
* the caller has created the bidirectional connection.
|
|
2824
|
+
*/
|
|
2825
|
+
setSubagentBridge(subagentId, bridge) {
|
|
2826
|
+
const subagent = this.subagents.get(subagentId);
|
|
2827
|
+
if (!subagent) throw new Error(`Subagent "${subagentId}" not found`);
|
|
2828
|
+
subagent.context.parentBridge = bridge;
|
|
2829
|
+
}
|
|
2830
|
+
async stop(subagentId) {
|
|
2831
|
+
const subagent = this.subagents.get(subagentId);
|
|
2832
|
+
if (!subagent) return;
|
|
2833
|
+
this.terminating.add(subagentId);
|
|
2834
|
+
subagent.abortController.abort();
|
|
2835
|
+
subagent.status = "stopped";
|
|
2836
|
+
subagent.currentTask = void 0;
|
|
2837
|
+
subagent.context.parentBridge = null;
|
|
2838
|
+
this.emit("subagent.stopped", { subagentId, reason: "stopped by coordinator" });
|
|
2839
|
+
}
|
|
2840
|
+
async stopAll() {
|
|
2841
|
+
this.drainPendingAsAborted("Coordinator stopAll() drained the pending queue");
|
|
2842
|
+
await Promise.allSettled([...this.subagents.keys()].map((id) => this.stop(id)));
|
|
2843
|
+
}
|
|
2844
|
+
getStatus() {
|
|
2845
|
+
return {
|
|
2846
|
+
coordinatorId: this.coordinatorId,
|
|
2847
|
+
subagents: Array.from(this.subagents.entries()).map(([id, s]) => ({
|
|
2848
|
+
id,
|
|
2849
|
+
name: s.config.name,
|
|
2850
|
+
status: s.status,
|
|
2851
|
+
currentTask: s.currentTask
|
|
2852
|
+
})),
|
|
2853
|
+
pendingTasks: this.pendingTasks.length,
|
|
2854
|
+
completedTasks: this.completedResults.length,
|
|
2855
|
+
totalIterations: this.totalIterations,
|
|
2856
|
+
done: this.isDone()
|
|
2857
|
+
};
|
|
2858
|
+
}
|
|
2859
|
+
/** Expose snapshot of completed results — useful for callers awaiting all done. */
|
|
2860
|
+
results() {
|
|
2861
|
+
return this.completedResults;
|
|
2862
|
+
}
|
|
2863
|
+
/**
|
|
2864
|
+
* Wait for one or more tasks to complete and return their results.
|
|
2865
|
+
* If a task is already done when called, returns immediately.
|
|
2866
|
+
* Resolves to an array in the same order as `taskIds`.
|
|
2867
|
+
*/
|
|
2868
|
+
async awaitTasks(taskIds) {
|
|
2869
|
+
return Promise.all(
|
|
2870
|
+
taskIds.map((id) => {
|
|
2871
|
+
const cached = this.completedResults.find((r) => r.taskId === id);
|
|
2872
|
+
if (cached) return cached;
|
|
2873
|
+
return new Promise((resolve, reject) => {
|
|
2874
|
+
const timeout = setTimeout(() => {
|
|
2875
|
+
this.off("task.completed", handler);
|
|
2876
|
+
reject(new Error(`awaitTasks timed out waiting for task "${id}"`));
|
|
2877
|
+
}, this.config.timeoutMs ?? 3e5);
|
|
2878
|
+
const handler = ({ result }) => {
|
|
2879
|
+
if (result.taskId === id) {
|
|
2880
|
+
clearTimeout(timeout);
|
|
2881
|
+
this.off("task.completed", handler);
|
|
2882
|
+
resolve(result);
|
|
2883
|
+
}
|
|
2884
|
+
};
|
|
2885
|
+
this.on("task.completed", handler);
|
|
2886
|
+
});
|
|
2887
|
+
})
|
|
2888
|
+
);
|
|
2889
|
+
}
|
|
2890
|
+
/**
|
|
2891
|
+
* Manual completion — for callers that drive subagents without a runner
|
|
2892
|
+
* (e.g. external orchestrators). When a runner is configured the coordinator
|
|
2893
|
+
* calls this itself.
|
|
2894
|
+
*/
|
|
2895
|
+
completeTask(result) {
|
|
2896
|
+
this.recordCompletion(result);
|
|
2897
|
+
}
|
|
2898
|
+
// --- internal dispatching ---------------------------------------------
|
|
2899
|
+
tryDispatchNext() {
|
|
2900
|
+
while (this.canDispatch()) {
|
|
2901
|
+
const dispatchable = this.takeNextDispatchableTask();
|
|
2902
|
+
if (!dispatchable) {
|
|
2903
|
+
if (this.pendingTasks.length > 0 && !this.hasLiveSubagent()) {
|
|
2904
|
+
this.drainPendingAsAborted(
|
|
2905
|
+
"No live subagent available \u2014 all stopped or mid-termination"
|
|
2906
|
+
);
|
|
2907
|
+
}
|
|
2908
|
+
return;
|
|
2909
|
+
}
|
|
2910
|
+
const { subagentId, task } = dispatchable;
|
|
2911
|
+
this.runDispatched(subagentId, task).catch((err) => {
|
|
2912
|
+
this.recordCompletion({
|
|
2913
|
+
subagentId,
|
|
2914
|
+
taskId: task.id,
|
|
2915
|
+
status: "failed",
|
|
2916
|
+
error: classifySubagentError(err),
|
|
2917
|
+
iterations: 0,
|
|
2918
|
+
toolCalls: 0,
|
|
2919
|
+
durationMs: 0
|
|
2920
|
+
});
|
|
2921
|
+
});
|
|
2922
|
+
}
|
|
2923
|
+
}
|
|
2924
|
+
canDispatch() {
|
|
2925
|
+
const max = this.config.maxConcurrent ?? 4;
|
|
2926
|
+
return this.inFlight < max && this.pendingTasks.length > 0;
|
|
2927
|
+
}
|
|
2928
|
+
takeNextDispatchableTask() {
|
|
2929
|
+
for (let i = 0; i < this.pendingTasks.length; i++) {
|
|
2930
|
+
const task = this.pendingTasks[i];
|
|
2931
|
+
const subagentId = task.subagentId ? this.isIdleSubagent(task.subagentId) ? task.subagentId : null : this.findIdleSubagent();
|
|
2932
|
+
if (!subagentId) continue;
|
|
2933
|
+
this.pendingTasks.splice(i, 1);
|
|
2934
|
+
return { subagentId, task };
|
|
2935
|
+
}
|
|
2936
|
+
return null;
|
|
2937
|
+
}
|
|
2938
|
+
findIdleSubagent() {
|
|
2939
|
+
for (const [id, s] of this.subagents) {
|
|
2940
|
+
if (s.status === "idle" && !this.terminating.has(id)) return id;
|
|
2941
|
+
}
|
|
2942
|
+
return null;
|
|
2943
|
+
}
|
|
2944
|
+
isIdleSubagent(id) {
|
|
2945
|
+
const subagent = this.subagents.get(id);
|
|
2946
|
+
return !!subagent && subagent.status === "idle" && !this.terminating.has(id);
|
|
2947
|
+
}
|
|
2948
|
+
/**
|
|
2949
|
+
* Returns true iff at least one spawned subagent could still
|
|
2950
|
+
* process a task. A "live" subagent is one that is not stopped
|
|
2951
|
+
* AND not mid-termination — `running` workers count because they
|
|
2952
|
+
* will eventually finish and become idle.
|
|
2953
|
+
*
|
|
2954
|
+
* When no subagent has ever been spawned, returns `true` so a
|
|
2955
|
+
* pre-spawn `assign()` simply queues (legacy behaviour). The
|
|
2956
|
+
* dead-end detection only fires after `stop()` has retired every
|
|
2957
|
+
* spawned worker.
|
|
2958
|
+
*
|
|
2959
|
+
* Used by `tryDispatchNext` to detect a dead-end pending queue.
|
|
2960
|
+
*/
|
|
2961
|
+
hasLiveSubagent() {
|
|
2962
|
+
if (this.subagents.size === 0) return true;
|
|
2963
|
+
for (const [id, s] of this.subagents) {
|
|
2964
|
+
if (s.status !== "stopped" && !this.terminating.has(id)) return true;
|
|
2965
|
+
}
|
|
2966
|
+
return false;
|
|
2967
|
+
}
|
|
2968
|
+
/**
|
|
2969
|
+
* Drain every pending task with a synthetic `aborted_by_parent`
|
|
2970
|
+
* completion event. Same shape as the `stopAll()` drain — we go
|
|
2971
|
+
* around `recordCompletion` because pending tasks were never
|
|
2972
|
+
* counted in `inFlight` and routing them through would trip the
|
|
2973
|
+
* underflow guard on every task after the first.
|
|
2974
|
+
*/
|
|
2975
|
+
drainPendingAsAborted(message) {
|
|
2976
|
+
const dropped = this.pendingTasks.splice(0, this.pendingTasks.length);
|
|
2977
|
+
for (const t of dropped) {
|
|
2978
|
+
const synthetic = {
|
|
2979
|
+
subagentId: t.subagentId ?? "unassigned",
|
|
2980
|
+
taskId: t.id,
|
|
2981
|
+
status: "stopped",
|
|
2982
|
+
error: {
|
|
2983
|
+
kind: "aborted_by_parent",
|
|
2984
|
+
message,
|
|
2985
|
+
retryable: false
|
|
2986
|
+
},
|
|
2987
|
+
iterations: 0,
|
|
2988
|
+
toolCalls: 0,
|
|
2989
|
+
durationMs: 0
|
|
2990
|
+
};
|
|
2991
|
+
this.completedResults.push(synthetic);
|
|
2992
|
+
this.emit("task.completed", { task: t, result: synthetic });
|
|
2993
|
+
}
|
|
2994
|
+
}
|
|
2995
|
+
async runDispatched(subagentId, task) {
|
|
2996
|
+
const subagent = this.subagents.get(subagentId);
|
|
2997
|
+
if (!subagent) return;
|
|
2998
|
+
if (this.terminating.has(subagentId) || subagent.status === "stopped") {
|
|
2999
|
+
this.recordCompletion({
|
|
3000
|
+
subagentId,
|
|
3001
|
+
taskId: task.id,
|
|
3002
|
+
status: "stopped",
|
|
3003
|
+
error: {
|
|
3004
|
+
kind: "aborted_by_parent",
|
|
3005
|
+
message: "Subagent was terminated before task could start",
|
|
3006
|
+
retryable: false
|
|
3007
|
+
},
|
|
3008
|
+
iterations: 0,
|
|
3009
|
+
toolCalls: 0,
|
|
3010
|
+
durationMs: 0
|
|
3011
|
+
});
|
|
3012
|
+
return;
|
|
3013
|
+
}
|
|
3014
|
+
subagent.status = "running";
|
|
3015
|
+
subagent.currentTask = task.id;
|
|
3016
|
+
task.subagentId = subagentId;
|
|
3017
|
+
subagent.context.tasks.push(task);
|
|
3018
|
+
this.emit("task.assigned", { task, subagentId });
|
|
3019
|
+
const configWithRosterDefaults = applyRosterBudget(subagent.config);
|
|
3020
|
+
const budget = new SubagentBudget({
|
|
3021
|
+
maxIterations: configWithRosterDefaults.maxIterations ?? this.config.defaultBudget?.maxIterations,
|
|
3022
|
+
maxToolCalls: task.maxToolCalls ?? configWithRosterDefaults.maxToolCalls ?? this.config.defaultBudget?.maxToolCalls,
|
|
3023
|
+
maxTokens: configWithRosterDefaults.maxTokens ?? this.config.defaultBudget?.maxTokens,
|
|
3024
|
+
maxCostUsd: configWithRosterDefaults.maxCostUsd ?? this.config.defaultBudget?.maxCostUsd,
|
|
3025
|
+
timeoutMs: task.timeoutMs ?? configWithRosterDefaults.timeoutMs ?? this.config.defaultBudget?.timeoutMs
|
|
3026
|
+
});
|
|
3027
|
+
subagent.activeBudget = budget;
|
|
3028
|
+
if (!this.runner) {
|
|
3029
|
+
return;
|
|
3030
|
+
}
|
|
3031
|
+
this.inFlight++;
|
|
3032
|
+
const startTime = Date.now();
|
|
3033
|
+
const runCtx = {
|
|
3034
|
+
subagentId,
|
|
3035
|
+
config: subagent.config,
|
|
3036
|
+
budget,
|
|
3037
|
+
signal: subagent.abortController.signal,
|
|
3038
|
+
bridge: subagent.context.parentBridge || null
|
|
3039
|
+
};
|
|
3040
|
+
let result;
|
|
3041
|
+
budget.start();
|
|
3042
|
+
try {
|
|
3043
|
+
const outcome = await this.executeWithTimeout(this.runner, task, runCtx, budget);
|
|
3044
|
+
result = {
|
|
3045
|
+
subagentId,
|
|
3046
|
+
taskId: task.id,
|
|
3047
|
+
status: "success",
|
|
3048
|
+
result: outcome.result,
|
|
3049
|
+
iterations: outcome.iterations,
|
|
3050
|
+
toolCalls: outcome.toolCalls,
|
|
3051
|
+
durationMs: Date.now() - startTime
|
|
3052
|
+
};
|
|
3053
|
+
} catch (err) {
|
|
3054
|
+
const status = err instanceof BudgetExceededError && err.kind === "timeout" ? "timeout" : subagent.abortController.signal.aborted ? "stopped" : "failed";
|
|
3055
|
+
const usage = budget.usage();
|
|
3056
|
+
result = {
|
|
3057
|
+
subagentId,
|
|
3058
|
+
taskId: task.id,
|
|
3059
|
+
status,
|
|
3060
|
+
error: classifySubagentError(err, {
|
|
3061
|
+
parentAborted: subagent.abortController.signal.aborted
|
|
3062
|
+
}),
|
|
3063
|
+
iterations: usage.iterations,
|
|
3064
|
+
toolCalls: usage.toolCalls,
|
|
3065
|
+
durationMs: Date.now() - startTime
|
|
3066
|
+
};
|
|
3067
|
+
}
|
|
3068
|
+
this.recordCompletion(result);
|
|
3069
|
+
}
|
|
3070
|
+
async executeWithTimeout(runner, task, ctx, budget) {
|
|
3071
|
+
const initialTimeoutMs = budget.limits.timeoutMs;
|
|
3072
|
+
if (initialTimeoutMs === void 0) return runner(task, ctx);
|
|
3073
|
+
const start = Date.now();
|
|
3074
|
+
let timer = null;
|
|
3075
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
3076
|
+
const armFor = (ms) => {
|
|
3077
|
+
if (timer) clearTimeout(timer);
|
|
3078
|
+
timer = setTimeout(async () => {
|
|
3079
|
+
const elapsed = Date.now() - start;
|
|
3080
|
+
const limit = budget.limits.timeoutMs ?? initialTimeoutMs;
|
|
3081
|
+
if (!budget.onThreshold) {
|
|
3082
|
+
this.subagents.get(ctx.subagentId)?.abortController.abort();
|
|
3083
|
+
reject(new BudgetExceededError("timeout", limit, elapsed));
|
|
3084
|
+
return;
|
|
3085
|
+
}
|
|
3086
|
+
try {
|
|
3087
|
+
const result = budget.onThreshold({
|
|
3088
|
+
kind: "timeout",
|
|
3089
|
+
used: elapsed,
|
|
3090
|
+
limit,
|
|
3091
|
+
requestDecision: () => new Promise((resolveDecision) => {
|
|
3092
|
+
budget._events?.emit("budget.threshold_reached", {
|
|
3093
|
+
kind: "timeout",
|
|
3094
|
+
used: elapsed,
|
|
3095
|
+
limit,
|
|
3096
|
+
timeoutMs: 3e4,
|
|
3097
|
+
extend: (extra) => resolveDecision({ extend: extra }),
|
|
3098
|
+
deny: () => resolveDecision("stop")
|
|
3099
|
+
});
|
|
3100
|
+
})
|
|
3101
|
+
});
|
|
3102
|
+
const decision = typeof result === "string" ? result : await result;
|
|
3103
|
+
if (decision === "continue") {
|
|
3104
|
+
armFor(Math.max(1e3, limit));
|
|
3105
|
+
return;
|
|
3106
|
+
}
|
|
3107
|
+
if (decision === "throw" || decision === "stop") {
|
|
3108
|
+
armFor(Math.max(1e3, limit));
|
|
3109
|
+
return;
|
|
3110
|
+
}
|
|
3111
|
+
if (decision.extend.timeoutMs !== void 0) {
|
|
3112
|
+
budget.limits.timeoutMs = decision.extend.timeoutMs;
|
|
3113
|
+
const newLimit = decision.extend.timeoutMs;
|
|
3114
|
+
const remaining = Math.max(1e3, newLimit - elapsed);
|
|
3115
|
+
armFor(remaining);
|
|
3116
|
+
return;
|
|
3117
|
+
}
|
|
3118
|
+
this.subagents.get(ctx.subagentId)?.abortController.abort();
|
|
3119
|
+
reject(new BudgetExceededError("timeout", limit, elapsed));
|
|
3120
|
+
} catch (err) {
|
|
3121
|
+
this.subagents.get(ctx.subagentId)?.abortController.abort();
|
|
3122
|
+
reject(
|
|
3123
|
+
err instanceof BudgetExceededError ? err : new BudgetExceededError("timeout", limit, elapsed)
|
|
3124
|
+
);
|
|
3125
|
+
}
|
|
3126
|
+
}, ms);
|
|
3127
|
+
};
|
|
3128
|
+
armFor(initialTimeoutMs);
|
|
3129
|
+
});
|
|
3130
|
+
try {
|
|
3131
|
+
return await Promise.race([runner(task, ctx), timeoutPromise]);
|
|
3132
|
+
} finally {
|
|
3133
|
+
if (timer) clearTimeout(timer);
|
|
3134
|
+
}
|
|
3135
|
+
}
|
|
3136
|
+
recordCompletion(result) {
|
|
3137
|
+
this.completedResults.push(result);
|
|
3138
|
+
this.totalIterations += result.iterations;
|
|
3139
|
+
if (this.inFlight > 0) {
|
|
3140
|
+
this.inFlight--;
|
|
3141
|
+
} else if (this.runner) {
|
|
3142
|
+
this.emit("warning", {
|
|
3143
|
+
type: "inFlight_underflow",
|
|
3144
|
+
taskId: result.taskId,
|
|
3145
|
+
subagentId: result.subagentId
|
|
3146
|
+
});
|
|
3147
|
+
return;
|
|
3148
|
+
}
|
|
3149
|
+
const subagent = this.subagents.get(result.subagentId);
|
|
3150
|
+
if (subagent && subagent.status !== "stopped") {
|
|
3151
|
+
result.status === "failed" || result.status === "timeout";
|
|
3152
|
+
subagent.status = "idle";
|
|
3153
|
+
subagent.currentTask = void 0;
|
|
3154
|
+
if (subagent.abortController.signal.aborted) {
|
|
3155
|
+
subagent.abortController = new AbortController();
|
|
3156
|
+
}
|
|
3157
|
+
}
|
|
3158
|
+
this.terminating.delete(result.subagentId);
|
|
3159
|
+
this.emit("task.completed", {
|
|
3160
|
+
task: subagent?.context.tasks.find((t) => t.id === result.taskId) ?? { id: result.taskId },
|
|
3161
|
+
result
|
|
3162
|
+
});
|
|
3163
|
+
this.tryDispatchNext();
|
|
3164
|
+
if (this.isDone()) {
|
|
3165
|
+
this.emit("done", {
|
|
3166
|
+
results: this.completedResults,
|
|
3167
|
+
totalIterations: this.totalIterations
|
|
3168
|
+
});
|
|
3169
|
+
}
|
|
3170
|
+
}
|
|
3171
|
+
isDone() {
|
|
3172
|
+
if (this.config.doneCondition.type === "all_tasks_done") {
|
|
3173
|
+
return this.pendingTasks.length === 0 && this.inFlight === 0;
|
|
3174
|
+
}
|
|
3175
|
+
if (this.config.doneCondition.maxIterations !== void 0 && this.totalIterations >= this.config.doneCondition.maxIterations) {
|
|
3176
|
+
return true;
|
|
3177
|
+
}
|
|
3178
|
+
return false;
|
|
3179
|
+
}
|
|
3180
|
+
};
|
|
3181
|
+
function classifySubagentError(err, hints = {}) {
|
|
3182
|
+
const cause = err instanceof Error ? { name: err.name, message: err.message, stack: err.stack } : void 0;
|
|
3183
|
+
if (err instanceof ProviderError) {
|
|
3184
|
+
const baseMessage2 = err.describe();
|
|
3185
|
+
return providerErrorToSubagentError(err, baseMessage2, cause);
|
|
3186
|
+
}
|
|
3187
|
+
const baseMessage = err instanceof Error ? err.message : String(err);
|
|
3188
|
+
if (err instanceof BudgetExceededError) {
|
|
3189
|
+
const map = {
|
|
3190
|
+
iterations: "budget_iterations",
|
|
3191
|
+
tool_calls: "budget_tool_calls",
|
|
3192
|
+
tokens: "budget_tokens",
|
|
3193
|
+
cost: "budget_cost",
|
|
3194
|
+
timeout: "budget_timeout"
|
|
3195
|
+
};
|
|
3196
|
+
return {
|
|
3197
|
+
kind: map[err.kind],
|
|
3198
|
+
message: baseMessage,
|
|
3199
|
+
// Budgets are user-configured ceilings, not transient failures —
|
|
3200
|
+
// retrying with the same budget will hit the same ceiling. The
|
|
3201
|
+
// orchestrator must raise the budget or narrow the task first.
|
|
3202
|
+
retryable: false,
|
|
3203
|
+
cause
|
|
3204
|
+
};
|
|
3205
|
+
}
|
|
3206
|
+
if (hints.parentAborted) {
|
|
3207
|
+
return {
|
|
3208
|
+
kind: "aborted_by_parent",
|
|
3209
|
+
message: baseMessage,
|
|
3210
|
+
retryable: false,
|
|
3211
|
+
cause
|
|
3212
|
+
};
|
|
3213
|
+
}
|
|
3214
|
+
const lower = baseMessage.toLowerCase();
|
|
3215
|
+
if (/agent aborted$/i.test(baseMessage)) {
|
|
3216
|
+
return {
|
|
3217
|
+
kind: "aborted_by_parent",
|
|
3218
|
+
message: baseMessage,
|
|
3219
|
+
retryable: false,
|
|
3220
|
+
cause
|
|
3221
|
+
};
|
|
3222
|
+
}
|
|
3223
|
+
if (/agent exhausted iteration limit$/i.test(baseMessage)) {
|
|
3224
|
+
return { kind: "budget_iterations", message: baseMessage, retryable: false, cause };
|
|
3225
|
+
}
|
|
3226
|
+
if (/empty response$/i.test(baseMessage)) {
|
|
3227
|
+
return { kind: "empty_response", message: baseMessage, retryable: false, cause };
|
|
3228
|
+
}
|
|
3229
|
+
if (/^tool failed: /i.test(baseMessage)) {
|
|
3230
|
+
return { kind: "tool_failed", message: baseMessage, retryable: false, cause };
|
|
3231
|
+
}
|
|
3232
|
+
if (lower.includes("bridge transport") || /bridge.*(closed|disconnect)/i.test(baseMessage)) {
|
|
3233
|
+
return { kind: "bridge_failed", message: baseMessage, retryable: false, cause };
|
|
3234
|
+
}
|
|
3235
|
+
if (/context length|max.*tokens?.*exceeded|prompt is too long/i.test(baseMessage)) {
|
|
3236
|
+
return { kind: "context_overflow", message: baseMessage, retryable: false, cause };
|
|
3237
|
+
}
|
|
3238
|
+
return {
|
|
3239
|
+
kind: "unknown",
|
|
3240
|
+
message: baseMessage,
|
|
3241
|
+
retryable: false,
|
|
3242
|
+
cause
|
|
3243
|
+
};
|
|
3244
|
+
}
|
|
3245
|
+
function providerErrorToSubagentError(err, message, cause) {
|
|
3246
|
+
const status = err.status;
|
|
3247
|
+
if (status === 429 || err.body?.type === "rate_limit_error") {
|
|
3248
|
+
return {
|
|
3249
|
+
kind: "provider_rate_limit",
|
|
3250
|
+
message,
|
|
3251
|
+
retryable: true,
|
|
3252
|
+
// Conservative default: 5s. Provider-specific code can override
|
|
3253
|
+
// by emitting an error whose body carries an explicit hint.
|
|
3254
|
+
backoffMs: 5e3,
|
|
3255
|
+
cause
|
|
3256
|
+
};
|
|
3257
|
+
}
|
|
3258
|
+
if (status === 401 || status === 403 || err.body?.type === "authentication_error") {
|
|
3259
|
+
return { kind: "provider_auth", message, retryable: false, cause };
|
|
3260
|
+
}
|
|
3261
|
+
if (status === 408 || status === 0) {
|
|
3262
|
+
return { kind: "provider_timeout", message, retryable: true, cause };
|
|
3263
|
+
}
|
|
3264
|
+
if (status >= 500 && status < 600) {
|
|
3265
|
+
return {
|
|
3266
|
+
kind: "provider_5xx",
|
|
3267
|
+
message,
|
|
3268
|
+
retryable: true,
|
|
3269
|
+
backoffMs: 3e3,
|
|
3270
|
+
cause
|
|
3271
|
+
};
|
|
3272
|
+
}
|
|
3273
|
+
return { kind: "unknown", message, retryable: err.retryable, cause };
|
|
3274
|
+
}
|
|
3275
|
+
|
|
3276
|
+
// src/execution/parallel-eternal-engine.ts
|
|
3277
|
+
function sleep2(ms) {
|
|
3278
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
3279
|
+
}
|
|
3280
|
+
var GOAL_COMPLETE_MARKER2 = /^\s*\[goal[_\s-]?complete\]\s*$/im;
|
|
3281
|
+
var ParallelEternalEngine = class {
|
|
3282
|
+
constructor(opts) {
|
|
3283
|
+
this.opts = opts;
|
|
3284
|
+
this.goalPath = goalFilePath(opts.projectRoot);
|
|
3285
|
+
this.slots = Math.min(16, Math.max(1, opts.parallelSlots ?? 4));
|
|
3286
|
+
this.timeoutMs = opts.iterationTimeoutMs ?? 3e5;
|
|
3287
|
+
this.agentFactory = opts.subagentFactory ?? (async (config) => ({
|
|
3288
|
+
agent: this.opts.agent,
|
|
3289
|
+
events: this.opts.agent.events
|
|
3290
|
+
}));
|
|
3291
|
+
}
|
|
3292
|
+
opts;
|
|
3293
|
+
state = "idle";
|
|
3294
|
+
stopRequested = false;
|
|
3295
|
+
iterationsSinceCompact = 0;
|
|
3296
|
+
iterations = 0;
|
|
3297
|
+
consecutiveFailures = 0;
|
|
3298
|
+
goalPath;
|
|
3299
|
+
slots;
|
|
3300
|
+
timeoutMs;
|
|
3301
|
+
coordinator = null;
|
|
3302
|
+
agentFactory;
|
|
3303
|
+
get currentState() {
|
|
3304
|
+
return this.state;
|
|
3305
|
+
}
|
|
3306
|
+
stop() {
|
|
3307
|
+
this.stopRequested = true;
|
|
3308
|
+
void this.persistState("stopped").catch(() => {
|
|
3309
|
+
});
|
|
3310
|
+
this.state = "stopped";
|
|
3311
|
+
}
|
|
3312
|
+
async prime() {
|
|
3313
|
+
this.stopRequested = false;
|
|
3314
|
+
this.state = "running";
|
|
3315
|
+
await this.persistState("running");
|
|
3316
|
+
}
|
|
3317
|
+
async run() {
|
|
3318
|
+
this.state = "running";
|
|
3319
|
+
await this.persistState("running");
|
|
3320
|
+
const config = {
|
|
3321
|
+
coordinatorId: `parallel-${randomUUID().slice(0, 8)}`,
|
|
3322
|
+
maxConcurrent: this.slots,
|
|
3323
|
+
doneCondition: { type: "all_tasks_done" }
|
|
3324
|
+
};
|
|
3325
|
+
this.coordinator = new DefaultMultiAgentCoordinator(config);
|
|
3326
|
+
const runner = makeAgentSubagentRunner({ factory: this.agentFactory });
|
|
3327
|
+
this.coordinator.setRunner?.(runner);
|
|
3328
|
+
try {
|
|
3329
|
+
while (!this.stopRequested) {
|
|
3330
|
+
try {
|
|
3331
|
+
await this.runOneIteration();
|
|
3332
|
+
} catch (err) {
|
|
3333
|
+
this.consecutiveFailures++;
|
|
3334
|
+
this.opts.onError?.(err instanceof Error ? err : new Error(String(err)), this.consecutiveFailures);
|
|
3335
|
+
await this.appendFailure("engine error", err instanceof Error ? err.message : String(err));
|
|
3336
|
+
}
|
|
3337
|
+
if (this.stopRequested) break;
|
|
3338
|
+
await sleep2(2e3);
|
|
3339
|
+
}
|
|
3340
|
+
} finally {
|
|
3341
|
+
this.state = "stopped";
|
|
3342
|
+
await this.persistState("stopped").catch(() => {
|
|
3343
|
+
});
|
|
3344
|
+
}
|
|
3345
|
+
}
|
|
3346
|
+
/**
|
|
3347
|
+
* Execute one tick: decompose → fan-out → aggregate → compact.
|
|
3348
|
+
* Called by the REPL in its main loop (REPL drives, engine is stateless per tick).
|
|
3349
|
+
*/
|
|
3350
|
+
async runOneIteration() {
|
|
3351
|
+
this.iterations++;
|
|
3352
|
+
const goal = await loadGoal(this.goalPath);
|
|
3353
|
+
if (!goal) {
|
|
3354
|
+
this.stopRequested = true;
|
|
3355
|
+
return false;
|
|
3356
|
+
}
|
|
3357
|
+
if (goal.goalState !== "active") {
|
|
3358
|
+
this.stopRequested = true;
|
|
3359
|
+
return false;
|
|
3360
|
+
}
|
|
3361
|
+
if (!this.coordinator) {
|
|
3362
|
+
const config = {
|
|
3363
|
+
coordinatorId: `parallel-${randomUUID().slice(0, 8)}`,
|
|
3364
|
+
maxConcurrent: this.slots,
|
|
3365
|
+
doneCondition: { type: "all_tasks_done" }
|
|
3366
|
+
};
|
|
3367
|
+
this.coordinator = new DefaultMultiAgentCoordinator(config);
|
|
3368
|
+
const runner = makeAgentSubagentRunner({ factory: this.agentFactory });
|
|
3369
|
+
this.coordinator.setRunner?.(runner);
|
|
3370
|
+
}
|
|
3371
|
+
const tasks = await this.decomposeGoal(goal);
|
|
3372
|
+
if (!tasks || tasks.length === 0) {
|
|
3373
|
+
await sleep2(5e3);
|
|
3374
|
+
return false;
|
|
3375
|
+
}
|
|
3376
|
+
const fanOut = await this.fanOut(goal, tasks);
|
|
3377
|
+
this.iterationsSinceCompact++;
|
|
3378
|
+
const successCount = fanOut.results.filter((r) => r.status === "success").length;
|
|
3379
|
+
const status = fanOut.goalComplete ? "success" : fanOut.allSuccessful ? "success" : "failure";
|
|
3380
|
+
const note = [
|
|
3381
|
+
`${successCount}/${fanOut.results.length} subagents succeeded`,
|
|
3382
|
+
fanOut.goalComplete ? "[GOAL_COMPLETE]" : "",
|
|
3383
|
+
fanOut.partialOutput ? `Output: ${fanOut.partialOutput.slice(0, 120)}` : ""
|
|
3384
|
+
].filter(Boolean).join(" | ");
|
|
3385
|
+
await this.appendIterationEntry({
|
|
3386
|
+
source: "parallel",
|
|
3387
|
+
task: `parallel:${tasks.length} slots \u2014 ${tasks.slice(0, 3).join(", ")}${tasks.length > 3 ? "..." : ""}`,
|
|
3388
|
+
status,
|
|
3389
|
+
note
|
|
3390
|
+
});
|
|
3391
|
+
if (fanOut.goalComplete) {
|
|
3392
|
+
this.stopRequested = true;
|
|
3393
|
+
return true;
|
|
3394
|
+
}
|
|
3395
|
+
await this.maybeCompact();
|
|
3396
|
+
return fanOut.allSuccessful;
|
|
3397
|
+
}
|
|
3398
|
+
// -------------------------------------------------------------------------
|
|
3399
|
+
// Fan-out
|
|
3400
|
+
// -------------------------------------------------------------------------
|
|
3401
|
+
async fanOut(goal, tasks) {
|
|
3402
|
+
const coordinator = this.coordinator;
|
|
3403
|
+
const slotCount = Math.min(this.slots, tasks.length);
|
|
3404
|
+
const recentJournal = goal.journal.slice(-5).map((e) => ` #${e.iteration} [${e.status}] ${e.task}${e.note ? ` \u2014 ${e.note.slice(0, 80)}` : ""}`).join("\n");
|
|
3405
|
+
const directivePreamble = [
|
|
3406
|
+
"\u2550\u2550\u2550 ETERNAL AUTONOMY \u2014 parallel task slot \u2550\u2550\u2550",
|
|
3407
|
+
"",
|
|
3408
|
+
`Mission: ${goal.goal}`,
|
|
3409
|
+
`Total parallel slots: ${slotCount}`,
|
|
3410
|
+
"",
|
|
3411
|
+
recentJournal ? `Recent journal (last 5):
|
|
3412
|
+
${recentJournal}` : "No prior iterations.",
|
|
3413
|
+
"",
|
|
3414
|
+
"\u2500\u2500 EXECUTION PROTOCOL \u2500\u2500",
|
|
3415
|
+
"\u2022 Execute the assigned task end-to-end using multiple tool calls.",
|
|
3416
|
+
"\u2022 Emit `[done]` on its own line when the task is complete.",
|
|
3417
|
+
"\u2022 Do not ask for confirmation \u2014 YOLO is active.",
|
|
3418
|
+
"\u2022 If the overall Mission is accomplished, emit `[GOAL_COMPLETE]` followed by a verification recipe.",
|
|
3419
|
+
"\u2022 Keep output concise \u2014 summarize findings, do not transcribe files."
|
|
3420
|
+
].join("\n");
|
|
3421
|
+
const taskIds = [];
|
|
3422
|
+
const subagentIds = [];
|
|
3423
|
+
const spawnPromises = [];
|
|
3424
|
+
for (let i = 0; i < slotCount; i++) {
|
|
3425
|
+
const task = tasks[i];
|
|
3426
|
+
const subagentId = `parallel-${this.iterations}-${i}`;
|
|
3427
|
+
const taskId = randomUUID();
|
|
3428
|
+
const spec = {
|
|
3429
|
+
id: taskId,
|
|
3430
|
+
description: `${directivePreamble}
|
|
3431
|
+
|
|
3432
|
+
\u2500\u2500 SLOT ${i + 1}/${slotCount} \u2500\u2500
|
|
3433
|
+
Task: ${task}
|
|
3434
|
+
`,
|
|
3435
|
+
subagentId
|
|
3436
|
+
};
|
|
3437
|
+
spawnPromises.push((async () => {
|
|
3438
|
+
try {
|
|
3439
|
+
await coordinator.spawn({
|
|
3440
|
+
id: subagentId,
|
|
3441
|
+
name: `slot-${subagentId.slice(-6)}`,
|
|
3442
|
+
maxIterations: 50,
|
|
3443
|
+
maxToolCalls: 200,
|
|
3444
|
+
timeoutMs: this.timeoutMs
|
|
3445
|
+
});
|
|
3446
|
+
subagentIds.push(subagentId);
|
|
3447
|
+
taskIds.push(taskId);
|
|
3448
|
+
await coordinator.assign(spec);
|
|
3449
|
+
} catch {
|
|
3450
|
+
}
|
|
3451
|
+
})());
|
|
3452
|
+
}
|
|
3453
|
+
await Promise.all(spawnPromises);
|
|
3454
|
+
if (taskIds.length === 0) {
|
|
3455
|
+
return { results: [], allSuccessful: false, goalComplete: false, partialOutput: "" };
|
|
3456
|
+
}
|
|
3457
|
+
let results = [];
|
|
3458
|
+
try {
|
|
3459
|
+
const ctrl = new AbortController();
|
|
3460
|
+
const timer = setTimeout(() => ctrl.abort(), this.timeoutMs + 6e4);
|
|
3461
|
+
try {
|
|
3462
|
+
results = await coordinator.awaitTasks(taskIds);
|
|
3463
|
+
} finally {
|
|
3464
|
+
clearTimeout(timer);
|
|
3465
|
+
}
|
|
3466
|
+
} catch {
|
|
3467
|
+
results = coordinator.results().slice(-taskIds.length);
|
|
3468
|
+
}
|
|
3469
|
+
const allSuccessful = results.length > 0 && results.every((r) => r.status === "success");
|
|
3470
|
+
const goalComplete = results.some(
|
|
3471
|
+
(r) => r.status === "success" && typeof r.result === "string" && GOAL_COMPLETE_MARKER2.test(r.result)
|
|
3472
|
+
);
|
|
3473
|
+
const partialOutput = results.map((r) => typeof r.result === "string" ? r.result : "").filter(Boolean).join("\n\n");
|
|
3474
|
+
return { results, allSuccessful, goalComplete, partialOutput };
|
|
3475
|
+
}
|
|
3476
|
+
// -------------------------------------------------------------------------
|
|
3477
|
+
// Goal decomposition
|
|
3478
|
+
// -------------------------------------------------------------------------
|
|
3479
|
+
async decomposeGoal(goal) {
|
|
3480
|
+
const todos = this.opts.agent.ctx?.todos;
|
|
3481
|
+
const tasks = [];
|
|
3482
|
+
if (Array.isArray(todos)) {
|
|
3483
|
+
const pending = todos.filter((t) => t.status === "pending").slice(0, this.slots);
|
|
3484
|
+
for (const t of pending) {
|
|
3485
|
+
tasks.push(`[todo] ${t.content}`);
|
|
3486
|
+
}
|
|
3487
|
+
}
|
|
3488
|
+
if (tasks.length < this.slots) {
|
|
3489
|
+
try {
|
|
3490
|
+
const gitStatus = await (this.opts.gitStatusReader?.() ?? this.readGitStatus());
|
|
3491
|
+
const dirty = gitStatus.trim();
|
|
3492
|
+
if (dirty) {
|
|
3493
|
+
const lines = dirty.split("\n").slice(0, this.slots - tasks.length);
|
|
3494
|
+
for (const line of lines) {
|
|
3495
|
+
const file = line.replace(/^[ MADRUC?]{2}\s*/, "").trim();
|
|
3496
|
+
if (file) tasks.push(`[git] inspect and fix: ${file}`);
|
|
3497
|
+
}
|
|
3498
|
+
}
|
|
3499
|
+
} catch {
|
|
3500
|
+
}
|
|
3501
|
+
}
|
|
3502
|
+
if (tasks.length < this.slots) {
|
|
3503
|
+
const remaining = this.slots - tasks.length;
|
|
3504
|
+
const brainstormed = await this.brainstormSubtasks(goal, remaining);
|
|
3505
|
+
tasks.push(...brainstormed);
|
|
3506
|
+
}
|
|
3507
|
+
return tasks.length > 0 ? tasks.slice(0, this.slots) : null;
|
|
3508
|
+
}
|
|
3509
|
+
async readGitStatus() {
|
|
3510
|
+
const { execFile: execFile2 } = await import('child_process');
|
|
3511
|
+
const { promisify: promisify2 } = await import('util');
|
|
3512
|
+
const execFileP2 = promisify2(execFile2);
|
|
3513
|
+
const { stdout } = await execFileP2("git", ["status", "--porcelain"], {
|
|
3514
|
+
cwd: this.opts.projectRoot,
|
|
3515
|
+
timeout: 5e3
|
|
3516
|
+
});
|
|
3517
|
+
return stdout;
|
|
3518
|
+
}
|
|
3519
|
+
async brainstormSubtasks(goal, count) {
|
|
3520
|
+
const lastFew = goal.journal.slice(-5).map((e) => ` - [${e.status}] ${e.task}`).join("\n");
|
|
3521
|
+
const directive = [
|
|
3522
|
+
`Decompose this goal into exactly ${count} independent sub-tasks for parallel execution.`,
|
|
3523
|
+
"",
|
|
3524
|
+
`Goal: ${goal.goal}`,
|
|
3525
|
+
"",
|
|
3526
|
+
lastFew ? `Recent:
|
|
3527
|
+
${lastFew}` : "No prior iterations.",
|
|
3528
|
+
"",
|
|
3529
|
+
`Output exactly ${count} tasks, one per line, under 120 chars each.`,
|
|
3530
|
+
"Format: TASK-1 | TASK-2 | ... (pipe-separated, no numbering, no preamble).",
|
|
3531
|
+
"Each task must be independently actionable with no shared dependencies."
|
|
3532
|
+
].join("\n");
|
|
3533
|
+
try {
|
|
3534
|
+
const ctrl = new AbortController();
|
|
3535
|
+
const timer = setTimeout(() => ctrl.abort(), 6e4);
|
|
3536
|
+
try {
|
|
3537
|
+
const result = await this.opts.agent.run([{ type: "text", text: directive }], {
|
|
3538
|
+
signal: ctrl.signal,
|
|
3539
|
+
maxIterations: 1
|
|
3540
|
+
});
|
|
3541
|
+
if (result.status !== "done") return [];
|
|
3542
|
+
const text = (result.finalText ?? "").trim();
|
|
3543
|
+
if (!text) return [];
|
|
3544
|
+
const tasks = text.split("|").map((t) => t.trim()).filter((t) => t.length > 10 && t.length < 240);
|
|
3545
|
+
return tasks.slice(0, count);
|
|
3546
|
+
} finally {
|
|
3547
|
+
clearTimeout(timer);
|
|
3548
|
+
}
|
|
3549
|
+
} catch {
|
|
3550
|
+
return [];
|
|
3551
|
+
}
|
|
3552
|
+
}
|
|
3553
|
+
// -------------------------------------------------------------------------
|
|
3554
|
+
// Compaction
|
|
3555
|
+
// -------------------------------------------------------------------------
|
|
3556
|
+
async maybeCompact() {
|
|
3557
|
+
const compactor = this.opts.compactor;
|
|
3558
|
+
if (!compactor) return;
|
|
3559
|
+
const ctx = this.opts.agent.ctx;
|
|
3560
|
+
if (!ctx) return;
|
|
3561
|
+
const shouldRun = this.iterationsSinceCompact >= (this.opts.compactEveryNIterations ?? 25);
|
|
3562
|
+
if (!shouldRun) return;
|
|
3563
|
+
const report = await compactor.compact(ctx, { aggressive: false });
|
|
3564
|
+
this.iterationsSinceCompact = 0;
|
|
3565
|
+
await this.appendIterationEntry({
|
|
3566
|
+
source: "manual",
|
|
3567
|
+
task: "compaction (cadence)",
|
|
3568
|
+
status: "success",
|
|
3569
|
+
note: `saved ~${report.before - report.after} tokens`
|
|
3570
|
+
});
|
|
3571
|
+
}
|
|
3572
|
+
// -------------------------------------------------------------------------
|
|
3573
|
+
// Helpers
|
|
3574
|
+
// -------------------------------------------------------------------------
|
|
3575
|
+
async appendIterationEntry(entry) {
|
|
3576
|
+
const current = await loadGoal(this.goalPath);
|
|
3577
|
+
if (!current) return;
|
|
3578
|
+
const updated = appendJournal(current, entry);
|
|
3579
|
+
await saveGoal(this.goalPath, updated);
|
|
3580
|
+
const entryWithMeta = {
|
|
3581
|
+
at: (this.opts.now?.() ?? /* @__PURE__ */ new Date()).toISOString(),
|
|
3582
|
+
iteration: updated.iterations,
|
|
3583
|
+
...entry
|
|
3584
|
+
};
|
|
3585
|
+
this.opts.onIteration?.(entryWithMeta);
|
|
3586
|
+
}
|
|
3587
|
+
async appendFailure(task, note) {
|
|
3588
|
+
await this.appendIterationEntry({ source: "manual", task, status: "failure", note });
|
|
3589
|
+
}
|
|
3590
|
+
async persistState(state) {
|
|
3591
|
+
const current = await loadGoal(this.goalPath);
|
|
3592
|
+
if (!current) return;
|
|
3593
|
+
if (current.engineState === state) return;
|
|
3594
|
+
await saveGoal(this.goalPath, { ...current, engineState: state });
|
|
3595
|
+
}
|
|
3596
|
+
};
|
|
3597
|
+
|
|
2234
3598
|
// src/execution/autonomy-prompt-contributor.ts
|
|
2235
3599
|
function makeAutonomyPromptContributor(opts) {
|
|
2236
3600
|
return async (ctx) => {
|
|
@@ -2367,78 +3731,6 @@ function buildGoalPreamble(goal) {
|
|
|
2367
3731
|
].join("\n");
|
|
2368
3732
|
}
|
|
2369
3733
|
|
|
2370
|
-
// src/types/provider.ts
|
|
2371
|
-
var ProviderError = class extends WrongStackError {
|
|
2372
|
-
status;
|
|
2373
|
-
retryable;
|
|
2374
|
-
providerId;
|
|
2375
|
-
body;
|
|
2376
|
-
constructor(message, status, retryable, providerId, opts = {}) {
|
|
2377
|
-
super({
|
|
2378
|
-
message,
|
|
2379
|
-
code: providerStatusToCode(status, opts.body?.type),
|
|
2380
|
-
subsystem: "provider",
|
|
2381
|
-
severity: status >= 500 ? "error" : "warning",
|
|
2382
|
-
recoverable: retryable,
|
|
2383
|
-
context: { providerId, status },
|
|
2384
|
-
cause: opts.cause
|
|
2385
|
-
});
|
|
2386
|
-
this.name = "ProviderError";
|
|
2387
|
-
this.status = status;
|
|
2388
|
-
this.retryable = retryable;
|
|
2389
|
-
this.providerId = providerId;
|
|
2390
|
-
this.body = opts.body;
|
|
2391
|
-
}
|
|
2392
|
-
/**
|
|
2393
|
-
* Render a one-line, user-facing description. Designed for the CLI/TUI
|
|
2394
|
-
* status line and the agent's retry warning. Avoids dumping raw JSON
|
|
2395
|
-
* (which is what users see today when a 529 lands and the log message
|
|
2396
|
-
* includes the full `{"type":"error",...}` body).
|
|
2397
|
-
*
|
|
2398
|
-
* Examples:
|
|
2399
|
-
* "minimax-coding-plan overloaded (529): High traffic detected. Upgrade for highspeed model. [req 06534785201de9c0…]"
|
|
2400
|
-
* "openai rate limited (429): Retry after 12s"
|
|
2401
|
-
* "anthropic invalid request (400): messages.0.role must be one of 'user'|'assistant'"
|
|
2402
|
-
* "groq HTTP 500 (server error)"
|
|
2403
|
-
*/
|
|
2404
|
-
describe() {
|
|
2405
|
-
const kind = describeStatus(this.status, this.body?.type);
|
|
2406
|
-
const head = `${this.providerId} ${kind}`;
|
|
2407
|
-
const detail = this.body?.message?.trim();
|
|
2408
|
-
const reqId = this.body?.requestId ? ` [req ${this.body.requestId.slice(0, 16)}${this.body.requestId.length > 16 ? "\u2026" : ""}]` : "";
|
|
2409
|
-
if (detail && detail.length > 0) {
|
|
2410
|
-
return `${head}: ${truncate(detail, 240)}${reqId}`;
|
|
2411
|
-
}
|
|
2412
|
-
return `${head}${reqId}`;
|
|
2413
|
-
}
|
|
2414
|
-
};
|
|
2415
|
-
function describeStatus(status, type) {
|
|
2416
|
-
if (status === 0) return "network error";
|
|
2417
|
-
if (type === "overloaded_error" || status === 529) return `overloaded (${status})`;
|
|
2418
|
-
if (type === "rate_limit_error" || status === 429) return `rate limited (${status})`;
|
|
2419
|
-
if (type === "authentication_error" || status === 401) return `auth failed (${status})`;
|
|
2420
|
-
if (type === "permission_error" || status === 403) return `forbidden (${status})`;
|
|
2421
|
-
if (type === "not_found_error" || status === 404) return `not found (${status})`;
|
|
2422
|
-
if (type === "invalid_request_error" || status === 400) return `invalid request (${status})`;
|
|
2423
|
-
if (status === 408) return `timeout (${status})`;
|
|
2424
|
-
if (status >= 500 && status < 600) return `HTTP ${status} (server error)`;
|
|
2425
|
-
if (type) return `${type} (${status})`;
|
|
2426
|
-
return `HTTP ${status}`;
|
|
2427
|
-
}
|
|
2428
|
-
function truncate(s, n) {
|
|
2429
|
-
return s.length <= n ? s : `${s.slice(0, n - 1)}\u2026`;
|
|
2430
|
-
}
|
|
2431
|
-
function providerStatusToCode(status, type) {
|
|
2432
|
-
if (status === 0) return "PROVIDER_NETWORK_ERROR";
|
|
2433
|
-
if (type === "rate_limit_error" || status === 429) return "PROVIDER_RATE_LIMITED";
|
|
2434
|
-
if (type === "authentication_error" || status === 401) return "PROVIDER_AUTH_FAILED";
|
|
2435
|
-
if (type === "overloaded_error" || status === 529) return "PROVIDER_OVERLOADED";
|
|
2436
|
-
if (type === "invalid_request_error" || status === 400) return "PROVIDER_INVALID_REQUEST";
|
|
2437
|
-
if (status === 408) return "PROVIDER_NETWORK_ERROR";
|
|
2438
|
-
if (status >= 500) return "PROVIDER_SERVER_ERROR";
|
|
2439
|
-
return "PROVIDER_INVALID_REQUEST";
|
|
2440
|
-
}
|
|
2441
|
-
|
|
2442
3734
|
// src/execution/retry-policy.ts
|
|
2443
3735
|
var DefaultRetryPolicy = class _DefaultRetryPolicy {
|
|
2444
3736
|
static NETWORK_ERR_RE = /ECONN|ETIMEDOUT|ETIME|ENOTFOUND|EAI_AGAIN|fetch failed/i;
|
|
@@ -2679,6 +3971,6 @@ function parseDescription(raw) {
|
|
|
2679
3971
|
return { trigger, scope };
|
|
2680
3972
|
}
|
|
2681
3973
|
|
|
2682
|
-
export { AutoCompactionMiddleware, AutonomousRunner, DefaultErrorHandler, DefaultRetryPolicy, DefaultSkillLoader, DoneConditionChecker, EternalAutonomyEngine, HybridCompactor, IntelligentCompactor, SelectiveCompactor, ToolExecutor, buildGoalPreamble, makeAutonomyPromptContributor };
|
|
3974
|
+
export { AutoCompactionMiddleware, AutonomousRunner, DefaultErrorHandler, DefaultRetryPolicy, DefaultSkillLoader, DoneConditionChecker, EternalAutonomyEngine, HybridCompactor, IntelligentCompactor, ParallelEternalEngine, SelectiveCompactor, ToolExecutor, buildGoalPreamble, makeAutonomyPromptContributor };
|
|
2683
3975
|
//# sourceMappingURL=index.js.map
|
|
2684
3976
|
//# sourceMappingURL=index.js.map
|