@wrongstack/core 0.310.1 → 0.313.1
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/chronicle/index.js +84 -6391
- package/dist/chronicle/project-server.js +84 -4868
- package/dist/chunk-2BUUIMQB.js +225 -0
- package/dist/chunk-2FK4WTFF.js +207 -0
- package/dist/chunk-2FQL4PDC.js +292 -0
- package/dist/chunk-2KKODIQU.js +2256 -0
- package/dist/chunk-2YU4WVBW.js +134 -0
- package/dist/chunk-3E244UDA.js +356 -0
- package/dist/chunk-3XPVB7BA.js +135 -0
- package/dist/chunk-4LJJYBAU.js +388 -0
- package/dist/chunk-4VWO3ZXL.js +342 -0
- package/dist/chunk-4XV6IR37.js +187 -0
- package/dist/chunk-4ZHTKJHM.js +183 -0
- package/dist/chunk-52RTVH4M.js +361 -0
- package/dist/chunk-57GEWK5X.js +26 -0
- package/dist/chunk-5DSMEVA4.js +931 -0
- package/dist/chunk-5RP3QTRL.js +1223 -0
- package/dist/chunk-5S437PRC.js +50 -0
- package/dist/chunk-5XDUGTKN.js +511 -0
- package/dist/chunk-62V2AFVG.js +422 -0
- package/dist/chunk-65RDIOR2.js +2470 -0
- package/dist/chunk-6AKOTGLM.js +597 -0
- package/dist/chunk-7ZHZE5EO.js +28 -0
- package/dist/chunk-A455ATZB.js +140 -0
- package/dist/chunk-AD7ZFZCS.js +46 -0
- package/dist/chunk-AI4LLQ2U.js +856 -0
- package/dist/chunk-ASX6AW36.js +789 -0
- package/dist/chunk-AT4V4XGI.js +3522 -0
- package/dist/chunk-AWNQ6ED6.js +311 -0
- package/dist/chunk-B4AMLZPB.js +9 -0
- package/dist/chunk-BAKVJLEV.js +417 -0
- package/dist/chunk-BJHVULHO.js +55 -0
- package/dist/chunk-CB2HFKTP.js +9 -0
- package/dist/chunk-CE4ZOCOI.js +1640 -0
- package/dist/chunk-CQQL4ZOQ.js +151 -0
- package/dist/chunk-DEIQMBD2.js +65 -0
- package/dist/chunk-E6I5RXVK.js +58 -0
- package/dist/chunk-EFEHDQGY.js +1589 -0
- package/dist/chunk-EODDVJBM.js +82 -0
- package/dist/chunk-EPC3GZD4.js +308 -0
- package/dist/chunk-FAYRX2AD.js +13 -0
- package/dist/chunk-FFSX2QDQ.js +362 -0
- package/dist/chunk-FHAMQS7Q.js +5856 -0
- package/dist/chunk-FIV4IJL7.js +2263 -0
- package/dist/chunk-FNXVT7HF.js +52 -0
- package/dist/chunk-FSWCWSTS.js +395 -0
- package/dist/chunk-FW5LDDMJ.js +16464 -0
- package/dist/chunk-FXQLIX3K.js +211 -0
- package/dist/chunk-G5AAKEEZ.js +228 -0
- package/dist/chunk-G5S3S5ZN.js +38 -0
- package/dist/chunk-GAZEWQEZ.js +231 -0
- package/dist/chunk-GFJOMSHG.js +125 -0
- package/dist/chunk-GFXZTHNI.js +158 -0
- package/dist/chunk-GM54GJBU.js +960 -0
- package/dist/chunk-GPQ3NJPR.js +900 -0
- package/dist/chunk-GTCU6DZ3.js +1 -0
- package/dist/chunk-H5E77KUJ.js +15 -0
- package/dist/chunk-H72H4XDU.js +1023 -0
- package/dist/chunk-HA4ZYA6T.js +178 -0
- package/dist/chunk-I45KAW2N.js +5020 -0
- package/dist/chunk-IQ35HMQL.js +1396 -0
- package/dist/chunk-IYFBFX34.js +86 -0
- package/dist/chunk-JJPMWPB6.js +109 -0
- package/dist/chunk-JTHKOYWU.js +663 -0
- package/dist/chunk-JVGPR3TS.js +365 -0
- package/dist/chunk-JVKB2TBL.js +48 -0
- package/dist/chunk-JX7VBZDK.js +31 -0
- package/dist/chunk-K3G6JBUC.js +5232 -0
- package/dist/chunk-K4YUNKU7.js +589 -0
- package/dist/chunk-KBZGE6LN.js +710 -0
- package/dist/chunk-KDQJCT2J.js +45 -0
- package/dist/chunk-KI4L2DUL.js +560 -0
- package/dist/chunk-KJ2ZTQSL.js +47 -0
- package/dist/chunk-KSJ5UTP7.js +154 -0
- package/dist/chunk-LHOA5DWA.js +28 -0
- package/dist/chunk-LX5QWLBL.js +27 -0
- package/dist/chunk-LZDDLNCZ.js +279 -0
- package/dist/chunk-M2WF6AXG.js +115 -0
- package/dist/chunk-M7BS2K6U.js +1 -0
- package/dist/chunk-NDBIPFNY.js +102 -0
- package/dist/chunk-NEMIWEXC.js +1199 -0
- package/dist/chunk-NFAYHPRH.js +323 -0
- package/dist/chunk-O37BQ65S.js +21 -0
- package/dist/chunk-O74MJHXU.js +187 -0
- package/dist/chunk-OJZEX73F.js +112 -0
- package/dist/chunk-PAAWLLZX.js +74 -0
- package/dist/chunk-PFXFQQRT.js +339 -0
- package/dist/chunk-PFZO3C6H.js +14 -0
- package/dist/chunk-PHH7LD45.js +113 -0
- package/dist/chunk-QRTG4XTI.js +188 -0
- package/dist/chunk-QVOVMXJR.js +1351 -0
- package/dist/chunk-RCQYT2I4.js +757 -0
- package/dist/chunk-RD6DEE36.js +1178 -0
- package/dist/chunk-RJIZEJ42.js +30 -0
- package/dist/chunk-RPOA7IAE.js +150 -0
- package/dist/chunk-SN2DQRL6.js +1172 -0
- package/dist/chunk-SQJVFEBV.js +41 -0
- package/dist/chunk-SQP64RCS.js +5391 -0
- package/dist/chunk-SVT4RNTR.js +49 -0
- package/dist/chunk-SWUFN7XM.js +43 -0
- package/dist/chunk-SXMVYC5H.js +174 -0
- package/dist/chunk-SZ6AYUIZ.js +23 -0
- package/dist/chunk-T3KGZBU5.js +3171 -0
- package/dist/chunk-TBRL2RCC.js +9 -0
- package/dist/chunk-TRT2HRWZ.js +18 -0
- package/dist/chunk-TYW2GI5I.js +1077 -0
- package/dist/chunk-UIWONWVG.js +4959 -0
- package/dist/chunk-VB3H5WWB.js +151 -0
- package/dist/chunk-W4A6QA3D.js +258 -0
- package/dist/chunk-WBPVKEEE.js +227 -0
- package/dist/chunk-WEBMSQP2.js +123 -0
- package/dist/chunk-WOEZV3Q4.js +333 -0
- package/dist/chunk-WQQON3SV.js +473 -0
- package/dist/chunk-WQTOVMGV.js +119 -0
- package/dist/chunk-WZO23AWO.js +2779 -0
- package/dist/chunk-XBDP4QPW.js +7 -0
- package/dist/chunk-XEBHSJLT.js +103 -0
- package/dist/chunk-XQ2ECNK3.js +1 -0
- package/dist/chunk-XQ2XGIBF.js +2086 -0
- package/dist/chunk-Y2GE5TSI.js +355 -0
- package/dist/chunk-YBUJCZ57.js +351 -0
- package/dist/chunk-YNCVSFGF.js +149 -0
- package/dist/chunk-YUPMXOGP.js +282 -0
- package/dist/chunk-YY6APYUM.js +49 -0
- package/dist/chunk-Z2IJUO45.js +691 -0
- package/dist/chunk-ZEHUCJHY.js +129 -0
- package/dist/chunk-ZYU2YOJ6.js +1496 -0
- package/dist/chunk-ZZ4V3WIZ.js +163 -0
- package/dist/coordination/agents/index.js +145 -6260
- package/dist/coordination/index.js +365 -34323
- package/dist/coordination/mailbox-project-server.js +48 -3220
- package/dist/coordination/mailbox-types.d.ts +13 -1
- package/dist/coordination/provider-status-tracker.d.ts +4 -0
- package/dist/coordination/sqlite-mailbox.d.ts +2 -1
- package/dist/core/agent.d.ts +14 -1
- package/dist/core/index.js +267 -12479
- package/dist/core/model-ref.js +5 -23
- package/dist/core/provider-runner.d.ts +36 -1
- package/dist/design/index.js +50 -1087
- package/dist/execution/index.d.ts +1 -1
- package/dist/execution/index.js +1599 -21887
- package/dist/execution/prompt-enhancer.js +16 -535
- package/dist/execution/retry-policy.d.ts +31 -1
- package/dist/extension/index.js +7 -311
- package/dist/goal/index.js +33 -3435
- package/dist/hooks/index.js +14 -1138
- package/dist/hq/index.js +112 -5139
- package/dist/hq/protocol.js +21 -668
- package/dist/index.js +1410 -92655
- package/dist/infrastructure/index.js +64 -4811
- package/dist/kernel/events/provider-events.d.ts +15 -0
- package/dist/kernel/events/wrongtrace-events.d.ts +26 -0
- package/dist/kernel/events.d.ts +16 -7
- package/dist/kernel/index.js +14 -992
- package/dist/models/index.js +40 -1117
- package/dist/notifications/index.js +3 -108
- package/dist/observability/index.js +17 -279
- package/dist/plugin/index.js +584 -14835
- package/dist/prompts/index.js +14 -948
- package/dist/registry/index.js +9 -822
- package/dist/registry-R23UBE7H.js +14 -0
- package/dist/replay/index.js +6 -84
- package/dist/review-finding-store-DKAV3AS6.js +21 -0
- package/dist/review-report-store-OWY3EC3N.js +19 -0
- package/dist/security/index.js +114 -2202
- package/dist/session-catalog/index.js +36 -2103
- package/dist/session-catalog/project-server.js +42 -1435
- package/dist/skills/index.js +35 -1578
- package/dist/storage/index.js +182 -15600
- package/dist/tasking/index.js +10 -676
- package/dist/tools/index.js +52 -9589
- package/dist/types/config/autonomy.d.ts +5 -0
- package/dist/types/config/tools.d.ts +2 -2
- package/dist/types/config/ui.d.ts +1 -1
- package/dist/types/errors.d.ts +18 -0
- package/dist/types/index.js +121 -1341
- package/dist/types/provider-runner.d.ts +8 -0
- package/dist/types/session-markers.js +8 -144
- package/dist/utils/error.js +3 -4
- package/dist/utils/expect-defined.js +3 -9
- package/dist/utils/heap-watchdog.js +6 -706
- package/dist/utils/index.js +269 -5685
- package/dist/utils/sage-output-block.js +5 -42
- package/dist/wiring/proxy-rewrite.d.ts +92 -4
- package/dist/wiring/proxy-rewrite.js +20 -61
- package/dist/worktree/index.js +6 -887
- package/package.json +4 -4
|
@@ -0,0 +1,3171 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AGENT_CATALOG,
|
|
3
|
+
ALL_AGENT_DEFINITIONS,
|
|
4
|
+
ROLE_SKILL_SETS,
|
|
5
|
+
SHADOW_AGENT_SKILLS,
|
|
6
|
+
TOOLS,
|
|
7
|
+
agentPrompt
|
|
8
|
+
} from "./chunk-FHAMQS7Q.js";
|
|
9
|
+
import {
|
|
10
|
+
setBtwNote
|
|
11
|
+
} from "./chunk-SQJVFEBV.js";
|
|
12
|
+
import {
|
|
13
|
+
ToolCapabilities
|
|
14
|
+
} from "./chunk-A455ATZB.js";
|
|
15
|
+
import {
|
|
16
|
+
SECRET_FILE_MODE
|
|
17
|
+
} from "./chunk-H5E77KUJ.js";
|
|
18
|
+
import {
|
|
19
|
+
ProviderError
|
|
20
|
+
} from "./chunk-NFAYHPRH.js";
|
|
21
|
+
import {
|
|
22
|
+
safeParse
|
|
23
|
+
} from "./chunk-RPOA7IAE.js";
|
|
24
|
+
import {
|
|
25
|
+
readBundledInstructionText,
|
|
26
|
+
renderInstructionTemplate
|
|
27
|
+
} from "./chunk-5S437PRC.js";
|
|
28
|
+
import {
|
|
29
|
+
AgentError,
|
|
30
|
+
ERROR_CODES
|
|
31
|
+
} from "./chunk-WOEZV3Q4.js";
|
|
32
|
+
import {
|
|
33
|
+
toErrorMessage
|
|
34
|
+
} from "./chunk-TBRL2RCC.js";
|
|
35
|
+
|
|
36
|
+
// src/coordination/subagent-finish.ts
|
|
37
|
+
var SUBAGENT_FINISH_REQUESTED_EVENT = "subagent.finish_requested";
|
|
38
|
+
var DEFAULT_SUBAGENT_FINISH_GRACE_MS = 12e4;
|
|
39
|
+
function resolveGracefulFinish(config) {
|
|
40
|
+
const raw = config.gracefulFinish;
|
|
41
|
+
if (raw === void 0 || raw === false) return void 0;
|
|
42
|
+
if (raw === true) return { graceMs: DEFAULT_SUBAGENT_FINISH_GRACE_MS };
|
|
43
|
+
const graceMs = typeof raw.graceMs === "number" && Number.isFinite(raw.graceMs) && raw.graceMs > 0 ? Math.floor(raw.graceMs) : DEFAULT_SUBAGENT_FINISH_GRACE_MS;
|
|
44
|
+
return { graceMs };
|
|
45
|
+
}
|
|
46
|
+
function buildSubagentFinishNotice(input) {
|
|
47
|
+
const localTime = new Date(input.deadlineMs).toISOString();
|
|
48
|
+
const seconds = Math.max(1, Math.round(input.graceMs / 1e3));
|
|
49
|
+
const timeLeft = input.graceMs > 0 ? `You have roughly ${seconds} seconds (until ${localTime}) of legitimate working time left.` : `Your working-time window is already spent (deadline was ${localTime}) \u2014 finish now.`;
|
|
50
|
+
return [
|
|
51
|
+
"[SUBAGENT FINISH] The leader agent has finished its work.",
|
|
52
|
+
`Reason: ${input.reason}`,
|
|
53
|
+
timeLeft,
|
|
54
|
+
"Finish your task now, in this turn: complete the thought you are working on, stop",
|
|
55
|
+
"starting new tool calls unless one is strictly required to finish, and write your",
|
|
56
|
+
"final answer or report as your final output, then end your turn.",
|
|
57
|
+
"Do not restart the task and do not begin new work."
|
|
58
|
+
].join("\n");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// src/coordination/subagent-budget.ts
|
|
62
|
+
var TIMEOUT_PREEMPT_FRACTION = 0.85;
|
|
63
|
+
var DECISION_TIMEOUT_MS = 6e4;
|
|
64
|
+
var BudgetExceededError = class extends Error {
|
|
65
|
+
kind;
|
|
66
|
+
limit;
|
|
67
|
+
observed;
|
|
68
|
+
constructor(kind, limit, observed) {
|
|
69
|
+
super(`Budget exceeded: ${kind} (limit=${limit}, observed=${observed})`);
|
|
70
|
+
this.name = "BudgetExceededError";
|
|
71
|
+
this.kind = kind;
|
|
72
|
+
this.limit = limit;
|
|
73
|
+
this.observed = observed;
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
var BudgetThresholdSignal = class extends Error {
|
|
77
|
+
kind;
|
|
78
|
+
limit;
|
|
79
|
+
used;
|
|
80
|
+
/** Resolves to 'extend' (with optional new limits) or 'stop' */
|
|
81
|
+
decision;
|
|
82
|
+
constructor(kind, limit, used, decision) {
|
|
83
|
+
super(`Budget soft limit: ${kind} (limit=${limit}, used=${used})`);
|
|
84
|
+
this.name = "BudgetThresholdSignal";
|
|
85
|
+
this.kind = kind;
|
|
86
|
+
this.limit = limit;
|
|
87
|
+
this.used = used;
|
|
88
|
+
this.decision = decision;
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
var SubagentBudget = class _SubagentBudget {
|
|
92
|
+
limits;
|
|
93
|
+
/** Patch one or more budget limits in-place after construction.
|
|
94
|
+
* Used by the coordinator watchdog when granting an extension.
|
|
95
|
+
* All fields are optional — only provided fields are updated.
|
|
96
|
+
* This is the single write path for limit mutations so that future
|
|
97
|
+
* validation or side-effects live in one place (M1). */
|
|
98
|
+
patchLimits(ext) {
|
|
99
|
+
if (ext.maxIterations !== void 0) {
|
|
100
|
+
this.limits.maxIterations = ext.maxIterations;
|
|
101
|
+
}
|
|
102
|
+
if (ext.maxToolCalls !== void 0) {
|
|
103
|
+
this.limits.maxToolCalls = ext.maxToolCalls;
|
|
104
|
+
}
|
|
105
|
+
if (ext.maxTokens !== void 0) {
|
|
106
|
+
this.limits.maxTokens = ext.maxTokens;
|
|
107
|
+
}
|
|
108
|
+
if (ext.maxCostUsd !== void 0) {
|
|
109
|
+
this.limits.maxCostUsd = ext.maxCostUsd;
|
|
110
|
+
}
|
|
111
|
+
if (ext.timeoutMs !== void 0) {
|
|
112
|
+
this.limits.timeoutMs = ext.timeoutMs;
|
|
113
|
+
}
|
|
114
|
+
if (ext.idleTimeoutMs !== void 0) {
|
|
115
|
+
this.limits.idleTimeoutMs = ext.idleTimeoutMs;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Graceful-finish state (see coordination/subagent-finish.ts).
|
|
120
|
+
* `_finishNotified` guards the single in-band emission; `_grace` records a
|
|
121
|
+
* granted working-time extension past the original wall-clock deadline.
|
|
122
|
+
* They are separate because the two callers want different semantics:
|
|
123
|
+
* the watchdog grants grace at the deadline crossing (notify + extend),
|
|
124
|
+
* while an explicit leader-finished request only notifies — a subagent
|
|
125
|
+
* well inside its budget keeps its full legitimate working time and simply
|
|
126
|
+
* accelerates.
|
|
127
|
+
*/
|
|
128
|
+
_finishNotified = false;
|
|
129
|
+
_grace = null;
|
|
130
|
+
/** True once the in-band finish notification has been emitted. */
|
|
131
|
+
get finishNotified() {
|
|
132
|
+
return this._finishNotified;
|
|
133
|
+
}
|
|
134
|
+
/** True once a grace window has been granted past the original deadline. */
|
|
135
|
+
get graceGranted() {
|
|
136
|
+
return this._grace !== null;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Notify the subagent in-band to finish its task in its own turn:
|
|
140
|
+
* `subagent.finish_requested` is emitted on the wired EventBus and the
|
|
141
|
+
* agent loop folds the notice into the conversation between tool batches.
|
|
142
|
+
* Nothing aborts — this is a notification, never an interrupt.
|
|
143
|
+
*
|
|
144
|
+
* `opts.graceMs` additionally extends the wall-clock ceiling by that window
|
|
145
|
+
* (used by the watchdog at a deadline crossing, so the model gets working
|
|
146
|
+
* time instead of a kill). Omit it to notify without touching the budget —
|
|
147
|
+
* the subagent keeps its existing time budget and just accelerates.
|
|
148
|
+
*
|
|
149
|
+
* Returns `true` when this call did something (emitted the notification
|
|
150
|
+
* and/or granted grace); `false` when there was nothing to do (already
|
|
151
|
+
* notified, grace already granted, no EventBus wired, budget not started).
|
|
152
|
+
*/
|
|
153
|
+
notifyFinish(reason, opts, now = Date.now) {
|
|
154
|
+
if (!this._events) return false;
|
|
155
|
+
if (this.startTime === null) return false;
|
|
156
|
+
const shouldEmit = !this._finishNotified;
|
|
157
|
+
const rawGrace = opts?.graceMs;
|
|
158
|
+
const shouldGrant = rawGrace !== void 0 && this._grace === null;
|
|
159
|
+
if (!shouldEmit && !shouldGrant) return false;
|
|
160
|
+
let grantedGraceMs = 0;
|
|
161
|
+
let graceDeadlineMs;
|
|
162
|
+
if (shouldGrant && rawGrace !== void 0) {
|
|
163
|
+
grantedGraceMs = Number.isFinite(rawGrace) && rawGrace > 0 ? Math.floor(rawGrace) : DEFAULT_SUBAGENT_FINISH_GRACE_MS;
|
|
164
|
+
graceDeadlineMs = now() + grantedGraceMs;
|
|
165
|
+
this._grace = { deadlineMs: graceDeadlineMs, graceMs: grantedGraceMs };
|
|
166
|
+
this.patchLimits({ timeoutMs: graceDeadlineMs - this.startTime });
|
|
167
|
+
}
|
|
168
|
+
if (shouldEmit) {
|
|
169
|
+
this._finishNotified = true;
|
|
170
|
+
const effectiveDeadlineMs = graceDeadlineMs ?? (this.limits.timeoutMs !== void 0 ? this.startTime + this.limits.timeoutMs : now() + DEFAULT_SUBAGENT_FINISH_GRACE_MS);
|
|
171
|
+
const effectiveGraceMs = Math.max(0, effectiveDeadlineMs - now());
|
|
172
|
+
const subagentId = this._subagentId;
|
|
173
|
+
this._events.emit(SUBAGENT_FINISH_REQUESTED_EVENT, {
|
|
174
|
+
// Omitted entirely when the budget was built without an id — an
|
|
175
|
+
// empty string is an address that matches nothing.
|
|
176
|
+
...subagentId !== void 0 ? { subagentId } : {},
|
|
177
|
+
reason,
|
|
178
|
+
deadlineMs: effectiveDeadlineMs,
|
|
179
|
+
graceMs: effectiveGraceMs,
|
|
180
|
+
notice: buildSubagentFinishNotice({
|
|
181
|
+
reason,
|
|
182
|
+
deadlineMs: effectiveDeadlineMs,
|
|
183
|
+
graceMs: effectiveGraceMs
|
|
184
|
+
})
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
return true;
|
|
188
|
+
}
|
|
189
|
+
/** Epoch ms by which the subagent should have produced its final output,
|
|
190
|
+
* once a grace window was granted. Undefined before that. */
|
|
191
|
+
get finishDeadlineMs() {
|
|
192
|
+
return this._grace?.deadlineMs;
|
|
193
|
+
}
|
|
194
|
+
iterations = 0;
|
|
195
|
+
toolCalls = 0;
|
|
196
|
+
tokenInput = 0;
|
|
197
|
+
tokenOutput = 0;
|
|
198
|
+
costUsd = 0;
|
|
199
|
+
startTime = null;
|
|
200
|
+
/**
|
|
201
|
+
* Timestamp of the most recent activity (iteration / tool call / token
|
|
202
|
+
* usage / streamed progress). Drives the idle timeout — reset by
|
|
203
|
+
* `markActivity()`. Initialised to `start()` time so a never-active agent
|
|
204
|
+
* still eventually trips its idle window.
|
|
205
|
+
*/
|
|
206
|
+
lastActivityTime = null;
|
|
207
|
+
_onThreshold;
|
|
208
|
+
_sessionId;
|
|
209
|
+
/** Owning subagent id — used to address the graceful-finish event. */
|
|
210
|
+
_subagentId;
|
|
211
|
+
/** True when only the coordinator watchdog may enforce wall-clock limits. */
|
|
212
|
+
_wallClockWatchdogOwned;
|
|
213
|
+
/**
|
|
214
|
+
* Hard cap on how long `_negotiateExtension` waits for the coordinator to
|
|
215
|
+
* respond before defaulting to 'stop'. Without this fallback an absent
|
|
216
|
+
* or hung listener (Director not built / event filter detached mid-run)
|
|
217
|
+
* leaves the budget over-limit and never enforces anything.
|
|
218
|
+
*/
|
|
219
|
+
static DECISION_TIMEOUT_MS = DECISION_TIMEOUT_MS;
|
|
220
|
+
/**
|
|
221
|
+
* Injected by the runner when wiring the budget to its EventBus.
|
|
222
|
+
* Used to emit `budget.threshold_reached` events in `'auto'` mode.
|
|
223
|
+
*/
|
|
224
|
+
_events;
|
|
225
|
+
/**
|
|
226
|
+
* Guard against dual-path races between the coordinator watchdog
|
|
227
|
+
* (`executeWithTimeout`) and the budget's own `checkTimeout()`.
|
|
228
|
+
* Both paths detect `elapsed >= timeoutMs` and can emit
|
|
229
|
+
* `budget.threshold_reached` for kind `'timeout'` simultaneously.
|
|
230
|
+
* Set to the current `timeoutMs` ceiling by the coordinator BEFORE
|
|
231
|
+
* calling `onThreshold`, and cleared after the negotiation resolves.
|
|
232
|
+
* `checkTimeout()` skips its wall-clock check while this is set so
|
|
233
|
+
* the coordinator's watchdog is the sole source of wall-clock timeout
|
|
234
|
+
* events — `checkTimeout()` focuses exclusively on `idle_timeout`.
|
|
235
|
+
*/
|
|
236
|
+
_watchdogActive;
|
|
237
|
+
/** Returns the timeout ceiling currently being negotiated by the watchdog,
|
|
238
|
+
* or `undefined` when no wall-clock negotiation is in flight.
|
|
239
|
+
* Used by `executeWithTimeout` to detect a stale lock (M3). */
|
|
240
|
+
get watchdogActive() {
|
|
241
|
+
return this._watchdogActive;
|
|
242
|
+
}
|
|
243
|
+
/** Called by the coordinator watchdog BEFORE calling `onThreshold` so that
|
|
244
|
+
* `checkTimeout()` skips its wall-clock check for this ceiling. Prevents
|
|
245
|
+
* the budget's own `checkTimeout()` from emitting a second
|
|
246
|
+
* `budget.threshold_reached` event while the watchdog is already
|
|
247
|
+
* negotiating the same wall-clock deadline (C1). */
|
|
248
|
+
setWatchdogNegotiation(timeoutMs) {
|
|
249
|
+
this._watchdogActive = timeoutMs;
|
|
250
|
+
}
|
|
251
|
+
/** Clears the watchdog guard after negotiation resolves. Called in the
|
|
252
|
+
* `finally` block of both the pre-empt and deadline branches so it fires
|
|
253
|
+
* on every exit path: grant, deny, throw, or error. */
|
|
254
|
+
clearWatchdogNegotiation() {
|
|
255
|
+
this._watchdogActive = void 0;
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Negotiation mode — controls whether a threshold hit tries to emit
|
|
259
|
+
* `budget.threshold_reached` and wait for a coordinator decision, or
|
|
260
|
+
* falls straight through to a synchronous hard stop.
|
|
261
|
+
*
|
|
262
|
+
* `'auto'` (default) — emit on the EventBus and wait; times out to 'stop'.
|
|
263
|
+
* `'sync'` — throw `BudgetExceededError` immediately regardless of listeners.
|
|
264
|
+
*/
|
|
265
|
+
_mode;
|
|
266
|
+
/**
|
|
267
|
+
* Optional callback for soft-limit handling. When set, the budget will
|
|
268
|
+
* invoke it rather than throw immediately. The handler decides whether to
|
|
269
|
+
* throw synchronously, continue, or ask the coordinator for an extension.
|
|
270
|
+
*/
|
|
271
|
+
get onThreshold() {
|
|
272
|
+
return this._onThreshold;
|
|
273
|
+
}
|
|
274
|
+
set onThreshold(fn) {
|
|
275
|
+
this._onThreshold = fn;
|
|
276
|
+
}
|
|
277
|
+
/** Returns the current negotiation mode. */
|
|
278
|
+
get mode() {
|
|
279
|
+
return this._mode;
|
|
280
|
+
}
|
|
281
|
+
constructor(limits = {}, mode = "auto", options = {}) {
|
|
282
|
+
this._mode = mode;
|
|
283
|
+
this._sessionId = options.sessionId;
|
|
284
|
+
this._subagentId = options.subagentId;
|
|
285
|
+
this._wallClockWatchdogOwned = options.wallClockWatchdogOwned === true;
|
|
286
|
+
this.limits = { ...limits };
|
|
287
|
+
}
|
|
288
|
+
currentSessionId() {
|
|
289
|
+
const value = typeof this._sessionId === "function" ? this._sessionId() : this._sessionId;
|
|
290
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
291
|
+
}
|
|
292
|
+
start() {
|
|
293
|
+
this.startTime = Date.now();
|
|
294
|
+
this.lastActivityTime = this.startTime;
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Reset the idle clock. Called on any sign of forward progress —
|
|
298
|
+
* iterations, tool calls, token usage, and streamed tool/text progress —
|
|
299
|
+
* so a long-but-productive subagent never trips its `idleTimeoutMs`.
|
|
300
|
+
*/
|
|
301
|
+
markActivity() {
|
|
302
|
+
this.lastActivityTime = Date.now();
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Milliseconds since the last activity signal. Returns 0 before `start()`
|
|
306
|
+
* (nothing to measure yet). Used by the coordinator watchdog to decide
|
|
307
|
+
* whether to re-arm (still active) or reap (genuinely idle).
|
|
308
|
+
*/
|
|
309
|
+
idleMs() {
|
|
310
|
+
const since = this.lastActivityTime ?? this.startTime;
|
|
311
|
+
return since === null ? 0 : Date.now() - since;
|
|
312
|
+
}
|
|
313
|
+
/** Returns true if we're within 10% of any limit — useful for pre-flight checks. */
|
|
314
|
+
isNearLimit() {
|
|
315
|
+
const { maxIterations, maxToolCalls, maxTokens, maxCostUsd } = this.limits;
|
|
316
|
+
if (maxIterations && this.iterations >= maxIterations * 0.9) return true;
|
|
317
|
+
if (maxToolCalls && this.toolCalls >= maxToolCalls * 0.9) return true;
|
|
318
|
+
if (maxTokens && this.tokenInput + this.tokenOutput >= maxTokens * 0.9) return true;
|
|
319
|
+
if (maxCostUsd && this.costUsd >= maxCostUsd * 0.9) return true;
|
|
320
|
+
return false;
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Synchronous budget check. Always throws synchronously so callers (especially
|
|
324
|
+
* test event handlers using `expect().toThrow()`) get an unhandled rejection
|
|
325
|
+
* when the budget is exceeded without a handler.
|
|
326
|
+
*
|
|
327
|
+
* Decision table:
|
|
328
|
+
* - no `onThreshold` handler → throw `BudgetExceededError` (hard stop, always)
|
|
329
|
+
* - `mode === 'sync'` → throw `BudgetExceededError` (hard stop, always)
|
|
330
|
+
* - `mode === 'auto'` + no listener → throw `BudgetExceededError` (hard stop; no one to ask)
|
|
331
|
+
* - `mode === 'auto'` + listener → throw `BudgetThresholdSignal` with async decision promise
|
|
332
|
+
*/
|
|
333
|
+
/**
|
|
334
|
+
* Collects all exceeded budget kinds into a single NOOP-free negotiation.
|
|
335
|
+
* Called by recordIteration / recordToolCall / recordUsage — each may call
|
|
336
|
+
* this for its own kind. The first call starts the negotiation and stores
|
|
337
|
+
* the Promise in _pendingNegotiation. Subsequent calls for DIFFERENT
|
|
338
|
+
* kinds (while a negotiation is in flight) are NOOPs — they don't start
|
|
339
|
+
* new conversations with the coordinator. This prevents an EventBus flood
|
|
340
|
+
* when multiple budget kinds are exceeded simultaneously in one iteration.
|
|
341
|
+
*
|
|
342
|
+
* Returns the kinds that were found to be exceeded (for logging/debugging).
|
|
343
|
+
*/
|
|
344
|
+
checkLimits(elapsedMs) {
|
|
345
|
+
const exceeded = [];
|
|
346
|
+
if (this.limits.maxIterations !== void 0 && this.iterations > this.limits.maxIterations) {
|
|
347
|
+
exceeded.push({ kind: "iterations", used: this.iterations, limit: this.limits.maxIterations });
|
|
348
|
+
}
|
|
349
|
+
if (this.limits.maxToolCalls !== void 0 && this.toolCalls > this.limits.maxToolCalls) {
|
|
350
|
+
exceeded.push({ kind: "tool_calls", used: this.toolCalls, limit: this.limits.maxToolCalls });
|
|
351
|
+
}
|
|
352
|
+
const totalTokens = this.tokenInput + this.tokenOutput;
|
|
353
|
+
if (this.limits.maxTokens !== void 0 && totalTokens > this.limits.maxTokens) {
|
|
354
|
+
exceeded.push({ kind: "tokens", used: totalTokens, limit: this.limits.maxTokens });
|
|
355
|
+
}
|
|
356
|
+
if (this.limits.maxCostUsd !== void 0 && this.costUsd > this.limits.maxCostUsd) {
|
|
357
|
+
exceeded.push({ kind: "cost", used: this.costUsd, limit: this.limits.maxCostUsd });
|
|
358
|
+
}
|
|
359
|
+
if (elapsedMs !== void 0) {
|
|
360
|
+
const idle = this.idleMs();
|
|
361
|
+
if (this.limits.idleTimeoutMs !== void 0 && idle > this.limits.idleTimeoutMs) {
|
|
362
|
+
exceeded.push({ kind: "idle_timeout", used: idle, limit: this.limits.idleTimeoutMs });
|
|
363
|
+
}
|
|
364
|
+
const wallOwnedByWatchdog = this._wallClockWatchdogOwned || this._onThreshold !== void 0 && this._watchdogActive === this.limits.timeoutMs;
|
|
365
|
+
if (this.limits.timeoutMs !== void 0 && elapsedMs > this.limits.timeoutMs && !wallOwnedByWatchdog) {
|
|
366
|
+
exceeded.push({ kind: "timeout", used: elapsedMs, limit: this.limits.timeoutMs });
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
if (exceeded.length === 0) return [];
|
|
370
|
+
if (!this._onThreshold) {
|
|
371
|
+
const first2 = exceeded[0] ?? { kind: "iterations", limit: 0, used: 0 };
|
|
372
|
+
throw new BudgetExceededError(first2.kind, first2.limit, first2.used);
|
|
373
|
+
}
|
|
374
|
+
if (this._mode === "sync") {
|
|
375
|
+
const first2 = exceeded[0] ?? { kind: "iterations", limit: 0, used: 0 };
|
|
376
|
+
throw new BudgetExceededError(first2.kind, first2.limit, first2.used);
|
|
377
|
+
}
|
|
378
|
+
const bus = this._events;
|
|
379
|
+
if (!bus) {
|
|
380
|
+
const first2 = exceeded[0] ?? { kind: "iterations", limit: 0, used: 0 };
|
|
381
|
+
throw new BudgetExceededError(first2.kind, first2.limit, first2.used);
|
|
382
|
+
}
|
|
383
|
+
const first = exceeded[0] ?? { kind: "iterations", limit: 0, used: 0 };
|
|
384
|
+
if (bus.hasListenerFor("budget.threshold_reached")) {
|
|
385
|
+
for (const entry of exceeded) {
|
|
386
|
+
if (this._pendingNegotiations.has(entry.kind)) continue;
|
|
387
|
+
this._pendingNegotiations.set(entry.kind, this._negotiateExtension(entry));
|
|
388
|
+
}
|
|
389
|
+
const decision = this._pendingNegotiations.get(first.kind);
|
|
390
|
+
if (!decision) throw new Error(`No pending negotiation for ${first.kind}`);
|
|
391
|
+
throw new BudgetThresholdSignal(first.kind, first.limit, first.used, decision);
|
|
392
|
+
}
|
|
393
|
+
let hardStop = null;
|
|
394
|
+
for (const entry of exceeded) {
|
|
395
|
+
if (this._pendingNegotiations.has(entry.kind)) continue;
|
|
396
|
+
const marker = Promise.resolve("stop");
|
|
397
|
+
this._pendingNegotiations.set(entry.kind, marker);
|
|
398
|
+
void marker.finally(() => this._pendingNegotiations.delete(entry.kind));
|
|
399
|
+
const sync = this._invokeHandlerSync(entry);
|
|
400
|
+
if (!sync) hardStop ??= new BudgetExceededError(entry.kind, entry.limit, entry.used);
|
|
401
|
+
}
|
|
402
|
+
if (hardStop) throw hardStop;
|
|
403
|
+
return exceeded;
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* Invoke `onThreshold` once for `entry` on the NO-LISTENER path and report
|
|
407
|
+
* whether it decided synchronously. Returns `true` when the handler returned
|
|
408
|
+
* a synchronous decision (already honored — an `extend` patched the limits),
|
|
409
|
+
* or `false` when it returned a Promise (async; the caller hard-stops, since
|
|
410
|
+
* there is no listener to resolve the negotiation). The handler is given the
|
|
411
|
+
* full info shape (`requestDecision` plus direct `extend`/`deny`) so both
|
|
412
|
+
* recording handlers and policy handlers work without a wired listener.
|
|
413
|
+
*/
|
|
414
|
+
_invokeHandlerSync(entry) {
|
|
415
|
+
const handler = this._onThreshold;
|
|
416
|
+
if (!handler) return false;
|
|
417
|
+
let extendArg;
|
|
418
|
+
const result = handler({
|
|
419
|
+
kind: entry.kind,
|
|
420
|
+
used: entry.used,
|
|
421
|
+
limit: entry.limit,
|
|
422
|
+
requestDecision: () => this._busRequestDecision(entry),
|
|
423
|
+
// Direct hooks for synchronous policy/recording handlers.
|
|
424
|
+
extend: (extra) => {
|
|
425
|
+
extendArg = extra;
|
|
426
|
+
},
|
|
427
|
+
deny: () => {
|
|
428
|
+
}
|
|
429
|
+
});
|
|
430
|
+
if (result && typeof result.then === "function") return false;
|
|
431
|
+
if (result === "throw") return false;
|
|
432
|
+
if (result && typeof result === "object" && "extend" in result) {
|
|
433
|
+
extendArg = result.extend;
|
|
434
|
+
}
|
|
435
|
+
if (extendArg) this.patchLimits(extendArg);
|
|
436
|
+
return true;
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Emit `budget.threshold_reached` and resolve to the listener's verdict.
|
|
440
|
+
* Resolves to `'stop'` immediately when there is no listener (or no bus) so
|
|
441
|
+
* no negotiation can hang and no fallback timer leaks. Mirrors the
|
|
442
|
+
* coordinator watchdog's own request path so both agree on the no-listener
|
|
443
|
+
* default.
|
|
444
|
+
*/
|
|
445
|
+
_busRequestDecision(entry) {
|
|
446
|
+
const bus = this._events;
|
|
447
|
+
if (!bus?.hasListenerFor("budget.threshold_reached")) {
|
|
448
|
+
return Promise.resolve("stop");
|
|
449
|
+
}
|
|
450
|
+
return new Promise((resolve) => {
|
|
451
|
+
let resolved = false;
|
|
452
|
+
const respond = (d) => {
|
|
453
|
+
if (resolved) return;
|
|
454
|
+
resolved = true;
|
|
455
|
+
clearTimeout(fallback);
|
|
456
|
+
resolve(d);
|
|
457
|
+
};
|
|
458
|
+
const fallback = setTimeout(() => respond("stop"), _SubagentBudget.DECISION_TIMEOUT_MS);
|
|
459
|
+
const sessionId = this.currentSessionId();
|
|
460
|
+
bus.emit("budget.threshold_reached", {
|
|
461
|
+
...sessionId ? { sessionId } : {},
|
|
462
|
+
kind: entry.kind,
|
|
463
|
+
used: entry.used,
|
|
464
|
+
limit: entry.limit,
|
|
465
|
+
timeoutMs: _SubagentBudget.DECISION_TIMEOUT_MS,
|
|
466
|
+
// deny() wins over a same-dispatch extend(): a listener that both grants
|
|
467
|
+
// and denies (or two listeners disagreeing) is resolved as a stop. The
|
|
468
|
+
// grant is deferred a microtask so a synchronous deny in the same emit
|
|
469
|
+
// pre-empts it; async grants still resolve normally.
|
|
470
|
+
extend: (extra) => queueMicrotask(() => respond({ extend: extra })),
|
|
471
|
+
deny: () => respond("stop")
|
|
472
|
+
});
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* Per-kind in-flight negotiation Promises. Each budget kind can have its
|
|
477
|
+
* own concurrent negotiation — e.g. iterations and timeout can both
|
|
478
|
+
* be exceeded simultaneously without blocking each other. The same kind
|
|
479
|
+
* cannot start two concurrent negotiations (dedup guard).
|
|
480
|
+
* Cleared in `_negotiateExtension`'s `finally` block.
|
|
481
|
+
*/
|
|
482
|
+
_pendingNegotiations = /* @__PURE__ */ new Map();
|
|
483
|
+
/**
|
|
484
|
+
* Drive the threshold handler to a decision. Resolves with `'stop'`
|
|
485
|
+
* (signal the runner to abort) or `{ extend: ... }` (limits already
|
|
486
|
+
* patched in-place; the runner should not abort). Clears the
|
|
487
|
+
* per-kind slot in `_pendingNegotiations` in `finally`.
|
|
488
|
+
*
|
|
489
|
+
* The 'continue' return from a sync handler is treated as
|
|
490
|
+
* `{ extend: {} }` — keep going without patching; next overrun fires
|
|
491
|
+
* a fresh signal.
|
|
492
|
+
*/
|
|
493
|
+
async _negotiateExtension(entry) {
|
|
494
|
+
if (!this._onThreshold) {
|
|
495
|
+
return "stop";
|
|
496
|
+
}
|
|
497
|
+
try {
|
|
498
|
+
const result = this._onThreshold({
|
|
499
|
+
kind: entry.kind,
|
|
500
|
+
used: entry.used,
|
|
501
|
+
limit: entry.limit,
|
|
502
|
+
// One event for THIS kind only — each exceeded kind has its own
|
|
503
|
+
// negotiation (and its own resolve), so there is no cross-kind
|
|
504
|
+
// first-wins drop and no O(N^2) re-emission.
|
|
505
|
+
requestDecision: () => this._busRequestDecision(entry),
|
|
506
|
+
extend: (extra) => {
|
|
507
|
+
this.patchLimits(extra);
|
|
508
|
+
},
|
|
509
|
+
deny: () => {
|
|
510
|
+
}
|
|
511
|
+
});
|
|
512
|
+
if (result === "throw") return "stop";
|
|
513
|
+
if (result === "continue") return { extend: {} };
|
|
514
|
+
const decision = await result;
|
|
515
|
+
if (decision === "stop") return "stop";
|
|
516
|
+
this.patchLimits(decision.extend);
|
|
517
|
+
return decision;
|
|
518
|
+
} finally {
|
|
519
|
+
this._pendingNegotiations.delete(entry.kind);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
recordIteration() {
|
|
523
|
+
this.iterations++;
|
|
524
|
+
this.markActivity();
|
|
525
|
+
void this.checkLimits();
|
|
526
|
+
}
|
|
527
|
+
recordToolCall() {
|
|
528
|
+
this.toolCalls++;
|
|
529
|
+
this.markActivity();
|
|
530
|
+
void this.checkLimits();
|
|
531
|
+
}
|
|
532
|
+
recordUsage(usage, costUsd = 0) {
|
|
533
|
+
this.tokenInput += usage.input;
|
|
534
|
+
this.tokenOutput += usage.output;
|
|
535
|
+
this.costUsd += costUsd;
|
|
536
|
+
this.markActivity();
|
|
537
|
+
void this.checkLimits();
|
|
538
|
+
}
|
|
539
|
+
/**
|
|
540
|
+
* Wall-clock / idle budget check. Delegates to `checkLimits(elapsed)`, so
|
|
541
|
+
* `timeout` and `idle_timeout` follow the SAME negotiation path as the other
|
|
542
|
+
* kinds — they are NOT a special-cased hard stop. This is deliberate: a
|
|
543
|
+
* heartbeat-aware policy (see `attachAutoExtend` and `CollabSession`) grants
|
|
544
|
+
* a timeout extension only while the agent is making progress and denies it
|
|
545
|
+
* once the agent is genuinely stuck, which is safer than an unconditional
|
|
546
|
+
* hard kill of a long-but-working agent. The runner translates the resulting
|
|
547
|
+
* `BudgetThresholdSignal` decision (`extend` → patch limits in place,
|
|
548
|
+
* `stop` → abort) just like every other kind.
|
|
549
|
+
*
|
|
550
|
+
* Decision table (same as `checkLimits`):
|
|
551
|
+
* - no `onThreshold` handler → throw `BudgetExceededError` (hard stop)
|
|
552
|
+
* - `mode === 'sync'` → throw `BudgetExceededError` (hard stop)
|
|
553
|
+
* - `mode === 'auto'` + no listener → throw `BudgetExceededError` (no one to ask)
|
|
554
|
+
* - `mode === 'auto'` + listener → throw `BudgetThresholdSignal` (negotiated;
|
|
555
|
+
* a heartbeat-aware policy may extend the timeout)
|
|
556
|
+
*/
|
|
557
|
+
checkTimeout() {
|
|
558
|
+
if (this.startTime === null) return;
|
|
559
|
+
const { timeoutMs, idleTimeoutMs } = this.limits;
|
|
560
|
+
if (timeoutMs === void 0 && idleTimeoutMs === void 0) return;
|
|
561
|
+
const elapsed = Date.now() - this.startTime;
|
|
562
|
+
const wallSkipped = this._onThreshold !== void 0 && this._watchdogActive !== void 0 && timeoutMs !== void 0 && this._watchdogActive === timeoutMs;
|
|
563
|
+
const wallTripped = this._wallClockWatchdogOwned || wallSkipped ? false : timeoutMs !== void 0 && elapsed > timeoutMs;
|
|
564
|
+
const idleTripped = idleTimeoutMs !== void 0 && this.idleMs() > idleTimeoutMs;
|
|
565
|
+
if (!wallTripped && !idleTripped) return;
|
|
566
|
+
void this.checkLimits(elapsed);
|
|
567
|
+
}
|
|
568
|
+
/** Returns true if a wall-clock or idle timeout has occurred without throwing. */
|
|
569
|
+
isTimedOut() {
|
|
570
|
+
if (this.startTime === null) return false;
|
|
571
|
+
const { timeoutMs, idleTimeoutMs } = this.limits;
|
|
572
|
+
if (timeoutMs !== void 0 && Date.now() - this.startTime > timeoutMs) return true;
|
|
573
|
+
if (idleTimeoutMs !== void 0 && this.idleMs() > idleTimeoutMs) return true;
|
|
574
|
+
return false;
|
|
575
|
+
}
|
|
576
|
+
usage() {
|
|
577
|
+
return {
|
|
578
|
+
iterations: this.iterations,
|
|
579
|
+
toolCalls: this.toolCalls,
|
|
580
|
+
tokens: {
|
|
581
|
+
input: this.tokenInput,
|
|
582
|
+
output: this.tokenOutput,
|
|
583
|
+
total: this.tokenInput + this.tokenOutput
|
|
584
|
+
},
|
|
585
|
+
costUsd: this.costUsd,
|
|
586
|
+
elapsedMs: this.startTime === null ? 0 : Date.now() - this.startTime
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
};
|
|
590
|
+
|
|
591
|
+
// src/coordination/subagent-result-tool.ts
|
|
592
|
+
var SUBAGENT_STRUCTURED_REPORT_META_KEY = "subagentStructuredReport";
|
|
593
|
+
var MAX_SUBAGENT_STRUCTURED_REPORT_CHARS = 8e3;
|
|
594
|
+
var MAX_SUMMARY_CHARS = 1500;
|
|
595
|
+
var MAX_FINDINGS = 16;
|
|
596
|
+
var MAX_FILES = 64;
|
|
597
|
+
var MAX_NEXT_STEPS = 12;
|
|
598
|
+
var MAX_ITEM_CHARS = 500;
|
|
599
|
+
function normalizeSubagentStructuredReport(value) {
|
|
600
|
+
if (!isRecord(value)) return void 0;
|
|
601
|
+
const summary = nonEmptyString(value["summary"]);
|
|
602
|
+
const findings = stringArray(value["findings"]);
|
|
603
|
+
const filesExamined = stringArray(value["files_examined"]);
|
|
604
|
+
const nextSteps = stringArray(value["suggested_next_steps"]);
|
|
605
|
+
const confidence = value["confidence"];
|
|
606
|
+
const completion = value["completion"];
|
|
607
|
+
const remainingWork = optionalString(value["remaining_work"]);
|
|
608
|
+
if (!summary || summary.length > MAX_SUMMARY_CHARS || !findings || findings.length > MAX_FINDINGS || !filesExamined || filesExamined.length > MAX_FILES || !nextSteps || nextSteps.length > MAX_NEXT_STEPS || [...findings, ...filesExamined, ...nextSteps].some(
|
|
609
|
+
(item) => item.trim().length > MAX_ITEM_CHARS
|
|
610
|
+
) || typeof confidence !== "number" || !Number.isFinite(confidence) || confidence < 0 || confidence > 1 || completion !== void 0 && completion !== "complete" && completion !== "partial" || completion === "partial" && !remainingWork || remainingWork !== void 0 && remainingWork.length > MAX_SUMMARY_CHARS) {
|
|
611
|
+
return void 0;
|
|
612
|
+
}
|
|
613
|
+
const report = {
|
|
614
|
+
summary,
|
|
615
|
+
findings: findings.map((item) => item.trim()).filter(Boolean),
|
|
616
|
+
files_examined: filesExamined.map((item) => item.trim()).filter(Boolean),
|
|
617
|
+
confidence,
|
|
618
|
+
suggested_next_steps: nextSteps.map((item) => item.trim()).filter(Boolean),
|
|
619
|
+
...completion === "complete" || completion === "partial" ? { completion } : {},
|
|
620
|
+
...remainingWork ? { remaining_work: remainingWork } : {}
|
|
621
|
+
};
|
|
622
|
+
return JSON.stringify(report).length <= MAX_SUBAGENT_STRUCTURED_REPORT_CHARS ? report : void 0;
|
|
623
|
+
}
|
|
624
|
+
function readSubagentStructuredReport(ctx) {
|
|
625
|
+
return normalizeSubagentStructuredReport(ctx?.meta?.[SUBAGENT_STRUCTURED_REPORT_META_KEY]);
|
|
626
|
+
}
|
|
627
|
+
function formatSubagentStructuredReport(report) {
|
|
628
|
+
const findings = report.findings.length ? report.findings.map((item) => `- ${item}`).join("\n") : "- (none)";
|
|
629
|
+
const files = report.files_examined.length ? report.files_examined.join(", ") : "(none)";
|
|
630
|
+
const next = report.suggested_next_steps.length ? report.suggested_next_steps.map((item) => `- ${item}`).join("\n") : "- (none)";
|
|
631
|
+
return [
|
|
632
|
+
report.summary,
|
|
633
|
+
report.completion ? `Completion: ${report.completion}` : void 0,
|
|
634
|
+
report.remaining_work ? `Remaining work: ${report.remaining_work}` : void 0,
|
|
635
|
+
`
|
|
636
|
+
Findings:
|
|
637
|
+
${findings}`,
|
|
638
|
+
`
|
|
639
|
+
Files examined: ${files}`,
|
|
640
|
+
`Confidence: ${report.confidence.toFixed(2)}`,
|
|
641
|
+
`
|
|
642
|
+
Suggested next steps:
|
|
643
|
+
${next}`
|
|
644
|
+
].filter((line) => typeof line === "string").join("\n");
|
|
645
|
+
}
|
|
646
|
+
function makeSubagentResultTool() {
|
|
647
|
+
return {
|
|
648
|
+
name: "submit_result",
|
|
649
|
+
description: "Submit the task result as a compact structured report for the Director. Call once near the end, before your final text response.",
|
|
650
|
+
permission: "auto",
|
|
651
|
+
mutating: false,
|
|
652
|
+
capabilities: [ToolCapabilities.COORDINATION_RESULT_SUBMIT],
|
|
653
|
+
inputSchema: {
|
|
654
|
+
type: "object",
|
|
655
|
+
properties: {
|
|
656
|
+
summary: {
|
|
657
|
+
type: "string",
|
|
658
|
+
maxLength: MAX_SUMMARY_CHARS,
|
|
659
|
+
description: "Concise outcome summary."
|
|
660
|
+
},
|
|
661
|
+
findings: {
|
|
662
|
+
type: "array",
|
|
663
|
+
maxItems: MAX_FINDINGS,
|
|
664
|
+
items: { type: "string" },
|
|
665
|
+
description: "Atomic findings or changes, with evidence when useful."
|
|
666
|
+
},
|
|
667
|
+
files_examined: {
|
|
668
|
+
type: "array",
|
|
669
|
+
maxItems: MAX_FILES,
|
|
670
|
+
items: { type: "string" },
|
|
671
|
+
description: "Project-relative files materially read or changed."
|
|
672
|
+
},
|
|
673
|
+
confidence: {
|
|
674
|
+
type: "number",
|
|
675
|
+
minimum: 0,
|
|
676
|
+
maximum: 1,
|
|
677
|
+
description: "Confidence from 0.0 to 1.0."
|
|
678
|
+
},
|
|
679
|
+
suggested_next_steps: {
|
|
680
|
+
type: "array",
|
|
681
|
+
maxItems: MAX_NEXT_STEPS,
|
|
682
|
+
items: { type: "string" },
|
|
683
|
+
description: "Concrete follow-ups; use an empty array when none remain."
|
|
684
|
+
},
|
|
685
|
+
completion: {
|
|
686
|
+
type: "string",
|
|
687
|
+
enum: ["complete", "partial"],
|
|
688
|
+
description: "Use `partial` only when the task is intentionally stopping at a clean checkpoint and another worker should continue."
|
|
689
|
+
},
|
|
690
|
+
remaining_work: {
|
|
691
|
+
type: "string",
|
|
692
|
+
maxLength: MAX_SUMMARY_CHARS,
|
|
693
|
+
description: "Concrete work left for a successor. Required by runtime validation when completion is `partial`."
|
|
694
|
+
}
|
|
695
|
+
},
|
|
696
|
+
required: ["summary", "findings", "files_examined", "confidence", "suggested_next_steps"],
|
|
697
|
+
additionalProperties: false
|
|
698
|
+
},
|
|
699
|
+
async execute(input, ctx) {
|
|
700
|
+
const report = normalizeSubagentStructuredReport(input);
|
|
701
|
+
if (!report) {
|
|
702
|
+
return {
|
|
703
|
+
ok: false,
|
|
704
|
+
error: "Invalid report: summary/findings/files_examined/confidence/suggested_next_steps are required and confidence must be 0..1."
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
ctx.meta[SUBAGENT_STRUCTURED_REPORT_META_KEY] = report;
|
|
708
|
+
return {
|
|
709
|
+
ok: true,
|
|
710
|
+
findings: report.findings.length,
|
|
711
|
+
filesExamined: report.files_examined.length,
|
|
712
|
+
confidence: report.confidence
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
function isRecord(value) {
|
|
718
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
719
|
+
}
|
|
720
|
+
function nonEmptyString(value) {
|
|
721
|
+
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
722
|
+
}
|
|
723
|
+
function optionalString(value) {
|
|
724
|
+
return value === void 0 ? void 0 : nonEmptyString(value);
|
|
725
|
+
}
|
|
726
|
+
function stringArray(value) {
|
|
727
|
+
return Array.isArray(value) && value.every((item) => typeof item === "string") ? value : void 0;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
// src/coordination/agent-subagent-runner.ts
|
|
731
|
+
function withDisabledToolFiltering(factory) {
|
|
732
|
+
return async (config, task) => {
|
|
733
|
+
const result = await factory(config, task);
|
|
734
|
+
const disabled = config.disabledTools ?? [];
|
|
735
|
+
if (disabled.length === 0) return result;
|
|
736
|
+
const registry = result.agent.tools;
|
|
737
|
+
if (registry && typeof registry.unregister === "function") {
|
|
738
|
+
for (const toolName of disabled) {
|
|
739
|
+
registry.unregister(toolName);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
return result;
|
|
743
|
+
};
|
|
744
|
+
}
|
|
745
|
+
function makeAgentSubagentRunner(opts) {
|
|
746
|
+
const format = opts.formatTaskInput ?? defaultFormatTaskInput;
|
|
747
|
+
return async (task, ctx) => {
|
|
748
|
+
const taskStartedAt = Date.now();
|
|
749
|
+
const factoryResult = await opts.factory(ctx.config, task);
|
|
750
|
+
const { agent, events } = factoryResult;
|
|
751
|
+
const taskContext = task.context ?? {};
|
|
752
|
+
const hasExplicitTelemetryTask = typeof taskContext["telemetryTaskId"] === "string";
|
|
753
|
+
const telemetryTaskId = hasExplicitTelemetryTask ? taskContext["telemetryTaskId"] : task.id;
|
|
754
|
+
const telemetryRunId = typeof taskContext["telemetryRunId"] === "string" ? taskContext["telemetryRunId"] : void 0;
|
|
755
|
+
const telemetryBoardId = typeof taskContext["telemetryBoardId"] === "string" ? taskContext["telemetryBoardId"] : void 0;
|
|
756
|
+
const agentContext = agent.ctx;
|
|
757
|
+
if (agentContext?.meta) {
|
|
758
|
+
agentContext.meta["subagentTaskId"] = task.id;
|
|
759
|
+
const kanbanContext = taskContext["kanban"];
|
|
760
|
+
if (kanbanContext && typeof kanbanContext === "object") {
|
|
761
|
+
agentContext.meta["kanban"] = kanbanContext;
|
|
762
|
+
}
|
|
763
|
+
if (hasExplicitTelemetryTask) {
|
|
764
|
+
agentContext.meta["telemetryTaskId"] = telemetryTaskId;
|
|
765
|
+
if (telemetryRunId) agentContext.meta["telemetryRunId"] = telemetryRunId;
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
if (hasExplicitTelemetryTask) {
|
|
769
|
+
agentContext?.setCurrentKanbanTask?.(telemetryTaskId, telemetryBoardId);
|
|
770
|
+
} else {
|
|
771
|
+
const kanbanContext = taskContext["kanban"];
|
|
772
|
+
if (kanbanContext && typeof kanbanContext === "object") {
|
|
773
|
+
const kanban = kanbanContext;
|
|
774
|
+
const boardId = typeof kanban["boardId"] === "string" ? kanban["boardId"] : void 0;
|
|
775
|
+
const taskId = typeof kanban["taskId"] === "string" ? kanban["taskId"] : void 0;
|
|
776
|
+
if (taskId) agentContext?.setCurrentKanbanTask?.(taskId, boardId);
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
const detachFleet = opts.fleetBus?.attach(ctx.subagentId, events, task.id);
|
|
780
|
+
const aborter = new AbortController();
|
|
781
|
+
ctx.budget._events = events;
|
|
782
|
+
ctx.budget.onThreshold = ({ requestDecision }) => requestDecision();
|
|
783
|
+
let budgetError = null;
|
|
784
|
+
const onBudgetError = (err) => {
|
|
785
|
+
if (err instanceof BudgetThresholdSignal) {
|
|
786
|
+
budgetError = err;
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
789
|
+
aborter.abort();
|
|
790
|
+
budgetError = err instanceof BudgetExceededError ? err : new BudgetExceededError(
|
|
791
|
+
"tool_calls",
|
|
792
|
+
0,
|
|
793
|
+
0
|
|
794
|
+
);
|
|
795
|
+
if (budgetError !== err && err instanceof Error) {
|
|
796
|
+
budgetError.message += ` (caused by: ${err.message})`;
|
|
797
|
+
}
|
|
798
|
+
};
|
|
799
|
+
let lastToolFailed = null;
|
|
800
|
+
const unsub = [];
|
|
801
|
+
unsub.push(
|
|
802
|
+
events.on("tool.executed", (e) => {
|
|
803
|
+
if (hasExplicitTelemetryTask) {
|
|
804
|
+
opts.hostEvents?.emit("subagent.tool_executed", {
|
|
805
|
+
sessionId: e.sessionId,
|
|
806
|
+
subagentId: ctx.subagentId,
|
|
807
|
+
agentSessionId: e.sessionId,
|
|
808
|
+
agentName: e.agentName ?? ctx.config.name,
|
|
809
|
+
taskId: telemetryTaskId,
|
|
810
|
+
runId: telemetryRunId,
|
|
811
|
+
traceId: e.traceId,
|
|
812
|
+
id: e.id,
|
|
813
|
+
name: e.name,
|
|
814
|
+
durationMs: e.durationMs,
|
|
815
|
+
ok: e.ok,
|
|
816
|
+
// Raw tool inputs may contain credentials or sensitive arguments.
|
|
817
|
+
// Task telemetry crosses from the isolated worker bus to the shared
|
|
818
|
+
// host bus, so expose only execution metadata at this boundary.
|
|
819
|
+
outputBytes: e.outputBytes,
|
|
820
|
+
outputTokens: e.outputTokens,
|
|
821
|
+
outputLines: e.outputLines
|
|
822
|
+
});
|
|
823
|
+
}
|
|
824
|
+
try {
|
|
825
|
+
ctx.budget.recordToolCall();
|
|
826
|
+
} catch (eb) {
|
|
827
|
+
onBudgetError(eb);
|
|
828
|
+
}
|
|
829
|
+
if (e.ok === false) {
|
|
830
|
+
lastToolFailed = e.name;
|
|
831
|
+
} else if (e.ok === true) {
|
|
832
|
+
lastToolFailed = null;
|
|
833
|
+
}
|
|
834
|
+
}),
|
|
835
|
+
events.on("file.event", (e) => {
|
|
836
|
+
if (!hasExplicitTelemetryTask) return;
|
|
837
|
+
if (e.runId === telemetryRunId && e.taskId === telemetryTaskId) return;
|
|
838
|
+
opts.hostEvents?.emit("file.event", {
|
|
839
|
+
...e,
|
|
840
|
+
scope: "task",
|
|
841
|
+
taskId: telemetryTaskId,
|
|
842
|
+
boardId: telemetryBoardId,
|
|
843
|
+
runId: telemetryRunId
|
|
844
|
+
});
|
|
845
|
+
}),
|
|
846
|
+
events.on("provider.response", (e) => {
|
|
847
|
+
try {
|
|
848
|
+
ctx.budget.recordUsage(e.usage);
|
|
849
|
+
} catch (e2) {
|
|
850
|
+
void onBudgetError(e2);
|
|
851
|
+
}
|
|
852
|
+
}),
|
|
853
|
+
events.on("iteration.started", () => {
|
|
854
|
+
try {
|
|
855
|
+
ctx.budget.recordIteration();
|
|
856
|
+
} catch (e) {
|
|
857
|
+
void onBudgetError(e);
|
|
858
|
+
}
|
|
859
|
+
const u = ctx.budget.usage();
|
|
860
|
+
const since = u.iterations - lastSummaryAtIteration;
|
|
861
|
+
if (since >= SUMMARY_INTERVAL) {
|
|
862
|
+
lastSummaryAtIteration = u.iterations;
|
|
863
|
+
events.emit("subagent.iteration_summary", {
|
|
864
|
+
subagentId: ctx.subagentId,
|
|
865
|
+
iteration: u.iterations,
|
|
866
|
+
toolCalls: u.toolCalls,
|
|
867
|
+
costUsd: u.costUsd,
|
|
868
|
+
currentTool: currentToolName,
|
|
869
|
+
partialText: streamingTextAcc.trim() || void 0
|
|
870
|
+
});
|
|
871
|
+
}
|
|
872
|
+
}),
|
|
873
|
+
// D3: cooperative timeout enforcement DURING a long tool call.
|
|
874
|
+
// The iteration-loop checkTimeout() only fires between agent
|
|
875
|
+
// iterations — a single `bash sleep 3600` call would otherwise
|
|
876
|
+
// park inside one tool execution while the timeout silently
|
|
877
|
+
// passes, relying solely on the coordinator's hard Promise.race
|
|
878
|
+
// to interrupt. Tools that emit `tool.progress` (bash chunks,
|
|
879
|
+
// fetch byte progress, spawn-stream stdout) give us a heartbeat
|
|
880
|
+
// we can hang the check on. When the budget trips here:
|
|
881
|
+
// 1. onBudgetError sets budgetError + aborter.abort()
|
|
882
|
+
// 2. aborter signal propagates to agent.run → tool executor
|
|
883
|
+
// 3. tool's own signal listener kills the child process
|
|
884
|
+
// Cheap: O(1) per progress event, and the budget short-circuits
|
|
885
|
+
// when timeoutMs is unset (most subagents have one set anyway).
|
|
886
|
+
events.on("tool.progress", () => {
|
|
887
|
+
ctx.budget.markActivity();
|
|
888
|
+
try {
|
|
889
|
+
ctx.budget.checkTimeout();
|
|
890
|
+
} catch (e) {
|
|
891
|
+
void onBudgetError(e);
|
|
892
|
+
}
|
|
893
|
+
})
|
|
894
|
+
);
|
|
895
|
+
let currentToolName;
|
|
896
|
+
let streamingTextAcc = "";
|
|
897
|
+
let lastSummaryAtIteration = 0;
|
|
898
|
+
const SUMMARY_INTERVAL = 25;
|
|
899
|
+
unsub.push(
|
|
900
|
+
events.on("tool.started", (e) => {
|
|
901
|
+
currentToolName = e.name;
|
|
902
|
+
}),
|
|
903
|
+
events.on("provider.text_delta", (e) => {
|
|
904
|
+
ctx.budget.markActivity();
|
|
905
|
+
streamingTextAcc = (streamingTextAcc + e.text).slice(-2e3);
|
|
906
|
+
const text = streamingTextAcc.trim();
|
|
907
|
+
if (text) {
|
|
908
|
+
ctx.reportProgress?.({ text, source: "stream", capturedAt: Date.now() });
|
|
909
|
+
}
|
|
910
|
+
})
|
|
911
|
+
);
|
|
912
|
+
const onParentAbort = () => aborter.abort();
|
|
913
|
+
ctx.signal.addEventListener("abort", onParentAbort);
|
|
914
|
+
if (resolveGracefulFinish(ctx.config)) {
|
|
915
|
+
unsub.push(
|
|
916
|
+
events.on("subagent.finish_requested", (e) => {
|
|
917
|
+
if (e.subagentId && e.subagentId !== ctx.subagentId) return;
|
|
918
|
+
setBtwNote(agent.ctx, e.notice);
|
|
919
|
+
})
|
|
920
|
+
);
|
|
921
|
+
}
|
|
922
|
+
let result;
|
|
923
|
+
try {
|
|
924
|
+
result = await agent.run(format(task, ctx.config), { signal: aborter.signal });
|
|
925
|
+
const finalText2 = result.finalText?.trim();
|
|
926
|
+
if (finalText2) {
|
|
927
|
+
ctx.reportProgress?.({ text: finalText2, source: "run_result", capturedAt: Date.now() });
|
|
928
|
+
}
|
|
929
|
+
events.emit("subagent.task_completed", {
|
|
930
|
+
subagentId: ctx.subagentId,
|
|
931
|
+
taskId: task.id,
|
|
932
|
+
status: result.status === "done" ? "success" : "failed",
|
|
933
|
+
iterations: result.iterations,
|
|
934
|
+
toolCalls: ctx.budget.usage().toolCalls,
|
|
935
|
+
durationMs: Date.now() - taskStartedAt,
|
|
936
|
+
finalText: result.finalText?.trim() || void 0
|
|
937
|
+
});
|
|
938
|
+
} finally {
|
|
939
|
+
detachFleet?.();
|
|
940
|
+
ctx.signal.removeEventListener("abort", onParentAbort);
|
|
941
|
+
for (const u of unsub) u();
|
|
942
|
+
if (factoryResult.dispose) {
|
|
943
|
+
try {
|
|
944
|
+
await factoryResult.dispose();
|
|
945
|
+
} catch {
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
if (budgetError) {
|
|
950
|
+
if ("decision" in budgetError) {
|
|
951
|
+
try {
|
|
952
|
+
const decision = await budgetError.decision;
|
|
953
|
+
if (decision === "stop") {
|
|
954
|
+
budgetError = new BudgetExceededError(
|
|
955
|
+
budgetError.kind,
|
|
956
|
+
budgetError.limit,
|
|
957
|
+
budgetError.used
|
|
958
|
+
);
|
|
959
|
+
} else {
|
|
960
|
+
budgetError = null;
|
|
961
|
+
}
|
|
962
|
+
} catch {
|
|
963
|
+
budgetError = new BudgetExceededError(
|
|
964
|
+
budgetError.kind,
|
|
965
|
+
budgetError.limit,
|
|
966
|
+
budgetError.used
|
|
967
|
+
);
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
if (budgetError) throw budgetError;
|
|
971
|
+
}
|
|
972
|
+
if (result.status === "failed") {
|
|
973
|
+
throw result.error instanceof AgentError ? result.error : new AgentError({
|
|
974
|
+
message: result.error instanceof Error ? result.error.message : String(result.error ?? "agent failed"),
|
|
975
|
+
code: ERROR_CODES.AGENT_RUN_FAILED,
|
|
976
|
+
cause: result.error
|
|
977
|
+
});
|
|
978
|
+
}
|
|
979
|
+
if (result.status === "aborted") {
|
|
980
|
+
throw new AgentError({
|
|
981
|
+
message: "agent aborted",
|
|
982
|
+
code: ERROR_CODES.AGENT_ABORTED
|
|
983
|
+
});
|
|
984
|
+
}
|
|
985
|
+
if (result.status === "max_iterations") {
|
|
986
|
+
throw new AgentError({
|
|
987
|
+
message: "agent exhausted iteration limit",
|
|
988
|
+
code: ERROR_CODES.AGENT_ITERATION_LIMIT,
|
|
989
|
+
recoverable: true
|
|
990
|
+
});
|
|
991
|
+
}
|
|
992
|
+
const usage = ctx.budget.usage();
|
|
993
|
+
const finalText = (result.finalText ?? "").trim();
|
|
994
|
+
if (finalText.length === 0 && usage.toolCalls === 0) {
|
|
995
|
+
throw new AgentError({
|
|
996
|
+
message: "empty response \u2014 agent produced no text and no tool calls",
|
|
997
|
+
code: ERROR_CODES.AGENT_RUN_FAILED,
|
|
998
|
+
context: { iterations: result.iterations }
|
|
999
|
+
});
|
|
1000
|
+
}
|
|
1001
|
+
if (finalText.length === 0 && lastToolFailed !== null) {
|
|
1002
|
+
throw new AgentError({
|
|
1003
|
+
message: `unrecovered tool failure: ${lastToolFailed} \u2014 agent ended turn without acknowledging the error`,
|
|
1004
|
+
code: ERROR_CODES.AGENT_RUN_FAILED,
|
|
1005
|
+
context: { tool: lastToolFailed, iterations: result.iterations }
|
|
1006
|
+
});
|
|
1007
|
+
}
|
|
1008
|
+
return {
|
|
1009
|
+
result: result.finalText,
|
|
1010
|
+
report: readSubagentStructuredReport(agentContext),
|
|
1011
|
+
iterations: result.iterations,
|
|
1012
|
+
toolCalls: usage.toolCalls
|
|
1013
|
+
};
|
|
1014
|
+
};
|
|
1015
|
+
}
|
|
1016
|
+
function defaultFormatTaskInput(task) {
|
|
1017
|
+
return task.description ?? "";
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
// src/coordination/brain-heuristics.ts
|
|
1021
|
+
var BLOCKED_RESOLVED_MARKERS = /\b(?:resolved|fixed|completed|unblocked|available|done|merged|landed|shipped)\b/;
|
|
1022
|
+
var COMPETING_ALTERNATIVE = /\bor\b/;
|
|
1023
|
+
function isBlockedResolved(question, context, markers = BLOCKED_RESOLVED_MARKERS) {
|
|
1024
|
+
return question.includes("blocked") && !COMPETING_ALTERNATIVE.test(question) && markers.test(context);
|
|
1025
|
+
}
|
|
1026
|
+
var DEFAULT_BRAIN_HEURISTICS = {
|
|
1027
|
+
lowRiskAutoAnswer: true,
|
|
1028
|
+
blockedResolved: true,
|
|
1029
|
+
deadlockSkip: true,
|
|
1030
|
+
retryExhausted: true,
|
|
1031
|
+
continuePing: true
|
|
1032
|
+
};
|
|
1033
|
+
var escapeRegex = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1034
|
+
function compileResolutionMarkers(words) {
|
|
1035
|
+
const cleaned = (words ?? []).map((w) => w.trim()).filter((w) => w.length > 0);
|
|
1036
|
+
if (cleaned.length === 0) return BLOCKED_RESOLVED_MARKERS;
|
|
1037
|
+
return new RegExp(`\\b(?:${cleaned.map(escapeRegex).join("|")})\\b`, "i");
|
|
1038
|
+
}
|
|
1039
|
+
function resolveBrainHeuristics(cfg) {
|
|
1040
|
+
return {
|
|
1041
|
+
lowRiskAutoAnswer: cfg?.lowRiskAutoAnswer ?? DEFAULT_BRAIN_HEURISTICS.lowRiskAutoAnswer,
|
|
1042
|
+
blockedResolved: cfg?.blockedResolved ?? DEFAULT_BRAIN_HEURISTICS.blockedResolved,
|
|
1043
|
+
deadlockSkip: cfg?.deadlockSkip ?? DEFAULT_BRAIN_HEURISTICS.deadlockSkip,
|
|
1044
|
+
retryExhausted: cfg?.retryExhausted ?? DEFAULT_BRAIN_HEURISTICS.retryExhausted,
|
|
1045
|
+
continuePing: cfg?.continuePing ?? DEFAULT_BRAIN_HEURISTICS.continuePing,
|
|
1046
|
+
blockedResolvedMarkers: compileResolutionMarkers(cfg?.blockedResolvedMarkers)
|
|
1047
|
+
};
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
// src/coordination/brain-telemetry.ts
|
|
1051
|
+
var DETERMINISTIC_BRAIN_TIERS = /* @__PURE__ */ new Set(
|
|
1052
|
+
["rule", "policy", "heuristic", "cache", "ledger-guard", "terminal"]
|
|
1053
|
+
);
|
|
1054
|
+
var tierByRequest = /* @__PURE__ */ new WeakMap();
|
|
1055
|
+
function markDecisionTier(request, tier) {
|
|
1056
|
+
tierByRequest.set(request, tier);
|
|
1057
|
+
}
|
|
1058
|
+
function readDecisionTier(request) {
|
|
1059
|
+
return tierByRequest.get(request);
|
|
1060
|
+
}
|
|
1061
|
+
function isDeterministicTier(tier) {
|
|
1062
|
+
return tier !== void 0 && DETERMINISTIC_BRAIN_TIERS.has(tier);
|
|
1063
|
+
}
|
|
1064
|
+
var BrainTierCounter = class {
|
|
1065
|
+
counts = /* @__PURE__ */ new Map();
|
|
1066
|
+
total = 0;
|
|
1067
|
+
unattributed = 0;
|
|
1068
|
+
/** Count one decision. `undefined` counts toward `unattributed`. */
|
|
1069
|
+
record(tier) {
|
|
1070
|
+
this.total += 1;
|
|
1071
|
+
if (tier === void 0) {
|
|
1072
|
+
this.unattributed += 1;
|
|
1073
|
+
return;
|
|
1074
|
+
}
|
|
1075
|
+
this.counts.set(tier, (this.counts.get(tier) ?? 0) + 1);
|
|
1076
|
+
}
|
|
1077
|
+
snapshot() {
|
|
1078
|
+
const byTier = {};
|
|
1079
|
+
let deterministic = 0;
|
|
1080
|
+
let llmBacked = 0;
|
|
1081
|
+
for (const [tier, count] of this.counts) {
|
|
1082
|
+
byTier[tier] = count;
|
|
1083
|
+
if (DETERMINISTIC_BRAIN_TIERS.has(tier)) deterministic += count;
|
|
1084
|
+
else if (tier === "council" || tier === "llm") llmBacked += count;
|
|
1085
|
+
}
|
|
1086
|
+
return {
|
|
1087
|
+
byTier,
|
|
1088
|
+
total: this.total,
|
|
1089
|
+
deterministic,
|
|
1090
|
+
llmBacked,
|
|
1091
|
+
unattributed: this.unattributed
|
|
1092
|
+
};
|
|
1093
|
+
}
|
|
1094
|
+
reset() {
|
|
1095
|
+
this.counts.clear();
|
|
1096
|
+
this.total = 0;
|
|
1097
|
+
this.unattributed = 0;
|
|
1098
|
+
}
|
|
1099
|
+
};
|
|
1100
|
+
|
|
1101
|
+
// src/coordination/brain.ts
|
|
1102
|
+
var BRAIN_RISK_LEVELS = {
|
|
1103
|
+
low: 0,
|
|
1104
|
+
medium: 1,
|
|
1105
|
+
high: 2,
|
|
1106
|
+
critical: 3
|
|
1107
|
+
};
|
|
1108
|
+
var ObservableBrainArbiter = class {
|
|
1109
|
+
constructor(inner, events) {
|
|
1110
|
+
this.inner = inner;
|
|
1111
|
+
this.events = events;
|
|
1112
|
+
}
|
|
1113
|
+
inner;
|
|
1114
|
+
events;
|
|
1115
|
+
async decide(request) {
|
|
1116
|
+
this.events.emit("brain.decision_requested", { sessionId: request.sessionId, request, at: Date.now() });
|
|
1117
|
+
const decision = await this.inner.decide(request);
|
|
1118
|
+
const event = decision.type === "ask_human" ? "brain.decision_ask_human" : decision.type === "deny" ? "brain.decision_denied" : "brain.decision_answered";
|
|
1119
|
+
const tier = readDecisionTier(request);
|
|
1120
|
+
this.events.emit(event, {
|
|
1121
|
+
sessionId: request.sessionId,
|
|
1122
|
+
request,
|
|
1123
|
+
decision,
|
|
1124
|
+
at: Date.now(),
|
|
1125
|
+
...tier ? { tier } : {}
|
|
1126
|
+
});
|
|
1127
|
+
return decision;
|
|
1128
|
+
}
|
|
1129
|
+
};
|
|
1130
|
+
var BrainDecisionQueue = class {
|
|
1131
|
+
constructor(events, opts = {}) {
|
|
1132
|
+
this.events = events;
|
|
1133
|
+
this.opts = opts;
|
|
1134
|
+
this.offAnswer = this.events.on("brain.human_answered", (answer) => {
|
|
1135
|
+
const pending = this.pending.get(answer.id);
|
|
1136
|
+
if (!pending) return;
|
|
1137
|
+
this.pending.delete(answer.id);
|
|
1138
|
+
if (pending.timer) clearTimeout(pending.timer);
|
|
1139
|
+
markDecisionTier(pending.request, "human");
|
|
1140
|
+
if (answer.deny) {
|
|
1141
|
+
pending.resolve({ type: "deny", reason: answer.text ?? "Denied by human." });
|
|
1142
|
+
return;
|
|
1143
|
+
}
|
|
1144
|
+
const option = pending.request.options?.find((o) => o.id === answer.optionId);
|
|
1145
|
+
pending.resolve({
|
|
1146
|
+
type: "answer",
|
|
1147
|
+
optionId: answer.optionId,
|
|
1148
|
+
text: answer.text ?? option?.label ?? answer.optionId ?? "Human answered.",
|
|
1149
|
+
rationale: "Human answered a Brain escalation prompt."
|
|
1150
|
+
});
|
|
1151
|
+
});
|
|
1152
|
+
}
|
|
1153
|
+
events;
|
|
1154
|
+
opts;
|
|
1155
|
+
pending = /* @__PURE__ */ new Map();
|
|
1156
|
+
offAnswer;
|
|
1157
|
+
async requestHumanDecision(request) {
|
|
1158
|
+
const ask = {
|
|
1159
|
+
type: "ask_human",
|
|
1160
|
+
prompt: formatHumanPrompt(request),
|
|
1161
|
+
options: request.options,
|
|
1162
|
+
rationale: "Decision escalated to human authority."
|
|
1163
|
+
};
|
|
1164
|
+
const pending = new Promise((resolve) => {
|
|
1165
|
+
const entry = { request, resolve };
|
|
1166
|
+
if (this.opts.timeoutMs && this.opts.timeoutMs > 0) {
|
|
1167
|
+
entry.timer = setTimeout(() => {
|
|
1168
|
+
this.pending.delete(request.id);
|
|
1169
|
+
markDecisionTier(request, "terminal");
|
|
1170
|
+
resolve(
|
|
1171
|
+
this.opts.onTimeout?.(request) ?? {
|
|
1172
|
+
type: "deny",
|
|
1173
|
+
reason: "Brain human decision timed out."
|
|
1174
|
+
}
|
|
1175
|
+
);
|
|
1176
|
+
}, this.opts.timeoutMs);
|
|
1177
|
+
}
|
|
1178
|
+
this.pending.set(request.id, entry);
|
|
1179
|
+
});
|
|
1180
|
+
this.events.emit("brain.decision_ask_human", { sessionId: request.sessionId, request, decision: ask, at: Date.now() });
|
|
1181
|
+
return pending;
|
|
1182
|
+
}
|
|
1183
|
+
dispose() {
|
|
1184
|
+
this.offAnswer();
|
|
1185
|
+
for (const [id, pending] of this.pending) {
|
|
1186
|
+
if (pending.timer) clearTimeout(pending.timer);
|
|
1187
|
+
pending.resolve({ type: "deny", reason: "Brain decision queue disposed." });
|
|
1188
|
+
this.pending.delete(id);
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
};
|
|
1192
|
+
function terminalPolicyDecision(request, policy = "conservative") {
|
|
1193
|
+
if (policy === "deny-all") {
|
|
1194
|
+
return {
|
|
1195
|
+
type: "deny",
|
|
1196
|
+
reason: `Headless terminal policy "deny-all": "${request.question}" was not auto-approved (risk: ` + request.risk + "). The proposed action was not taken."
|
|
1197
|
+
};
|
|
1198
|
+
}
|
|
1199
|
+
const recommended = request.options?.find((option) => option.recommended);
|
|
1200
|
+
const acceptRecommended = recommended !== void 0 && (policy === "continue-on-recommended" || request.risk === "low" || request.risk === "medium");
|
|
1201
|
+
if (recommended && acceptRecommended) {
|
|
1202
|
+
return {
|
|
1203
|
+
type: "answer",
|
|
1204
|
+
optionId: recommended.id,
|
|
1205
|
+
text: recommended.label,
|
|
1206
|
+
rationale: `Headless terminal policy: caller-recommended option accepted at ${request.risk} risk (no human available by configuration).`
|
|
1207
|
+
};
|
|
1208
|
+
}
|
|
1209
|
+
if (request.fallback === "continue") {
|
|
1210
|
+
return {
|
|
1211
|
+
type: "answer",
|
|
1212
|
+
text: "Continue with the caller default.",
|
|
1213
|
+
rationale: "Headless terminal policy: request declares continue as its safe fallback."
|
|
1214
|
+
};
|
|
1215
|
+
}
|
|
1216
|
+
return {
|
|
1217
|
+
type: "deny",
|
|
1218
|
+
reason: `Headless terminal policy: no safe automatic option for "${request.question}" (risk: ${request.risk}). The proposed action was not taken.`
|
|
1219
|
+
};
|
|
1220
|
+
}
|
|
1221
|
+
var EscalationRoutingBrainArbiter = class {
|
|
1222
|
+
constructor(inner, queue, getMode, getTerminalPolicy) {
|
|
1223
|
+
this.inner = inner;
|
|
1224
|
+
this.queue = queue;
|
|
1225
|
+
this.getMode = getMode;
|
|
1226
|
+
this.getTerminalPolicy = getTerminalPolicy;
|
|
1227
|
+
}
|
|
1228
|
+
inner;
|
|
1229
|
+
queue;
|
|
1230
|
+
getMode;
|
|
1231
|
+
getTerminalPolicy;
|
|
1232
|
+
async decide(request) {
|
|
1233
|
+
const decision = await this.inner.decide(request);
|
|
1234
|
+
if (decision.type !== "ask_human") return decision;
|
|
1235
|
+
if (this.getMode() === "interactive" && this.queue) {
|
|
1236
|
+
return this.queue.requestHumanDecision(request);
|
|
1237
|
+
}
|
|
1238
|
+
markDecisionTier(request, "terminal");
|
|
1239
|
+
return terminalPolicyDecision(request, this.getTerminalPolicy?.() ?? "conservative");
|
|
1240
|
+
}
|
|
1241
|
+
};
|
|
1242
|
+
var HumanEscalatingBrainArbiter = class {
|
|
1243
|
+
constructor(inner, queue) {
|
|
1244
|
+
this.inner = inner;
|
|
1245
|
+
this.queue = queue;
|
|
1246
|
+
}
|
|
1247
|
+
inner;
|
|
1248
|
+
queue;
|
|
1249
|
+
async decide(request) {
|
|
1250
|
+
const decision = await this.inner.decide(request);
|
|
1251
|
+
if (decision.type !== "ask_human") return decision;
|
|
1252
|
+
return this.queue.requestHumanDecision(request);
|
|
1253
|
+
}
|
|
1254
|
+
};
|
|
1255
|
+
var DefaultBrainArbiter = class {
|
|
1256
|
+
heuristics;
|
|
1257
|
+
constructor(opts = {}) {
|
|
1258
|
+
const resolved = resolveBrainHeuristics(opts.heuristics);
|
|
1259
|
+
this.heuristics = {
|
|
1260
|
+
...resolved,
|
|
1261
|
+
lowRiskAutoAnswer: opts.allowLowRiskAutoAnswer ?? resolved.lowRiskAutoAnswer
|
|
1262
|
+
};
|
|
1263
|
+
}
|
|
1264
|
+
async decide(request) {
|
|
1265
|
+
const recommended = request.options?.find((option) => option.recommended);
|
|
1266
|
+
if (this.heuristics.lowRiskAutoAnswer && request.risk === "low" && recommended) {
|
|
1267
|
+
markDecisionTier(request, "heuristic");
|
|
1268
|
+
return {
|
|
1269
|
+
type: "answer",
|
|
1270
|
+
optionId: recommended.id,
|
|
1271
|
+
text: recommended.label,
|
|
1272
|
+
rationale: "Low-risk request with an explicit recommended option."
|
|
1273
|
+
};
|
|
1274
|
+
}
|
|
1275
|
+
if (this.heuristics.blockedResolved && !request.options?.length && request.fallback === "continue" && request.context) {
|
|
1276
|
+
if (isBlockedResolved(
|
|
1277
|
+
request.question.toLowerCase(),
|
|
1278
|
+
request.context.toLowerCase(),
|
|
1279
|
+
this.heuristics.blockedResolvedMarkers
|
|
1280
|
+
)) {
|
|
1281
|
+
markDecisionTier(request, "heuristic");
|
|
1282
|
+
return {
|
|
1283
|
+
type: "answer",
|
|
1284
|
+
text: "Blocker resolved. Continue with the previously blocked work.",
|
|
1285
|
+
rationale: "Heuristic: blocking dependency explicitly resolved \u2014 resuming."
|
|
1286
|
+
};
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
markDecisionTier(request, "policy");
|
|
1290
|
+
switch (request.fallback) {
|
|
1291
|
+
case "deny":
|
|
1292
|
+
return {
|
|
1293
|
+
type: "deny",
|
|
1294
|
+
reason: `Brain could not safely decide: ${request.question}`
|
|
1295
|
+
};
|
|
1296
|
+
case "continue":
|
|
1297
|
+
return {
|
|
1298
|
+
type: "answer",
|
|
1299
|
+
text: "Continue with the caller default.",
|
|
1300
|
+
rationale: "No safe Brain decision was available; request fallback is continue."
|
|
1301
|
+
};
|
|
1302
|
+
case "ask_human":
|
|
1303
|
+
return {
|
|
1304
|
+
type: "ask_human",
|
|
1305
|
+
prompt: formatHumanPrompt(request),
|
|
1306
|
+
options: request.options,
|
|
1307
|
+
rationale: "Decision requires human authority or lacks a safe automatic option."
|
|
1308
|
+
};
|
|
1309
|
+
default:
|
|
1310
|
+
return {
|
|
1311
|
+
type: "deny",
|
|
1312
|
+
reason: `Brain could not safely decide (unknown fallback: ${String(request.fallback)}): ${request.question}`
|
|
1313
|
+
};
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
};
|
|
1317
|
+
function formatHumanPrompt(request) {
|
|
1318
|
+
const lines = [
|
|
1319
|
+
`Brain requires human decision for ${request.source}:`,
|
|
1320
|
+
`Question: ${request.question}`
|
|
1321
|
+
];
|
|
1322
|
+
if (request.context?.trim()) {
|
|
1323
|
+
lines.push("", "Context:", request.context.trim());
|
|
1324
|
+
}
|
|
1325
|
+
if (request.options?.length) {
|
|
1326
|
+
lines.push("", "Options:");
|
|
1327
|
+
for (const option of request.options) {
|
|
1328
|
+
const risk = option.risk ? ` [risk: ${option.risk}]` : "";
|
|
1329
|
+
const consequence = option.consequence ? ` \u2014 ${option.consequence}` : "";
|
|
1330
|
+
lines.push(`- ${option.id}: ${option.label}${risk}${consequence}`);
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
lines.push("", `Risk: ${request.risk}`);
|
|
1334
|
+
return lines.join("\n");
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
// src/coordination/brain-ledger.ts
|
|
1338
|
+
import { createReadStream } from "node:fs";
|
|
1339
|
+
import { access, appendFile, mkdir, rename, stat } from "node:fs/promises";
|
|
1340
|
+
import { dirname } from "node:path";
|
|
1341
|
+
import { createInterface } from "node:readline";
|
|
1342
|
+
var QUESTION_MAX = 200;
|
|
1343
|
+
var DETAIL_MAX = 240;
|
|
1344
|
+
var truncate = (s, max) => s === void 0 ? void 0 : s.length > max ? `${s.slice(0, max - 1)}\u2026` : s;
|
|
1345
|
+
function brainDecisionKey(source, question) {
|
|
1346
|
+
const normalized = question.toLowerCase().replace(/[^\s]*\d[^\s]*/g, "#").replace(/\s+/g, " ").trim().slice(0, 100);
|
|
1347
|
+
return `${source ?? "?"}:${normalized}`;
|
|
1348
|
+
}
|
|
1349
|
+
function fmtAge(ms) {
|
|
1350
|
+
const s = Math.max(0, Math.round(ms / 1e3));
|
|
1351
|
+
if (s < 60) return `${s}s ago`;
|
|
1352
|
+
if (s < 3600) return `${Math.round(s / 60)}m ago`;
|
|
1353
|
+
return `${Math.round(s / 3600)}h ago`;
|
|
1354
|
+
}
|
|
1355
|
+
function createLedgerGuardBrainArbiter(opts) {
|
|
1356
|
+
const denyAfter = opts.denyAfter ?? 3;
|
|
1357
|
+
return {
|
|
1358
|
+
async decide(request) {
|
|
1359
|
+
if (denyAfter > 0) {
|
|
1360
|
+
const streak = opts.failureStreakFor(request);
|
|
1361
|
+
if (streak >= denyAfter) {
|
|
1362
|
+
markDecisionTier(request, "ledger-guard");
|
|
1363
|
+
return {
|
|
1364
|
+
type: "deny",
|
|
1365
|
+
reason: `Ledger guard: the last ${streak} approved decisions of this kind all ended in observed failures \u2014 denying deterministically until a success is recorded. (${request.question.slice(0, 120)})`
|
|
1366
|
+
};
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
return opts.inner.decide(request);
|
|
1370
|
+
}
|
|
1371
|
+
};
|
|
1372
|
+
}
|
|
1373
|
+
var BrainDecisionLedger = class _BrainDecisionLedger {
|
|
1374
|
+
constructor(opts) {
|
|
1375
|
+
this.opts = opts;
|
|
1376
|
+
this.maxMemoryEntries = opts.maxMemoryEntries ?? 500;
|
|
1377
|
+
this.retryWindowMs = opts.interventionRetryWindowMs ?? 10 * 6e4;
|
|
1378
|
+
this.now = opts.now ?? Date.now;
|
|
1379
|
+
}
|
|
1380
|
+
opts;
|
|
1381
|
+
entries = [];
|
|
1382
|
+
outcomeByRequest = /* @__PURE__ */ new Map();
|
|
1383
|
+
/** subagentId → decision request ids awaiting that subagent's task result. */
|
|
1384
|
+
pendingBudgetOutcomes = /* @__PURE__ */ new Map();
|
|
1385
|
+
lastInterventionByKind = /* @__PURE__ */ new Map();
|
|
1386
|
+
unsubscribers = [];
|
|
1387
|
+
/** Serialized fire-and-forget writes — awaited by `stop()` (drain). */
|
|
1388
|
+
writeChain = Promise.resolve();
|
|
1389
|
+
pendingWriteCount = 0;
|
|
1390
|
+
pendingWriteBytes = 0;
|
|
1391
|
+
static MAX_PENDING_WRITES = 1e3;
|
|
1392
|
+
static MAX_PENDING_WRITE_BYTES = 8 * 1024 * 1024;
|
|
1393
|
+
/** Rotate the JSONL to `.1` past this size so it can't grow unbounded. */
|
|
1394
|
+
static MAX_FILE_BYTES = 5 * 1024 * 1024;
|
|
1395
|
+
/** Stat the file for rotation only once per this many writes. */
|
|
1396
|
+
static ROTATE_CHECK_EVERY = 100;
|
|
1397
|
+
writesSinceRotateCheck = 0;
|
|
1398
|
+
dirReady = false;
|
|
1399
|
+
maxMemoryEntries;
|
|
1400
|
+
retryWindowMs;
|
|
1401
|
+
now;
|
|
1402
|
+
/**
|
|
1403
|
+
* Subscribe to the EventBus and seed the in-memory ring from the JSONL
|
|
1404
|
+
* tail. The returned promise resolves once the seed load finished
|
|
1405
|
+
* (best-effort — a missing/corrupt file is an empty history, not an error).
|
|
1406
|
+
*/
|
|
1407
|
+
async start() {
|
|
1408
|
+
const { events } = this.opts;
|
|
1409
|
+
this.unsubscribers.push(
|
|
1410
|
+
events.on("brain.decision_answered", (e) => {
|
|
1411
|
+
this.record({
|
|
1412
|
+
at: e.at,
|
|
1413
|
+
sessionId: e.sessionId,
|
|
1414
|
+
requestId: e.request.id,
|
|
1415
|
+
kind: "answered",
|
|
1416
|
+
source: e.request.source,
|
|
1417
|
+
risk: e.request.risk,
|
|
1418
|
+
question: truncate(e.request.question, QUESTION_MAX),
|
|
1419
|
+
optionId: e.decision.type === "answer" ? e.decision.optionId : void 0,
|
|
1420
|
+
detail: e.decision.type === "answer" ? truncate(e.decision.rationale ?? e.decision.text, DETAIL_MAX) : void 0
|
|
1421
|
+
});
|
|
1422
|
+
}),
|
|
1423
|
+
events.on("brain.decision_denied", (e) => {
|
|
1424
|
+
this.record({
|
|
1425
|
+
at: e.at,
|
|
1426
|
+
sessionId: e.sessionId,
|
|
1427
|
+
requestId: e.request.id,
|
|
1428
|
+
kind: "denied",
|
|
1429
|
+
source: e.request.source,
|
|
1430
|
+
risk: e.request.risk,
|
|
1431
|
+
question: truncate(e.request.question, QUESTION_MAX),
|
|
1432
|
+
detail: e.decision.type === "deny" ? truncate(e.decision.reason, DETAIL_MAX) : void 0
|
|
1433
|
+
});
|
|
1434
|
+
}),
|
|
1435
|
+
events.on("brain.decision_ask_human", (e) => {
|
|
1436
|
+
this.record({
|
|
1437
|
+
at: e.at,
|
|
1438
|
+
sessionId: e.sessionId,
|
|
1439
|
+
requestId: e.request.id,
|
|
1440
|
+
kind: "ask_human",
|
|
1441
|
+
source: e.request.source,
|
|
1442
|
+
risk: e.request.risk,
|
|
1443
|
+
question: truncate(e.request.question, QUESTION_MAX)
|
|
1444
|
+
});
|
|
1445
|
+
}),
|
|
1446
|
+
events.on("brain.intervention", (e) => {
|
|
1447
|
+
this.record({
|
|
1448
|
+
at: e.at,
|
|
1449
|
+
sessionId: e.sessionId,
|
|
1450
|
+
requestId: e.request.id,
|
|
1451
|
+
kind: "intervention",
|
|
1452
|
+
source: e.request.source,
|
|
1453
|
+
risk: e.request.risk,
|
|
1454
|
+
question: truncate(`[${e.kind}] ${e.request.question}`, QUESTION_MAX),
|
|
1455
|
+
detail: e.intervened ? "steered" : "observed only"
|
|
1456
|
+
});
|
|
1457
|
+
if (!e.intervened) return;
|
|
1458
|
+
const prev = this.lastInterventionByKind.get(e.kind);
|
|
1459
|
+
if (prev && e.at - prev.at <= this.retryWindowMs) {
|
|
1460
|
+
this.recordOutcome(
|
|
1461
|
+
prev.requestId,
|
|
1462
|
+
"failure",
|
|
1463
|
+
`same signal (${e.kind}) re-triggered ${fmtAge(e.at - prev.at).replace(" ago", " later")}`,
|
|
1464
|
+
e.sessionId
|
|
1465
|
+
);
|
|
1466
|
+
}
|
|
1467
|
+
this.lastInterventionByKind.set(e.kind, { requestId: e.request.id, at: e.at });
|
|
1468
|
+
}),
|
|
1469
|
+
// ── Outcome correlation: budget extensions ────────────────────────
|
|
1470
|
+
events.on("subagent.budget_extended", (e) => {
|
|
1471
|
+
const requestId = `director-budget-${e.subagentId}-${e.kind}`;
|
|
1472
|
+
if (!this.entries.some((entry) => entry.requestId === requestId)) return;
|
|
1473
|
+
let set = this.pendingBudgetOutcomes.get(e.subagentId);
|
|
1474
|
+
if (!set) {
|
|
1475
|
+
set = /* @__PURE__ */ new Set();
|
|
1476
|
+
this.pendingBudgetOutcomes.set(e.subagentId, set);
|
|
1477
|
+
}
|
|
1478
|
+
set.add(requestId);
|
|
1479
|
+
}),
|
|
1480
|
+
events.on("subagent.task_completed", (e) => {
|
|
1481
|
+
const pending = this.pendingBudgetOutcomes.get(e.subagentId);
|
|
1482
|
+
if (!pending) return;
|
|
1483
|
+
this.pendingBudgetOutcomes.delete(e.subagentId);
|
|
1484
|
+
const outcome = e.status === "success" ? "success" : "failure";
|
|
1485
|
+
for (const requestId of pending) {
|
|
1486
|
+
this.recordOutcome(
|
|
1487
|
+
requestId,
|
|
1488
|
+
outcome,
|
|
1489
|
+
`budget-extended subagent's task ${e.status}`,
|
|
1490
|
+
e.sessionId
|
|
1491
|
+
);
|
|
1492
|
+
}
|
|
1493
|
+
})
|
|
1494
|
+
);
|
|
1495
|
+
await this.load();
|
|
1496
|
+
}
|
|
1497
|
+
/** Unsubscribe and drain pending writes. Safe to call twice. */
|
|
1498
|
+
async stop() {
|
|
1499
|
+
for (const off of this.unsubscribers) off();
|
|
1500
|
+
this.unsubscribers.length = 0;
|
|
1501
|
+
await this.writeChain.catch(() => {
|
|
1502
|
+
});
|
|
1503
|
+
}
|
|
1504
|
+
/**
|
|
1505
|
+
* Attach a real-world outcome to an earlier decision. Appended to the
|
|
1506
|
+
* JSONL, folded into future digests, and re-emitted as `brain.outcome`.
|
|
1507
|
+
*/
|
|
1508
|
+
recordOutcome(requestId, outcome, detail, sessionId) {
|
|
1509
|
+
this.outcomeByRequest.set(requestId, outcome);
|
|
1510
|
+
this.record({
|
|
1511
|
+
at: this.now(),
|
|
1512
|
+
sessionId,
|
|
1513
|
+
requestId,
|
|
1514
|
+
kind: "outcome",
|
|
1515
|
+
outcome,
|
|
1516
|
+
detail: truncate(detail, DETAIL_MAX)
|
|
1517
|
+
});
|
|
1518
|
+
this.opts.events.emit("brain.outcome", {
|
|
1519
|
+
sessionId,
|
|
1520
|
+
requestId,
|
|
1521
|
+
outcome,
|
|
1522
|
+
detail,
|
|
1523
|
+
at: this.now()
|
|
1524
|
+
});
|
|
1525
|
+
}
|
|
1526
|
+
/** Last `n` ledger rows, oldest first. */
|
|
1527
|
+
tail(n) {
|
|
1528
|
+
return this.entries.slice(-n);
|
|
1529
|
+
}
|
|
1530
|
+
/**
|
|
1531
|
+
* Consecutive most-recent FAILURE outcomes among answered decisions of
|
|
1532
|
+
* this request's group. Decisions whose outcome is still unknown are
|
|
1533
|
+
* skipped (they neither extend nor break the streak); the first known
|
|
1534
|
+
* SUCCESS ends it. Powers the deterministic ledger guard: "the last K
|
|
1535
|
+
* times we approved this, it went wrong — stop approving".
|
|
1536
|
+
*/
|
|
1537
|
+
failureStreakFor(request) {
|
|
1538
|
+
const key = brainDecisionKey(request.source, request.question);
|
|
1539
|
+
let streak = 0;
|
|
1540
|
+
for (let i = this.entries.length - 1; i >= 0; i--) {
|
|
1541
|
+
const e = this.entries[i];
|
|
1542
|
+
if (e?.kind !== "answered" || e.requestId === request.id || e.question === void 0 || brainDecisionKey(e.source, e.question) !== key) {
|
|
1543
|
+
continue;
|
|
1544
|
+
}
|
|
1545
|
+
const outcome = this.outcomeByRequest.get(e.requestId);
|
|
1546
|
+
if (outcome === void 0) continue;
|
|
1547
|
+
if (outcome === "success") break;
|
|
1548
|
+
streak += 1;
|
|
1549
|
+
}
|
|
1550
|
+
return streak;
|
|
1551
|
+
}
|
|
1552
|
+
/**
|
|
1553
|
+
* Short decision-history block for the LLM/council prompt: how similar
|
|
1554
|
+
* past decisions went and — when known — how they turned OUT. Returns
|
|
1555
|
+
* undefined when there is no relevant history (no prompt noise).
|
|
1556
|
+
*/
|
|
1557
|
+
digestFor(request) {
|
|
1558
|
+
const key = brainDecisionKey(request.source, request.question);
|
|
1559
|
+
const related = this.entries.filter(
|
|
1560
|
+
(e) => (e.kind === "answered" || e.kind === "denied") && e.requestId !== request.id && e.question !== void 0 && brainDecisionKey(e.source, e.question) === key
|
|
1561
|
+
);
|
|
1562
|
+
if (related.length === 0) return void 0;
|
|
1563
|
+
const recent = related.slice(-5).reverse();
|
|
1564
|
+
const now = this.now();
|
|
1565
|
+
const lines = recent.map((e) => {
|
|
1566
|
+
const what = e.kind === "denied" ? "denied" : e.optionId ? `chose [${e.optionId}]` : "answered";
|
|
1567
|
+
const outcome = this.outcomeByRequest.get(e.requestId);
|
|
1568
|
+
const suffix = outcome === "failure" ? " \u2192 later FAILED" : outcome === "success" ? " \u2192 later succeeded" : "";
|
|
1569
|
+
return `- ${fmtAge(now - e.at)}: ${what}${suffix}`;
|
|
1570
|
+
});
|
|
1571
|
+
const approved = related.filter((e) => e.kind === "answered");
|
|
1572
|
+
const failedAfter = approved.filter(
|
|
1573
|
+
(e) => this.outcomeByRequest.get(e.requestId) === "failure"
|
|
1574
|
+
).length;
|
|
1575
|
+
const summary = `Summary: ${related.length} similar decision(s); ${approved.length} answered${failedAfter > 0 ? ` (${failedAfter} later failed)` : ""}, ${related.length - approved.length} denied.`;
|
|
1576
|
+
return [...lines, summary].join("\n");
|
|
1577
|
+
}
|
|
1578
|
+
// ── Private ───────────────────────────────────────────────────────────
|
|
1579
|
+
record(entry) {
|
|
1580
|
+
this.entries.push(entry);
|
|
1581
|
+
if (this.entries.length > this.maxMemoryEntries) {
|
|
1582
|
+
const evictedRequestIds = /* @__PURE__ */ new Set();
|
|
1583
|
+
const evictCount = this.entries.length - this.maxMemoryEntries;
|
|
1584
|
+
const evicted = this.entries.splice(0, evictCount);
|
|
1585
|
+
for (const e of evicted) {
|
|
1586
|
+
if (e.requestId) evictedRequestIds.add(e.requestId);
|
|
1587
|
+
}
|
|
1588
|
+
if (evictedRequestIds.size > 0) {
|
|
1589
|
+
const remainingRequestIds = /* @__PURE__ */ new Set();
|
|
1590
|
+
for (const e of this.entries) {
|
|
1591
|
+
if (e.requestId) remainingRequestIds.add(e.requestId);
|
|
1592
|
+
}
|
|
1593
|
+
for (const id of evictedRequestIds) {
|
|
1594
|
+
if (!remainingRequestIds.has(id)) this.outcomeByRequest.delete(id);
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
this.append(entry);
|
|
1599
|
+
}
|
|
1600
|
+
append(entry) {
|
|
1601
|
+
let line;
|
|
1602
|
+
try {
|
|
1603
|
+
line = `${JSON.stringify(entry)}
|
|
1604
|
+
`;
|
|
1605
|
+
} catch {
|
|
1606
|
+
return;
|
|
1607
|
+
}
|
|
1608
|
+
const bytes = Buffer.byteLength(line, "utf8");
|
|
1609
|
+
if (this.pendingWriteCount >= _BrainDecisionLedger.MAX_PENDING_WRITES || bytes > _BrainDecisionLedger.MAX_PENDING_WRITE_BYTES || this.pendingWriteBytes + bytes > _BrainDecisionLedger.MAX_PENDING_WRITE_BYTES) {
|
|
1610
|
+
return;
|
|
1611
|
+
}
|
|
1612
|
+
this.pendingWriteCount += 1;
|
|
1613
|
+
this.pendingWriteBytes += bytes;
|
|
1614
|
+
this.writeChain = this.writeChain.then(async () => {
|
|
1615
|
+
if (!this.dirReady) {
|
|
1616
|
+
await mkdir(dirname(this.opts.filePath), { recursive: true });
|
|
1617
|
+
this.dirReady = true;
|
|
1618
|
+
}
|
|
1619
|
+
await this.maybeRotate();
|
|
1620
|
+
await appendFile(this.opts.filePath, line, { encoding: "utf8", mode: SECRET_FILE_MODE });
|
|
1621
|
+
}).catch(() => {
|
|
1622
|
+
}).finally(() => {
|
|
1623
|
+
this.pendingWriteCount = Math.max(0, this.pendingWriteCount - 1);
|
|
1624
|
+
this.pendingWriteBytes = Math.max(0, this.pendingWriteBytes - bytes);
|
|
1625
|
+
});
|
|
1626
|
+
}
|
|
1627
|
+
/**
|
|
1628
|
+
* Size-bounded rotation: past {@link MAX_FILE_BYTES} the file moves to a
|
|
1629
|
+
* single `.1` sibling (previous `.1` is overwritten), bounding disk to
|
|
1630
|
+
* ~2× the cap. Checked once per {@link ROTATE_CHECK_EVERY} writes; runs on
|
|
1631
|
+
* the write chain, so it never races an append. The ring `load()` reads
|
|
1632
|
+
* only the tail, so rotation never costs queryable history.
|
|
1633
|
+
*/
|
|
1634
|
+
async maybeRotate() {
|
|
1635
|
+
this.writesSinceRotateCheck += 1;
|
|
1636
|
+
if (this.writesSinceRotateCheck < _BrainDecisionLedger.ROTATE_CHECK_EVERY) return;
|
|
1637
|
+
this.writesSinceRotateCheck = 0;
|
|
1638
|
+
try {
|
|
1639
|
+
const info = await stat(this.opts.filePath);
|
|
1640
|
+
if (info.size < _BrainDecisionLedger.MAX_FILE_BYTES) return;
|
|
1641
|
+
await rename(this.opts.filePath, `${this.opts.filePath}.1`);
|
|
1642
|
+
} catch {
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
/** Seed the in-memory ring from the JSONL tail (cross-session learning). */
|
|
1646
|
+
async load() {
|
|
1647
|
+
try {
|
|
1648
|
+
await access(this.opts.filePath);
|
|
1649
|
+
} catch {
|
|
1650
|
+
return;
|
|
1651
|
+
}
|
|
1652
|
+
const seed = [];
|
|
1653
|
+
const input = createReadStream(this.opts.filePath, { encoding: "utf8" });
|
|
1654
|
+
const lines = createInterface({ input, crlfDelay: Number.POSITIVE_INFINITY });
|
|
1655
|
+
try {
|
|
1656
|
+
for await (const line of lines) {
|
|
1657
|
+
if (!line.trim()) continue;
|
|
1658
|
+
try {
|
|
1659
|
+
const parsed = JSON.parse(line);
|
|
1660
|
+
if (typeof parsed?.requestId === "string" && typeof parsed.at === "number") {
|
|
1661
|
+
seed.push(parsed);
|
|
1662
|
+
if (seed.length > this.maxMemoryEntries) seed.shift();
|
|
1663
|
+
}
|
|
1664
|
+
} catch {
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
} catch {
|
|
1668
|
+
return;
|
|
1669
|
+
} finally {
|
|
1670
|
+
lines.close();
|
|
1671
|
+
input.destroy();
|
|
1672
|
+
}
|
|
1673
|
+
this.entries.unshift(...seed);
|
|
1674
|
+
if (this.entries.length > this.maxMemoryEntries) {
|
|
1675
|
+
this.entries.splice(0, this.entries.length - this.maxMemoryEntries);
|
|
1676
|
+
}
|
|
1677
|
+
for (const e of this.entries) {
|
|
1678
|
+
if (e.kind === "outcome" && e.outcome) this.outcomeByRequest.set(e.requestId, e.outcome);
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
};
|
|
1682
|
+
|
|
1683
|
+
// src/coordination/dispatcher.ts
|
|
1684
|
+
var DEFAULT_DISPATCH_ROLE = "executor";
|
|
1685
|
+
var FALLBACK_DEFINITION = {
|
|
1686
|
+
config: { role: "unknown", name: "Unknown Agent" },
|
|
1687
|
+
budget: {},
|
|
1688
|
+
capability: { phase: "meta", summary: "", keywords: [] }
|
|
1689
|
+
};
|
|
1690
|
+
function normalize(text) {
|
|
1691
|
+
return ` ${text.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim()} `;
|
|
1692
|
+
}
|
|
1693
|
+
function scoreAgents(task, catalog = AGENT_CATALOG) {
|
|
1694
|
+
const haySet = new Set(normalize(task).split(/\s+/).filter(Boolean));
|
|
1695
|
+
const out = [];
|
|
1696
|
+
for (const def of Object.values(catalog)) {
|
|
1697
|
+
if (!def?.config?.role) continue;
|
|
1698
|
+
let score = 0;
|
|
1699
|
+
const matched = [];
|
|
1700
|
+
for (const kw of def.capability.keywords) {
|
|
1701
|
+
const needleWords = normalize(kw).split(/\s+/).filter(Boolean);
|
|
1702
|
+
const allPresent = needleWords.every((w) => haySet.has(w));
|
|
1703
|
+
if (allPresent) {
|
|
1704
|
+
score += needleWords.length;
|
|
1705
|
+
matched.push(kw);
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
if (score > 0) {
|
|
1709
|
+
out.push({ role: def.config.role, name: def.config.name, score, matched });
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1712
|
+
out.sort((a, b) => b.score - a.score);
|
|
1713
|
+
return out;
|
|
1714
|
+
}
|
|
1715
|
+
function heuristicConfidence(candidates) {
|
|
1716
|
+
if (candidates.length === 0) return 0;
|
|
1717
|
+
const top = candidates[0]?.score ?? 0;
|
|
1718
|
+
const second = candidates[1]?.score ?? 0;
|
|
1719
|
+
const strength = Math.min(1, top / 3);
|
|
1720
|
+
const margin = (top - second + 1) / (top + 1);
|
|
1721
|
+
return Math.min(1, strength * margin);
|
|
1722
|
+
}
|
|
1723
|
+
async function dispatchAgent(task, opts = {}) {
|
|
1724
|
+
const catalog = opts.catalog ?? AGENT_CATALOG;
|
|
1725
|
+
const threshold = opts.confidenceThreshold ?? 0.4;
|
|
1726
|
+
const maxCandidates = opts.maxCandidates ?? 6;
|
|
1727
|
+
const candidates = scoreAgents(task, catalog);
|
|
1728
|
+
const confidence = heuristicConfidence(candidates);
|
|
1729
|
+
const top = candidates[0];
|
|
1730
|
+
if (top && confidence >= threshold) {
|
|
1731
|
+
return {
|
|
1732
|
+
role: top.role,
|
|
1733
|
+
definition: catalog[top.role] ?? FALLBACK_DEFINITION,
|
|
1734
|
+
confidence,
|
|
1735
|
+
method: "heuristic",
|
|
1736
|
+
reason: `Matched keywords: ${top.matched.slice(0, 4).join(", ")}`,
|
|
1737
|
+
alternatives: candidates.slice(1, maxCandidates)
|
|
1738
|
+
};
|
|
1739
|
+
}
|
|
1740
|
+
if (opts.classifier) {
|
|
1741
|
+
const pool = (candidates.length > 0 ? candidates.slice(0, maxCandidates).map((c) => catalog[c.role] ?? FALLBACK_DEFINITION) : Object.values(catalog)).map((d) => ({
|
|
1742
|
+
role: d.config.role,
|
|
1743
|
+
name: d.config.name,
|
|
1744
|
+
summary: d.capability.summary
|
|
1745
|
+
}));
|
|
1746
|
+
try {
|
|
1747
|
+
const choice = await opts.classifier(task, pool);
|
|
1748
|
+
if (choice && catalog[choice.role]) {
|
|
1749
|
+
return {
|
|
1750
|
+
role: choice.role,
|
|
1751
|
+
definition: catalog[choice.role] ?? FALLBACK_DEFINITION,
|
|
1752
|
+
confidence: 1,
|
|
1753
|
+
method: "llm",
|
|
1754
|
+
reason: choice.reason ?? "Selected by LLM classifier",
|
|
1755
|
+
alternatives: candidates.slice(0, maxCandidates).filter((c) => c.role !== choice.role)
|
|
1756
|
+
};
|
|
1757
|
+
}
|
|
1758
|
+
} catch {
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1761
|
+
if (top) {
|
|
1762
|
+
return {
|
|
1763
|
+
role: top.role,
|
|
1764
|
+
definition: catalog[top.role] ?? FALLBACK_DEFINITION,
|
|
1765
|
+
confidence,
|
|
1766
|
+
method: "heuristic",
|
|
1767
|
+
reason: `Weak match (${top.matched.slice(0, 3).join(", ") || "low signal"})`,
|
|
1768
|
+
alternatives: candidates.slice(1, maxCandidates)
|
|
1769
|
+
};
|
|
1770
|
+
}
|
|
1771
|
+
const fallbackRole = catalog[DEFAULT_DISPATCH_ROLE] ? DEFAULT_DISPATCH_ROLE : Object.keys(catalog)[0] ?? DEFAULT_DISPATCH_ROLE;
|
|
1772
|
+
return {
|
|
1773
|
+
role: fallbackRole,
|
|
1774
|
+
definition: catalog[fallbackRole] ?? FALLBACK_DEFINITION,
|
|
1775
|
+
confidence: 0,
|
|
1776
|
+
method: "fallback",
|
|
1777
|
+
reason: "No keyword signal; defaulting to the generalist Executor",
|
|
1778
|
+
alternatives: []
|
|
1779
|
+
};
|
|
1780
|
+
}
|
|
1781
|
+
function makeLLMClassifier(complete) {
|
|
1782
|
+
return async (task, candidates) => {
|
|
1783
|
+
const list = candidates.map((c, i) => `${i + 1}. ${c.role} \u2014 ${c.summary}`).join("\n");
|
|
1784
|
+
const prompt = renderInstructionTemplate(readBundledInstructionText("llm/agent-router.md"), {
|
|
1785
|
+
task,
|
|
1786
|
+
agents: list
|
|
1787
|
+
});
|
|
1788
|
+
const raw = (await complete(prompt)).trim();
|
|
1789
|
+
const match = raw.match(/\{[\s\S]*\}/);
|
|
1790
|
+
if (!match) return null;
|
|
1791
|
+
const parsed = safeParse(
|
|
1792
|
+
match[0]
|
|
1793
|
+
);
|
|
1794
|
+
if (!parsed.ok || !parsed.value || typeof parsed.value.role !== "string") return null;
|
|
1795
|
+
const role = parsed.value.role.trim();
|
|
1796
|
+
const valid = candidates.some((c) => c.role === role);
|
|
1797
|
+
if (!valid) return null;
|
|
1798
|
+
return {
|
|
1799
|
+
role,
|
|
1800
|
+
reason: typeof parsed.value.reason === "string" ? parsed.value.reason : void 0
|
|
1801
|
+
};
|
|
1802
|
+
};
|
|
1803
|
+
}
|
|
1804
|
+
|
|
1805
|
+
// src/coordination/subagent-nicknames.ts
|
|
1806
|
+
var NICKNAME_POOL = {
|
|
1807
|
+
// Physics & fundamental sciences
|
|
1808
|
+
"einstein": { name: "Einstein", domain: "physics" },
|
|
1809
|
+
"newton": { name: "Newton", domain: "physics" },
|
|
1810
|
+
"feynman": { name: "Feynman", domain: "physics" },
|
|
1811
|
+
"dirac": { name: "Dirac", domain: "physics" },
|
|
1812
|
+
"bohr": { name: "Bohr", domain: "physics" },
|
|
1813
|
+
"planck": { name: "Planck", domain: "physics" },
|
|
1814
|
+
"curie": { name: "Curie", domain: "physics" },
|
|
1815
|
+
"fermi": { name: "Fermi", domain: "physics" },
|
|
1816
|
+
"heisenberg": { name: "Heisenberg", domain: "physics" },
|
|
1817
|
+
"schrodinger": { name: "Schr\xF6dinger", domain: "physics" },
|
|
1818
|
+
// Mathematics
|
|
1819
|
+
"euclid": { name: "Euclid", domain: "math" },
|
|
1820
|
+
"gauss": { name: "Gauss", domain: "math" },
|
|
1821
|
+
"turing": { name: "Turing", domain: "math" },
|
|
1822
|
+
"poincare": { name: "Poincar\xE9", domain: "math" },
|
|
1823
|
+
"riemann": { name: "Riemann", domain: "math" },
|
|
1824
|
+
"hilbert": { name: "Hilbert", domain: "math" },
|
|
1825
|
+
"pythagoras": { name: "Pythagoras", domain: "math" },
|
|
1826
|
+
// Computing & information theory
|
|
1827
|
+
"von-neumann": { name: "Von Neumann", domain: "computing" },
|
|
1828
|
+
"shannon": { name: "Shannon", domain: "computing" },
|
|
1829
|
+
"hopper": { name: "Hopper", domain: "computing" },
|
|
1830
|
+
"backus": { name: "Backus", domain: "computing" },
|
|
1831
|
+
"knuth": { name: "Knuth", domain: "computing" },
|
|
1832
|
+
"torvalds": { name: "Torvalds", domain: "computing" },
|
|
1833
|
+
"stallman": { name: "Stallman", domain: "computing" },
|
|
1834
|
+
"berners-lee": { name: "Berners-Lee", domain: "computing" },
|
|
1835
|
+
"babbage": { name: "Babbage", domain: "computing" },
|
|
1836
|
+
"lovelace": { name: "Lovelace", domain: "computing" },
|
|
1837
|
+
"klein": { name: "Klein", domain: "computing" },
|
|
1838
|
+
// Electronics & electrical engineering
|
|
1839
|
+
"edison": { name: "Edison", domain: "ee" },
|
|
1840
|
+
"tesla": { name: "Tesla", domain: "ee" },
|
|
1841
|
+
"faraday": { name: "Faraday", domain: "ee" },
|
|
1842
|
+
"maxwell": { name: "Maxwell", domain: "ee" },
|
|
1843
|
+
"ohm": { name: "Ohm", domain: "ee" },
|
|
1844
|
+
"bell": { name: "Bell", domain: "ee" },
|
|
1845
|
+
"marconi": { name: "Marconi", domain: "ee" },
|
|
1846
|
+
"lamarr": { name: "Lamarr", domain: "ee" },
|
|
1847
|
+
// General science / multi-disciplinary
|
|
1848
|
+
"darwin": { name: "Darwin", domain: "biology" },
|
|
1849
|
+
"mendel": { name: "Mendel", domain: "biology" },
|
|
1850
|
+
"pasteur": { name: "Pasteur", domain: "biology" },
|
|
1851
|
+
"hawking": { name: "Hawking", domain: "cosmology" },
|
|
1852
|
+
"sagan": { name: "Sagan", domain: "cosmology" },
|
|
1853
|
+
// Exploration & navigation
|
|
1854
|
+
"columbus": { name: "Columbus", domain: "exploration" },
|
|
1855
|
+
"polo": { name: "Polo", domain: "exploration" },
|
|
1856
|
+
"magellan": { name: "Magellan", domain: "exploration" },
|
|
1857
|
+
// Chemistry / materials
|
|
1858
|
+
"lavoisier": { name: "Lavoisier", domain: "chemistry" },
|
|
1859
|
+
"mendeleev": { name: "Mendeleev", domain: "chemistry" }
|
|
1860
|
+
};
|
|
1861
|
+
var ALL_NICKNAMES = Object.entries(NICKNAME_POOL);
|
|
1862
|
+
var NAME_TO_KEY = Object.fromEntries(
|
|
1863
|
+
ALL_NICKNAMES.map(([key, entry]) => [entry.name, key])
|
|
1864
|
+
);
|
|
1865
|
+
var DOMAIN_PREFERENCES = {
|
|
1866
|
+
"security": ["shannon", "turing", "lamarr", "stallman"],
|
|
1867
|
+
"bug-hunter": ["darwin", "curie", "feynman", "fermi"],
|
|
1868
|
+
"refactor": ["gauss", "hilbert", "euclid", "planck"],
|
|
1869
|
+
"audit-log": ["sagan", "hawking", "poincare", "newton"],
|
|
1870
|
+
"planner": ["hilbert", "gauss", "turing", "euclid"],
|
|
1871
|
+
"researcher": ["sagan", "hawking", "darwin", "pasteur"],
|
|
1872
|
+
"explorer": ["marconi", "bell", "columbus", "polo"],
|
|
1873
|
+
"testing": ["pasteur", "curie", "fermi", "bohr"],
|
|
1874
|
+
"frontend": ["lovelace", "hopper", "babbage", "backus"],
|
|
1875
|
+
"backend": ["torvalds", "stallman", "von-neumann", "backus"],
|
|
1876
|
+
"database": ["turing", "shannon", "backus", "knuth"],
|
|
1877
|
+
"devops": ["tesla", "edison", "faraday", "bell"],
|
|
1878
|
+
"security-scanner": ["shannon", "turing", "lamarr", "stallman"],
|
|
1879
|
+
"refactor-planner": ["gauss", "hilbert", "planck", "newton"],
|
|
1880
|
+
"architect": ["von-neumann", "turing", "gauss", "hilbert"],
|
|
1881
|
+
"critic": ["einstein", "feynman", "dirac", "bohr"],
|
|
1882
|
+
"e2e": ["hopper", "bell", "marconi", "tesla"],
|
|
1883
|
+
"performance": ["knuth", "gauss", "planck", "feynman"],
|
|
1884
|
+
"chaos": ["tesla", "edison", "curie", "fermi"],
|
|
1885
|
+
"cost": ["ohm", "bell", "marconi", "tesla"],
|
|
1886
|
+
// default fallback
|
|
1887
|
+
"default": ["einstein", "newton", "curie", "tesla", "edison", "turing", "shannon", "hopper", "knuth", "stallman"]
|
|
1888
|
+
};
|
|
1889
|
+
function assignNickname(role, used) {
|
|
1890
|
+
const preferences = [
|
|
1891
|
+
...DOMAIN_PREFERENCES[role] ?? [],
|
|
1892
|
+
...DOMAIN_PREFERENCES["default"] ?? []
|
|
1893
|
+
];
|
|
1894
|
+
for (const key of preferences) {
|
|
1895
|
+
const entry = NICKNAME_POOL[key];
|
|
1896
|
+
if (entry && !used.has(key)) {
|
|
1897
|
+
return { key, display: `${entry.name} (${formatRole(role)})` };
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1900
|
+
for (const [key, entry] of ALL_NICKNAMES) {
|
|
1901
|
+
if (!used.has(key)) {
|
|
1902
|
+
return { key, display: `${entry.name} (${formatRole(role)})` };
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1905
|
+
const counter = used.size + 1;
|
|
1906
|
+
return { key: `scientist-${counter}`, display: `Scientist #${counter} (${formatRole(role)})` };
|
|
1907
|
+
}
|
|
1908
|
+
function nicknameKeyFromDisplay(display) {
|
|
1909
|
+
const base = display.replace(/\s*\([^)]*\)\s*$/, "").trim();
|
|
1910
|
+
const key = NAME_TO_KEY[base];
|
|
1911
|
+
if (key) return key;
|
|
1912
|
+
const synthesized = base.match(/^Scientist #(\d+)$/);
|
|
1913
|
+
return synthesized ? `scientist-${synthesized[1]}` : void 0;
|
|
1914
|
+
}
|
|
1915
|
+
function formatRole(role) {
|
|
1916
|
+
return role.split(/[-_]/).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
|
|
1917
|
+
}
|
|
1918
|
+
|
|
1919
|
+
// src/coordination/fleet.ts
|
|
1920
|
+
function defineAgent(id, name, promptRole = id, provider) {
|
|
1921
|
+
return {
|
|
1922
|
+
id,
|
|
1923
|
+
name,
|
|
1924
|
+
role: id,
|
|
1925
|
+
prompt: agentPrompt(promptRole),
|
|
1926
|
+
...provider === void 0 ? {} : { provider }
|
|
1927
|
+
};
|
|
1928
|
+
}
|
|
1929
|
+
var AUDIT_LOG_AGENT = defineAgent("audit-log", "Audit Log");
|
|
1930
|
+
var BUG_HUNTER_AGENT = defineAgent("bug-hunter", "Bug Hunter");
|
|
1931
|
+
var REFACTOR_PLANNER_AGENT = defineAgent("refactor-planner", "Refactor Planner");
|
|
1932
|
+
var SECURITY_SCANNER_AGENT = defineAgent("security-scanner", "Security Scanner");
|
|
1933
|
+
var SHADOW_AGENT = {
|
|
1934
|
+
...defineAgent("shadow-agent", "Shadow"),
|
|
1935
|
+
skillNames: [...SHADOW_AGENT_SKILLS]
|
|
1936
|
+
};
|
|
1937
|
+
var EXPLORE_COMPANION_AGENT = {
|
|
1938
|
+
...defineAgent("explore-companion", "Explore Companion"),
|
|
1939
|
+
tools: [...TOOLS.read, ...TOOLS.index],
|
|
1940
|
+
// Read-only, triple-enforced: allowlist has no write/bash, and the
|
|
1941
|
+
// disabled list blocks the escape hatches explicitly.
|
|
1942
|
+
disabledTools: [
|
|
1943
|
+
"write",
|
|
1944
|
+
"edit",
|
|
1945
|
+
"replace",
|
|
1946
|
+
"patch",
|
|
1947
|
+
"bash",
|
|
1948
|
+
"exec",
|
|
1949
|
+
"delegate",
|
|
1950
|
+
"spawn_subagent",
|
|
1951
|
+
"assign_task"
|
|
1952
|
+
],
|
|
1953
|
+
skillNames: [...ROLE_SKILL_SETS["explore-companion"]],
|
|
1954
|
+
spawnBudgetExempt: true,
|
|
1955
|
+
// Findings travel via mailbox + submit_result, not the leader's stream.
|
|
1956
|
+
textStream: "silent",
|
|
1957
|
+
toolStream: "silent"
|
|
1958
|
+
};
|
|
1959
|
+
var CHAOS_MONKEY_AGENT = {
|
|
1960
|
+
...defineAgent("chaos-monkey", "Chaos Monkey"),
|
|
1961
|
+
tools: [...TOOLS.build],
|
|
1962
|
+
skillNames: ["testing", "typescript-strict"],
|
|
1963
|
+
spawnBudgetExempt: true,
|
|
1964
|
+
// Run in the live checkout: mutation targets are usually freshly
|
|
1965
|
+
// written and uncommitted — a worktree spawned from HEAD would not
|
|
1966
|
+
// contain them and every mutant would drift. The mutation_test tool
|
|
1967
|
+
// honors this value as its default; callers can still override per
|
|
1968
|
+
// call via its `chaosWorktree` input when targets are committed and
|
|
1969
|
+
// isolation is wanted.
|
|
1970
|
+
worktree: "off",
|
|
1971
|
+
// Report travels via submit_result + final text, not the leader's stream.
|
|
1972
|
+
textStream: "silent",
|
|
1973
|
+
toolStream: "silent"
|
|
1974
|
+
};
|
|
1975
|
+
var CRITIC_AGENT = defineAgent("critic", "Critic");
|
|
1976
|
+
var GENERIC_AGENT = defineAgent("generic", "Generic Project Agent");
|
|
1977
|
+
function withDispatchMetadata(definition) {
|
|
1978
|
+
const summary = definition.capability?.summary?.trim();
|
|
1979
|
+
if (!summary) return definition.config;
|
|
1980
|
+
return {
|
|
1981
|
+
...definition.config,
|
|
1982
|
+
dispatch: { summary, keywords: [...definition.capability.keywords ?? []] }
|
|
1983
|
+
};
|
|
1984
|
+
}
|
|
1985
|
+
var FLEET_ROSTER = {
|
|
1986
|
+
"audit-log": AUDIT_LOG_AGENT,
|
|
1987
|
+
"bug-hunter": BUG_HUNTER_AGENT,
|
|
1988
|
+
"refactor-planner": REFACTOR_PLANNER_AGENT,
|
|
1989
|
+
"security-scanner": SECURITY_SCANNER_AGENT,
|
|
1990
|
+
critic: CRITIC_AGENT,
|
|
1991
|
+
generic: GENERIC_AGENT,
|
|
1992
|
+
"shadow-agent": SHADOW_AGENT,
|
|
1993
|
+
"explore-companion": EXPLORE_COMPANION_AGENT,
|
|
1994
|
+
"chaos-monkey": CHAOS_MONKEY_AGENT,
|
|
1995
|
+
...Object.fromEntries(
|
|
1996
|
+
ALL_AGENT_DEFINITIONS.map((d) => [d.config.role, withDispatchMetadata(d)])
|
|
1997
|
+
)
|
|
1998
|
+
};
|
|
1999
|
+
var DEFAULT_IDLE_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
2000
|
+
var FLEET_ROSTER_BUDGETS = {
|
|
2001
|
+
"audit-log": { timeoutMs: 3 * 60 * 60 * 1e3, maxIterations: 2500, maxToolCalls: 7500 },
|
|
2002
|
+
"bug-hunter": { timeoutMs: 5 * 60 * 60 * 1e3, maxIterations: 4e3, maxToolCalls: 1e4 },
|
|
2003
|
+
"refactor-planner": { timeoutMs: 4 * 60 * 60 * 1e3, maxIterations: 3e3, maxToolCalls: 9e3 },
|
|
2004
|
+
"security-scanner": { timeoutMs: 5 * 60 * 60 * 1e3, maxIterations: 4e3, maxToolCalls: 1e4 },
|
|
2005
|
+
critic: { timeoutMs: 3 * 60 * 60 * 1e3, maxIterations: 2e3, maxToolCalls: 6e3 },
|
|
2006
|
+
generic: { idleTimeoutMs: DEFAULT_IDLE_TIMEOUT_MS, maxIterations: 2500, maxToolCalls: 7500 },
|
|
2007
|
+
"shadow-agent": {
|
|
2008
|
+
idleTimeoutMs: DEFAULT_IDLE_TIMEOUT_MS,
|
|
2009
|
+
maxIterations: 1e3,
|
|
2010
|
+
maxToolCalls: 2500,
|
|
2011
|
+
// Monitoring passes routinely include a large system prompt plus fleet
|
|
2012
|
+
// context. 30k was below observed single-pass usage and therefore triggered
|
|
2013
|
+
// a post-response negotiation every run; 96k leaves headroom while the cost
|
|
2014
|
+
// and tool/iteration caps still bound runaway work.
|
|
2015
|
+
maxTokens: 96e3,
|
|
2016
|
+
maxCostUsd: 0.5
|
|
2017
|
+
},
|
|
2018
|
+
"explore-companion": {
|
|
2019
|
+
idleTimeoutMs: DEFAULT_IDLE_TIMEOUT_MS,
|
|
2020
|
+
maxIterations: 3e3,
|
|
2021
|
+
maxToolCalls: 8e3,
|
|
2022
|
+
maxTokens: 96e3,
|
|
2023
|
+
maxCostUsd: 0.5
|
|
2024
|
+
},
|
|
2025
|
+
"chaos-monkey": {
|
|
2026
|
+
// A mutation pass is many short apply/run/restore cycles — per-mutant
|
|
2027
|
+
// work is tiny, but a large plan (25 mutants/file × N files) needs
|
|
2028
|
+
// headroom. Idle-based reaping covers a stalled pass.
|
|
2029
|
+
idleTimeoutMs: DEFAULT_IDLE_TIMEOUT_MS,
|
|
2030
|
+
maxIterations: 2e3,
|
|
2031
|
+
maxToolCalls: 6e3,
|
|
2032
|
+
maxTokens: 96e3,
|
|
2033
|
+
maxCostUsd: 0.5
|
|
2034
|
+
},
|
|
2035
|
+
...Object.fromEntries(
|
|
2036
|
+
ALL_AGENT_DEFINITIONS.map((d) => [d.config.role, d.budget])
|
|
2037
|
+
)
|
|
2038
|
+
};
|
|
2039
|
+
var GENERIC_SUBAGENT_BUDGET = {
|
|
2040
|
+
idleTimeoutMs: DEFAULT_IDLE_TIMEOUT_MS,
|
|
2041
|
+
maxIterations: 2500,
|
|
2042
|
+
maxToolCalls: 7500
|
|
2043
|
+
};
|
|
2044
|
+
function applyRosterBudget(cfg) {
|
|
2045
|
+
const roleBudget = cfg.role ? FLEET_ROSTER_BUDGETS[cfg.role] : void 0;
|
|
2046
|
+
const defaultBudget = roleBudget ?? (cfg.name ? GENERIC_SUBAGENT_BUDGET : void 0);
|
|
2047
|
+
if (!defaultBudget) return cfg;
|
|
2048
|
+
return {
|
|
2049
|
+
...cfg,
|
|
2050
|
+
// Wall-clock cap is opt-in only: forward an explicit `cfg.timeoutMs`, but
|
|
2051
|
+
// do NOT impose the roster's historical multi-hour wall-clock default — it
|
|
2052
|
+
// killed agents that were still actively working. Reaping is idle-based.
|
|
2053
|
+
timeoutMs: cfg.timeoutMs,
|
|
2054
|
+
// Idle window is the default reaper. Resets on activity, so a long-but-
|
|
2055
|
+
// productive run is never killed; only a genuine stall is reaped.
|
|
2056
|
+
idleTimeoutMs: cfg.idleTimeoutMs ?? defaultBudget.idleTimeoutMs ?? DEFAULT_IDLE_TIMEOUT_MS,
|
|
2057
|
+
maxIterations: cfg.maxIterations ?? defaultBudget.maxIterations,
|
|
2058
|
+
maxToolCalls: cfg.maxToolCalls ?? defaultBudget.maxToolCalls,
|
|
2059
|
+
maxTokens: cfg.maxTokens ?? defaultBudget.maxTokens,
|
|
2060
|
+
maxCostUsd: cfg.maxCostUsd ?? defaultBudget.maxCostUsd
|
|
2061
|
+
};
|
|
2062
|
+
}
|
|
2063
|
+
var ALL_FLEET_AGENTS = Object.values(FLEET_ROSTER);
|
|
2064
|
+
var CLINE_AGENT = defineAgent("cline", "Cline", "acp-cline", "acp");
|
|
2065
|
+
var GEMINI_CLI_AGENT = defineAgent("gemini-cli", "Gemini CLI", "acp-gemini-cli", "acp");
|
|
2066
|
+
var COPILOT_AGENT = defineAgent("copilot", "GitHub Copilot", "acp-copilot", "acp");
|
|
2067
|
+
var OPENHANDS_AGENT = defineAgent("openhands", "OpenHands", "acp-openhands", "acp");
|
|
2068
|
+
var GOOSE_AGENT = defineAgent("goose", "Goose", "acp-goose", "acp");
|
|
2069
|
+
var ACP_AGENTS = [
|
|
2070
|
+
CLINE_AGENT,
|
|
2071
|
+
GEMINI_CLI_AGENT,
|
|
2072
|
+
COPILOT_AGENT,
|
|
2073
|
+
OPENHANDS_AGENT,
|
|
2074
|
+
GOOSE_AGENT
|
|
2075
|
+
];
|
|
2076
|
+
var ACP_AGENT_BUDGET = {
|
|
2077
|
+
timeoutMs: 10 * 60 * 60 * 1e3,
|
|
2078
|
+
maxIterations: 8e3,
|
|
2079
|
+
maxToolCalls: 2e4
|
|
2080
|
+
};
|
|
2081
|
+
for (const agent of ACP_AGENTS) {
|
|
2082
|
+
FLEET_ROSTER_BUDGETS[agent.role] = { ...ACP_AGENT_BUDGET };
|
|
2083
|
+
}
|
|
2084
|
+
var FLEET_ROSTER_WITHACP = {
|
|
2085
|
+
...FLEET_ROSTER,
|
|
2086
|
+
...Object.fromEntries(ACP_AGENTS.map((a) => [a.role, a]))
|
|
2087
|
+
};
|
|
2088
|
+
|
|
2089
|
+
// src/coordination/multi-agent-coordinator.ts
|
|
2090
|
+
import { randomUUID } from "node:crypto";
|
|
2091
|
+
import { EventEmitter } from "node:events";
|
|
2092
|
+
|
|
2093
|
+
// src/coordination/coordinator/error-classifier.ts
|
|
2094
|
+
function classifySubagentError(err, hints = {}) {
|
|
2095
|
+
if (err instanceof AgentError && err.cause) {
|
|
2096
|
+
return classifySubagentError(err.cause, hints);
|
|
2097
|
+
}
|
|
2098
|
+
const cause = err instanceof Error ? { name: err.name, message: err.message, stack: err.stack } : void 0;
|
|
2099
|
+
if (ProviderError.isProviderError(err)) {
|
|
2100
|
+
const baseMessage2 = err.describe();
|
|
2101
|
+
return providerErrorToSubagentError(err, baseMessage2, cause);
|
|
2102
|
+
}
|
|
2103
|
+
const baseMessage = toErrorMessage(err);
|
|
2104
|
+
if (err instanceof BudgetExceededError) {
|
|
2105
|
+
const map = {
|
|
2106
|
+
iterations: "budget_iterations",
|
|
2107
|
+
tool_calls: "budget_tool_calls",
|
|
2108
|
+
tokens: "budget_tokens",
|
|
2109
|
+
cost: "budget_cost",
|
|
2110
|
+
timeout: "budget_timeout",
|
|
2111
|
+
idle_timeout: "budget_timeout"
|
|
2112
|
+
};
|
|
2113
|
+
return {
|
|
2114
|
+
kind: map[err.kind],
|
|
2115
|
+
message: baseMessage,
|
|
2116
|
+
retryable: false,
|
|
2117
|
+
cause
|
|
2118
|
+
};
|
|
2119
|
+
}
|
|
2120
|
+
if (hints.parentAborted) {
|
|
2121
|
+
return { kind: "aborted_by_parent", message: baseMessage, retryable: false, cause };
|
|
2122
|
+
}
|
|
2123
|
+
const lower = baseMessage.toLowerCase();
|
|
2124
|
+
if (/agent aborted$/i.test(baseMessage)) {
|
|
2125
|
+
return { kind: "aborted_by_parent", message: baseMessage, retryable: false, cause };
|
|
2126
|
+
}
|
|
2127
|
+
if (/agent exhausted iteration limit$/i.test(baseMessage)) {
|
|
2128
|
+
return { kind: "budget_iterations", message: baseMessage, retryable: false, cause };
|
|
2129
|
+
}
|
|
2130
|
+
if (/empty response/i.test(baseMessage)) {
|
|
2131
|
+
return { kind: "empty_response", message: baseMessage, retryable: false, cause };
|
|
2132
|
+
}
|
|
2133
|
+
if (/^tool failed: /i.test(baseMessage)) {
|
|
2134
|
+
return { kind: "tool_failed", message: baseMessage, retryable: false, cause };
|
|
2135
|
+
}
|
|
2136
|
+
if (lower.includes("bridge transport") || /bridge.*(closed|disconnect)/i.test(baseMessage)) {
|
|
2137
|
+
return { kind: "bridge_failed", message: baseMessage, retryable: false, cause };
|
|
2138
|
+
}
|
|
2139
|
+
if (/context length|max.*tokens?.*exceeded|prompt is too long/i.test(baseMessage)) {
|
|
2140
|
+
return { kind: "context_overflow", message: baseMessage, retryable: false, cause };
|
|
2141
|
+
}
|
|
2142
|
+
return { kind: "unknown", message: baseMessage, retryable: false, cause };
|
|
2143
|
+
}
|
|
2144
|
+
function providerErrorToSubagentError(err, message, cause) {
|
|
2145
|
+
switch (err.kind) {
|
|
2146
|
+
case "rate_limit":
|
|
2147
|
+
case "quota_exhausted":
|
|
2148
|
+
return {
|
|
2149
|
+
kind: "provider_rate_limit",
|
|
2150
|
+
message,
|
|
2151
|
+
retryable: true,
|
|
2152
|
+
backoffMs: err.body?.retryAfterMs ?? 5e3,
|
|
2153
|
+
cause
|
|
2154
|
+
};
|
|
2155
|
+
case "auth":
|
|
2156
|
+
return { kind: "provider_auth", message, retryable: false, cause };
|
|
2157
|
+
case "timeout":
|
|
2158
|
+
case "network":
|
|
2159
|
+
return { kind: "provider_timeout", message, retryable: true, cause };
|
|
2160
|
+
case "overloaded":
|
|
2161
|
+
case "server":
|
|
2162
|
+
case "stream_hang":
|
|
2163
|
+
return { kind: "provider_5xx", message, retryable: true, backoffMs: 3e3, cause };
|
|
2164
|
+
case "context_overflow":
|
|
2165
|
+
return { kind: "context_overflow", message, retryable: false, cause };
|
|
2166
|
+
case "invalid_request":
|
|
2167
|
+
case "content_filter":
|
|
2168
|
+
case "unknown":
|
|
2169
|
+
return { kind: "unknown", message, retryable: err.retryable, cause };
|
|
2170
|
+
}
|
|
2171
|
+
}
|
|
2172
|
+
|
|
2173
|
+
// src/coordination/multi-agent-timeout.ts
|
|
2174
|
+
async function executeSubagentWithTimeout({
|
|
2175
|
+
runner,
|
|
2176
|
+
task,
|
|
2177
|
+
ctx,
|
|
2178
|
+
budget,
|
|
2179
|
+
preemptFraction = TIMEOUT_PREEMPT_FRACTION,
|
|
2180
|
+
abortSubagent,
|
|
2181
|
+
currentSessionId,
|
|
2182
|
+
gracefulFinish
|
|
2183
|
+
}) {
|
|
2184
|
+
const initialTimeoutMs = budget.limits.timeoutMs;
|
|
2185
|
+
const idleLimitMs = budget.limits.idleTimeoutMs;
|
|
2186
|
+
if (initialTimeoutMs === void 0 && idleLimitMs === void 0) {
|
|
2187
|
+
return runner(task, ctx);
|
|
2188
|
+
}
|
|
2189
|
+
const start = Date.now();
|
|
2190
|
+
let timer = null;
|
|
2191
|
+
let PreemptState;
|
|
2192
|
+
((PreemptState2) => {
|
|
2193
|
+
PreemptState2["ACTIVE"] = "active";
|
|
2194
|
+
PreemptState2["LOCKED"] = "locked";
|
|
2195
|
+
})(PreemptState || (PreemptState = {}));
|
|
2196
|
+
let preemptedCeiling = null;
|
|
2197
|
+
let preemptState = "active" /* ACTIVE */;
|
|
2198
|
+
let lastGrantActivityTs = -1;
|
|
2199
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
2200
|
+
const terminate = (kind, limit, used) => {
|
|
2201
|
+
abortSubagent(ctx.subagentId);
|
|
2202
|
+
reject(
|
|
2203
|
+
budget._events?.hasListenerFor("budget.threshold_reached") ? new Error(`subagent stopped: budget ${kind} (limit=${limit}, used=${used})`) : new BudgetExceededError(kind, limit, used)
|
|
2204
|
+
);
|
|
2205
|
+
};
|
|
2206
|
+
const armFor = (ms) => {
|
|
2207
|
+
if (timer) clearTimeout(timer);
|
|
2208
|
+
timer = setTimeout(onTick, Math.max(0, ms));
|
|
2209
|
+
};
|
|
2210
|
+
const scheduleNext = () => {
|
|
2211
|
+
const wallLimit = budget.limits.timeoutMs ?? initialTimeoutMs;
|
|
2212
|
+
const wallRemaining = initialTimeoutMs === void 0 ? Number.POSITIVE_INFINITY : wallLimit - (Date.now() - start);
|
|
2213
|
+
const idleRemaining = idleLimitMs === void 0 || gracefulFinish !== void 0 && initialTimeoutMs !== void 0 ? Number.POSITIVE_INFINITY : (budget.limits.idleTimeoutMs ?? idleLimitMs) - budget.idleMs();
|
|
2214
|
+
const preemptRemaining = initialTimeoutMs === void 0 || preemptedCeiling === wallLimit || gracefulFinish !== void 0 ? Number.POSITIVE_INFINITY : wallLimit * preemptFraction - (Date.now() - start);
|
|
2215
|
+
const next = Math.min(wallRemaining, idleRemaining, preemptRemaining);
|
|
2216
|
+
if (!Number.isFinite(next)) {
|
|
2217
|
+
if (timer) {
|
|
2218
|
+
clearTimeout(timer);
|
|
2219
|
+
timer = null;
|
|
2220
|
+
}
|
|
2221
|
+
return;
|
|
2222
|
+
}
|
|
2223
|
+
armFor(Math.max(25, next));
|
|
2224
|
+
};
|
|
2225
|
+
const negotiateTimeout = async (used, limit) => {
|
|
2226
|
+
const handler = budget.onThreshold;
|
|
2227
|
+
if (!handler) return "stop";
|
|
2228
|
+
const result = handler({
|
|
2229
|
+
kind: "timeout",
|
|
2230
|
+
used,
|
|
2231
|
+
limit,
|
|
2232
|
+
requestDecision: () => {
|
|
2233
|
+
if (!budget._events?.hasListenerFor("budget.threshold_reached")) {
|
|
2234
|
+
return Promise.resolve("stop");
|
|
2235
|
+
}
|
|
2236
|
+
return new Promise((resolveDecision) => {
|
|
2237
|
+
let settled = false;
|
|
2238
|
+
const resolve = (d) => {
|
|
2239
|
+
if (settled) return;
|
|
2240
|
+
settled = true;
|
|
2241
|
+
resolveDecision(d);
|
|
2242
|
+
};
|
|
2243
|
+
const fallback = setTimeout(() => resolve("stop"), DECISION_TIMEOUT_MS);
|
|
2244
|
+
const sessionId = currentSessionId();
|
|
2245
|
+
budget._events?.emit("budget.threshold_reached", {
|
|
2246
|
+
...sessionId ? { sessionId } : {},
|
|
2247
|
+
kind: "timeout",
|
|
2248
|
+
used,
|
|
2249
|
+
limit,
|
|
2250
|
+
timeoutMs: DECISION_TIMEOUT_MS,
|
|
2251
|
+
extend: (extra) => {
|
|
2252
|
+
clearTimeout(fallback);
|
|
2253
|
+
queueMicrotask(() => resolve({ extend: extra }));
|
|
2254
|
+
},
|
|
2255
|
+
deny: () => {
|
|
2256
|
+
clearTimeout(fallback);
|
|
2257
|
+
resolve("stop");
|
|
2258
|
+
}
|
|
2259
|
+
});
|
|
2260
|
+
});
|
|
2261
|
+
}
|
|
2262
|
+
});
|
|
2263
|
+
return typeof result === "string" ? result : await result;
|
|
2264
|
+
};
|
|
2265
|
+
const onTick = async () => {
|
|
2266
|
+
const elapsed = Date.now() - start;
|
|
2267
|
+
const wallLimit = initialTimeoutMs === void 0 ? void 0 : budget.limits.timeoutMs ?? initialTimeoutMs;
|
|
2268
|
+
const idleLimit = idleLimitMs === void 0 ? void 0 : budget.limits.idleTimeoutMs ?? idleLimitMs;
|
|
2269
|
+
const wallExceeded = wallLimit !== void 0 && elapsed >= wallLimit;
|
|
2270
|
+
const idleExceeded = idleLimit !== void 0 && budget.idleMs() >= idleLimit;
|
|
2271
|
+
if (idleExceeded && !wallExceeded) {
|
|
2272
|
+
if (gracefulFinish !== void 0 && initialTimeoutMs !== void 0) {
|
|
2273
|
+
scheduleNext();
|
|
2274
|
+
return;
|
|
2275
|
+
}
|
|
2276
|
+
const sessionId = currentSessionId();
|
|
2277
|
+
budget._events?.emit("budget.threshold_reached", {
|
|
2278
|
+
...sessionId ? { sessionId } : {},
|
|
2279
|
+
kind: "idle_timeout",
|
|
2280
|
+
used: budget.idleMs(),
|
|
2281
|
+
limit: idleLimit ?? 0,
|
|
2282
|
+
timeoutMs: DECISION_TIMEOUT_MS,
|
|
2283
|
+
extend: () => {
|
|
2284
|
+
},
|
|
2285
|
+
deny: () => {
|
|
2286
|
+
}
|
|
2287
|
+
});
|
|
2288
|
+
abortSubagent(ctx.subagentId);
|
|
2289
|
+
reject(new BudgetExceededError("idle_timeout", idleLimit ?? 0, budget.idleMs()));
|
|
2290
|
+
return;
|
|
2291
|
+
}
|
|
2292
|
+
if (wallLimit !== void 0 && !wallExceeded && gracefulFinish === void 0 && budget.onThreshold && preemptState === "active" /* ACTIVE */ && elapsed >= wallLimit * preemptFraction) {
|
|
2293
|
+
const activityTs = Date.now() - budget.idleMs();
|
|
2294
|
+
if (activityTs <= lastGrantActivityTs) {
|
|
2295
|
+
preemptState = "locked" /* LOCKED */;
|
|
2296
|
+
preemptedCeiling = wallLimit;
|
|
2297
|
+
scheduleNext();
|
|
2298
|
+
return;
|
|
2299
|
+
}
|
|
2300
|
+
budget.setWatchdogNegotiation(wallLimit);
|
|
2301
|
+
try {
|
|
2302
|
+
const decision = await negotiateTimeout(elapsed, wallLimit);
|
|
2303
|
+
if (typeof decision !== "string" && decision.extend.timeoutMs !== void 0) {
|
|
2304
|
+
budget.patchLimits({ timeoutMs: decision.extend.timeoutMs });
|
|
2305
|
+
lastGrantActivityTs = Date.now() - budget.idleMs();
|
|
2306
|
+
preemptState = "active" /* ACTIVE */;
|
|
2307
|
+
preemptedCeiling = null;
|
|
2308
|
+
} else {
|
|
2309
|
+
preemptState = "locked" /* LOCKED */;
|
|
2310
|
+
preemptedCeiling = wallLimit;
|
|
2311
|
+
}
|
|
2312
|
+
} catch {
|
|
2313
|
+
preemptState = "locked" /* LOCKED */;
|
|
2314
|
+
preemptedCeiling = wallLimit;
|
|
2315
|
+
} finally {
|
|
2316
|
+
budget.clearWatchdogNegotiation();
|
|
2317
|
+
}
|
|
2318
|
+
scheduleNext();
|
|
2319
|
+
return;
|
|
2320
|
+
}
|
|
2321
|
+
if (!wallExceeded) {
|
|
2322
|
+
scheduleNext();
|
|
2323
|
+
return;
|
|
2324
|
+
}
|
|
2325
|
+
const limit = wallLimit ?? 0;
|
|
2326
|
+
if (gracefulFinish !== void 0) {
|
|
2327
|
+
if (!budget.graceGranted) {
|
|
2328
|
+
const reason = `wall-clock budget of ${Math.round(limit / 1e3)}s reached`;
|
|
2329
|
+
if (budget.notifyFinish(reason, { graceMs: gracefulFinish.graceMs })) {
|
|
2330
|
+
scheduleNext();
|
|
2331
|
+
return;
|
|
2332
|
+
}
|
|
2333
|
+
abortSubagent(ctx.subagentId);
|
|
2334
|
+
reject(new BudgetExceededError("timeout", limit, elapsed));
|
|
2335
|
+
return;
|
|
2336
|
+
} else {
|
|
2337
|
+
abortSubagent(ctx.subagentId);
|
|
2338
|
+
reject(new BudgetExceededError("timeout", limit, elapsed));
|
|
2339
|
+
return;
|
|
2340
|
+
}
|
|
2341
|
+
}
|
|
2342
|
+
if (!budget.onThreshold) {
|
|
2343
|
+
abortSubagent(ctx.subagentId);
|
|
2344
|
+
reject(new BudgetExceededError("timeout", limit, elapsed));
|
|
2345
|
+
return;
|
|
2346
|
+
}
|
|
2347
|
+
budget.setWatchdogNegotiation(limit);
|
|
2348
|
+
try {
|
|
2349
|
+
const decision = await negotiateTimeout(elapsed, limit);
|
|
2350
|
+
if (decision === "throw") {
|
|
2351
|
+
terminate("timeout", limit, elapsed);
|
|
2352
|
+
return;
|
|
2353
|
+
}
|
|
2354
|
+
if (decision === "continue") {
|
|
2355
|
+
preemptState = "locked" /* LOCKED */;
|
|
2356
|
+
preemptedCeiling = wallLimit;
|
|
2357
|
+
armFor(Math.max(1e3, limit));
|
|
2358
|
+
return;
|
|
2359
|
+
}
|
|
2360
|
+
if (decision === "stop") {
|
|
2361
|
+
terminate("timeout", limit, elapsed);
|
|
2362
|
+
return;
|
|
2363
|
+
}
|
|
2364
|
+
if (decision.extend.timeoutMs !== void 0) {
|
|
2365
|
+
budget.patchLimits({ timeoutMs: decision.extend.timeoutMs });
|
|
2366
|
+
lastGrantActivityTs = Date.now() - budget.idleMs();
|
|
2367
|
+
preemptState = "active" /* ACTIVE */;
|
|
2368
|
+
preemptedCeiling = null;
|
|
2369
|
+
scheduleNext();
|
|
2370
|
+
return;
|
|
2371
|
+
}
|
|
2372
|
+
terminate("timeout", limit, elapsed);
|
|
2373
|
+
return;
|
|
2374
|
+
} catch (err) {
|
|
2375
|
+
abortSubagent(ctx.subagentId);
|
|
2376
|
+
reject(
|
|
2377
|
+
err instanceof BudgetExceededError ? err : new BudgetExceededError("timeout", limit, elapsed)
|
|
2378
|
+
);
|
|
2379
|
+
return;
|
|
2380
|
+
} finally {
|
|
2381
|
+
budget.clearWatchdogNegotiation();
|
|
2382
|
+
}
|
|
2383
|
+
};
|
|
2384
|
+
scheduleNext();
|
|
2385
|
+
});
|
|
2386
|
+
try {
|
|
2387
|
+
return await Promise.race([runner(task, ctx), timeoutPromise]);
|
|
2388
|
+
} finally {
|
|
2389
|
+
if (timer) clearTimeout(timer);
|
|
2390
|
+
}
|
|
2391
|
+
}
|
|
2392
|
+
|
|
2393
|
+
// src/coordination/multi-agent-coordinator.ts
|
|
2394
|
+
var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends EventEmitter {
|
|
2395
|
+
coordinatorId;
|
|
2396
|
+
config;
|
|
2397
|
+
runner;
|
|
2398
|
+
sessionId;
|
|
2399
|
+
fleetBus;
|
|
2400
|
+
subagents = /* @__PURE__ */ new Map();
|
|
2401
|
+
/**
|
|
2402
|
+
* Base nickname keys already handed out this run (e.g. `einstein`, `tesla`).
|
|
2403
|
+
* Prevents two workers sharing a name. Direct `coordinator.spawn()` callers
|
|
2404
|
+
* (parallel/eternal engine, SDD parallel run) don't go through
|
|
2405
|
+
* `Director.spawn()` where nicknames are normally assigned, so the
|
|
2406
|
+
* coordinator upgrades placeholder names ("Executor", "slot-ab12cd", role
|
|
2407
|
+
* names) to memorable ones here — that's what surfaces in the fleet monitor.
|
|
2408
|
+
*/
|
|
2409
|
+
usedNicknames = /* @__PURE__ */ new Set();
|
|
2410
|
+
/** Maps subagentId → nickname key (e.g. 'einstein'). Used to free the slot on remove(). */
|
|
2411
|
+
subagentNicknames = /* @__PURE__ */ new Map();
|
|
2412
|
+
pendingTasks = [];
|
|
2413
|
+
completedResults = [];
|
|
2414
|
+
/** Prevents completedResults from growing unbounded in long-running coordinators. */
|
|
2415
|
+
static MAX_COMPLETED_RESULTS = 2e5;
|
|
2416
|
+
/** Caps each subagent's retained task history (see assign()); bounds RAM + the recordCompletion lookup. */
|
|
2417
|
+
static MAX_SUBAGENT_TASK_HISTORY = 64;
|
|
2418
|
+
totalIterations = 0;
|
|
2419
|
+
inFlight = 0;
|
|
2420
|
+
/**
|
|
2421
|
+
* Subagents currently being stopped. Set on entry to `stop()`, cleared
|
|
2422
|
+
* once `recordCompletion` lands the terminal TaskResult. Used by
|
|
2423
|
+
* `runDispatched` and `findIdleSubagent` to refuse mid-flight dispatch
|
|
2424
|
+
* to a subagent the caller has already asked to terminate — closes the
|
|
2425
|
+
* assign+terminate race where a fresh task could land on a worker that
|
|
2426
|
+
* was about to be killed.
|
|
2427
|
+
*/
|
|
2428
|
+
terminating = /* @__PURE__ */ new Set();
|
|
2429
|
+
constructor(config, options = {}) {
|
|
2430
|
+
super();
|
|
2431
|
+
this.setMaxListeners(0);
|
|
2432
|
+
this.coordinatorId = config.coordinatorId;
|
|
2433
|
+
this.config = config;
|
|
2434
|
+
this.runner = options.runner;
|
|
2435
|
+
this.sessionId = options.sessionId;
|
|
2436
|
+
}
|
|
2437
|
+
currentSessionId() {
|
|
2438
|
+
const value = typeof this.sessionId === "function" ? this.sessionId() : this.sessionId;
|
|
2439
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
2440
|
+
}
|
|
2441
|
+
/**
|
|
2442
|
+
* Replace the runner after construction. Used when the runner depends
|
|
2443
|
+
* on infrastructure (e.g. FleetBus) that isn't available until after
|
|
2444
|
+
* the coordinator's owning Director is built.
|
|
2445
|
+
*/
|
|
2446
|
+
setRunner(runner) {
|
|
2447
|
+
this.runner = runner;
|
|
2448
|
+
}
|
|
2449
|
+
/**
|
|
2450
|
+
* Wire a FleetBus for director-mode event emission. Call after the
|
|
2451
|
+
* FleetManager is constructed so the coordinator can emit lifecycle
|
|
2452
|
+
* events the TUI and monitoring tools subscribe to.
|
|
2453
|
+
*/
|
|
2454
|
+
setFleetBus(fleet) {
|
|
2455
|
+
this.fleetBus = fleet;
|
|
2456
|
+
}
|
|
2457
|
+
/**
|
|
2458
|
+
* Change the in-flight dispatch ceiling at runtime. Lowering does NOT
|
|
2459
|
+
* preempt running tasks — already-dispatched subagents finish their
|
|
2460
|
+
* current task; only future dispatches respect the new cap. Raising
|
|
2461
|
+
* immediately tries to fill the freed slots from the pending queue.
|
|
2462
|
+
*/
|
|
2463
|
+
setMaxConcurrent(n) {
|
|
2464
|
+
if (!Number.isFinite(n) || n < 1) {
|
|
2465
|
+
throw new Error(`maxConcurrent must be a finite integer >= 1, got ${n}`);
|
|
2466
|
+
}
|
|
2467
|
+
this.config.maxConcurrent = Math.floor(n);
|
|
2468
|
+
this.tryDispatchNext();
|
|
2469
|
+
}
|
|
2470
|
+
/**
|
|
2471
|
+
* Upgrade a placeholder/role-derived name to a memorable scientist nickname
|
|
2472
|
+
* (e.g. "Einstein (Executor)"). A name is treated as a placeholder when it is
|
|
2473
|
+
* empty, equals the role (case-insensitive), is a generic default
|
|
2474
|
+
* ("subagent"/"adhoc"/"generic"), or is an auto-generated `slot-…` id.
|
|
2475
|
+
* Explicit, human-chosen names — including nicknames already assigned by
|
|
2476
|
+
* `Director.spawn()` — are left untouched, so this never double-assigns.
|
|
2477
|
+
*/
|
|
2478
|
+
withNickname(subagent, subagentId) {
|
|
2479
|
+
const role = subagent.role ?? "subagent";
|
|
2480
|
+
const name = subagent.name?.trim() ?? "";
|
|
2481
|
+
const isPlaceholder = name === "" || name.toLowerCase() === role.toLowerCase() || name === "subagent" || name === "adhoc" || name === "generic" || /^slot-/.test(name);
|
|
2482
|
+
if (!isPlaceholder) return subagent;
|
|
2483
|
+
const { key, display } = assignNickname(role, this.usedNicknames);
|
|
2484
|
+
this.usedNicknames.add(key);
|
|
2485
|
+
this.subagentNicknames.set(subagentId, key);
|
|
2486
|
+
return { ...subagent, name: display };
|
|
2487
|
+
}
|
|
2488
|
+
async spawn(subagent) {
|
|
2489
|
+
const id = subagent.id || randomUUID();
|
|
2490
|
+
const cfg = this.withNickname(subagent, id);
|
|
2491
|
+
if (this.subagents.has(id)) {
|
|
2492
|
+
throw new Error(`Subagent id "${id}" already exists \u2014 refusing to overwrite`);
|
|
2493
|
+
}
|
|
2494
|
+
const context = {
|
|
2495
|
+
subagentId: id,
|
|
2496
|
+
tasks: [],
|
|
2497
|
+
// Wired later by the caller via setSubagentBridge() once the
|
|
2498
|
+
// bidirectional bridge is created. Readers must null-check / use
|
|
2499
|
+
// hasParentBridge() — the type now reflects this.
|
|
2500
|
+
parentBridge: null,
|
|
2501
|
+
doneCondition: this.config.doneCondition,
|
|
2502
|
+
maxConcurrent: this.config.maxConcurrent ?? 16
|
|
2503
|
+
};
|
|
2504
|
+
this.subagents.set(id, {
|
|
2505
|
+
config: { ...cfg, id },
|
|
2506
|
+
context,
|
|
2507
|
+
status: "idle",
|
|
2508
|
+
abortController: new AbortController()
|
|
2509
|
+
});
|
|
2510
|
+
this.emit("subagent.started", { subagent: { ...cfg, id } });
|
|
2511
|
+
this.fleetBus?.emit({
|
|
2512
|
+
subagentId: id,
|
|
2513
|
+
ts: Date.now(),
|
|
2514
|
+
type: "subagent.assigned",
|
|
2515
|
+
payload: {
|
|
2516
|
+
subagentId: id,
|
|
2517
|
+
name: subagent.name,
|
|
2518
|
+
provider: subagent.provider,
|
|
2519
|
+
model: subagent.model
|
|
2520
|
+
}
|
|
2521
|
+
});
|
|
2522
|
+
this.emitCoordinatorStats();
|
|
2523
|
+
return { subagentId: id, agentId: id };
|
|
2524
|
+
}
|
|
2525
|
+
async assign(task) {
|
|
2526
|
+
this.pendingTasks.push(task);
|
|
2527
|
+
this.tryDispatchNext();
|
|
2528
|
+
}
|
|
2529
|
+
async delegate(to, msg) {
|
|
2530
|
+
const subagent = this.subagents.get(to);
|
|
2531
|
+
if (!subagent) throw new Error(`Subagent "${to}" not found`);
|
|
2532
|
+
if (!subagent.context.parentBridge) {
|
|
2533
|
+
throw new Error(`Subagent "${to}" has no parentBridge \u2014 call setSubagentBridge() first`);
|
|
2534
|
+
}
|
|
2535
|
+
await subagent.context.parentBridge.send(msg);
|
|
2536
|
+
}
|
|
2537
|
+
/**
|
|
2538
|
+
* Wire up the communication bridge for a subagent. Call after spawn() once
|
|
2539
|
+
* the caller has created the bidirectional connection.
|
|
2540
|
+
*/
|
|
2541
|
+
setSubagentBridge(subagentId, bridge) {
|
|
2542
|
+
const subagent = this.subagents.get(subagentId);
|
|
2543
|
+
if (!subagent) throw new Error(`Subagent "${subagentId}" not found`);
|
|
2544
|
+
subagent.context.parentBridge = bridge;
|
|
2545
|
+
}
|
|
2546
|
+
async stop(subagentId) {
|
|
2547
|
+
const subagent = this.subagents.get(subagentId);
|
|
2548
|
+
if (!subagent) return;
|
|
2549
|
+
this.terminating.add(subagentId);
|
|
2550
|
+
subagent.abortController.abort();
|
|
2551
|
+
subagent.status = "stopped";
|
|
2552
|
+
subagent.currentTask = void 0;
|
|
2553
|
+
subagent.context.parentBridge = null;
|
|
2554
|
+
this.emit("subagent.stopped", { subagentId, reason: "stopped by coordinator" });
|
|
2555
|
+
this.fleetBus?.emit({
|
|
2556
|
+
subagentId,
|
|
2557
|
+
ts: Date.now(),
|
|
2558
|
+
type: "subagent.stopped",
|
|
2559
|
+
payload: { subagentId, reason: "stopped by coordinator" }
|
|
2560
|
+
});
|
|
2561
|
+
this.emitCoordinatorStats();
|
|
2562
|
+
}
|
|
2563
|
+
async stopAll() {
|
|
2564
|
+
this.drainPendingAsAborted("Coordinator stopAll() drained the pending queue");
|
|
2565
|
+
await Promise.allSettled([...this.subagents.keys()].map((id) => this.stop(id)));
|
|
2566
|
+
}
|
|
2567
|
+
/**
|
|
2568
|
+
* Get current coordinator stats for monitoring/debugging.
|
|
2569
|
+
*/
|
|
2570
|
+
getStats() {
|
|
2571
|
+
let running = 0;
|
|
2572
|
+
let idle = 0;
|
|
2573
|
+
let stopped = 0;
|
|
2574
|
+
for (const [, entry] of this.subagents) {
|
|
2575
|
+
if (entry.status === "running") running++;
|
|
2576
|
+
else if (entry.status === "idle") idle++;
|
|
2577
|
+
else stopped++;
|
|
2578
|
+
}
|
|
2579
|
+
return {
|
|
2580
|
+
total: this.subagents.size,
|
|
2581
|
+
running,
|
|
2582
|
+
idle,
|
|
2583
|
+
stopped,
|
|
2584
|
+
inFlight: this.inFlight,
|
|
2585
|
+
pending: this.pendingTasks.length,
|
|
2586
|
+
completed: this.completedResults.length
|
|
2587
|
+
};
|
|
2588
|
+
}
|
|
2589
|
+
/** Emit a reactive coordinator.stats event on FleetBus so the TUI can subscribe. */
|
|
2590
|
+
emitCoordinatorStats() {
|
|
2591
|
+
const stats = this.getStats();
|
|
2592
|
+
const subagentStatuses = Array.from(this.subagents.entries()).map(([id, s]) => ({
|
|
2593
|
+
subagentId: id,
|
|
2594
|
+
taskId: s.currentTask ?? "",
|
|
2595
|
+
status: s.status,
|
|
2596
|
+
assigned: s.context.parentBridge !== null
|
|
2597
|
+
}));
|
|
2598
|
+
const sessionId = this.currentSessionId();
|
|
2599
|
+
this.fleetBus?.emit({
|
|
2600
|
+
subagentId: this.coordinatorId,
|
|
2601
|
+
ts: Date.now(),
|
|
2602
|
+
type: "coordinator.stats",
|
|
2603
|
+
payload: {
|
|
2604
|
+
...sessionId ? { sessionId } : {},
|
|
2605
|
+
...stats,
|
|
2606
|
+
subagentStatuses
|
|
2607
|
+
}
|
|
2608
|
+
});
|
|
2609
|
+
}
|
|
2610
|
+
getStatus() {
|
|
2611
|
+
return {
|
|
2612
|
+
coordinatorId: this.coordinatorId,
|
|
2613
|
+
subagents: Array.from(this.subagents.entries()).map(([id, s]) => ({
|
|
2614
|
+
id,
|
|
2615
|
+
name: s.config.name,
|
|
2616
|
+
status: s.status,
|
|
2617
|
+
currentTask: s.currentTask
|
|
2618
|
+
})),
|
|
2619
|
+
pendingTasks: this.pendingTasks.length,
|
|
2620
|
+
completedTasks: this.completedResults.length,
|
|
2621
|
+
totalIterations: this.totalIterations,
|
|
2622
|
+
done: this.isDone()
|
|
2623
|
+
};
|
|
2624
|
+
}
|
|
2625
|
+
/** Expose snapshot of completed results — useful for callers awaiting all done. */
|
|
2626
|
+
results() {
|
|
2627
|
+
return this.completedResults;
|
|
2628
|
+
}
|
|
2629
|
+
/** Defensive snapshot of the still-queued (not yet dispatched) tasks. */
|
|
2630
|
+
listPendingTasks() {
|
|
2631
|
+
return this.pendingTasks.map((t) => ({ ...t }));
|
|
2632
|
+
}
|
|
2633
|
+
/**
|
|
2634
|
+
* Re-pin a still-PENDING task to a different subagent (`undefined` =
|
|
2635
|
+
* unpin, any idle worker may take it), then try to dispatch. Returns
|
|
2636
|
+
* `false` when the task is not in the pending queue — already
|
|
2637
|
+
* dispatched, completed, or unknown. Running tasks can never be pulled;
|
|
2638
|
+
* they can only be steered or terminated. The mutation is synchronous
|
|
2639
|
+
* (same tick as the check), so it cannot race a concurrent dispatch:
|
|
2640
|
+
* `tryDispatchNext` runs on this same call stack or a later one.
|
|
2641
|
+
*
|
|
2642
|
+
* The task keeps its id, so waiters, the report-back notifier, and
|
|
2643
|
+
* checkpoint bookkeeping resolve unchanged when it eventually completes.
|
|
2644
|
+
*/
|
|
2645
|
+
retargetPendingTask(taskId, subagentId) {
|
|
2646
|
+
const task = this.pendingTasks.find((t) => t.id === taskId);
|
|
2647
|
+
if (!task) return false;
|
|
2648
|
+
if (subagentId !== void 0 && !this.subagents.has(subagentId)) return false;
|
|
2649
|
+
task.subagentId = subagentId;
|
|
2650
|
+
this.tryDispatchNext();
|
|
2651
|
+
return true;
|
|
2652
|
+
}
|
|
2653
|
+
/**
|
|
2654
|
+
* Wait for one or more tasks to complete and return their results.
|
|
2655
|
+
* If a task is already done when called, returns immediately.
|
|
2656
|
+
* Resolves to an array in the same order as `taskIds`.
|
|
2657
|
+
*/
|
|
2658
|
+
async awaitTasks(taskIds, opts) {
|
|
2659
|
+
const timeoutMs = opts?.timeoutMs ?? this.config.timeoutMs ?? 3e5;
|
|
2660
|
+
return Promise.all(
|
|
2661
|
+
taskIds.map((id) => {
|
|
2662
|
+
const cached = this.completedResults.find((r) => r.taskId === id);
|
|
2663
|
+
if (cached) return cached;
|
|
2664
|
+
return new Promise((resolve, reject) => {
|
|
2665
|
+
const timeout = setTimeout(() => {
|
|
2666
|
+
this.off("task.completed", handler);
|
|
2667
|
+
reject(new Error(`awaitTasks timed out waiting for task "${id}"`));
|
|
2668
|
+
}, timeoutMs);
|
|
2669
|
+
const handler = ({ result }) => {
|
|
2670
|
+
if (result.taskId === id) {
|
|
2671
|
+
clearTimeout(timeout);
|
|
2672
|
+
this.off("task.completed", handler);
|
|
2673
|
+
resolve(result);
|
|
2674
|
+
}
|
|
2675
|
+
};
|
|
2676
|
+
this.on("task.completed", handler);
|
|
2677
|
+
});
|
|
2678
|
+
})
|
|
2679
|
+
);
|
|
2680
|
+
}
|
|
2681
|
+
/**
|
|
2682
|
+
* Wait until AT LEAST ONE of the named tasks completes. Returns every
|
|
2683
|
+
* requested result already available at that moment (drain-what's-done)
|
|
2684
|
+
* plus the ids still outstanding, so callers can loop "handle finishers,
|
|
2685
|
+
* re-await the remainder" instead of blocking on the whole batch.
|
|
2686
|
+
*
|
|
2687
|
+
* Unlike `awaitTasks`, this never rejects and deliberately does NOT
|
|
2688
|
+
* inherit `config.timeoutMs` — a "return whatever is done" call has no
|
|
2689
|
+
* business timing out unless the caller asks for a window explicitly.
|
|
2690
|
+
*/
|
|
2691
|
+
async awaitTasksAny(taskIds, opts) {
|
|
2692
|
+
const ids = new Set(taskIds);
|
|
2693
|
+
const completed = this.completedResults.filter((r) => ids.has(r.taskId));
|
|
2694
|
+
if (completed.length > 0 || ids.size === 0) {
|
|
2695
|
+
const done = new Set(completed.map((r) => r.taskId));
|
|
2696
|
+
return { completed, pending: taskIds.filter((id) => !done.has(id)) };
|
|
2697
|
+
}
|
|
2698
|
+
return new Promise((resolve) => {
|
|
2699
|
+
let timer;
|
|
2700
|
+
const handler = ({ result }) => {
|
|
2701
|
+
if (!ids.has(result.taskId)) return;
|
|
2702
|
+
if (timer) clearTimeout(timer);
|
|
2703
|
+
this.off("task.completed", handler);
|
|
2704
|
+
resolve({
|
|
2705
|
+
completed: [result],
|
|
2706
|
+
pending: taskIds.filter((id) => id !== result.taskId)
|
|
2707
|
+
});
|
|
2708
|
+
};
|
|
2709
|
+
if (opts?.timeoutMs !== void 0) {
|
|
2710
|
+
timer = setTimeout(() => {
|
|
2711
|
+
this.off("task.completed", handler);
|
|
2712
|
+
resolve({ completed: [], pending: [...taskIds], timedOut: true });
|
|
2713
|
+
}, opts.timeoutMs);
|
|
2714
|
+
}
|
|
2715
|
+
this.on("task.completed", handler);
|
|
2716
|
+
});
|
|
2717
|
+
}
|
|
2718
|
+
/**
|
|
2719
|
+
* Manual completion — for callers that drive subagents without a runner
|
|
2720
|
+
* (e.g. external orchestrators). When a runner is configured the coordinator
|
|
2721
|
+
* calls this itself.
|
|
2722
|
+
*/
|
|
2723
|
+
completeTask(result) {
|
|
2724
|
+
this.recordCompletion(result);
|
|
2725
|
+
}
|
|
2726
|
+
/**
|
|
2727
|
+
* Ask every RUNNING subagent that opted into `gracefulFinish` to finish its
|
|
2728
|
+
* task in its own turn (see coordination/subagent-finish.ts). This is the
|
|
2729
|
+
* leader-side entry point for "the leader agent has finished": it delivers
|
|
2730
|
+
* an in-band notification between tool batches — never an interrupt, never
|
|
2731
|
+
* an abort. Each notified subagent keeps its existing time budget and
|
|
2732
|
+
* accelerates; the watchdog still bounds the maximum lifetime.
|
|
2733
|
+
*
|
|
2734
|
+
* Subagents without the policy opted in are deliberately untouched — their
|
|
2735
|
+
* lifecycle remains the legacy watchdog contract.
|
|
2736
|
+
*
|
|
2737
|
+
* Returns the number of subagents actually notified.
|
|
2738
|
+
*/
|
|
2739
|
+
requestFinish(reason) {
|
|
2740
|
+
let notified = 0;
|
|
2741
|
+
for (const subagent of this.subagents.values()) {
|
|
2742
|
+
if (subagent.status !== "running") continue;
|
|
2743
|
+
if (!resolveGracefulFinish(subagent.config)) continue;
|
|
2744
|
+
const budget = subagent.activeBudget;
|
|
2745
|
+
if (!budget) continue;
|
|
2746
|
+
const usage = budget.usage();
|
|
2747
|
+
if (usage.iterations === 0 && usage.toolCalls === 0) continue;
|
|
2748
|
+
if (budget.notifyFinish(reason)) notified++;
|
|
2749
|
+
}
|
|
2750
|
+
return notified;
|
|
2751
|
+
}
|
|
2752
|
+
// --- internal dispatching ---------------------------------------------
|
|
2753
|
+
tryDispatchNext() {
|
|
2754
|
+
while (this.canDispatch()) {
|
|
2755
|
+
const dispatchable = this.takeNextDispatchableTask();
|
|
2756
|
+
if (!dispatchable) {
|
|
2757
|
+
if (this.pendingTasks.length > 0 && !this.hasLiveSubagent()) {
|
|
2758
|
+
this.drainPendingAsAborted(
|
|
2759
|
+
"No live subagent available \u2014 all stopped or mid-termination"
|
|
2760
|
+
);
|
|
2761
|
+
}
|
|
2762
|
+
return;
|
|
2763
|
+
}
|
|
2764
|
+
const { subagentId, task } = dispatchable;
|
|
2765
|
+
this.runDispatched(subagentId, task).catch((err) => {
|
|
2766
|
+
this.recordCompletion({
|
|
2767
|
+
subagentId,
|
|
2768
|
+
taskId: task.id,
|
|
2769
|
+
status: "failed",
|
|
2770
|
+
error: classifySubagentError(err),
|
|
2771
|
+
iterations: 0,
|
|
2772
|
+
toolCalls: 0,
|
|
2773
|
+
durationMs: 0
|
|
2774
|
+
});
|
|
2775
|
+
});
|
|
2776
|
+
}
|
|
2777
|
+
}
|
|
2778
|
+
canDispatch() {
|
|
2779
|
+
const max = this.config.maxConcurrent ?? 16;
|
|
2780
|
+
return this.inFlight < max && this.pendingTasks.length > 0;
|
|
2781
|
+
}
|
|
2782
|
+
takeNextDispatchableTask() {
|
|
2783
|
+
for (let i = 0; i < this.pendingTasks.length; i++) {
|
|
2784
|
+
const task = this.pendingTasks[i];
|
|
2785
|
+
if (!task) continue;
|
|
2786
|
+
const subagentId = task.subagentId ? this.isIdleSubagent(task.subagentId) ? task.subagentId : null : this.findIdleSubagent();
|
|
2787
|
+
if (!subagentId) continue;
|
|
2788
|
+
this.pendingTasks.splice(i, 1);
|
|
2789
|
+
return { subagentId, task };
|
|
2790
|
+
}
|
|
2791
|
+
return null;
|
|
2792
|
+
}
|
|
2793
|
+
findIdleSubagent() {
|
|
2794
|
+
for (const [id, s] of this.subagents) {
|
|
2795
|
+
if (s.status === "idle" && !this.terminating.has(id)) return id;
|
|
2796
|
+
}
|
|
2797
|
+
return null;
|
|
2798
|
+
}
|
|
2799
|
+
isIdleSubagent(id) {
|
|
2800
|
+
const subagent = this.subagents.get(id);
|
|
2801
|
+
return !!subagent && subagent.status === "idle" && !this.terminating.has(id);
|
|
2802
|
+
}
|
|
2803
|
+
/**
|
|
2804
|
+
* Returns true iff at least one spawned subagent could still
|
|
2805
|
+
* process a task. A "live" subagent is one that is not stopped
|
|
2806
|
+
* AND not mid-termination — `running` workers count because they
|
|
2807
|
+
* will eventually finish and become idle.
|
|
2808
|
+
*
|
|
2809
|
+
* When no subagent has ever been spawned, returns `true` so a
|
|
2810
|
+
* pre-spawn `assign()` simply queues (legacy behaviour). The
|
|
2811
|
+
* dead-end detection only fires after `stop()` has retired every
|
|
2812
|
+
* spawned worker.
|
|
2813
|
+
*
|
|
2814
|
+
* Used by `tryDispatchNext` to detect a dead-end pending queue.
|
|
2815
|
+
*/
|
|
2816
|
+
hasLiveSubagent() {
|
|
2817
|
+
if (this.subagents.size === 0) return true;
|
|
2818
|
+
for (const [id, s] of this.subagents) {
|
|
2819
|
+
if (s.status !== "stopped" && !this.terminating.has(id)) return true;
|
|
2820
|
+
}
|
|
2821
|
+
return false;
|
|
2822
|
+
}
|
|
2823
|
+
/**
|
|
2824
|
+
* Drain every pending task with a synthetic `aborted_by_parent`
|
|
2825
|
+
* completion event. Same shape as the `stopAll()` drain — we go
|
|
2826
|
+
* around `recordCompletion` because pending tasks were never
|
|
2827
|
+
* counted in `inFlight` and routing them through would trip the
|
|
2828
|
+
* underflow guard on every task after the first.
|
|
2829
|
+
*/
|
|
2830
|
+
drainPendingAsAborted(message) {
|
|
2831
|
+
const dropped = this.pendingTasks.splice(0, this.pendingTasks.length);
|
|
2832
|
+
for (const t of dropped) this.emitPendingAborted(t, message);
|
|
2833
|
+
}
|
|
2834
|
+
/**
|
|
2835
|
+
* Emit a synthetic `stopped`/`aborted_by_parent` completion for a single
|
|
2836
|
+
* PENDING task — one that was never counted in `inFlight`. This MUST bypass
|
|
2837
|
+
* `recordCompletion`: that path does `inFlight--`, which for a pending task
|
|
2838
|
+
* steals a decrement from a genuinely in-flight task and trips the underflow
|
|
2839
|
+
* guard — suppressing that real task's `task.completed` and hanging its
|
|
2840
|
+
* `awaitTasks()` caller. Pushes the result and fires the event directly.
|
|
2841
|
+
*/
|
|
2842
|
+
emitPendingAborted(task, message) {
|
|
2843
|
+
const synthetic = {
|
|
2844
|
+
subagentId: task.subagentId ?? "unassigned",
|
|
2845
|
+
taskId: task.id,
|
|
2846
|
+
status: "stopped",
|
|
2847
|
+
error: {
|
|
2848
|
+
kind: "aborted_by_parent",
|
|
2849
|
+
message,
|
|
2850
|
+
retryable: false
|
|
2851
|
+
},
|
|
2852
|
+
iterations: 0,
|
|
2853
|
+
toolCalls: 0,
|
|
2854
|
+
durationMs: 0
|
|
2855
|
+
};
|
|
2856
|
+
this.completedResults.push(synthetic);
|
|
2857
|
+
this.trimCompletedResults();
|
|
2858
|
+
this.emit("task.completed", { task, result: synthetic });
|
|
2859
|
+
}
|
|
2860
|
+
trimCompletedResults() {
|
|
2861
|
+
if (this.completedResults.length > _DefaultMultiAgentCoordinator.MAX_COMPLETED_RESULTS) {
|
|
2862
|
+
this.completedResults.splice(
|
|
2863
|
+
0,
|
|
2864
|
+
this.completedResults.length - _DefaultMultiAgentCoordinator.MAX_COMPLETED_RESULTS
|
|
2865
|
+
);
|
|
2866
|
+
}
|
|
2867
|
+
}
|
|
2868
|
+
async runDispatched(subagentId, task) {
|
|
2869
|
+
const subagent = this.subagents.get(subagentId);
|
|
2870
|
+
if (!subagent) return;
|
|
2871
|
+
if (this.terminating.has(subagentId) || subagent.status === "stopped") {
|
|
2872
|
+
this.recordCompletion({
|
|
2873
|
+
subagentId,
|
|
2874
|
+
taskId: task.id,
|
|
2875
|
+
status: "stopped",
|
|
2876
|
+
error: {
|
|
2877
|
+
kind: "aborted_by_parent",
|
|
2878
|
+
message: "Subagent was terminated before task could start",
|
|
2879
|
+
retryable: false
|
|
2880
|
+
},
|
|
2881
|
+
iterations: 0,
|
|
2882
|
+
toolCalls: 0,
|
|
2883
|
+
durationMs: 0
|
|
2884
|
+
});
|
|
2885
|
+
return;
|
|
2886
|
+
}
|
|
2887
|
+
subagent.status = "running";
|
|
2888
|
+
subagent.currentTask = task.id;
|
|
2889
|
+
task.subagentId = subagentId;
|
|
2890
|
+
subagent.context.tasks.push(task);
|
|
2891
|
+
if (subagent.context.tasks.length > _DefaultMultiAgentCoordinator.MAX_SUBAGENT_TASK_HISTORY) {
|
|
2892
|
+
subagent.context.tasks.splice(
|
|
2893
|
+
0,
|
|
2894
|
+
subagent.context.tasks.length - _DefaultMultiAgentCoordinator.MAX_SUBAGENT_TASK_HISTORY
|
|
2895
|
+
);
|
|
2896
|
+
}
|
|
2897
|
+
this.fleetBus?.emit({
|
|
2898
|
+
subagentId,
|
|
2899
|
+
taskId: task.id,
|
|
2900
|
+
ts: Date.now(),
|
|
2901
|
+
type: "subagent.running",
|
|
2902
|
+
payload: { subagentId, taskId: task.id }
|
|
2903
|
+
});
|
|
2904
|
+
this.emit("task.assigned", { task, subagentId });
|
|
2905
|
+
this.emitCoordinatorStats();
|
|
2906
|
+
const rawMaxIterations = subagent.config.maxIterations;
|
|
2907
|
+
const rawMaxToolCalls = subagent.config.maxToolCalls;
|
|
2908
|
+
const rawMaxTokens = subagent.config.maxTokens;
|
|
2909
|
+
const rawMaxCostUsd = subagent.config.maxCostUsd;
|
|
2910
|
+
const rawTimeoutMs = subagent.config.timeoutMs;
|
|
2911
|
+
const rawIdleTimeoutMs = subagent.config.idleTimeoutMs;
|
|
2912
|
+
const configWithRosterDefaults = applyRosterBudget(subagent.config);
|
|
2913
|
+
const budget = new SubagentBudget(
|
|
2914
|
+
{
|
|
2915
|
+
maxIterations: rawMaxIterations ?? this.config.defaultBudget?.maxIterations ?? configWithRosterDefaults.maxIterations,
|
|
2916
|
+
maxToolCalls: rawMaxToolCalls ?? this.config.defaultBudget?.maxToolCalls ?? configWithRosterDefaults.maxToolCalls,
|
|
2917
|
+
maxTokens: rawMaxTokens ?? this.config.defaultBudget?.maxTokens ?? configWithRosterDefaults.maxTokens,
|
|
2918
|
+
maxCostUsd: rawMaxCostUsd ?? this.config.defaultBudget?.maxCostUsd ?? configWithRosterDefaults.maxCostUsd,
|
|
2919
|
+
// Wall-clock cap is opt-in (explicit config / defaultBudget only); the
|
|
2920
|
+
// roster no longer supplies one. Idle is the default reaper.
|
|
2921
|
+
timeoutMs: rawTimeoutMs ?? this.config.defaultBudget?.timeoutMs ?? configWithRosterDefaults.timeoutMs,
|
|
2922
|
+
idleTimeoutMs: rawIdleTimeoutMs ?? this.config.defaultBudget?.idleTimeoutMs ?? configWithRosterDefaults.idleTimeoutMs
|
|
2923
|
+
},
|
|
2924
|
+
"auto",
|
|
2925
|
+
{
|
|
2926
|
+
sessionId: () => this.currentSessionId(),
|
|
2927
|
+
subagentId,
|
|
2928
|
+
// Graceful-finish runs own wall-clock enforcement to the watchdog so
|
|
2929
|
+
// the notify-then-bound lifecycle cannot be raced by tool.progress
|
|
2930
|
+
// heartbeats calling checkTimeout() (see subagent-budget.ts).
|
|
2931
|
+
...resolveGracefulFinish(subagent.config) ? { wallClockWatchdogOwned: true } : {}
|
|
2932
|
+
}
|
|
2933
|
+
);
|
|
2934
|
+
subagent.activeBudget = budget;
|
|
2935
|
+
if (!this.runner) {
|
|
2936
|
+
return;
|
|
2937
|
+
}
|
|
2938
|
+
this.inFlight++;
|
|
2939
|
+
const startTime = Date.now();
|
|
2940
|
+
let latestPartial;
|
|
2941
|
+
const runCtx = {
|
|
2942
|
+
subagentId,
|
|
2943
|
+
config: subagent.config,
|
|
2944
|
+
budget,
|
|
2945
|
+
signal: subagent.abortController.signal,
|
|
2946
|
+
bridge: subagent.context.parentBridge || null,
|
|
2947
|
+
reportProgress: (partial) => {
|
|
2948
|
+
const text = partial.text.trim();
|
|
2949
|
+
if (!text) return;
|
|
2950
|
+
latestPartial = {
|
|
2951
|
+
...partial,
|
|
2952
|
+
// A partial is context recovery, not a transcript replacement.
|
|
2953
|
+
text: text.slice(-4e3)
|
|
2954
|
+
};
|
|
2955
|
+
}
|
|
2956
|
+
};
|
|
2957
|
+
let result;
|
|
2958
|
+
budget.start();
|
|
2959
|
+
try {
|
|
2960
|
+
const outcome = await this.executeWithTimeout(
|
|
2961
|
+
this.runner,
|
|
2962
|
+
task,
|
|
2963
|
+
runCtx,
|
|
2964
|
+
budget,
|
|
2965
|
+
subagent.config.preemptFraction,
|
|
2966
|
+
resolveGracefulFinish(subagent.config)
|
|
2967
|
+
);
|
|
2968
|
+
result = {
|
|
2969
|
+
subagentId,
|
|
2970
|
+
taskId: task.id,
|
|
2971
|
+
status: "success",
|
|
2972
|
+
result: outcome.result,
|
|
2973
|
+
...outcome.report ? { report: outcome.report } : {},
|
|
2974
|
+
iterations: outcome.iterations,
|
|
2975
|
+
toolCalls: outcome.toolCalls,
|
|
2976
|
+
durationMs: Date.now() - startTime
|
|
2977
|
+
};
|
|
2978
|
+
} catch (err) {
|
|
2979
|
+
const status = err instanceof BudgetExceededError && (err.kind === "timeout" || err.kind === "idle_timeout") ? "timeout" : subagent.abortController.signal.aborted ? "stopped" : "failed";
|
|
2980
|
+
const usage = budget.usage();
|
|
2981
|
+
result = {
|
|
2982
|
+
subagentId,
|
|
2983
|
+
taskId: task.id,
|
|
2984
|
+
status,
|
|
2985
|
+
error: classifySubagentError(err, {
|
|
2986
|
+
parentAborted: subagent.abortController.signal.aborted
|
|
2987
|
+
}),
|
|
2988
|
+
...latestPartial ? { partial: latestPartial } : {},
|
|
2989
|
+
iterations: usage.iterations,
|
|
2990
|
+
toolCalls: usage.toolCalls,
|
|
2991
|
+
durationMs: Date.now() - startTime
|
|
2992
|
+
};
|
|
2993
|
+
}
|
|
2994
|
+
this.recordCompletion(result);
|
|
2995
|
+
}
|
|
2996
|
+
async executeWithTimeout(runner, task, ctx, budget, preemptFraction, gracefulFinish) {
|
|
2997
|
+
return executeSubagentWithTimeout({
|
|
2998
|
+
runner,
|
|
2999
|
+
task,
|
|
3000
|
+
ctx,
|
|
3001
|
+
budget,
|
|
3002
|
+
preemptFraction,
|
|
3003
|
+
gracefulFinish,
|
|
3004
|
+
abortSubagent: (subagentId) => this.subagents.get(subagentId)?.abortController.abort(),
|
|
3005
|
+
currentSessionId: () => this.currentSessionId()
|
|
3006
|
+
});
|
|
3007
|
+
}
|
|
3008
|
+
recordCompletion(result) {
|
|
3009
|
+
this.completedResults.push(result);
|
|
3010
|
+
this.trimCompletedResults();
|
|
3011
|
+
this.totalIterations += result.iterations;
|
|
3012
|
+
if (this.inFlight > 0) {
|
|
3013
|
+
this.inFlight--;
|
|
3014
|
+
} else if (this.runner) {
|
|
3015
|
+
this.emit("warning", {
|
|
3016
|
+
type: "inFlight_underflow",
|
|
3017
|
+
taskId: result.taskId,
|
|
3018
|
+
subagentId: result.subagentId
|
|
3019
|
+
});
|
|
3020
|
+
return;
|
|
3021
|
+
}
|
|
3022
|
+
const subagent = this.subagents.get(result.subagentId);
|
|
3023
|
+
if (subagent && subagent.status !== "stopped") {
|
|
3024
|
+
const failed = result.status === "failed" || result.status === "timeout";
|
|
3025
|
+
subagent.status = "idle";
|
|
3026
|
+
void failed;
|
|
3027
|
+
subagent.currentTask = void 0;
|
|
3028
|
+
if (subagent.abortController.signal.aborted) {
|
|
3029
|
+
subagent.abortController = new AbortController();
|
|
3030
|
+
}
|
|
3031
|
+
this.fleetBus?.emit({
|
|
3032
|
+
subagentId: result.subagentId,
|
|
3033
|
+
ts: Date.now(),
|
|
3034
|
+
type: "subagent.idle",
|
|
3035
|
+
payload: { subagentId: result.subagentId }
|
|
3036
|
+
});
|
|
3037
|
+
}
|
|
3038
|
+
this.terminating.delete(result.subagentId);
|
|
3039
|
+
this.emit("task.completed", {
|
|
3040
|
+
task: subagent?.context.tasks.find((t) => t.id === result.taskId) ?? { id: result.taskId },
|
|
3041
|
+
result
|
|
3042
|
+
});
|
|
3043
|
+
this.fleetBus?.emit({
|
|
3044
|
+
subagentId: result.subagentId,
|
|
3045
|
+
taskId: result.taskId,
|
|
3046
|
+
ts: Date.now(),
|
|
3047
|
+
type: "subagent.completed",
|
|
3048
|
+
payload: {
|
|
3049
|
+
subagentId: result.subagentId,
|
|
3050
|
+
taskId: result.taskId,
|
|
3051
|
+
status: result.status,
|
|
3052
|
+
result: result.result,
|
|
3053
|
+
report: result.report,
|
|
3054
|
+
partial: result.partial,
|
|
3055
|
+
iterations: result.iterations,
|
|
3056
|
+
toolCalls: result.toolCalls,
|
|
3057
|
+
durationMs: result.durationMs
|
|
3058
|
+
}
|
|
3059
|
+
});
|
|
3060
|
+
this.tryDispatchNext();
|
|
3061
|
+
this.emitCoordinatorStats();
|
|
3062
|
+
if (this.isDone()) {
|
|
3063
|
+
this.emit("done", {
|
|
3064
|
+
results: this.completedResults,
|
|
3065
|
+
totalIterations: this.totalIterations
|
|
3066
|
+
});
|
|
3067
|
+
}
|
|
3068
|
+
}
|
|
3069
|
+
/**
|
|
3070
|
+
* Stop a subagent and remove it from the coordinator. Releases all
|
|
3071
|
+
* associated resources (AbortController, context, budget state).
|
|
3072
|
+
* The subagent entry is deleted so the id can be reused in a future spawn.
|
|
3073
|
+
*/
|
|
3074
|
+
async remove(subagentId) {
|
|
3075
|
+
const subagent = this.subagents.get(subagentId);
|
|
3076
|
+
if (!subagent) return;
|
|
3077
|
+
if (subagent.status === "running" || subagent.status === "idle") {
|
|
3078
|
+
this.terminating.add(subagentId);
|
|
3079
|
+
subagent.abortController.abort();
|
|
3080
|
+
subagent.status = "stopped";
|
|
3081
|
+
}
|
|
3082
|
+
this.subagents.delete(subagentId);
|
|
3083
|
+
this.terminating.delete(subagentId);
|
|
3084
|
+
const nicknameKey = this.subagentNicknames.get(subagentId);
|
|
3085
|
+
if (nicknameKey) {
|
|
3086
|
+
this.usedNicknames.delete(nicknameKey);
|
|
3087
|
+
this.subagentNicknames.delete(subagentId);
|
|
3088
|
+
}
|
|
3089
|
+
const orphaned = this.pendingTasks.filter((t) => t.subagentId === subagentId);
|
|
3090
|
+
this.pendingTasks = this.pendingTasks.filter((t) => t.subagentId !== subagentId);
|
|
3091
|
+
for (const t of orphaned) {
|
|
3092
|
+
this.emitPendingAborted(
|
|
3093
|
+
t,
|
|
3094
|
+
`Subagent "${subagentId}" was removed while task "${t.id}" was pending`
|
|
3095
|
+
);
|
|
3096
|
+
}
|
|
3097
|
+
this.fleetBus?.emit({
|
|
3098
|
+
subagentId,
|
|
3099
|
+
ts: Date.now(),
|
|
3100
|
+
type: "subagent.removed",
|
|
3101
|
+
payload: { subagentId }
|
|
3102
|
+
});
|
|
3103
|
+
this.emitCoordinatorStats();
|
|
3104
|
+
}
|
|
3105
|
+
isDone() {
|
|
3106
|
+
if (this.config.doneCondition.type === "all_tasks_done") {
|
|
3107
|
+
return this.pendingTasks.length === 0 && this.inFlight === 0;
|
|
3108
|
+
}
|
|
3109
|
+
if (this.config.doneCondition.maxIterations !== void 0 && this.totalIterations >= this.config.doneCondition.maxIterations) {
|
|
3110
|
+
return true;
|
|
3111
|
+
}
|
|
3112
|
+
return false;
|
|
3113
|
+
}
|
|
3114
|
+
};
|
|
3115
|
+
|
|
3116
|
+
export {
|
|
3117
|
+
TIMEOUT_PREEMPT_FRACTION,
|
|
3118
|
+
DECISION_TIMEOUT_MS,
|
|
3119
|
+
BudgetExceededError,
|
|
3120
|
+
BudgetThresholdSignal,
|
|
3121
|
+
SubagentBudget,
|
|
3122
|
+
SUBAGENT_STRUCTURED_REPORT_META_KEY,
|
|
3123
|
+
MAX_SUBAGENT_STRUCTURED_REPORT_CHARS,
|
|
3124
|
+
normalizeSubagentStructuredReport,
|
|
3125
|
+
readSubagentStructuredReport,
|
|
3126
|
+
formatSubagentStructuredReport,
|
|
3127
|
+
makeSubagentResultTool,
|
|
3128
|
+
withDisabledToolFiltering,
|
|
3129
|
+
makeAgentSubagentRunner,
|
|
3130
|
+
BLOCKED_RESOLVED_MARKERS,
|
|
3131
|
+
COMPETING_ALTERNATIVE,
|
|
3132
|
+
isBlockedResolved,
|
|
3133
|
+
DEFAULT_BRAIN_HEURISTICS,
|
|
3134
|
+
compileResolutionMarkers,
|
|
3135
|
+
resolveBrainHeuristics,
|
|
3136
|
+
DETERMINISTIC_BRAIN_TIERS,
|
|
3137
|
+
markDecisionTier,
|
|
3138
|
+
readDecisionTier,
|
|
3139
|
+
isDeterministicTier,
|
|
3140
|
+
BrainTierCounter,
|
|
3141
|
+
BRAIN_RISK_LEVELS,
|
|
3142
|
+
ObservableBrainArbiter,
|
|
3143
|
+
BrainDecisionQueue,
|
|
3144
|
+
terminalPolicyDecision,
|
|
3145
|
+
EscalationRoutingBrainArbiter,
|
|
3146
|
+
HumanEscalatingBrainArbiter,
|
|
3147
|
+
DefaultBrainArbiter,
|
|
3148
|
+
formatHumanPrompt,
|
|
3149
|
+
brainDecisionKey,
|
|
3150
|
+
createLedgerGuardBrainArbiter,
|
|
3151
|
+
BrainDecisionLedger,
|
|
3152
|
+
DEFAULT_DISPATCH_ROLE,
|
|
3153
|
+
scoreAgents,
|
|
3154
|
+
dispatchAgent,
|
|
3155
|
+
makeLLMClassifier,
|
|
3156
|
+
assignNickname,
|
|
3157
|
+
nicknameKeyFromDisplay,
|
|
3158
|
+
AUDIT_LOG_AGENT,
|
|
3159
|
+
BUG_HUNTER_AGENT,
|
|
3160
|
+
REFACTOR_PLANNER_AGENT,
|
|
3161
|
+
SECURITY_SCANNER_AGENT,
|
|
3162
|
+
GENERIC_AGENT,
|
|
3163
|
+
FLEET_ROSTER,
|
|
3164
|
+
FLEET_ROSTER_BUDGETS,
|
|
3165
|
+
applyRosterBudget,
|
|
3166
|
+
ALL_FLEET_AGENTS,
|
|
3167
|
+
ACP_AGENTS,
|
|
3168
|
+
FLEET_ROSTER_WITHACP,
|
|
3169
|
+
DefaultMultiAgentCoordinator
|
|
3170
|
+
};
|
|
3171
|
+
//# sourceMappingURL=chunk-T3KGZBU5.js.map
|