@aragon-agent/cli 0.5.1 → 0.5.3
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 +117 -0
- package/README.md +155 -5
- 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 +131 -6
- package/dist/commands/builtins.js +82 -0
- 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 +63 -4
- package/dist/config/env.d.ts +2 -2
- package/dist/config/env.js +36 -2
- package/dist/config/history-commands.d.ts +20 -0
- package/dist/config/history-commands.js +71 -0
- package/dist/config/load.d.ts +15 -0
- package/dist/config/load.js +88 -5
- 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 +125 -9
- package/dist/config/schema.js +90 -5
- package/dist/config/store.js +13 -4
- package/dist/config/ui-state.d.ts +52 -0
- package/dist/config/ui-state.js +149 -0
- package/dist/input/mouse-events.d.ts +61 -0
- package/dist/input/mouse-events.js +153 -0
- package/dist/input/stdin-mouse-filter.d.ts +42 -0
- package/dist/input/stdin-mouse-filter.js +153 -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 +9 -1
- package/dist/ui/App.js +140 -21
- package/dist/ui/Composer.d.ts +6 -1
- package/dist/ui/Composer.js +2 -2
- package/dist/ui/PromptInput.d.ts +15 -1
- package/dist/ui/PromptInput.js +38 -1
- 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 +22 -0
- package/dist/ui/glyphs.js +10 -0
- package/dist/ui/layout/AppShell.d.ts +20 -1
- package/dist/ui/layout/AppShell.js +39 -4
- package/dist/ui/layout/ScrollIndicator.d.ts +31 -0
- package/dist/ui/layout/ScrollIndicator.js +15 -0
- package/dist/ui/layout/ScrollViewport.d.ts +8 -2
- package/dist/ui/layout/ScrollViewport.js +24 -4
- package/dist/ui/layout/regions.d.ts +55 -0
- package/dist/ui/layout/regions.js +52 -0
- package/dist/ui/layout/scroll-indicator.d.ts +27 -0
- package/dist/ui/layout/scroll-indicator.js +35 -0
- package/dist/ui/layout/scroll.d.ts +9 -0
- package/dist/ui/layout/scroll.js +23 -0
- package/dist/ui/overlays/HelpOverlay.js +14 -0
- package/dist/ui/screen.d.ts +14 -1
- package/dist/ui/screen.js +37 -3
- package/dist/ui/transcript-text.js +20 -0
- package/dist/ui/use-startup-notices.d.ts +11 -1
- package/dist/ui/use-startup-notices.js +29 -2
- package/dist/ui/use-wheel-routing.d.ts +68 -0
- package/dist/ui/use-wheel-routing.js +139 -0
- package/package.json +2 -2
package/dist/agent/controller.js
CHANGED
|
@@ -9,10 +9,14 @@
|
|
|
9
9
|
* from the event stream by the reducer (TUI) and the headless writer.
|
|
10
10
|
*/
|
|
11
11
|
import { Agent, initProviders, ModelRegistry, } from '@aragon-agent/core';
|
|
12
|
-
import { isAdapterProvider } from '../config/schema.js';
|
|
12
|
+
import { clampTeamConfig, isAdapterProvider } from '../config/schema.js';
|
|
13
13
|
import { makeGetApiKey } from '../config/load.js';
|
|
14
14
|
import { updatePersistedConfig } from '../config/store.js';
|
|
15
15
|
import { createBuiltinTools } from '../tools/index.js';
|
|
16
|
+
import { TeamRuntime } from '../team/runtime.js';
|
|
17
|
+
import { TeamHumanQueue } from '../team/human-queue.js';
|
|
18
|
+
import { createTaskTool } from '../team/task-tool.js';
|
|
19
|
+
import { buildTeamBlock } from '../team/prompt.js';
|
|
16
20
|
import { createPlanTools } from '../tools/plan-tools.js';
|
|
17
21
|
import { DENY_ALL_HUMAN_INPUT } from '../tools/human-input.js';
|
|
18
22
|
import { createNodeSkillHost } from '../skills/node-host.js';
|
|
@@ -48,6 +52,25 @@ export class AgentController {
|
|
|
48
52
|
askRounds = 0;
|
|
49
53
|
/** Static: whether this session could ever render a question overlay. */
|
|
50
54
|
planInteractive;
|
|
55
|
+
// -----------------------------------------------------------------------
|
|
56
|
+
// Team mode (team-subagents §4.5) — TWO FLAGS, AND THEY ARE NOT THE SAME.
|
|
57
|
+
//
|
|
58
|
+
// `teamRegistered` is decided ONCE, at construction, and can never change:
|
|
59
|
+
// it is whether the `task` tool is in `this.tools` at all. The array is
|
|
60
|
+
// immutable for the life of the session (see the note in the constructor),
|
|
61
|
+
// so `/team on` in a session started with `--no-team` CANNOT add it.
|
|
62
|
+
//
|
|
63
|
+
// `teamEnabled` is live. `/team off` flips it, which makes `task` refuse
|
|
64
|
+
// through its own guard and drops `<team_mode>` from the next prompt rebuild
|
|
65
|
+
// — `rebuildSystemPrompt()` IS a live path, unlike the tool array.
|
|
66
|
+
//
|
|
67
|
+
// Conflating them is P0-2: the prompt would advertise a tool that was never
|
|
68
|
+
// registered, every call would come back "unknown tool", and the model would
|
|
69
|
+
// have no way to discover why (D-17 / R-18).
|
|
70
|
+
// -----------------------------------------------------------------------
|
|
71
|
+
teamRegistered;
|
|
72
|
+
teamEnabled;
|
|
73
|
+
teamRuntime;
|
|
51
74
|
constructor(config, deps = {}) {
|
|
52
75
|
this.config = config;
|
|
53
76
|
this.cwd = config.cwd;
|
|
@@ -81,6 +104,40 @@ export class AgentController {
|
|
|
81
104
|
this.skills.reportUnknownForcedSkills();
|
|
82
105
|
}
|
|
83
106
|
const confirm = deps.confirm;
|
|
107
|
+
// --- Team mode ---------------------------------------------------------
|
|
108
|
+
this.teamRegistered = config.team.enabled;
|
|
109
|
+
this.teamEnabled = config.team.enabled;
|
|
110
|
+
this.teamRuntime = this.teamRegistered
|
|
111
|
+
? new TeamRuntime({
|
|
112
|
+
getConfig: () => this.config,
|
|
113
|
+
providerRegistry: this.providerRegistry,
|
|
114
|
+
getCwd: () => this.cwd,
|
|
115
|
+
getMode: () => this.effectiveMode,
|
|
116
|
+
getApiKey: (id) => this.resolveKey(id),
|
|
117
|
+
// `skill_find` only, and the lead's always-on bodies. See the long
|
|
118
|
+
// note in `subagent.ts` for why `skill` itself is withheld (D-16).
|
|
119
|
+
...(this.skillsEnabled
|
|
120
|
+
? {
|
|
121
|
+
skillRegistry: this.skills.getRegistry(),
|
|
122
|
+
alwaysBlock: () => this.skills.alwaysBlock(),
|
|
123
|
+
toolPolicy: () => this.skills.toolPolicyDecision(() => this.toolNames()),
|
|
124
|
+
// Label-decorating, and it DOES NOT advance the lead's
|
|
125
|
+
// escalation counter (P1-3). That counter exists to tell ONE
|
|
126
|
+
// agent to stop retrying ONE tool; a child cannot retry the
|
|
127
|
+
// lead's turn, and letting children spend the lead's budget
|
|
128
|
+
// makes the escalation fire on the lead's next legitimate call.
|
|
129
|
+
onChildToolPolicyEvent: (e) => deps.notify?.('warn', `[${e.label}] ${e.verdict.message ?? `Blocked ${e.tool}`}`),
|
|
130
|
+
}
|
|
131
|
+
: {}),
|
|
132
|
+
// NOT routed through `withHumanWait` (I-3, and this one is subtle):
|
|
133
|
+
// the lead's watchdog is ALREADY paused for the whole dispatch, and
|
|
134
|
+
// `IdleWatchdog.pause()` is a boolean rather than a counter — so a
|
|
135
|
+
// nested resume when one child's dialog closed would re-arm the lead
|
|
136
|
+
// mid-dispatch and abort it at `idleTimeoutMs`. The queue pauses the
|
|
137
|
+
// CHILD's watchdog instead, which is the one that needs it (P0-4).
|
|
138
|
+
...(confirm ? { humanQueue: new TeamHumanQueue(confirm) } : {}),
|
|
139
|
+
})
|
|
140
|
+
: null;
|
|
84
141
|
this.tools = createBuiltinTools({
|
|
85
142
|
getCwd: () => this.cwd,
|
|
86
143
|
confirmTools: config.confirmTools,
|
|
@@ -110,6 +167,27 @@ export class AgentController {
|
|
|
110
167
|
withHumanWait: (fn) => this.withHumanWait(fn),
|
|
111
168
|
onPlanApproved: () => this.setAgentMode('build', { force: true }),
|
|
112
169
|
}),
|
|
170
|
+
// THROUGH THE FACTORY, NEVER APPENDED AFTERWARDS (D-15 / P0-1).
|
|
171
|
+
// `tools.test.ts::C7` asserts `HOST_TOOL_NAMES` equals what
|
|
172
|
+
// `createBuiltinTools` produces, so appending `task` here would turn that
|
|
173
|
+
// test red with a message about two lists of names.
|
|
174
|
+
//
|
|
175
|
+
// Empty when team mode was off at construction, which leaves the array
|
|
176
|
+
// byte-identical to the pre-team build (AC-11).
|
|
177
|
+
teamTools: this.teamRuntime
|
|
178
|
+
? [
|
|
179
|
+
createTaskTool({
|
|
180
|
+
runtime: this.teamRuntime,
|
|
181
|
+
// Read LIVE: `/team off` flips the flag, it cannot unregister.
|
|
182
|
+
isTeamEnabled: () => this.teamEnabled,
|
|
183
|
+
maxSubagents: () => this.config.team.maxSubagents,
|
|
184
|
+
hasApiKey: () => this.hasApiKey(),
|
|
185
|
+
activeProvider: () => this.config.provider,
|
|
186
|
+
modelCost: () => this.getModelInfo().cost,
|
|
187
|
+
withPausedWatchdog: (fn) => this.withPausedWatchdog(fn),
|
|
188
|
+
}),
|
|
189
|
+
]
|
|
190
|
+
: [],
|
|
113
191
|
// Read at CALL time, never cached: the mode flips mid-run when the user
|
|
114
192
|
// approves a plan, and a cached 'plan' would refuse the work they just
|
|
115
193
|
// authorized.
|
|
@@ -149,6 +227,11 @@ export class AgentController {
|
|
|
149
227
|
toolTimeoutOverrides: {
|
|
150
228
|
ask_user: config.planModeHumanTimeoutMs,
|
|
151
229
|
submit_plan: config.planModeHumanTimeoutMs,
|
|
230
|
+
// A fan-out is not a hung tool either. Like the two above, this
|
|
231
|
+
// number only bites BECAUSE `task` races `ctx.signal` — the
|
|
232
|
+
// executor's timeout is cooperative, so an override is inert for any
|
|
233
|
+
// tool that does not listen (I-2).
|
|
234
|
+
...(this.teamRegistered ? { task: config.team.dispatchTimeoutMs } : {}),
|
|
152
235
|
},
|
|
153
236
|
},
|
|
154
237
|
});
|
|
@@ -193,6 +276,16 @@ export class AgentController {
|
|
|
193
276
|
agentMode: this.effectiveMode,
|
|
194
277
|
planInteractive: this.planInteractive,
|
|
195
278
|
planMaxAskRounds: this.config.planModeMaxAskRounds,
|
|
279
|
+
// BOTH flags, and both are required (§4.5). `teamRegistered` false means
|
|
280
|
+
// there is no `task` tool to advertise; `teamEnabled` false means the user
|
|
281
|
+
// turned it off for this session. Advertising in either case would
|
|
282
|
+
// describe a capability the model does not have.
|
|
283
|
+
teamBlock: this.teamRegistered && this.teamEnabled
|
|
284
|
+
? buildTeamBlock({
|
|
285
|
+
maxSubagents: this.config.team.maxSubagents,
|
|
286
|
+
maxConcurrent: this.config.team.maxConcurrent,
|
|
287
|
+
})
|
|
288
|
+
: '',
|
|
196
289
|
});
|
|
197
290
|
}
|
|
198
291
|
rebuildSystemPrompt() {
|
|
@@ -293,6 +386,13 @@ export class AgentController {
|
|
|
293
386
|
}
|
|
294
387
|
abort() {
|
|
295
388
|
this.agent.abort();
|
|
389
|
+
// DELIBERATE REDUNDANCY (§3.8). Aborting the lead already aborts the
|
|
390
|
+
// in-flight `task` call's `ctx.signal`, which the runtime listens to — that
|
|
391
|
+
// path is correct today. Calling it directly as well makes "no child
|
|
392
|
+
// survives its dispatch" a property of THIS class rather than a consequence
|
|
393
|
+
// of signal plumbing three modules away, which is the same argument
|
|
394
|
+
// `App.tsx` records for `cancelPendingHuman()` on `agent_end`.
|
|
395
|
+
this.teamRuntime?.abortAll();
|
|
296
396
|
}
|
|
297
397
|
steer(text) {
|
|
298
398
|
// Mid-run interjection: promote the queue, keep what is already in force
|
|
@@ -393,8 +493,19 @@ export class AgentController {
|
|
|
393
493
|
* path must not leave the watchdog disarmed for the rest of the run. Also used
|
|
394
494
|
* by the `--confirm` gate, which had exactly this bug — a user who took four
|
|
395
495
|
* minutes over `Proceed? (y/N)` had their run killed by the watchdog.
|
|
496
|
+
*
|
|
497
|
+
* RENAMED FROM `withHumanWait`, WITH THE OLD NAME KEPT AS AN ALIAS (D-12). The
|
|
498
|
+
* mechanism is not about humans any more — a `task` dispatch is a long wait
|
|
499
|
+
* with no human in it at all — but the plan tools and the `--confirm` gate read
|
|
500
|
+
* correctly with the old name at their call sites, and renaming those would be
|
|
501
|
+
* churn in files this change should barely touch.
|
|
502
|
+
*
|
|
503
|
+
* NOT RE-ENTRANT, and that matters here: `IdleWatchdog.pause()` sets a boolean
|
|
504
|
+
* rather than a counter, so a nested call's `finally` re-arms the watchdog
|
|
505
|
+
* while the outer wait is still going. That is why a child's `--confirm`
|
|
506
|
+
* dialog does NOT route through this (see the constructor's `humanQueue`).
|
|
396
507
|
*/
|
|
397
|
-
async
|
|
508
|
+
async withPausedWatchdog(fn) {
|
|
398
509
|
this.agent.pauseIdleWatchdog();
|
|
399
510
|
try {
|
|
400
511
|
return await fn();
|
|
@@ -403,6 +514,70 @@ export class AgentController {
|
|
|
403
514
|
this.agent.resumeIdleWatchdog();
|
|
404
515
|
}
|
|
405
516
|
}
|
|
517
|
+
/** @see withPausedWatchdog — the name the plan tools and `--confirm` use. */
|
|
518
|
+
withHumanWait(fn) {
|
|
519
|
+
return this.withPausedWatchdog(fn);
|
|
520
|
+
}
|
|
521
|
+
// -----------------------------------------------------------------------
|
|
522
|
+
// Team mode (team-subagents §4.5)
|
|
523
|
+
// -----------------------------------------------------------------------
|
|
524
|
+
/** Whether `task` is in `this.tools`. Fixed at construction; see the fields. */
|
|
525
|
+
isTeamRegistered() {
|
|
526
|
+
return this.teamRegistered;
|
|
527
|
+
}
|
|
528
|
+
/** Whether `task` will actually dispatch. Flipped live by `/team on|off`. */
|
|
529
|
+
isTeamEnabled() {
|
|
530
|
+
return this.teamEnabled;
|
|
531
|
+
}
|
|
532
|
+
/**
|
|
533
|
+
* Flip the live switch and re-render the prompt.
|
|
534
|
+
*
|
|
535
|
+
* Turning it ON in a session that started with `--no-team` deliberately does
|
|
536
|
+
* NOT register the tool (D-17): the array is immutable, and advertising an
|
|
537
|
+
* unregistered tool is the dead end P0-2 is about. `/team` reports that
|
|
538
|
+
* honestly by checking `isTeamRegistered()` first.
|
|
539
|
+
*/
|
|
540
|
+
setTeamEnabled(enabled) {
|
|
541
|
+
this.teamEnabled = enabled;
|
|
542
|
+
this.rebuildSystemPrompt();
|
|
543
|
+
}
|
|
544
|
+
getTeamConfig() {
|
|
545
|
+
return this.config.team;
|
|
546
|
+
}
|
|
547
|
+
/**
|
|
548
|
+
* Adopt a new team config for this session and re-render the prompt (the block
|
|
549
|
+
* quotes the numbers). Clamped here as well as on the persist path, so a bad
|
|
550
|
+
* value can never reach the runtime.
|
|
551
|
+
*/
|
|
552
|
+
setTeamConfig(patch) {
|
|
553
|
+
const team = clampTeamConfig({ ...this.config.team, ...patch });
|
|
554
|
+
this.config = { ...this.config, team };
|
|
555
|
+
this.rebuildSystemPrompt();
|
|
556
|
+
return team;
|
|
557
|
+
}
|
|
558
|
+
/** Refused mid-dispatch by `/team`; half a dispatch under two ceilings is not
|
|
559
|
+
* a result anything can afterwards explain (the reasoning `/reload` uses). */
|
|
560
|
+
isTeamBusy() {
|
|
561
|
+
return this.teamRuntime?.isBusy() ?? false;
|
|
562
|
+
}
|
|
563
|
+
getTeamSnapshot() {
|
|
564
|
+
return this.teamRuntime?.snapshot() ?? null;
|
|
565
|
+
}
|
|
566
|
+
/**
|
|
567
|
+
* Subscribe to the CLI-local team event stream.
|
|
568
|
+
*
|
|
569
|
+
* Returns a no-op unsubscribe when team mode was off at construction, so every
|
|
570
|
+
* caller can subscribe unconditionally.
|
|
571
|
+
*/
|
|
572
|
+
subscribeTeam(listener) {
|
|
573
|
+
if (!this.teamRuntime)
|
|
574
|
+
return () => { };
|
|
575
|
+
return this.teamRuntime.subscribe(listener);
|
|
576
|
+
}
|
|
577
|
+
/** Abort every child and clear timers. Idempotent; safe to call twice. */
|
|
578
|
+
dispose() {
|
|
579
|
+
this.teamRuntime?.dispose();
|
|
580
|
+
}
|
|
406
581
|
// -----------------------------------------------------------------------
|
|
407
582
|
// Mutators
|
|
408
583
|
// -----------------------------------------------------------------------
|
package/dist/agent/headless.d.ts
CHANGED
|
@@ -6,12 +6,24 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import type { AgentEvent, ModelInfo } from '@aragon-agent/core';
|
|
8
8
|
import type { PreflightResult } from './controller.js';
|
|
9
|
+
import type { TeamEvent } from '../team/types.js';
|
|
9
10
|
/** The minimal controller surface headless mode needs (real or stubbed). */
|
|
10
11
|
export interface HeadlessController {
|
|
11
12
|
preflight(): PreflightResult;
|
|
12
13
|
subscribe(listener: (event: AgentEvent) => void): () => void;
|
|
13
14
|
prompt(text: string): Promise<void>;
|
|
14
15
|
getModelInfo(): ModelInfo;
|
|
16
|
+
/**
|
|
17
|
+
* The CLI-local team stream — OPTIONAL, deliberately (team-subagents §3.12 /
|
|
18
|
+
* P1-6).
|
|
19
|
+
*
|
|
20
|
+
* This interface is minimal so tests can satisfy it with an object literal.
|
|
21
|
+
* Making the member REQUIRED would break every existing stub for no benefit,
|
|
22
|
+
* and the subscription genuinely is optional: a controller with no team
|
|
23
|
+
* runtime has no team events. `runHeadless` calls it with `?.`, which keeps
|
|
24
|
+
* the `--no-team` headless path byte-identical to today's.
|
|
25
|
+
*/
|
|
26
|
+
subscribeTeam?(listener: (event: TeamEvent) => void): () => void;
|
|
15
27
|
}
|
|
16
28
|
export interface HeadlessOptions {
|
|
17
29
|
quiet?: boolean;
|
package/dist/agent/headless.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* derived from the event stream (R2), not from the resolved promise:
|
|
5
5
|
* 0 = success, 1 = agent error (incl. a silently-swallowed throw), 2 = config.
|
|
6
6
|
*/
|
|
7
|
-
import { formatCost, formatTokens, computeCost } from './usage.js';
|
|
7
|
+
import { formatCost, formatTokens, formatDuration, computeCost } from './usage.js';
|
|
8
8
|
import { formatStreamError } from './reducer.js';
|
|
9
9
|
/** Run a single prompt headlessly and resolve with the process exit code. */
|
|
10
10
|
export async function runHeadless(controller, prompt, options = {}) {
|
|
@@ -57,11 +57,51 @@ export async function runHeadless(controller, prompt, options = {}) {
|
|
|
57
57
|
break;
|
|
58
58
|
}
|
|
59
59
|
});
|
|
60
|
+
// Team mode is ON in headless: a fan-out needs no human (§3.12). What the
|
|
61
|
+
// children's spend must NOT do is vanish from the footer — the `[usage]` line
|
|
62
|
+
// is the only cost readout `-p` has, and a dispatch is where most of the
|
|
63
|
+
// tokens go.
|
|
64
|
+
const started = new Set();
|
|
65
|
+
const finished = new Set();
|
|
66
|
+
const unsubscribeTeam = controller.subscribeTeam?.((event) => {
|
|
67
|
+
if (event.type === 'usage') {
|
|
68
|
+
total.inputTokens += event.usage.inputTokens;
|
|
69
|
+
total.outputTokens += event.usage.outputTokens;
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (quiet)
|
|
73
|
+
return;
|
|
74
|
+
if (event.type === 'dispatch_start') {
|
|
75
|
+
err.write(`\n[team] dispatch ${event.specs.length} subagents\n`);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (event.type === 'agent_update') {
|
|
79
|
+
const { run } = event;
|
|
80
|
+
if (run.phase !== 'queued' && !started.has(run.label)) {
|
|
81
|
+
started.add(run.label);
|
|
82
|
+
err.write(`[team] ${run.label} start "${run.description}"\n`);
|
|
83
|
+
}
|
|
84
|
+
const terminal = run.phase === 'done' || run.phase === 'failed' || run.phase === 'aborted';
|
|
85
|
+
if (terminal && !finished.has(run.label)) {
|
|
86
|
+
finished.add(run.label);
|
|
87
|
+
const ms = Math.max(0, (run.endedAt ?? 0) - (run.startedAt ?? 0));
|
|
88
|
+
const verdict = run.phase === 'done' && !run.error ? 'ok' : run.phase;
|
|
89
|
+
err.write(`[team] ${run.label} ${verdict} ${formatDuration(ms)} ${run.toolCalls} tools\n`);
|
|
90
|
+
}
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (event.type === 'dispatch_end') {
|
|
94
|
+
const ok = event.outcome.runs.filter((r) => r.phase === 'done' && !r.error).length;
|
|
95
|
+
err.write(`[team] done ${ok}/${event.outcome.runs.length} ok` +
|
|
96
|
+
`${event.outcome.aborted ? ' (aborted)' : ''}\n`);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
60
99
|
try {
|
|
61
100
|
await controller.prompt(prompt);
|
|
62
101
|
}
|
|
63
102
|
finally {
|
|
64
103
|
unsubscribe();
|
|
104
|
+
unsubscribeTeam?.();
|
|
65
105
|
}
|
|
66
106
|
// Ensure a trailing newline so piped output is well-formed.
|
|
67
107
|
if (wroteText)
|
package/dist/agent/reducer.d.ts
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import type { AgentEvent, ModelCost, ToolResult, TokenUsage } from '@aragon-agent/core';
|
|
14
14
|
import type { AgentMode } from './agent-mode.js';
|
|
15
|
+
import type { DispatchOutcome, SubagentRun, SubagentSpec, TeamSnapshot } from '../team/types.js';
|
|
15
16
|
export type NoticeLevel = 'info' | 'warn' | 'error';
|
|
16
17
|
export type ToastLevel = NoticeLevel | 'success';
|
|
17
18
|
export type Overlay = null | 'settings' | 'model' | 'help' | 'confirm' | 'question' | 'plan';
|
|
@@ -55,6 +56,28 @@ export type Entry = {
|
|
|
55
56
|
kind: 'notice';
|
|
56
57
|
level: NoticeLevel;
|
|
57
58
|
text: string;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* A team dispatch (team-subagents §5.3). JSON-SERIALIZABLE by construction, so
|
|
62
|
+
* `/save` and `/resume` need no format change.
|
|
63
|
+
*
|
|
64
|
+
* `active: true` is normalized away ON LOAD, in `session/persist.ts`, not here
|
|
65
|
+
* (P1-5). A session saved mid-dispatch would otherwise resume with a card that
|
|
66
|
+
* claims to be running while nothing is: it spins forever, and because
|
|
67
|
+
* `Transcript`'s settled boundary is MONOTONIC an entry that never settles
|
|
68
|
+
* never reaches `<Static>` and is re-rendered on every frame for the rest of
|
|
69
|
+
* the session. The reducer never legitimately sees a stale-active entry — only
|
|
70
|
+
* the file does — which is why the fix belongs in the load path.
|
|
71
|
+
*/
|
|
72
|
+
| {
|
|
73
|
+
id: string;
|
|
74
|
+
kind: 'team';
|
|
75
|
+
dispatchId: string;
|
|
76
|
+
requested: number;
|
|
77
|
+
runs: SubagentRun[];
|
|
78
|
+
aborted: boolean;
|
|
79
|
+
durationMs?: number;
|
|
80
|
+
active: boolean;
|
|
58
81
|
};
|
|
59
82
|
export interface UsageTotal {
|
|
60
83
|
inputTokens: number;
|
|
@@ -81,7 +104,17 @@ export interface ViewState {
|
|
|
81
104
|
agentMode: AgentMode;
|
|
82
105
|
/** Set only by a DEFERRED `plan -> build`; drives the `PLAN -> BUILD` readout. */
|
|
83
106
|
pendingAgentMode: AgentMode | null;
|
|
107
|
+
/**
|
|
108
|
+
* The live team roster, or `null` when no dispatch is running.
|
|
109
|
+
*
|
|
110
|
+
* NEVER PERSISTED. `/save` writes `entries`, and the settled `team` entry is
|
|
111
|
+
* the history; this field is the ephemeral panel, and a resumed session has no
|
|
112
|
+
* live dispatch to show (§6.4).
|
|
113
|
+
*/
|
|
114
|
+
team: TeamSnapshot | null;
|
|
84
115
|
seq: number;
|
|
116
|
+
/** The `kind: 'team'` entry the live dispatch is writing into. */
|
|
117
|
+
teamEntryId?: string;
|
|
85
118
|
streamingId?: string;
|
|
86
119
|
turnProduced: boolean;
|
|
87
120
|
errorNoticed: boolean;
|
|
@@ -165,6 +198,21 @@ export type ViewAction = {
|
|
|
165
198
|
type: 'setAgentMode';
|
|
166
199
|
mode: AgentMode;
|
|
167
200
|
pending: AgentMode | null;
|
|
201
|
+
} | {
|
|
202
|
+
type: 'teamStart';
|
|
203
|
+
dispatchId: string;
|
|
204
|
+
requested: number;
|
|
205
|
+
specs: SubagentSpec[];
|
|
206
|
+
} | {
|
|
207
|
+
type: 'teamUpdate';
|
|
208
|
+
snapshot: TeamSnapshot;
|
|
209
|
+
} | {
|
|
210
|
+
type: 'teamUsage';
|
|
211
|
+
usage: TokenUsage;
|
|
212
|
+
costDelta: number;
|
|
213
|
+
} | {
|
|
214
|
+
type: 'teamEnd';
|
|
215
|
+
outcome: DispatchOutcome;
|
|
168
216
|
};
|
|
169
217
|
export declare const STORED_PREVIEW_CHARS = 8000;
|
|
170
218
|
export declare const STORED_PREVIEW_LINES = 200;
|
package/dist/agent/reducer.js
CHANGED
|
@@ -27,7 +27,9 @@ export function initialViewState() {
|
|
|
27
27
|
// `controller.getAgentMode()` so `--plan` renders on the first frame.
|
|
28
28
|
agentMode: 'build',
|
|
29
29
|
pendingAgentMode: null,
|
|
30
|
+
team: null,
|
|
30
31
|
seq: 0,
|
|
32
|
+
teamEntryId: undefined,
|
|
31
33
|
streamingId: undefined,
|
|
32
34
|
turnProduced: false,
|
|
33
35
|
errorNoticed: false,
|
|
@@ -367,8 +369,17 @@ export function viewReducer(state, action) {
|
|
|
367
369
|
entries: mapEntry(state.entries, state.streamingId, (e) => e.kind === 'assistant' ? { ...e, streaming: false, aborted: true } : e),
|
|
368
370
|
};
|
|
369
371
|
}
|
|
372
|
+
// `teamEntryId` is dropped with the entries it pointed at. Keeping it would
|
|
373
|
+
// leave `teamUpdate` mapping an id that no longer exists — harmless today,
|
|
374
|
+
// and a dangling reference the next reader has to reason about.
|
|
370
375
|
case 'clearTranscript':
|
|
371
|
-
return {
|
|
376
|
+
return {
|
|
377
|
+
...state,
|
|
378
|
+
entries: [],
|
|
379
|
+
streamingId: undefined,
|
|
380
|
+
expandedToolIds: {},
|
|
381
|
+
teamEntryId: undefined,
|
|
382
|
+
};
|
|
372
383
|
case 'resetConversation':
|
|
373
384
|
return {
|
|
374
385
|
...state,
|
|
@@ -377,6 +388,7 @@ export function viewReducer(state, action) {
|
|
|
377
388
|
expandedToolIds: {},
|
|
378
389
|
contextTokens: 0,
|
|
379
390
|
usageTotal: { inputTokens: 0, outputTokens: 0, costUsd: 0 },
|
|
391
|
+
teamEntryId: undefined,
|
|
380
392
|
};
|
|
381
393
|
case 'setOverlay':
|
|
382
394
|
return { ...state, overlay: action.overlay };
|
|
@@ -407,13 +419,90 @@ export function viewReducer(state, action) {
|
|
|
407
419
|
next[action.id] = true;
|
|
408
420
|
return { ...state, expandedToolIds: next };
|
|
409
421
|
}
|
|
422
|
+
// --- Team mode (team-subagents §5.3) ---------------------------------
|
|
423
|
+
case 'teamStart': {
|
|
424
|
+
const { id, seq } = nextId(state);
|
|
425
|
+
const runs = action.specs.map((spec) => ({
|
|
426
|
+
label: spec.label,
|
|
427
|
+
description: spec.description,
|
|
428
|
+
phase: 'queued',
|
|
429
|
+
turns: 0,
|
|
430
|
+
toolCalls: 0,
|
|
431
|
+
usage: { inputTokens: 0, outputTokens: 0 },
|
|
432
|
+
filesTouched: [],
|
|
433
|
+
messagesSent: 0,
|
|
434
|
+
}));
|
|
435
|
+
const entry = {
|
|
436
|
+
id,
|
|
437
|
+
kind: 'team',
|
|
438
|
+
dispatchId: action.dispatchId,
|
|
439
|
+
requested: action.requested,
|
|
440
|
+
runs,
|
|
441
|
+
aborted: false,
|
|
442
|
+
active: true,
|
|
443
|
+
};
|
|
444
|
+
return {
|
|
445
|
+
...state,
|
|
446
|
+
seq,
|
|
447
|
+
entries: [...state.entries, entry],
|
|
448
|
+
teamEntryId: id,
|
|
449
|
+
team: {
|
|
450
|
+
dispatchId: action.dispatchId,
|
|
451
|
+
active: true,
|
|
452
|
+
runs,
|
|
453
|
+
requested: action.requested,
|
|
454
|
+
startedAt: Date.now(),
|
|
455
|
+
messageCount: 0,
|
|
456
|
+
},
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
case 'teamUpdate': {
|
|
460
|
+
// One `mapEntry` on the KNOWN id, never a scan: this action arrives at up
|
|
461
|
+
// to `maxConcurrent` x 8/s and a reverse search over a long transcript on
|
|
462
|
+
// every one of them is exactly the cost the coalescer exists to avoid.
|
|
463
|
+
const entries = state.teamEntryId
|
|
464
|
+
? mapEntry(state.entries, state.teamEntryId, (e) => e.kind === 'team' ? { ...e, runs: action.snapshot.runs } : e)
|
|
465
|
+
: state.entries;
|
|
466
|
+
return { ...state, entries, team: action.snapshot };
|
|
467
|
+
}
|
|
468
|
+
case 'teamUsage':
|
|
469
|
+
// `usageTotal` AND NOTHING ELSE (§3.9 / AC-15). In particular NOT
|
|
470
|
+
// `contextTokens`: that gauge shows the LEAD's context occupancy against
|
|
471
|
+
// the model's window, and folding five children into it would read 180%
|
|
472
|
+
// on a perfectly healthy session. Child spend is real money and has to
|
|
473
|
+
// appear in the cost readout; it is not the lead's context.
|
|
474
|
+
return {
|
|
475
|
+
...state,
|
|
476
|
+
usageTotal: {
|
|
477
|
+
inputTokens: state.usageTotal.inputTokens + action.usage.inputTokens,
|
|
478
|
+
outputTokens: state.usageTotal.outputTokens + action.usage.outputTokens,
|
|
479
|
+
costUsd: state.usageTotal.costUsd + action.costDelta,
|
|
480
|
+
},
|
|
481
|
+
};
|
|
482
|
+
case 'teamEnd': {
|
|
483
|
+
const { outcome } = action;
|
|
484
|
+
const entries = state.teamEntryId
|
|
485
|
+
? mapEntry(state.entries, state.teamEntryId, (e) => e.kind === 'team'
|
|
486
|
+
? {
|
|
487
|
+
...e,
|
|
488
|
+
runs: outcome.runs,
|
|
489
|
+
aborted: outcome.aborted,
|
|
490
|
+
durationMs: Math.max(0, outcome.endedAt - outcome.startedAt),
|
|
491
|
+
active: false,
|
|
492
|
+
}
|
|
493
|
+
: e)
|
|
494
|
+
: state.entries;
|
|
495
|
+
// `team: null` is what makes the panel disappear — the requirement's
|
|
496
|
+
// "(dang you de shi hou)": it renders only while a dispatch is running.
|
|
497
|
+
return { ...state, entries, team: null, teamEntryId: undefined };
|
|
498
|
+
}
|
|
410
499
|
case 'restoreEntries': {
|
|
411
500
|
// Adopt a seq beyond any restored id to keep future ids unique.
|
|
412
501
|
const maxSeq = action.entries.reduce((acc, e) => {
|
|
413
502
|
const n = Number.parseInt(e.id.replace(/^e/, ''), 10);
|
|
414
503
|
return Number.isFinite(n) ? Math.max(acc, n) : acc;
|
|
415
504
|
}, state.seq);
|
|
416
|
-
return { ...state, entries: [...action.entries], seq: maxSeq };
|
|
505
|
+
return { ...state, entries: [...action.entries], seq: maxSeq, teamEntryId: undefined };
|
|
417
506
|
}
|
|
418
507
|
default:
|
|
419
508
|
return state;
|
|
@@ -36,6 +36,23 @@ export interface SystemPromptParams {
|
|
|
36
36
|
planInteractive?: boolean;
|
|
37
37
|
/** `planModeMaxAskRounds`, substituted into the block so the two agree. */
|
|
38
38
|
planMaxAskRounds?: number;
|
|
39
|
+
/**
|
|
40
|
+
* The rendered `<team_mode>` block for a LEAD with team mode on, or `''`.
|
|
41
|
+
*
|
|
42
|
+
* Spliced conditionally exactly as `skillsBlock` and the plan block are, which
|
|
43
|
+
* is what keeps invariant I-8 true: `team.enabled: false` (and `--no-team`)
|
|
44
|
+
* produce the pre-team prompt BYTE FOR BYTE.
|
|
45
|
+
*/
|
|
46
|
+
teamBlock?: string;
|
|
47
|
+
/**
|
|
48
|
+
* The rendered `<subagent_role>` block for a CHILD, or `''`.
|
|
49
|
+
*
|
|
50
|
+
* Never set together with `teamBlock`: a child has no `task` tool (D-3), and a
|
|
51
|
+
* lead is not a subagent. Two fields rather than one because the two blocks
|
|
52
|
+
* carry opposite instructions and a single "team text" parameter would make
|
|
53
|
+
* mixing them a one-character mistake.
|
|
54
|
+
*/
|
|
55
|
+
subagentBlock?: string;
|
|
39
56
|
}
|
|
40
57
|
/** Describe the active shell so the model does not emit incompatible syntax. */
|
|
41
58
|
export declare function describeShell(): {
|
|
@@ -37,6 +37,8 @@ export function buildSystemPrompt(params) {
|
|
|
37
37
|
maxAskRounds: params.planMaxAskRounds ?? 4,
|
|
38
38
|
})
|
|
39
39
|
: '';
|
|
40
|
+
const teamBlock = params.teamBlock ?? '';
|
|
41
|
+
const subagentBlock = params.subagentBlock ?? '';
|
|
40
42
|
return [
|
|
41
43
|
'You are AragonAgent, an autonomous coding assistant operating inside a terminal (TUI).',
|
|
42
44
|
'You help the user accomplish software-engineering tasks by reasoning step by step and using the provided tools.',
|
|
@@ -50,6 +52,8 @@ export function buildSystemPrompt(params) {
|
|
|
50
52
|
toolList,
|
|
51
53
|
...(hasSkills ? ['', skillsBlock] : []),
|
|
52
54
|
...(planBlock ? ['', planBlock] : []),
|
|
55
|
+
...(teamBlock ? ['', teamBlock] : []),
|
|
56
|
+
...(subagentBlock ? ['', subagentBlock] : []),
|
|
53
57
|
'',
|
|
54
58
|
'Operating guidance:',
|
|
55
59
|
'- Prefer reading files and inspecting the workspace before making changes.',
|