@aragon-agent/cli 0.5.2 → 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 +90 -0
- package/README.md +128 -1
- 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 +66 -2
- 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 +28 -2
- 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 +65 -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 +112 -16
- package/dist/config/schema.js +89 -6
- 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/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.js +97 -13
- 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 +12 -0
- 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
package/dist/config/store.js
CHANGED
|
@@ -14,7 +14,7 @@ import { dirname } from 'node:path';
|
|
|
14
14
|
import process from 'node:process';
|
|
15
15
|
import { getConfigPath, getSessionsDir } from './app-paths.js';
|
|
16
16
|
import { registerSecretsFrom } from '../logging/secret-registry.js';
|
|
17
|
-
import { CONFIG_VERSION, DEFAULT_CONFIG, clampLogConfig, clampSkillsConfig, } from './schema.js';
|
|
17
|
+
import { CONFIG_VERSION, DEFAULT_CONFIG, clampLogConfig, clampSkillsConfig, clampTeamConfig, stripLegacyStateKeys, } from './schema.js';
|
|
18
18
|
// Re-exported so the many existing importers of `store.js` keep working; the
|
|
19
19
|
// definitions live in `app-paths.ts` because they are pure path arithmetic.
|
|
20
20
|
export { getConfigPath, getSessionsDir };
|
|
@@ -82,19 +82,24 @@ export function writeConfigFile(config) {
|
|
|
82
82
|
* Unlike `readConfigFile`, this always returns a complete `PersistedConfig`.
|
|
83
83
|
*/
|
|
84
84
|
export function loadPersistedConfig() {
|
|
85
|
-
|
|
85
|
+
// STRIP BEFORE THE SPREAD, not after. `partial` is the raw file, so any
|
|
86
|
+
// `promptHistory` / `submitCount` / `mouseNoticeSeen` / `recentModels` left
|
|
87
|
+
// on disk would otherwise ride the spread into `merged` and be written back
|
|
88
|
+
// out by `updatePersistedConfig` — the "I deleted it and it grew back" bug
|
|
89
|
+
// that `stripLegacyStateKeys` exists to make impossible (§4.5).
|
|
90
|
+
const partial = stripLegacyStateKeys(readConfigFile().config ?? {});
|
|
86
91
|
return {
|
|
87
92
|
...DEFAULT_CONFIG,
|
|
88
93
|
...partial,
|
|
89
94
|
version: CONFIG_VERSION,
|
|
90
95
|
apiKeys: { ...DEFAULT_CONFIG.apiKeys, ...(partial.apiKeys ?? {}) },
|
|
91
|
-
recentModels: partial.recentModels ?? [],
|
|
92
|
-
promptHistory: partial.promptHistory ?? [],
|
|
93
96
|
// `skills` is the second nested object in this file and needs the same
|
|
94
97
|
// treatment `apiKeys` gets — see the note on updatePersistedConfig below.
|
|
95
98
|
skills: clampSkillsConfig({ ...DEFAULT_CONFIG.skills, ...(partial.skills ?? {}) }),
|
|
96
99
|
// `log` is the third. Same one-level rule, same merge.
|
|
97
100
|
log: clampLogConfig({ ...DEFAULT_CONFIG.log, ...(partial.log ?? {}) }),
|
|
101
|
+
// `team` is the fourth. Same one-level rule, same merge.
|
|
102
|
+
team: clampTeamConfig({ ...DEFAULT_CONFIG.team, ...(partial.team ?? {}) }),
|
|
98
103
|
};
|
|
99
104
|
}
|
|
100
105
|
/**
|
|
@@ -132,6 +137,10 @@ export function updatePersistedConfig(patch) {
|
|
|
132
137
|
// replace the whole section and quietly revert `maxFiles`, `previewChars`
|
|
133
138
|
// and — worst of all — `redactSecrets`.
|
|
134
139
|
log: clampLogConfig({ ...current.log, ...(patch.log ?? {}) }),
|
|
140
|
+
// The fourth section, and it needs the merge for the same reason: `/team max
|
|
141
|
+
// 4` sends `{ team: { maxSubagents: 4 } }`, which a shallow spread would
|
|
142
|
+
// turn into "changing the fan-out width silently reverted every timeout".
|
|
143
|
+
team: clampTeamConfig({ ...current.team, ...(patch.team ?? {}) }),
|
|
135
144
|
};
|
|
136
145
|
writeConfigFile(merged);
|
|
137
146
|
return merged;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `<home>/state.json` — UI bookkeeping (config-state-separation §4.4).
|
|
3
|
+
*
|
|
4
|
+
* ══════════════════════════════════════════════════════════════════════════
|
|
5
|
+
* WHAT MAY LIVE HERE: UI bookkeeping SCALARS. Counters, and "has this one-shot
|
|
6
|
+
* notice been shown" flags. NOTHING ELSE.
|
|
7
|
+
*
|
|
8
|
+
* Anything carrying user content, and anything that grows with usage, opens its
|
|
9
|
+
* own file. This rule is not a preference — it is the entire reason this module
|
|
10
|
+
* exists. `config.json` became a place where a prompt-by-prompt transcript sat
|
|
11
|
+
* next to the API key precisely because nobody had written a rule like this
|
|
12
|
+
* down, and a file called `state.json` is the most inviting drawer in the
|
|
13
|
+
* codebase (Q-4 / RV-8).
|
|
14
|
+
* ══════════════════════════════════════════════════════════════════════════
|
|
15
|
+
*
|
|
16
|
+
* Nothing here is a preference: none of these fields has a flag, an env var or
|
|
17
|
+
* a `config set` key, and none of them should ever get one. Losing the file
|
|
18
|
+
* costs a composer hint reappearing once.
|
|
19
|
+
*
|
|
20
|
+
* FAILURE POLICY (C2): never throws, exactly like `prompt-history.ts` and
|
|
21
|
+
* `skills/usage.ts`. Writes are immediate rather than debounced (D-7) — the
|
|
22
|
+
* file is one small object, and a debounce would add a flush-on-exit obligation
|
|
23
|
+
* whose failure mode is the counter silently not sticking.
|
|
24
|
+
*
|
|
25
|
+
* NOT the same thing as `migrate-legacy-state.ts`, despite the shared word:
|
|
26
|
+
* that one is the 0.4.x legacy-to-current brand migration and has nothing to do
|
|
27
|
+
* with this file.
|
|
28
|
+
*/
|
|
29
|
+
export declare const UI_STATE_FILENAME = "state.json";
|
|
30
|
+
export declare const UI_STATE_SCHEMA = 1;
|
|
31
|
+
export interface UiState {
|
|
32
|
+
schema: number;
|
|
33
|
+
/** Lifetime submit count; drives the composer hint fade (`HINT_FADE_AFTER`). */
|
|
34
|
+
submitCount: number;
|
|
35
|
+
/** Whether the one-shot "hold Shift to select text" notice has been shown. */
|
|
36
|
+
mouseNoticeSeen: boolean;
|
|
37
|
+
}
|
|
38
|
+
/** Read the state, caching it for the life of the process. Never throws. */
|
|
39
|
+
export declare function loadUiState(): UiState;
|
|
40
|
+
export declare function readSubmitCount(): number;
|
|
41
|
+
/** Increment, persist, and return the new value. */
|
|
42
|
+
export declare function bumpSubmitCount(): number;
|
|
43
|
+
export declare function getMouseNoticeSeen(): boolean;
|
|
44
|
+
export declare function setMouseNoticeSeen(seen: boolean): void;
|
|
45
|
+
/**
|
|
46
|
+
* Migration-only: fill in fields that are still at their default, and never
|
|
47
|
+
* overwrite a value already here. A second import — after a half-finished
|
|
48
|
+
* migration retries — must not walk the counter backwards.
|
|
49
|
+
*/
|
|
50
|
+
export declare function importLegacyUiState(patch: Partial<Pick<UiState, 'submitCount' | 'mouseNoticeSeen'>>): void;
|
|
51
|
+
/** Test-only: forget the cached state. */
|
|
52
|
+
export declare function resetUiStateForTests(): void;
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `<home>/state.json` — UI bookkeeping (config-state-separation §4.4).
|
|
3
|
+
*
|
|
4
|
+
* ══════════════════════════════════════════════════════════════════════════
|
|
5
|
+
* WHAT MAY LIVE HERE: UI bookkeeping SCALARS. Counters, and "has this one-shot
|
|
6
|
+
* notice been shown" flags. NOTHING ELSE.
|
|
7
|
+
*
|
|
8
|
+
* Anything carrying user content, and anything that grows with usage, opens its
|
|
9
|
+
* own file. This rule is not a preference — it is the entire reason this module
|
|
10
|
+
* exists. `config.json` became a place where a prompt-by-prompt transcript sat
|
|
11
|
+
* next to the API key precisely because nobody had written a rule like this
|
|
12
|
+
* down, and a file called `state.json` is the most inviting drawer in the
|
|
13
|
+
* codebase (Q-4 / RV-8).
|
|
14
|
+
* ══════════════════════════════════════════════════════════════════════════
|
|
15
|
+
*
|
|
16
|
+
* Nothing here is a preference: none of these fields has a flag, an env var or
|
|
17
|
+
* a `config set` key, and none of them should ever get one. Losing the file
|
|
18
|
+
* costs a composer hint reappearing once.
|
|
19
|
+
*
|
|
20
|
+
* FAILURE POLICY (C2): never throws, exactly like `prompt-history.ts` and
|
|
21
|
+
* `skills/usage.ts`. Writes are immediate rather than debounced (D-7) — the
|
|
22
|
+
* file is one small object, and a debounce would add a flush-on-exit obligation
|
|
23
|
+
* whose failure mode is the counter silently not sticking.
|
|
24
|
+
*
|
|
25
|
+
* NOT the same thing as `migrate-legacy-state.ts`, despite the shared word:
|
|
26
|
+
* that one is the 0.4.x legacy-to-current brand migration and has nothing to do
|
|
27
|
+
* with this file.
|
|
28
|
+
*/
|
|
29
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync, } from 'node:fs';
|
|
30
|
+
import process from 'node:process';
|
|
31
|
+
import { getHomeRoot, getUiStatePath } from './app-paths.js';
|
|
32
|
+
export const UI_STATE_FILENAME = 'state.json';
|
|
33
|
+
export const UI_STATE_SCHEMA = 1;
|
|
34
|
+
const DEFAULT_UI_STATE = {
|
|
35
|
+
schema: UI_STATE_SCHEMA,
|
|
36
|
+
submitCount: 0,
|
|
37
|
+
mouseNoticeSeen: false,
|
|
38
|
+
};
|
|
39
|
+
let memory = null;
|
|
40
|
+
function coerceCount(value) {
|
|
41
|
+
const n = typeof value === 'number' ? value : Number.parseInt(String(value ?? ''), 10);
|
|
42
|
+
if (!Number.isFinite(n) || n < 0)
|
|
43
|
+
return DEFAULT_UI_STATE.submitCount;
|
|
44
|
+
return Math.floor(n);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Per-field tolerance: one nonsense value must not take the other field with
|
|
48
|
+
* it. A wrong SCHEMA is different — the shape is no longer known, so the whole
|
|
49
|
+
* object falls back (the rule `skills/usage.ts` already follows).
|
|
50
|
+
*/
|
|
51
|
+
function readFromDisk() {
|
|
52
|
+
const path = getUiStatePath();
|
|
53
|
+
if (!existsSync(path))
|
|
54
|
+
return { ...DEFAULT_UI_STATE };
|
|
55
|
+
try {
|
|
56
|
+
const parsed = JSON.parse(readFileSync(path, 'utf-8'));
|
|
57
|
+
if (!parsed || typeof parsed !== 'object')
|
|
58
|
+
return { ...DEFAULT_UI_STATE };
|
|
59
|
+
if (parsed.schema !== UI_STATE_SCHEMA)
|
|
60
|
+
return { ...DEFAULT_UI_STATE };
|
|
61
|
+
return {
|
|
62
|
+
schema: UI_STATE_SCHEMA,
|
|
63
|
+
submitCount: coerceCount(parsed.submitCount),
|
|
64
|
+
mouseNoticeSeen: typeof parsed.mouseNoticeSeen === 'boolean'
|
|
65
|
+
? parsed.mouseNoticeSeen
|
|
66
|
+
: DEFAULT_UI_STATE.mouseNoticeSeen,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return { ...DEFAULT_UI_STATE };
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/** Read the state, caching it for the life of the process. Never throws. */
|
|
74
|
+
export function loadUiState() {
|
|
75
|
+
if (memory === null)
|
|
76
|
+
memory = readFromDisk();
|
|
77
|
+
return memory;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Atomic write: temp file, rename, re-chmod.
|
|
81
|
+
*
|
|
82
|
+
* `mkdirSync` first for the reason spelled out in `prompt-history.ts`: `<home>`
|
|
83
|
+
* is not guaranteed to exist, and without it the counter would silently never
|
|
84
|
+
* persist on a fresh install that also turned file logging off (RV-2).
|
|
85
|
+
* The `chmodSync` matches `store.ts` — rename does not preserve mode on every
|
|
86
|
+
* platform (RV-6).
|
|
87
|
+
*/
|
|
88
|
+
function writeToDisk(state) {
|
|
89
|
+
const path = getUiStatePath();
|
|
90
|
+
const tmp = `${path}.tmp-${process.pid}`;
|
|
91
|
+
try {
|
|
92
|
+
mkdirSync(getHomeRoot(), { recursive: true });
|
|
93
|
+
writeFileSync(tmp, `${JSON.stringify(state, null, 2)}\n`, { encoding: 'utf-8', mode: 0o600 });
|
|
94
|
+
renameSync(tmp, path);
|
|
95
|
+
if (process.platform !== 'win32')
|
|
96
|
+
chmodSync(path, 0o600);
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
try {
|
|
100
|
+
rmSync(tmp, { force: true });
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
// Nothing further to try; the value simply stays in memory (C2).
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
function update(patch) {
|
|
108
|
+
const next = { ...loadUiState(), ...patch, schema: UI_STATE_SCHEMA };
|
|
109
|
+
memory = next;
|
|
110
|
+
writeToDisk(next);
|
|
111
|
+
return next;
|
|
112
|
+
}
|
|
113
|
+
export function readSubmitCount() {
|
|
114
|
+
return loadUiState().submitCount;
|
|
115
|
+
}
|
|
116
|
+
/** Increment, persist, and return the new value. */
|
|
117
|
+
export function bumpSubmitCount() {
|
|
118
|
+
return update({ submitCount: readSubmitCount() + 1 }).submitCount;
|
|
119
|
+
}
|
|
120
|
+
export function getMouseNoticeSeen() {
|
|
121
|
+
return loadUiState().mouseNoticeSeen;
|
|
122
|
+
}
|
|
123
|
+
export function setMouseNoticeSeen(seen) {
|
|
124
|
+
update({ mouseNoticeSeen: seen });
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Migration-only: fill in fields that are still at their default, and never
|
|
128
|
+
* overwrite a value already here. A second import — after a half-finished
|
|
129
|
+
* migration retries — must not walk the counter backwards.
|
|
130
|
+
*/
|
|
131
|
+
export function importLegacyUiState(patch) {
|
|
132
|
+
const current = loadUiState();
|
|
133
|
+
const next = {};
|
|
134
|
+
if (patch.submitCount !== undefined &&
|
|
135
|
+
current.submitCount === DEFAULT_UI_STATE.submitCount) {
|
|
136
|
+
next.submitCount = coerceCount(patch.submitCount);
|
|
137
|
+
}
|
|
138
|
+
if (patch.mouseNoticeSeen !== undefined &&
|
|
139
|
+
current.mouseNoticeSeen === DEFAULT_UI_STATE.mouseNoticeSeen) {
|
|
140
|
+
next.mouseNoticeSeen = patch.mouseNoticeSeen === true;
|
|
141
|
+
}
|
|
142
|
+
if (Object.keys(next).length === 0)
|
|
143
|
+
return;
|
|
144
|
+
update(next);
|
|
145
|
+
}
|
|
146
|
+
/** Test-only: forget the cached state. */
|
|
147
|
+
export function resetUiStateForTests() {
|
|
148
|
+
memory = null;
|
|
149
|
+
}
|
|
@@ -161,3 +161,88 @@ export type AgentLogEvent = {
|
|
|
161
161
|
* (or a future second controller) read another's clock.
|
|
162
162
|
*/
|
|
163
163
|
export declare function attachAgentEvents(logger: Logger, controller: AgentEventSource): () => void;
|
|
164
|
+
/**
|
|
165
|
+
* What `attachTeamEvents` needs from a controller. Structural for the same
|
|
166
|
+
* reason `AgentEventSource` is, and OPTIONAL on the method: a controller built
|
|
167
|
+
* with team mode off has no runtime and therefore no events.
|
|
168
|
+
*/
|
|
169
|
+
export interface TeamEventSource {
|
|
170
|
+
subscribeTeam?(listener: (event: TeamLogEvent) => void): () => void;
|
|
171
|
+
}
|
|
172
|
+
/** The five members of the CLI-local `TeamEvent` union, as this module reads them. */
|
|
173
|
+
export type TeamLogEvent = {
|
|
174
|
+
type: 'dispatch_start';
|
|
175
|
+
dispatchId: string;
|
|
176
|
+
requested: number;
|
|
177
|
+
specs?: Array<{
|
|
178
|
+
label?: string;
|
|
179
|
+
description?: string;
|
|
180
|
+
prompt?: string;
|
|
181
|
+
}>;
|
|
182
|
+
} | {
|
|
183
|
+
type: 'agent_update';
|
|
184
|
+
dispatchId: string;
|
|
185
|
+
run?: {
|
|
186
|
+
label?: string;
|
|
187
|
+
phase?: string;
|
|
188
|
+
lastTool?: string;
|
|
189
|
+
};
|
|
190
|
+
} | {
|
|
191
|
+
type: 'usage';
|
|
192
|
+
dispatchId: string;
|
|
193
|
+
label?: string;
|
|
194
|
+
usage?: unknown;
|
|
195
|
+
} | {
|
|
196
|
+
type: 'message';
|
|
197
|
+
dispatchId: string;
|
|
198
|
+
message?: {
|
|
199
|
+
from?: string;
|
|
200
|
+
to?: string;
|
|
201
|
+
subject?: string;
|
|
202
|
+
body?: string;
|
|
203
|
+
};
|
|
204
|
+
} | {
|
|
205
|
+
type: 'dispatch_end';
|
|
206
|
+
dispatchId: string;
|
|
207
|
+
outcome?: {
|
|
208
|
+
startedAt?: number;
|
|
209
|
+
endedAt?: number;
|
|
210
|
+
aborted?: boolean;
|
|
211
|
+
runs?: Array<{
|
|
212
|
+
phase?: string;
|
|
213
|
+
error?: string;
|
|
214
|
+
filesTouched?: string[];
|
|
215
|
+
}>;
|
|
216
|
+
usage?: {
|
|
217
|
+
inputTokens?: number;
|
|
218
|
+
outputTokens?: number;
|
|
219
|
+
};
|
|
220
|
+
};
|
|
221
|
+
};
|
|
222
|
+
/**
|
|
223
|
+
* Record a dispatch's lifecycle.
|
|
224
|
+
*
|
|
225
|
+
* WITHOUT THIS, A DISPATCH LEAVES NO TRACE IN THE LOG AT ALL (P1-2 / R-19).
|
|
226
|
+
* `TeamEvent` is deliberately CLI-local (D-10), so `attachAgentEvents` cannot
|
|
227
|
+
* see it — which is the wrong outcome for the one feature in this CLI that runs
|
|
228
|
+
* five agents the user cannot watch, and whose only other artefact is a report
|
|
229
|
+
* written by the thing that failed. A separate function rather than a branch in
|
|
230
|
+
* `attachAgentEvents` (D-19): the two consume different unions and want
|
|
231
|
+
* different level policies.
|
|
232
|
+
*
|
|
233
|
+
* TWO RULES, BOTH ABOUT NOT TURNING A LOG INTO A LIABILITY:
|
|
234
|
+
*
|
|
235
|
+
* - A subagent's PROMPT and its SUMMARY are `trace` only, and go through the
|
|
236
|
+
* same redaction every other record does. A `prompt` is up to 8 000
|
|
237
|
+
* characters of whatever the lead decided to say; at `debug` it would
|
|
238
|
+
* dominate the file.
|
|
239
|
+
* - A `team_send` BODY is never recorded at any level. The subject is enough to
|
|
240
|
+
* reconstruct who talked to whom and when, which is the diagnostic question;
|
|
241
|
+
* the body is user content that reached no other sink.
|
|
242
|
+
*
|
|
243
|
+
* `agent_update` is recorded on PHASE TRANSITIONS ONLY, not on the 120 ms
|
|
244
|
+
* coalescer tick: that throttle exists to protect React, and five children at
|
|
245
|
+
* eight events a second each is 40 lines a second of noise that would push the
|
|
246
|
+
* retention window down to minutes.
|
|
247
|
+
*/
|
|
248
|
+
export declare function attachTeamEvents(logger: Logger, controller: TeamEventSource): () => void;
|
package/dist/logging/install.js
CHANGED
|
@@ -300,3 +300,97 @@ export function attachAgentEvents(logger, controller) {
|
|
|
300
300
|
}
|
|
301
301
|
});
|
|
302
302
|
}
|
|
303
|
+
/**
|
|
304
|
+
* Record a dispatch's lifecycle.
|
|
305
|
+
*
|
|
306
|
+
* WITHOUT THIS, A DISPATCH LEAVES NO TRACE IN THE LOG AT ALL (P1-2 / R-19).
|
|
307
|
+
* `TeamEvent` is deliberately CLI-local (D-10), so `attachAgentEvents` cannot
|
|
308
|
+
* see it — which is the wrong outcome for the one feature in this CLI that runs
|
|
309
|
+
* five agents the user cannot watch, and whose only other artefact is a report
|
|
310
|
+
* written by the thing that failed. A separate function rather than a branch in
|
|
311
|
+
* `attachAgentEvents` (D-19): the two consume different unions and want
|
|
312
|
+
* different level policies.
|
|
313
|
+
*
|
|
314
|
+
* TWO RULES, BOTH ABOUT NOT TURNING A LOG INTO A LIABILITY:
|
|
315
|
+
*
|
|
316
|
+
* - A subagent's PROMPT and its SUMMARY are `trace` only, and go through the
|
|
317
|
+
* same redaction every other record does. A `prompt` is up to 8 000
|
|
318
|
+
* characters of whatever the lead decided to say; at `debug` it would
|
|
319
|
+
* dominate the file.
|
|
320
|
+
* - A `team_send` BODY is never recorded at any level. The subject is enough to
|
|
321
|
+
* reconstruct who talked to whom and when, which is the diagnostic question;
|
|
322
|
+
* the body is user content that reached no other sink.
|
|
323
|
+
*
|
|
324
|
+
* `agent_update` is recorded on PHASE TRANSITIONS ONLY, not on the 120 ms
|
|
325
|
+
* coalescer tick: that throttle exists to protect React, and five children at
|
|
326
|
+
* eight events a second each is 40 lines a second of noise that would push the
|
|
327
|
+
* retention window down to minutes.
|
|
328
|
+
*/
|
|
329
|
+
export function attachTeamEvents(logger, controller) {
|
|
330
|
+
if (!controller.subscribeTeam)
|
|
331
|
+
return () => { };
|
|
332
|
+
const lastPhase = new Map();
|
|
333
|
+
return controller.subscribeTeam((event) => {
|
|
334
|
+
switch (event.type) {
|
|
335
|
+
case 'dispatch_start': {
|
|
336
|
+
const specs = event.specs ?? [];
|
|
337
|
+
logger.info('agent', 'team_dispatch_start', {
|
|
338
|
+
dispatchId: event.dispatchId,
|
|
339
|
+
requested: event.requested,
|
|
340
|
+
accepted: specs.length,
|
|
341
|
+
labels: specs.map((s) => s.label ?? '?'),
|
|
342
|
+
});
|
|
343
|
+
for (const spec of specs) {
|
|
344
|
+
logger.trace('agent', 'team_subagent_brief', {
|
|
345
|
+
dispatchId: event.dispatchId,
|
|
346
|
+
label: spec.label,
|
|
347
|
+
description: spec.description,
|
|
348
|
+
prompt: spec.prompt,
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
break;
|
|
352
|
+
}
|
|
353
|
+
case 'agent_update': {
|
|
354
|
+
const key = `${event.dispatchId}:${event.run?.label ?? '?'}`;
|
|
355
|
+
const phase = event.run?.phase ?? 'unknown';
|
|
356
|
+
if (lastPhase.get(key) === phase)
|
|
357
|
+
break;
|
|
358
|
+
lastPhase.set(key, phase);
|
|
359
|
+
logger.debug('agent', 'team_agent_update', {
|
|
360
|
+
dispatchId: event.dispatchId,
|
|
361
|
+
label: event.run?.label,
|
|
362
|
+
phase,
|
|
363
|
+
lastTool: event.run?.lastTool,
|
|
364
|
+
});
|
|
365
|
+
break;
|
|
366
|
+
}
|
|
367
|
+
case 'message':
|
|
368
|
+
logger.debug('agent', 'team_message', {
|
|
369
|
+
dispatchId: event.dispatchId,
|
|
370
|
+
from: event.message?.from,
|
|
371
|
+
to: event.message?.to,
|
|
372
|
+
// SUBJECT ONLY. See the rule above; the body never leaves the process.
|
|
373
|
+
subject: event.message?.subject,
|
|
374
|
+
});
|
|
375
|
+
break;
|
|
376
|
+
case 'dispatch_end': {
|
|
377
|
+
const outcome = event.outcome;
|
|
378
|
+
const runs = outcome?.runs ?? [];
|
|
379
|
+
const ok = runs.filter((r) => r.phase === 'done' && !r.error).length;
|
|
380
|
+
logger.info('agent', 'team_dispatch_end', {
|
|
381
|
+
dispatchId: event.dispatchId,
|
|
382
|
+
durationMs: Math.max(0, (outcome?.endedAt ?? 0) - (outcome?.startedAt ?? 0)),
|
|
383
|
+
ok,
|
|
384
|
+
failed: runs.length - ok,
|
|
385
|
+
aborted: outcome?.aborted === true,
|
|
386
|
+
in: outcome?.usage?.inputTokens ?? 0,
|
|
387
|
+
out: outcome?.usage?.outputTokens ?? 0,
|
|
388
|
+
filesTouched: [...new Set(runs.flatMap((r) => r.filesTouched ?? []))],
|
|
389
|
+
});
|
|
390
|
+
break;
|
|
391
|
+
}
|
|
392
|
+
default:
|
|
393
|
+
break;
|
|
394
|
+
}
|
|
395
|
+
});
|
|
396
|
+
}
|
package/dist/logging/logger.d.ts
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
import type { LogConfig } from '../config/schema.js';
|
|
24
24
|
import { type LogLevelName } from './levels.js';
|
|
25
25
|
/** Closed set, not free text: a typo'd scope is a record nobody can filter on. */
|
|
26
|
-
export type LogScope = 'cli' | 'config' | 'agent' | 'tool' | 'skills' | 'llm' | 'migrate' | 'log';
|
|
26
|
+
export type LogScope = 'cli' | 'config' | 'agent' | 'tool' | 'skills' | 'llm' | 'migrate' | 'log' | 'history';
|
|
27
27
|
export interface LogRecord {
|
|
28
28
|
ts: string;
|
|
29
29
|
lv: LogLevelName;
|
|
@@ -21,4 +21,20 @@ export declare function saveSession(filePath: string, data: {
|
|
|
21
21
|
messages: Message[];
|
|
22
22
|
entries: Entry[];
|
|
23
23
|
}): void;
|
|
24
|
+
/**
|
|
25
|
+
* Settle any team entry that was saved mid-dispatch (team-subagents §5.3 / P1-5).
|
|
26
|
+
*
|
|
27
|
+
* `saveSession` writes `entries` verbatim and `loadSession` validates only
|
|
28
|
+
* array-ness, so a session saved while a dispatch was in flight resumes with a
|
|
29
|
+
* `kind: 'team'` entry claiming `active: true` while nothing is running. TWO
|
|
30
|
+
* things then go wrong at once: the card spins forever, and `Transcript`'s
|
|
31
|
+
* settled boundary is MONOTONIC — an entry that never settles never reaches
|
|
32
|
+
* `<Static>` and is re-rendered on every frame for the rest of the session.
|
|
33
|
+
*
|
|
34
|
+
* `aborted: true` is also simply TRUE: the children died with the process.
|
|
35
|
+
*
|
|
36
|
+
* This belongs in the LOAD path rather than the reducer because the reducer
|
|
37
|
+
* never legitimately sees a stale-active entry — only the file does.
|
|
38
|
+
*/
|
|
39
|
+
export declare function normalizeLoadedEntries(entries: Entry[]): Entry[];
|
|
24
40
|
export declare function loadSession(filePath: string): SavedSession;
|
package/dist/session/persist.js
CHANGED
|
@@ -41,11 +41,29 @@ export function saveSession(filePath, data) {
|
|
|
41
41
|
};
|
|
42
42
|
writeFileSync(filePath, `${JSON.stringify(payload, null, 2)}\n`, 'utf-8');
|
|
43
43
|
}
|
|
44
|
+
/**
|
|
45
|
+
* Settle any team entry that was saved mid-dispatch (team-subagents §5.3 / P1-5).
|
|
46
|
+
*
|
|
47
|
+
* `saveSession` writes `entries` verbatim and `loadSession` validates only
|
|
48
|
+
* array-ness, so a session saved while a dispatch was in flight resumes with a
|
|
49
|
+
* `kind: 'team'` entry claiming `active: true` while nothing is running. TWO
|
|
50
|
+
* things then go wrong at once: the card spins forever, and `Transcript`'s
|
|
51
|
+
* settled boundary is MONOTONIC — an entry that never settles never reaches
|
|
52
|
+
* `<Static>` and is re-rendered on every frame for the rest of the session.
|
|
53
|
+
*
|
|
54
|
+
* `aborted: true` is also simply TRUE: the children died with the process.
|
|
55
|
+
*
|
|
56
|
+
* This belongs in the LOAD path rather than the reducer because the reducer
|
|
57
|
+
* never legitimately sees a stale-active entry — only the file does.
|
|
58
|
+
*/
|
|
59
|
+
export function normalizeLoadedEntries(entries) {
|
|
60
|
+
return entries.map((entry) => entry.kind === 'team' && entry.active ? { ...entry, active: false, aborted: true } : entry);
|
|
61
|
+
}
|
|
44
62
|
export function loadSession(filePath) {
|
|
45
63
|
const raw = readFileSync(filePath, 'utf-8');
|
|
46
64
|
const parsed = JSON.parse(raw);
|
|
47
65
|
if (!parsed || !Array.isArray(parsed.messages) || !Array.isArray(parsed.entries)) {
|
|
48
66
|
throw new Error('Invalid session file (missing messages/entries).');
|
|
49
67
|
}
|
|
50
|
-
return parsed;
|
|
68
|
+
return { ...parsed, entries: normalizeLoadedEntries(parsed.entries) };
|
|
51
69
|
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Activity strings for the live roster (team-live-activity F-1).
|
|
3
|
+
*
|
|
4
|
+
* ASCII ONLY: `src/team/**` is inside the glyph scanner's scope. That rule binds
|
|
5
|
+
* the LITERALS IN THIS FILE, not the runtime data flowing through it - a child
|
|
6
|
+
* may well be reading a CJK path, and `run.description` already carries model
|
|
7
|
+
* text into the same row.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* The only part of a tool call this module will ever render (P1-4).
|
|
11
|
+
*
|
|
12
|
+
* A PICKED SUBSET, NOT THE ARGS OBJECT. Storing `event.args` whole would put
|
|
13
|
+
* `write_file`'s `content` - a whole file - on the run, and `TeamRuntime.publish`
|
|
14
|
+
* shallow-copies the run into the live snapshot AND into every `agent_update`
|
|
15
|
+
* event, and `dispatch()` copies it again into `DispatchOutcome.runs`, which
|
|
16
|
+
* `TeamCard` holds in the transcript for the rest of the session. Two facts make
|
|
17
|
+
* that unacceptable rather than merely wasteful: the retention is unbounded in
|
|
18
|
+
* the file's size, and one `JSON.stringify` anywhere downstream would put a file
|
|
19
|
+
* body into a log - the exact liability the logging section spends a paragraph
|
|
20
|
+
* avoiding.
|
|
21
|
+
*
|
|
22
|
+
* This costs nothing in maintenance: the switch below is ALREADY a per-tool
|
|
23
|
+
* table, so a new tool is one entry either way.
|
|
24
|
+
*/
|
|
25
|
+
export interface ActivityArgs {
|
|
26
|
+
path?: string;
|
|
27
|
+
pattern?: string;
|
|
28
|
+
command?: string;
|
|
29
|
+
to?: string;
|
|
30
|
+
from?: string;
|
|
31
|
+
}
|
|
32
|
+
/** Pick at capture time. Called once per `tool_execution_start`. */
|
|
33
|
+
export declare function pickActivityArgs(args: unknown): ActivityArgs;
|
|
34
|
+
/**
|
|
35
|
+
* Strip anything that could corrupt the frame, collapse whitespace, and clamp.
|
|
36
|
+
*
|
|
37
|
+
* THE CONTROL-CHARACTER STRIP IS A CORRECTNESS BOUNDARY, NOT TIDINESS. This
|
|
38
|
+
* string is model-chosen and reaches an Ink <Text> unescaped. A child running
|
|
39
|
+
* `bash` with an ANSI escape in the command line would otherwise write that
|
|
40
|
+
* escape into the roster: at best a mangled row, at worst a cleared screen or a
|
|
41
|
+
* moved cursor in the middle of someone's transcript. `bash`'s own output never
|
|
42
|
+
* reaches the panel; its ARGUMENTS now do.
|
|
43
|
+
*
|
|
44
|
+
* SURROGATES ARE GUARDED AT BOTH ENDS (P2-3). The trailing guard is the obvious
|
|
45
|
+
* one. The LEADING one is needed because the caller feeds this a rolling tail
|
|
46
|
+
* built with `String.slice(-n)`, which cuts on a UTF-16 code-unit boundary and
|
|
47
|
+
* can therefore hand us a lone LOW surrogate at index 0. Either half of a split
|
|
48
|
+
* pair renders as a replacement character - the same failure `truncateBytes`
|
|
49
|
+
* guards one layer down (report.ts).
|
|
50
|
+
*/
|
|
51
|
+
export declare function sanitizeActivity(raw: string, maxChars: number): string;
|
|
52
|
+
/**
|
|
53
|
+
* One short phrase describing a tool call.
|
|
54
|
+
*
|
|
55
|
+
* `wide` false is the narrow-terminal form (D-4). For a path-valued argument
|
|
56
|
+
* that is the basename; for `bash` it is the PROGRAM NAME (P1-6). Running the
|
|
57
|
+
* command line through `basename` is wrong twice over: `basename` splits on `/`,
|
|
58
|
+
* so `npm test -w cli` comes back whole and `./scripts/build.sh --prod` comes
|
|
59
|
+
* back as `build.sh --prod`. Neither is shorter and the second is a lie about
|
|
60
|
+
* what ran.
|
|
61
|
+
*
|
|
62
|
+
* Anything not in the table renders as its own name. That is the correct
|
|
63
|
+
* degradation for `skill_find` and for any tool a later round registers on a
|
|
64
|
+
* child: a name is always true, and inventing an argument mapping for a tool
|
|
65
|
+
* this module has not been taught is how a formatter starts lying.
|
|
66
|
+
*/
|
|
67
|
+
export declare function describeToolActivity(toolName: string, args: ActivityArgs | undefined, wide: boolean): string;
|