@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.
Files changed (87) hide show
  1. package/CHANGELOG.md +127 -0
  2. package/README.md +193 -3
  3. package/dist/agent/controller.d.ts +51 -1
  4. package/dist/agent/controller.js +177 -2
  5. package/dist/agent/headless.d.ts +12 -0
  6. package/dist/agent/headless.js +41 -1
  7. package/dist/agent/reducer.d.ts +48 -0
  8. package/dist/agent/reducer.js +91 -2
  9. package/dist/agent/system-prompt.d.ts +17 -0
  10. package/dist/agent/system-prompt.js +4 -0
  11. package/dist/cli.js +98 -7
  12. package/dist/commands/builtins.d.ts +2 -2
  13. package/dist/commands/builtins.js +133 -3
  14. package/dist/config/app-paths.d.ts +17 -0
  15. package/dist/config/app-paths.js +21 -0
  16. package/dist/config/cli-commands.d.ts +26 -2
  17. package/dist/config/cli-commands.js +83 -6
  18. package/dist/config/env.d.ts +3 -2
  19. package/dist/config/env.js +44 -6
  20. package/dist/config/history-commands.d.ts +20 -0
  21. package/dist/config/history-commands.js +71 -0
  22. package/dist/config/load.d.ts +13 -0
  23. package/dist/config/load.js +107 -6
  24. package/dist/config/migrate-state-out-of-config.d.ts +32 -0
  25. package/dist/config/migrate-state-out-of-config.js +85 -0
  26. package/dist/config/prompt-history.d.ts +86 -0
  27. package/dist/config/prompt-history.js +332 -0
  28. package/dist/config/schema.d.ts +176 -18
  29. package/dist/config/schema.js +162 -8
  30. package/dist/config/store.js +40 -4
  31. package/dist/config/ui-state.d.ts +52 -0
  32. package/dist/config/ui-state.js +149 -0
  33. package/dist/logging/install.d.ts +85 -0
  34. package/dist/logging/install.js +94 -0
  35. package/dist/logging/logger.d.ts +1 -1
  36. package/dist/session/persist.d.ts +16 -0
  37. package/dist/session/persist.js +19 -1
  38. package/dist/team/activity.d.ts +67 -0
  39. package/dist/team/activity.js +130 -0
  40. package/dist/team/bus.d.ts +190 -0
  41. package/dist/team/bus.js +366 -0
  42. package/dist/team/comm-tools.d.ts +61 -0
  43. package/dist/team/comm-tools.js +221 -0
  44. package/dist/team/human-queue.d.ts +55 -0
  45. package/dist/team/human-queue.js +72 -0
  46. package/dist/team/limits.d.ts +123 -0
  47. package/dist/team/limits.js +128 -0
  48. package/dist/team/normalize.d.ts +39 -0
  49. package/dist/team/normalize.js +106 -0
  50. package/dist/team/panel-rows.d.ts +48 -0
  51. package/dist/team/panel-rows.js +75 -0
  52. package/dist/team/prompt.d.ts +49 -0
  53. package/dist/team/prompt.js +87 -0
  54. package/dist/team/report.d.ts +58 -0
  55. package/dist/team/report.js +205 -0
  56. package/dist/team/retry.d.ts +48 -0
  57. package/dist/team/retry.js +56 -0
  58. package/dist/team/runtime.d.ts +122 -0
  59. package/dist/team/runtime.js +406 -0
  60. package/dist/team/subagent.d.ts +90 -0
  61. package/dist/team/subagent.js +294 -0
  62. package/dist/team/task-tool.d.ts +47 -0
  63. package/dist/team/task-tool.js +118 -0
  64. package/dist/team/types.d.ts +172 -0
  65. package/dist/team/types.js +13 -0
  66. package/dist/tools/index.d.ts +48 -2
  67. package/dist/tools/index.js +26 -1
  68. package/dist/ui/App.d.ts +22 -1
  69. package/dist/ui/App.js +144 -18
  70. package/dist/ui/StatusBar.d.ts +21 -0
  71. package/dist/ui/StatusBar.js +5 -2
  72. package/dist/ui/TeamPanel.d.ts +57 -0
  73. package/dist/ui/TeamPanel.js +134 -0
  74. package/dist/ui/Transcript.js +10 -0
  75. package/dist/ui/entries/TeamCard.d.ts +31 -0
  76. package/dist/ui/entries/TeamCard.js +39 -0
  77. package/dist/ui/glyphs.d.ts +10 -0
  78. package/dist/ui/glyphs.js +6 -0
  79. package/dist/ui/layout/AppShell.d.ts +11 -1
  80. package/dist/ui/layout/AppShell.js +3 -3
  81. package/dist/ui/overlays/HelpOverlay.js +13 -0
  82. package/dist/ui/overlays/SettingsScreen.d.ts +12 -1
  83. package/dist/ui/overlays/SettingsScreen.js +38 -1
  84. package/dist/ui/transcript-text.js +20 -0
  85. package/dist/ui/use-startup-notices.d.ts +1 -1
  86. package/dist/ui/use-startup-notices.js +2 -1
  87. 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;