@aragon-agent/cli 0.5.2 → 0.5.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +127 -0
- package/README.md +193 -3
- package/dist/agent/controller.d.ts +51 -1
- package/dist/agent/controller.js +177 -2
- package/dist/agent/headless.d.ts +12 -0
- package/dist/agent/headless.js +41 -1
- package/dist/agent/reducer.d.ts +48 -0
- package/dist/agent/reducer.js +91 -2
- package/dist/agent/system-prompt.d.ts +17 -0
- package/dist/agent/system-prompt.js +4 -0
- package/dist/cli.js +98 -7
- package/dist/commands/builtins.d.ts +2 -2
- package/dist/commands/builtins.js +133 -3
- package/dist/config/app-paths.d.ts +17 -0
- package/dist/config/app-paths.js +21 -0
- package/dist/config/cli-commands.d.ts +26 -2
- package/dist/config/cli-commands.js +83 -6
- package/dist/config/env.d.ts +3 -2
- package/dist/config/env.js +44 -6
- package/dist/config/history-commands.d.ts +20 -0
- package/dist/config/history-commands.js +71 -0
- package/dist/config/load.d.ts +13 -0
- package/dist/config/load.js +107 -6
- package/dist/config/migrate-state-out-of-config.d.ts +32 -0
- package/dist/config/migrate-state-out-of-config.js +85 -0
- package/dist/config/prompt-history.d.ts +86 -0
- package/dist/config/prompt-history.js +332 -0
- package/dist/config/schema.d.ts +176 -18
- package/dist/config/schema.js +162 -8
- package/dist/config/store.js +40 -4
- package/dist/config/ui-state.d.ts +52 -0
- package/dist/config/ui-state.js +149 -0
- package/dist/logging/install.d.ts +85 -0
- package/dist/logging/install.js +94 -0
- package/dist/logging/logger.d.ts +1 -1
- package/dist/session/persist.d.ts +16 -0
- package/dist/session/persist.js +19 -1
- package/dist/team/activity.d.ts +67 -0
- package/dist/team/activity.js +130 -0
- package/dist/team/bus.d.ts +190 -0
- package/dist/team/bus.js +366 -0
- package/dist/team/comm-tools.d.ts +61 -0
- package/dist/team/comm-tools.js +221 -0
- package/dist/team/human-queue.d.ts +55 -0
- package/dist/team/human-queue.js +72 -0
- package/dist/team/limits.d.ts +123 -0
- package/dist/team/limits.js +128 -0
- package/dist/team/normalize.d.ts +39 -0
- package/dist/team/normalize.js +106 -0
- package/dist/team/panel-rows.d.ts +48 -0
- package/dist/team/panel-rows.js +75 -0
- package/dist/team/prompt.d.ts +49 -0
- package/dist/team/prompt.js +87 -0
- package/dist/team/report.d.ts +58 -0
- package/dist/team/report.js +205 -0
- package/dist/team/retry.d.ts +48 -0
- package/dist/team/retry.js +56 -0
- package/dist/team/runtime.d.ts +122 -0
- package/dist/team/runtime.js +406 -0
- package/dist/team/subagent.d.ts +90 -0
- package/dist/team/subagent.js +294 -0
- package/dist/team/task-tool.d.ts +47 -0
- package/dist/team/task-tool.js +118 -0
- package/dist/team/types.d.ts +172 -0
- package/dist/team/types.js +13 -0
- package/dist/tools/index.d.ts +48 -2
- package/dist/tools/index.js +26 -1
- package/dist/ui/App.d.ts +22 -1
- package/dist/ui/App.js +144 -18
- package/dist/ui/StatusBar.d.ts +21 -0
- package/dist/ui/StatusBar.js +5 -2
- package/dist/ui/TeamPanel.d.ts +57 -0
- package/dist/ui/TeamPanel.js +134 -0
- package/dist/ui/Transcript.js +10 -0
- package/dist/ui/entries/TeamCard.d.ts +31 -0
- package/dist/ui/entries/TeamCard.js +39 -0
- package/dist/ui/glyphs.d.ts +10 -0
- package/dist/ui/glyphs.js +6 -0
- package/dist/ui/layout/AppShell.d.ts +11 -1
- package/dist/ui/layout/AppShell.js +3 -3
- package/dist/ui/overlays/HelpOverlay.js +13 -0
- package/dist/ui/overlays/SettingsScreen.d.ts +12 -1
- package/dist/ui/overlays/SettingsScreen.js +38 -1
- package/dist/ui/transcript-text.js +20 -0
- package/dist/ui/use-startup-notices.d.ts +1 -1
- package/dist/ui/use-startup-notices.js +2 -1
- package/package.json +2 -2
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TeamRuntime — dispatch scheduling, budgets, the `TeamEvent` stream, abort and
|
|
3
|
+
* disposal (team-subagents §3.2 / §3.5 / §3.8).
|
|
4
|
+
*
|
|
5
|
+
* ASCII ONLY: `src/team/**` is inside the glyph scanner's scope.
|
|
6
|
+
*
|
|
7
|
+
* ALL CONCURRENCY LIVES INSIDE ONE TOOL EXECUTION (I-1 / D-1). The core agent
|
|
8
|
+
* loop runs tool calls strictly sequentially, and that is not an accident to be
|
|
9
|
+
* "fixed": plan mode's human-input bridge holds ONE outstanding request slot and
|
|
10
|
+
* is correct only because tool calls never overlap (I-P11). So `task` takes a
|
|
11
|
+
* BATCH and the fan-out is scheduled here, where it is ours to control. Anyone
|
|
12
|
+
* tempted to make `agent-loop.ts` concurrent to "fix" parallel delegation is
|
|
13
|
+
* about to silently mis-route a plan approval to a question wizard.
|
|
14
|
+
*
|
|
15
|
+
* The scheduler is a plain slot pool: an index cursor plus `maxConcurrent`
|
|
16
|
+
* worker loops, awaited with `Promise.allSettled`. No worker abstraction, no
|
|
17
|
+
* queue class, no event-loop trickery — every path is observable from the event
|
|
18
|
+
* stream.
|
|
19
|
+
*/
|
|
20
|
+
import { TeamBus } from './bus.js';
|
|
21
|
+
import { TEAM_LIMITS } from './limits.js';
|
|
22
|
+
import { shouldRetryColdStart } from './retry.js';
|
|
23
|
+
import { createSubagent, defaultSubagentFactory, } from './subagent.js';
|
|
24
|
+
export class TeamRuntime {
|
|
25
|
+
deps;
|
|
26
|
+
listeners = new Set();
|
|
27
|
+
live = [];
|
|
28
|
+
bus = null;
|
|
29
|
+
snapshotState = null;
|
|
30
|
+
busy = false;
|
|
31
|
+
aborted = false;
|
|
32
|
+
counter = 0;
|
|
33
|
+
dispatchTimer = null;
|
|
34
|
+
/**
|
|
35
|
+
* Every in-flight retry backoff, so an abort releases ALL of them (P1-7).
|
|
36
|
+
*
|
|
37
|
+
* A SET, NOT ONE FIELD, and the plural is load-bearing: the failure that
|
|
38
|
+
* triggers a cold start is usually provider-wide, so a 429 fails every
|
|
39
|
+
* in-flight child's first request in the same tick and up to `maxConcurrent`
|
|
40
|
+
* children enter backoff together. With a single stored resolver two of three
|
|
41
|
+
* are overwritten and only the last is released, so "an abort during the
|
|
42
|
+
* backoff window ends the dispatch promptly" would hold for one child and
|
|
43
|
+
* quietly not for the others.
|
|
44
|
+
*/
|
|
45
|
+
pendingBackoffs = new Set();
|
|
46
|
+
constructor(deps) {
|
|
47
|
+
this.deps = deps;
|
|
48
|
+
}
|
|
49
|
+
subscribe(listener) {
|
|
50
|
+
this.listeners.add(listener);
|
|
51
|
+
return () => this.listeners.delete(listener);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Unreachable while I-1 holds, and present anyway so "only one dispatch at a
|
|
55
|
+
* time" is a property of THIS module rather than a consequence of another
|
|
56
|
+
* package's loop shape (R-14).
|
|
57
|
+
*/
|
|
58
|
+
isBusy() {
|
|
59
|
+
return this.busy;
|
|
60
|
+
}
|
|
61
|
+
/** The live roster, for `/team` status. `null` when nothing is dispatching. */
|
|
62
|
+
snapshot() {
|
|
63
|
+
return this.snapshotState;
|
|
64
|
+
}
|
|
65
|
+
// -------------------------------------------------------------------------
|
|
66
|
+
// Dispatch
|
|
67
|
+
// -------------------------------------------------------------------------
|
|
68
|
+
async dispatch(specs, requested, signal) {
|
|
69
|
+
this.counter += 1;
|
|
70
|
+
const dispatchId = `d${this.counter}`;
|
|
71
|
+
const config = this.deps.getConfig();
|
|
72
|
+
const team = config.team;
|
|
73
|
+
const startedAt = Date.now();
|
|
74
|
+
this.busy = true;
|
|
75
|
+
this.aborted = false;
|
|
76
|
+
// DECLARED BEFORE THE BUS, ASSIGNED AFTER THE HANDLES. `canSend` closes over
|
|
77
|
+
// this binding so it reads the LIVE array: F-4 replaces `handles[index]`
|
|
78
|
+
// mid-dispatch, and a captured array or a captured handle would answer for
|
|
79
|
+
// the discarded child. The bus is built from `specs` and the handles are
|
|
80
|
+
// built from the bus, so the existing construction order forces the hoist.
|
|
81
|
+
// `canSend` is only ever called from inside a `team_wait` on a child that
|
|
82
|
+
// does not exist yet, so the TDZ window is not reachable.
|
|
83
|
+
let handles = [];
|
|
84
|
+
/**
|
|
85
|
+
* Whether `label` still has a loop that could call `team_send` (F-3 / P0-2).
|
|
86
|
+
*
|
|
87
|
+
* NEGATIVE, over TERMINAL PHASES ONLY. "The child's phase is running" is the
|
|
88
|
+
* natural way to write this and it is wrong on the SHIPPED DEFAULTS: at
|
|
89
|
+
* `maxConcurrent: 3, maxSubagents: 5`, children 4 and 5 sit in `queued` for
|
|
90
|
+
* the first part of every five-way dispatch, and a wait on one of them would
|
|
91
|
+
* be refused with "nobody can answer: a4 already finished". `queued` is not
|
|
92
|
+
* terminal - that child has not started yet and will.
|
|
93
|
+
*
|
|
94
|
+
* An unknown label permits, so this can never become a second, silent path
|
|
95
|
+
* to a refusal: `checkWaitable` has its own `unknown_sender` branch.
|
|
96
|
+
*/
|
|
97
|
+
const canSend = (label) => {
|
|
98
|
+
const handle = handles.find((h) => h.run.label === label);
|
|
99
|
+
if (!handle)
|
|
100
|
+
return true;
|
|
101
|
+
const phase = handle.run.phase;
|
|
102
|
+
return phase !== 'done' && phase !== 'failed' && phase !== 'aborted';
|
|
103
|
+
};
|
|
104
|
+
const bus = new TeamBus(specs.map((s) => s.label), {
|
|
105
|
+
onMessage: (message) => this.emit({ type: 'message', dispatchId, message }),
|
|
106
|
+
canSend,
|
|
107
|
+
});
|
|
108
|
+
this.bus = bus;
|
|
109
|
+
const subDeps = {
|
|
110
|
+
bus,
|
|
111
|
+
config,
|
|
112
|
+
providerRegistry: this.deps.providerRegistry,
|
|
113
|
+
getCwd: this.deps.getCwd,
|
|
114
|
+
getMode: this.deps.getMode,
|
|
115
|
+
getApiKey: this.deps.getApiKey,
|
|
116
|
+
...(this.deps.skillRegistry ? { skillRegistry: this.deps.skillRegistry } : {}),
|
|
117
|
+
...(this.deps.alwaysBlock ? { alwaysBlock: this.deps.alwaysBlock() } : {}),
|
|
118
|
+
...(this.deps.toolPolicy ? { toolPolicy: this.deps.toolPolicy } : {}),
|
|
119
|
+
...(this.deps.onChildToolPolicyEvent
|
|
120
|
+
? { onChildToolPolicyEvent: this.deps.onChildToolPolicyEvent }
|
|
121
|
+
: {}),
|
|
122
|
+
confirmTools: config.confirmTools,
|
|
123
|
+
...(this.deps.humanQueue ? { humanQueue: this.deps.humanQueue } : {}),
|
|
124
|
+
agentFactory: this.deps.agentFactory ?? defaultSubagentFactory,
|
|
125
|
+
};
|
|
126
|
+
// Hoisted into a factory so a retry can build a fresh child with IDENTICAL
|
|
127
|
+
// wiring (F-4). Replacing the child rather than re-prompting it is the only
|
|
128
|
+
// shape where the retried child is genuinely equivalent to the first:
|
|
129
|
+
// `Agent.prompt()` appends to the same history, so a second prompt would
|
|
130
|
+
// show the child its brief twice.
|
|
131
|
+
const makeHandle = (spec) => createSubagent(spec, subDeps, {
|
|
132
|
+
onUpdate: (run) => this.publish(dispatchId, run),
|
|
133
|
+
onUsage: (label, usage) => this.emit({ type: 'usage', dispatchId, label, usage }),
|
|
134
|
+
});
|
|
135
|
+
// ASSIGNMENT, not a declaration: a `const` here would shadow the `let` above
|
|
136
|
+
// the bus, `canSend` would see the empty array forever, and every peer would
|
|
137
|
+
// look live - which fails open rather than closed, so nothing would break
|
|
138
|
+
// loudly.
|
|
139
|
+
handles = specs.map(makeHandle);
|
|
140
|
+
this.live = handles;
|
|
141
|
+
this.snapshotState = {
|
|
142
|
+
dispatchId,
|
|
143
|
+
active: true,
|
|
144
|
+
runs: handles.map((h) => ({ ...h.run })),
|
|
145
|
+
requested,
|
|
146
|
+
startedAt,
|
|
147
|
+
messageCount: 0,
|
|
148
|
+
};
|
|
149
|
+
this.emit({ type: 'dispatch_start', dispatchId, requested, specs });
|
|
150
|
+
// TWO ABORT SOURCES, ON PURPOSE (§3.8). `ctx.signal` is the correct path
|
|
151
|
+
// today; `AgentController.abort()` also calls `abortAll()` directly, which
|
|
152
|
+
// makes "no child survives its dispatch" a property of the controller rather
|
|
153
|
+
// than a consequence of signal plumbing three modules away.
|
|
154
|
+
const onAbort = () => this.abortAll();
|
|
155
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
156
|
+
// The dispatch ceiling has to be enforced HERE as well as through
|
|
157
|
+
// `toolTimeoutOverrides.task`: the executor's timeout is COOPERATIVE (I-2),
|
|
158
|
+
// so the override only bites because `task` listens to `ctx.signal` — and a
|
|
159
|
+
// second, independent timer means a signal that never arrives still ends the
|
|
160
|
+
// dispatch.
|
|
161
|
+
this.dispatchTimer = setTimeout(() => this.abortAll(), team.dispatchTimeoutMs);
|
|
162
|
+
const runCtx = {
|
|
163
|
+
dispatchId,
|
|
164
|
+
handles,
|
|
165
|
+
makeHandle,
|
|
166
|
+
subagentTimeoutMs: team.subagentTimeoutMs,
|
|
167
|
+
};
|
|
168
|
+
const cursor = { next: 0 };
|
|
169
|
+
const workers = Array.from({ length: Math.max(1, Math.min(team.maxConcurrent, handles.length)) }, () => this.worker(runCtx, cursor));
|
|
170
|
+
await Promise.allSettled(workers);
|
|
171
|
+
if (this.dispatchTimer)
|
|
172
|
+
clearTimeout(this.dispatchTimer);
|
|
173
|
+
this.dispatchTimer = null;
|
|
174
|
+
signal?.removeEventListener('abort', onAbort);
|
|
175
|
+
for (const handle of handles)
|
|
176
|
+
handle.unsubscribe();
|
|
177
|
+
const outcome = {
|
|
178
|
+
dispatchId,
|
|
179
|
+
runs: handles.map((h) => ({ ...h.run, filesTouched: [...h.run.filesTouched] })),
|
|
180
|
+
requested,
|
|
181
|
+
startedAt,
|
|
182
|
+
endedAt: Date.now(),
|
|
183
|
+
aborted: this.aborted,
|
|
184
|
+
leadMail: bus.leadMail(),
|
|
185
|
+
usage: handles.reduce((acc, h) => ({
|
|
186
|
+
inputTokens: acc.inputTokens + h.run.usage.inputTokens,
|
|
187
|
+
outputTokens: acc.outputTokens + h.run.usage.outputTokens,
|
|
188
|
+
}), { inputTokens: 0, outputTokens: 0 }),
|
|
189
|
+
};
|
|
190
|
+
this.live = [];
|
|
191
|
+
this.bus = null;
|
|
192
|
+
this.snapshotState = null;
|
|
193
|
+
this.busy = false;
|
|
194
|
+
this.emit({ type: 'dispatch_end', dispatchId, outcome });
|
|
195
|
+
return outcome;
|
|
196
|
+
}
|
|
197
|
+
/** One slot of the pool: take the next index until the list is exhausted. */
|
|
198
|
+
async worker(ctx, cursor) {
|
|
199
|
+
for (;;) {
|
|
200
|
+
const index = cursor.next;
|
|
201
|
+
cursor.next += 1;
|
|
202
|
+
const handle = ctx.handles[index];
|
|
203
|
+
if (!handle)
|
|
204
|
+
return;
|
|
205
|
+
if (this.aborted) {
|
|
206
|
+
// Never started. Marked here rather than left `queued`, so the report
|
|
207
|
+
// accounts for every spec the dispatch accepted.
|
|
208
|
+
handle.run.phase = 'aborted';
|
|
209
|
+
handle.run.endedAt = Date.now();
|
|
210
|
+
this.publish(ctx.dispatchId, handle.run);
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
await this.runOne(ctx, index);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Run one child to a terminal phase, replacing it once if its very first
|
|
218
|
+
* request died on a transport error before it did anything at all (F-4).
|
|
219
|
+
*/
|
|
220
|
+
async runOne(ctx, index) {
|
|
221
|
+
const { dispatchId, handles, makeHandle, subagentTimeoutMs } = ctx;
|
|
222
|
+
for (let attempt = 0;; attempt += 1) {
|
|
223
|
+
const handle = handles[index];
|
|
224
|
+
const { run, agent } = handle;
|
|
225
|
+
run.phase = 'starting';
|
|
226
|
+
run.startedAt = Date.now();
|
|
227
|
+
this.publish(dispatchId, run);
|
|
228
|
+
let timedOut = false;
|
|
229
|
+
const timer = setTimeout(() => {
|
|
230
|
+
timedOut = true;
|
|
231
|
+
agent.abort();
|
|
232
|
+
}, subagentTimeoutMs);
|
|
233
|
+
try {
|
|
234
|
+
await agent.prompt(handle.spec.prompt);
|
|
235
|
+
}
|
|
236
|
+
catch (err) {
|
|
237
|
+
// `Agent.prompt()` resolves even on failure, so this only fires for a
|
|
238
|
+
// stub factory or a genuine throw. Recorded rather than swallowed: a
|
|
239
|
+
// child that failed for a reason nobody can see is the worst outcome the
|
|
240
|
+
// report can produce.
|
|
241
|
+
run.error = err instanceof Error ? err.message : String(err);
|
|
242
|
+
}
|
|
243
|
+
finally {
|
|
244
|
+
clearTimeout(timer);
|
|
245
|
+
}
|
|
246
|
+
// THE TWO ABORT EXCLUSIONS THAT MAKE THE PREDICATE SAFE. Every abort in
|
|
247
|
+
// this system reaches the provider as an aborted fetch, and `core` marks
|
|
248
|
+
// an `AbortError` retryable - so Esc, `ctx.signal`, the dispatch timeout
|
|
249
|
+
// (all three via `this.aborted`) and the per-child timeout (`timedOut`)
|
|
250
|
+
// are excluded HERE, before `shouldRetryColdStart` is ever consulted.
|
|
251
|
+
const retryable = !this.aborted && !timedOut && shouldRetryColdStart(run, attempt + 1);
|
|
252
|
+
if (!retryable) {
|
|
253
|
+
run.endedAt = Date.now();
|
|
254
|
+
this.settlePhase(run, timedOut, subagentTimeoutMs);
|
|
255
|
+
this.publish(dispatchId, run);
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
const fresh = this.replaceForRetry(dispatchId, handles, index, makeHandle);
|
|
259
|
+
await this.backoff(TEAM_LIMITS.retryBackoffMs);
|
|
260
|
+
if (this.aborted) {
|
|
261
|
+
fresh.run.phase = 'aborted';
|
|
262
|
+
fresh.run.endedAt = Date.now();
|
|
263
|
+
this.publish(dispatchId, fresh.run);
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Swap a cold-started child for a fresh one, carrying only what must survive.
|
|
270
|
+
*
|
|
271
|
+
* `this.live` IS `handles`, so the single assignment below also makes
|
|
272
|
+
* `abortAll()` abort the NEW child and makes the outcome carry the new run.
|
|
273
|
+
* Anyone tempted to "clean up" that aliasing into a defensive copy must update
|
|
274
|
+
* both places.
|
|
275
|
+
*/
|
|
276
|
+
replaceForRetry(dispatchId, handles, index, makeHandle) {
|
|
277
|
+
const handle = handles[index];
|
|
278
|
+
const carried = { ...handle.run.usage };
|
|
279
|
+
const retries = (handle.run.retries ?? 0) + 1;
|
|
280
|
+
handle.unsubscribe();
|
|
281
|
+
try {
|
|
282
|
+
handle.agent.abort();
|
|
283
|
+
}
|
|
284
|
+
catch {
|
|
285
|
+
// Already gone; this must not stop the retry.
|
|
286
|
+
}
|
|
287
|
+
const fresh = makeHandle(handle.spec);
|
|
288
|
+
// Provably {0,0} under the predicate above (no turn completed, so no
|
|
289
|
+
// `turn_end` and no usage event). Carried anyway so that a future relaxation
|
|
290
|
+
// of the predicate cannot silently under-report spend - the most misleading
|
|
291
|
+
// failure this feature can produce.
|
|
292
|
+
fresh.run.usage = carried;
|
|
293
|
+
fresh.run.retries = retries;
|
|
294
|
+
handles[index] = fresh;
|
|
295
|
+
this.publish(dispatchId, fresh.run);
|
|
296
|
+
return fresh;
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* The final phase of an attempt, in a FIXED ORDER: aborted, then timed out,
|
|
300
|
+
* then error, then no-summary, then done.
|
|
301
|
+
*
|
|
302
|
+
* Extracted verbatim so the retry loop has one exit and the order lives in one
|
|
303
|
+
* place.
|
|
304
|
+
*/
|
|
305
|
+
settlePhase(run, timedOut, subagentTimeoutMs) {
|
|
306
|
+
if (this.aborted) {
|
|
307
|
+
run.phase = 'aborted';
|
|
308
|
+
}
|
|
309
|
+
else if (timedOut) {
|
|
310
|
+
run.phase = 'failed';
|
|
311
|
+
run.error = run.error ?? `subagent exceeded ${Math.round(subagentTimeoutMs / 1000)}s`;
|
|
312
|
+
}
|
|
313
|
+
else if (run.error) {
|
|
314
|
+
run.phase = 'failed';
|
|
315
|
+
}
|
|
316
|
+
else if (!run.summary) {
|
|
317
|
+
// A child whose watchdog fired, or whose stream died silently, resolves
|
|
318
|
+
// with nothing. `[failed: run produced no output]` is the honest reading;
|
|
319
|
+
// `[ok]` with an empty section is not.
|
|
320
|
+
run.phase = 'failed';
|
|
321
|
+
run.error = 'run produced no output';
|
|
322
|
+
}
|
|
323
|
+
else {
|
|
324
|
+
run.phase = 'done';
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Sleep, releasing early when the dispatch aborts.
|
|
329
|
+
*
|
|
330
|
+
* The timer is `unref`'d so a pending backoff cannot hold the process open on
|
|
331
|
+
* exit, and the resolver is registered in `pendingBackoffs` so `abortAll()`
|
|
332
|
+
* releases EVERY one of them rather than only the last (P1-7).
|
|
333
|
+
*/
|
|
334
|
+
backoff(ms) {
|
|
335
|
+
return new Promise((resolve) => {
|
|
336
|
+
const done = () => {
|
|
337
|
+
clearTimeout(timer);
|
|
338
|
+
this.pendingBackoffs.delete(done);
|
|
339
|
+
resolve();
|
|
340
|
+
};
|
|
341
|
+
const timer = setTimeout(done, ms);
|
|
342
|
+
timer.unref?.();
|
|
343
|
+
this.pendingBackoffs.add(done);
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
// -------------------------------------------------------------------------
|
|
347
|
+
// Abort / dispose
|
|
348
|
+
// -------------------------------------------------------------------------
|
|
349
|
+
/**
|
|
350
|
+
* Stop every child and settle every blocking wait.
|
|
351
|
+
*
|
|
352
|
+
* Idempotent, and safe to call when nothing is running — `AgentController.abort()`
|
|
353
|
+
* calls it unconditionally.
|
|
354
|
+
*/
|
|
355
|
+
abortAll() {
|
|
356
|
+
if (!this.busy)
|
|
357
|
+
return;
|
|
358
|
+
this.aborted = true;
|
|
359
|
+
for (const handle of this.live) {
|
|
360
|
+
try {
|
|
361
|
+
handle.agent.abort();
|
|
362
|
+
}
|
|
363
|
+
catch {
|
|
364
|
+
// A child that is already gone must not stop the others being aborted.
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
this.bus?.cancelAllWaits();
|
|
368
|
+
// Every in-flight retry backoff, not just the last one registered: a
|
|
369
|
+
// provider-wide failure puts up to `maxConcurrent` children here at once.
|
|
370
|
+
for (const release of [...this.pendingBackoffs])
|
|
371
|
+
release();
|
|
372
|
+
}
|
|
373
|
+
/** Process-exit path: abort everything and clear timers (R-15). */
|
|
374
|
+
dispose() {
|
|
375
|
+
this.abortAll();
|
|
376
|
+
if (this.dispatchTimer)
|
|
377
|
+
clearTimeout(this.dispatchTimer);
|
|
378
|
+
this.dispatchTimer = null;
|
|
379
|
+
this.listeners.clear();
|
|
380
|
+
}
|
|
381
|
+
// -------------------------------------------------------------------------
|
|
382
|
+
// Events
|
|
383
|
+
// -------------------------------------------------------------------------
|
|
384
|
+
publish(dispatchId, run) {
|
|
385
|
+
const copy = { ...run, filesTouched: [...run.filesTouched] };
|
|
386
|
+
if (this.snapshotState && this.snapshotState.dispatchId === dispatchId) {
|
|
387
|
+
this.snapshotState = {
|
|
388
|
+
...this.snapshotState,
|
|
389
|
+
runs: this.snapshotState.runs.map((r) => (r.label === copy.label ? copy : r)),
|
|
390
|
+
messageCount: this.bus?.messageCount() ?? this.snapshotState.messageCount,
|
|
391
|
+
...(this.bus?.lastMessage() ? { lastMessage: this.bus.lastMessage() } : {}),
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
this.emit({ type: 'agent_update', dispatchId, run: copy });
|
|
395
|
+
}
|
|
396
|
+
emit(event) {
|
|
397
|
+
for (const listener of [...this.listeners]) {
|
|
398
|
+
try {
|
|
399
|
+
listener(event);
|
|
400
|
+
}
|
|
401
|
+
catch {
|
|
402
|
+
// One bad subscriber must not take the dispatch down with it.
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `createSubagent` — one child `Agent`, its tool set, and the instrumentation
|
|
3
|
+
* that turns core events into a `SubagentRun` (team-subagents §3.4).
|
|
4
|
+
*
|
|
5
|
+
* ASCII ONLY: `src/team/**` is inside the glyph scanner's scope.
|
|
6
|
+
*
|
|
7
|
+
* A CHILD IS BUILT FROM THE SAME FACTORY THE LEAD USES. That is the point: the
|
|
8
|
+
* `--confirm` gate, the skills ceiling and the plan-mode gate are re-applied one
|
|
9
|
+
* level down by the same wrappers, so delegation cannot be a way around any of
|
|
10
|
+
* them (§3.11 / R-6). What a child does NOT get is spelled out below, and each
|
|
11
|
+
* omission has a reason that is a correctness boundary rather than caution.
|
|
12
|
+
*/
|
|
13
|
+
import { type AgentConfig, type AgentEvent, type AgentTool, type ProviderRegistry, type SkillRegistry, type TokenUsage, type ToolPolicyDecision, type ToolPolicyVerdict } from '@aragon-agent/core';
|
|
14
|
+
import type { AgentMode } from '../agent/agent-mode.js';
|
|
15
|
+
import type { CliConfig } from '../config/schema.js';
|
|
16
|
+
import { TeamBus } from './bus.js';
|
|
17
|
+
import type { TeamHumanQueue, WatchdogPausable } from './human-queue.js';
|
|
18
|
+
import type { SubagentRun, SubagentSpec } from './types.js';
|
|
19
|
+
/**
|
|
20
|
+
* The slice of `Agent` a child is used through.
|
|
21
|
+
*
|
|
22
|
+
* Structural rather than the concrete class so `TeamRuntime` can be handed a
|
|
23
|
+
* stub factory and the whole scheduler can be tested without a network — the
|
|
24
|
+
* same seam `HeadlessController` already uses.
|
|
25
|
+
*/
|
|
26
|
+
export interface SubagentAgentLike extends WatchdogPausable {
|
|
27
|
+
prompt(text: string): Promise<void>;
|
|
28
|
+
abort(): void;
|
|
29
|
+
subscribe(listener: (event: AgentEvent) => void): () => void;
|
|
30
|
+
}
|
|
31
|
+
export type SubagentAgentFactory = (config: AgentConfig) => SubagentAgentLike;
|
|
32
|
+
/** The default factory: a real `Agent` from `@aragon-agent/core`. */
|
|
33
|
+
export declare const defaultSubagentFactory: SubagentAgentFactory;
|
|
34
|
+
/** Everything a child needs that is the same for every child in a dispatch. */
|
|
35
|
+
export interface SubagentDeps {
|
|
36
|
+
bus: TeamBus;
|
|
37
|
+
config: CliConfig;
|
|
38
|
+
providerRegistry: ProviderRegistry;
|
|
39
|
+
getCwd: () => string;
|
|
40
|
+
/** The lead's live session mode. Read at CALL time, never cached. */
|
|
41
|
+
getMode: () => AgentMode;
|
|
42
|
+
getApiKey: (providerId: string) => string | undefined;
|
|
43
|
+
/**
|
|
44
|
+
* The lead's `SkillRegistry`, used for `skill_find` ONLY.
|
|
45
|
+
*
|
|
46
|
+
* `undefined` when skills are off, in which case the child gets no skill tool
|
|
47
|
+
* at all — the same shape `--no-skills` produces for the lead.
|
|
48
|
+
*/
|
|
49
|
+
skillRegistry?: SkillRegistry;
|
|
50
|
+
/** The lead's always-on skill bodies, spliced into every child's prompt. */
|
|
51
|
+
alwaysBlock?: string;
|
|
52
|
+
toolPolicy?: () => ToolPolicyDecision;
|
|
53
|
+
/** Already label-decorating; see `TeamRuntime` (§3.11 / P1-3). */
|
|
54
|
+
onChildToolPolicyEvent?: (e: {
|
|
55
|
+
label: string;
|
|
56
|
+
tool: string;
|
|
57
|
+
verdict: ToolPolicyVerdict;
|
|
58
|
+
sourceNames: string[];
|
|
59
|
+
}) => void;
|
|
60
|
+
confirmTools: boolean;
|
|
61
|
+
/** Present only when `--confirm` is on AND a human channel exists. */
|
|
62
|
+
humanQueue?: TeamHumanQueue;
|
|
63
|
+
agentFactory: SubagentAgentFactory;
|
|
64
|
+
}
|
|
65
|
+
export interface SubagentHooks {
|
|
66
|
+
/** Coalesced at the source (§5.2); phase transitions always pass. */
|
|
67
|
+
onUpdate: (run: SubagentRun) => void;
|
|
68
|
+
onUsage: (label: string, usage: TokenUsage) => void;
|
|
69
|
+
}
|
|
70
|
+
export interface SubagentHandle {
|
|
71
|
+
spec: SubagentSpec;
|
|
72
|
+
run: SubagentRun;
|
|
73
|
+
agent: SubagentAgentLike;
|
|
74
|
+
tools: AgentTool[];
|
|
75
|
+
unsubscribe: () => void;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Build a child's tool array.
|
|
79
|
+
*
|
|
80
|
+
* Exported so `team-tool-gate.test.ts` can assert what a child may and may not
|
|
81
|
+
* call without constructing an `Agent` or reaching a network — the tool set IS
|
|
82
|
+
* the security boundary here, so it has to be directly checkable.
|
|
83
|
+
*/
|
|
84
|
+
export declare function buildSubagentTools(spec: SubagentSpec, deps: SubagentDeps, getAgent: () => WatchdogPausable | null): AgentTool[];
|
|
85
|
+
/**
|
|
86
|
+
* Construct a child and wire its instrumentation.
|
|
87
|
+
*
|
|
88
|
+
* The returned agent has NOT been prompted; `TeamRuntime` owns scheduling.
|
|
89
|
+
*/
|
|
90
|
+
export declare function createSubagent(spec: SubagentSpec, deps: SubagentDeps, hooks: SubagentHooks): SubagentHandle;
|