@gotgenes/pi-subagents 6.6.0 → 6.8.0
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 +30 -0
- package/docs/architecture/architecture.md +36 -34
- package/docs/plans/0110-agent-activity-tracker.md +297 -0
- package/docs/plans/0111-split-agent-record-lifecycle.md +582 -0
- package/docs/retro/0110-agent-activity-tracker.md +44 -0
- package/docs/retro/0118-settings-manager-apply-methods.md +40 -0
- package/package.json +1 -1
- package/src/agent-manager.ts +41 -21
- package/src/agent-record.ts +45 -34
- package/src/execution-state.ts +17 -0
- package/src/index.ts +2 -1
- package/src/notification-state.ts +27 -0
- package/src/notification.ts +12 -9
- package/src/record-observer.ts +6 -7
- package/src/runtime.ts +3 -2
- package/src/service-adapter.ts +8 -7
- package/src/tools/agent-tool.ts +13 -24
- package/src/tools/get-result-tool.ts +7 -5
- package/src/tools/steer-tool.ts +8 -6
- package/src/ui/agent-activity-tracker.ts +108 -0
- package/src/ui/agent-menu.ts +4 -4
- package/src/ui/agent-widget.ts +4 -17
- package/src/ui/conversation-viewer.ts +3 -3
- package/src/ui/ui-observer.ts +16 -23
- package/src/worktree-state.ts +35 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
---
|
|
2
|
+
issue: 118
|
|
3
|
+
issue_title: "refactor(pi-subagents): SettingsManager apply methods — eliminate cross-collaborator orchestration"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Retro: #118 — SettingsManager apply methods
|
|
7
|
+
|
|
8
|
+
## Final Retrospective (2026-05-21T21:00:00Z)
|
|
9
|
+
|
|
10
|
+
### Session summary
|
|
11
|
+
|
|
12
|
+
Planned and implemented 3 `apply*` methods on `SettingsManager` (`applyMaxConcurrent`, `applyDefaultMaxTurns`, `applyGraceTurns`) across 5 TDD cycles plus doc updates, released as `pi-subagents-v6.6.0`.
|
|
13
|
+
Each method owns the full consequence chain (normalize → set → callback → persist → emit → return toast), eliminating the LoD/Tell-Don't-Ask violation in `showSettings` that was identified during the #109 retro.
|
|
14
|
+
`notifyConcurrencyChanged` was removed from `AgentMenuManager`; the menu no longer coordinates between settings and the agent manager.
|
|
15
|
+
|
|
16
|
+
### Observations
|
|
17
|
+
|
|
18
|
+
#### What went well
|
|
19
|
+
|
|
20
|
+
- **Retro-driven improvement validated.**
|
|
21
|
+
Issue #118 was filed during the #109 retro as a LoD/Tell-Don't-Ask follow-up, and the plan-issue prompt's consumer call-site sketch heuristic (added in #109's retro) was already in the plan template.
|
|
22
|
+
The plan for #118 included concrete before/after call-site sketches that made the design unambiguous — no `ask-user` decision needed.
|
|
23
|
+
- **Interface-then-wiring TDD order worked cleanly.**
|
|
24
|
+
The #109 retro noted that interface changes propagate to `index.ts` immediately, forcing unplanned bridge edits.
|
|
25
|
+
This time the plan accounted for it: Cycle 4 committed only menu files (leaving a known `index.ts` type error), and Cycle 5 fixed the wiring in a separate commit.
|
|
26
|
+
The intermediate type error was contained and expected.
|
|
27
|
+
- **`defaultMaxTurns` branch consolidation.**
|
|
28
|
+
During Cycle 4, the separate `n === 0` and `n >= 1` branches in `showSettings` were consolidated to a single `n >= 0` check, since `applyDefaultMaxTurns` handles the 0→unlimited mapping internally.
|
|
29
|
+
This was a minor but correct simplification that emerged naturally from the Tell-Don't-Ask refactor.
|
|
30
|
+
|
|
31
|
+
#### What caused friction (agent side)
|
|
32
|
+
|
|
33
|
+
- No material friction.
|
|
34
|
+
All 5 TDD cycles completed without rework, failed edits, or unexpected test failures.
|
|
35
|
+
The plan was tight and the issue's "Proposed change" section was unambiguous.
|
|
36
|
+
|
|
37
|
+
#### What caused friction (user side)
|
|
38
|
+
|
|
39
|
+
- No material friction observed.
|
|
40
|
+
The session ran end-to-end (plan → implement → ship → release) without user intervention.
|
package/package.json
CHANGED
package/src/agent-manager.ts
CHANGED
|
@@ -13,11 +13,13 @@ import { AgentRecord } from "./agent-record.js";
|
|
|
13
13
|
import type { AgentRunner } from "./agent-runner.js";
|
|
14
14
|
import { AgentTypeRegistry } from "./agent-types.js";
|
|
15
15
|
import { debugLog } from "./debug.js";
|
|
16
|
+
import type { ExecutionState } from "./execution-state.js";
|
|
16
17
|
import { buildParentSnapshot } from "./parent-snapshot.js";
|
|
17
18
|
import { subscribeRecordObserver } from "./record-observer.js";
|
|
18
19
|
import type { RunConfig } from "./runtime.js";
|
|
19
20
|
import type { AgentInvocation, IsolationMode, ParentSnapshot, ShellExec, SubagentType, ThinkingLevel } from "./types.js";
|
|
20
21
|
import type { WorktreeManager } from "./worktree.js";
|
|
22
|
+
import { WorktreeState } from "./worktree-state.js";
|
|
21
23
|
|
|
22
24
|
export type OnAgentComplete = (record: AgentRecord) => void;
|
|
23
25
|
export type OnAgentStart = (record: AgentRecord) => void;
|
|
@@ -92,6 +94,8 @@ export class AgentManager {
|
|
|
92
94
|
private queue: { id: string; args: SpawnArgs }[] = [];
|
|
93
95
|
/** Number of currently running background agents. */
|
|
94
96
|
private runningBackground = 0;
|
|
97
|
+
/** Steers buffered for agents whose session hasn’t been created yet. */
|
|
98
|
+
private pendingSteers = new Map<string, string[]>();
|
|
95
99
|
|
|
96
100
|
constructor(options: AgentManagerOptions) {
|
|
97
101
|
this.runner = options.runner;
|
|
@@ -116,6 +120,19 @@ export class AgentManager {
|
|
|
116
120
|
this.drainQueue();
|
|
117
121
|
}
|
|
118
122
|
|
|
123
|
+
/**
|
|
124
|
+
* Buffer a steer message for an agent whose session isn’t ready yet.
|
|
125
|
+
* Returns false if the agent id is not tracked (already cleaned up or unknown).
|
|
126
|
+
* Called by steer-tool and service-adapter when record.execution is undefined.
|
|
127
|
+
*/
|
|
128
|
+
queueSteer(id: string, message: string): boolean {
|
|
129
|
+
if (!this.agents.has(id)) return false;
|
|
130
|
+
const steers = this.pendingSteers.get(id) ?? [];
|
|
131
|
+
steers.push(message);
|
|
132
|
+
this.pendingSteers.set(id, steers);
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
|
|
119
136
|
/**
|
|
120
137
|
* Spawn an agent and return its ID immediately (for background use).
|
|
121
138
|
* If the concurrency limit is reached, the agent is queued.
|
|
@@ -173,7 +190,7 @@ export class AgentManager {
|
|
|
173
190
|
'Initialize git and commit at least once, or omit `isolation`.',
|
|
174
191
|
);
|
|
175
192
|
}
|
|
176
|
-
record.
|
|
193
|
+
record.worktreeState = new WorktreeState(wt);
|
|
177
194
|
worktreeCwd = wt.path;
|
|
178
195
|
}
|
|
179
196
|
|
|
@@ -207,17 +224,18 @@ export class AgentManager {
|
|
|
207
224
|
signal: record.abortController!.signal,
|
|
208
225
|
registry: this.registry,
|
|
209
226
|
onSessionCreated: (session) => {
|
|
210
|
-
record.session = session;
|
|
211
227
|
// Capture the session file path early so it's available for display
|
|
212
228
|
// before the run completes (e.g. in background agent status messages).
|
|
213
|
-
const
|
|
214
|
-
|
|
229
|
+
const outputFile = session.sessionManager?.getSessionFile?.() ?? undefined;
|
|
230
|
+
// Set the execution-state collaborator — born complete at session creation.
|
|
231
|
+
record.execution = { session, outputFile };
|
|
215
232
|
// Flush any steers that arrived before the session was ready
|
|
216
|
-
|
|
217
|
-
|
|
233
|
+
const buffered = this.pendingSteers.get(id);
|
|
234
|
+
if (buffered?.length) {
|
|
235
|
+
for (const msg of buffered) {
|
|
218
236
|
session.steer(msg).catch(() => {});
|
|
219
237
|
}
|
|
220
|
-
|
|
238
|
+
this.pendingSteers.delete(id);
|
|
221
239
|
}
|
|
222
240
|
// Subscribe record observer for stats accumulation
|
|
223
241
|
unsubRecordObserver = subscribeRecordObserver(session, record, {
|
|
@@ -232,9 +250,9 @@ export class AgentManager {
|
|
|
232
250
|
|
|
233
251
|
// Clean up worktree before transition so the final result includes branch text
|
|
234
252
|
let finalResult = responseText;
|
|
235
|
-
if (record.
|
|
236
|
-
const wtResult = this.worktrees.cleanup(record.
|
|
237
|
-
record.
|
|
253
|
+
if (record.worktreeState) {
|
|
254
|
+
const wtResult = this.worktrees.cleanup(record.worktreeState, options.description);
|
|
255
|
+
record.worktreeState.recordCleanup(wtResult);
|
|
238
256
|
if (wtResult.hasChanges && wtResult.branch) {
|
|
239
257
|
finalResult += `\n\n---\nChanges saved to branch \`${wtResult.branch}\`. Merge with: \`git merge ${wtResult.branch}\``;
|
|
240
258
|
}
|
|
@@ -245,8 +263,8 @@ export class AgentManager {
|
|
|
245
263
|
else if (steered) record.markSteered(finalResult);
|
|
246
264
|
else record.markCompleted(finalResult);
|
|
247
265
|
|
|
248
|
-
|
|
249
|
-
|
|
266
|
+
// Update execution collaborator with final session/outputFile from runner
|
|
267
|
+
record.execution = { session, outputFile: sessionFile ?? record.execution?.outputFile };
|
|
250
268
|
|
|
251
269
|
if (options.isBackground) {
|
|
252
270
|
this.runningBackground--;
|
|
@@ -262,10 +280,11 @@ export class AgentManager {
|
|
|
262
280
|
detach();
|
|
263
281
|
|
|
264
282
|
// Best-effort worktree cleanup on error
|
|
265
|
-
if (record.
|
|
283
|
+
if (record.worktreeState) {
|
|
266
284
|
try {
|
|
267
|
-
const wtResult = this.worktrees.cleanup(record.
|
|
268
|
-
record.
|
|
285
|
+
const wtResult = this.worktrees.cleanup(record.worktreeState, options.description);
|
|
286
|
+
record.worktreeState.recordCleanup(wtResult);
|
|
287
|
+
|
|
269
288
|
} catch (err) { debugLog("cleanupWorktree on agent error", err); }
|
|
270
289
|
}
|
|
271
290
|
|
|
@@ -322,16 +341,17 @@ export class AgentManager {
|
|
|
322
341
|
signal?: AbortSignal,
|
|
323
342
|
): Promise<AgentRecord | undefined> {
|
|
324
343
|
const record = this.agents.get(id);
|
|
325
|
-
|
|
344
|
+
const session = record?.execution?.session;
|
|
345
|
+
if (!session) return undefined;
|
|
326
346
|
|
|
327
347
|
record.resetForResume(Date.now());
|
|
328
348
|
|
|
329
|
-
const unsubResume = subscribeRecordObserver(
|
|
349
|
+
const unsubResume = subscribeRecordObserver(session, record, {
|
|
330
350
|
onCompact: (r, info) => this.onCompact?.(r, info),
|
|
331
351
|
});
|
|
332
352
|
|
|
333
353
|
try {
|
|
334
|
-
const responseText = await this.runner.resume(
|
|
354
|
+
const responseText = await this.runner.resume(session, prompt, {
|
|
335
355
|
signal,
|
|
336
356
|
});
|
|
337
357
|
record.markCompleted(responseText);
|
|
@@ -373,9 +393,9 @@ export class AgentManager {
|
|
|
373
393
|
|
|
374
394
|
/** Dispose a record's session and remove it from the map. */
|
|
375
395
|
private removeRecord(id: string, record: AgentRecord): void {
|
|
376
|
-
record.session?.dispose?.();
|
|
377
|
-
record.session = undefined;
|
|
396
|
+
record.execution?.session?.dispose?.();
|
|
378
397
|
this.agents.delete(id);
|
|
398
|
+
this.pendingSteers.delete(id);
|
|
379
399
|
}
|
|
380
400
|
|
|
381
401
|
private cleanup() {
|
|
@@ -448,7 +468,7 @@ export class AgentManager {
|
|
|
448
468
|
// Clear queue
|
|
449
469
|
this.queue = [];
|
|
450
470
|
for (const record of this.agents.values()) {
|
|
451
|
-
record.session?.dispose();
|
|
471
|
+
record.execution?.session?.dispose();
|
|
452
472
|
}
|
|
453
473
|
this.agents.clear();
|
|
454
474
|
// Prune any orphaned git worktrees (crash recovery)
|
package/src/agent-record.ts
CHANGED
|
@@ -5,12 +5,19 @@
|
|
|
5
5
|
* by the class and exposed via transition methods. External code reads these
|
|
6
6
|
* fields through public properties but cannot write them directly.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
8
|
+
* Stats (toolUses, lifetimeUsage, compactionCount) are owned by the class and
|
|
9
|
+
* accumulated via mutation methods (incrementToolUses, addUsage, incrementCompactions).
|
|
10
|
+
*
|
|
11
|
+
* Phase-specific collaborators (execution, worktreeState, notification) are attached
|
|
12
|
+
* after construction as lifecycle information becomes available.
|
|
9
13
|
*/
|
|
10
14
|
|
|
11
|
-
import type {
|
|
15
|
+
import type { ExecutionState } from "./execution-state.js";
|
|
16
|
+
import type { NotificationState } from "./notification-state.js";
|
|
12
17
|
import type { AgentInvocation, SubagentType } from "./types.js";
|
|
13
18
|
import type { LifetimeUsage } from "./usage.js";
|
|
19
|
+
import { addUsage } from "./usage.js";
|
|
20
|
+
import type { WorktreeState } from "./worktree-state.js";
|
|
14
21
|
|
|
15
22
|
export type AgentRecordStatus =
|
|
16
23
|
| "queued"
|
|
@@ -30,19 +37,9 @@ export interface AgentRecordInit {
|
|
|
30
37
|
completedAt?: number;
|
|
31
38
|
result?: string;
|
|
32
39
|
error?: string;
|
|
33
|
-
toolUses?: number;
|
|
34
|
-
lifetimeUsage?: LifetimeUsage;
|
|
35
|
-
compactionCount?: number;
|
|
36
40
|
abortController?: AbortController;
|
|
37
41
|
invocation?: AgentInvocation;
|
|
38
|
-
session?: AgentSession;
|
|
39
42
|
promise?: Promise<string>;
|
|
40
|
-
resultConsumed?: boolean;
|
|
41
|
-
pendingSteers?: string[];
|
|
42
|
-
worktree?: { path: string; branch: string };
|
|
43
|
-
worktreeResult?: { hasChanges: boolean; branch?: string };
|
|
44
|
-
toolCallId?: string;
|
|
45
|
-
outputFile?: string;
|
|
46
43
|
}
|
|
47
44
|
|
|
48
45
|
export class AgentRecord {
|
|
@@ -68,19 +65,25 @@ export class AgentRecord {
|
|
|
68
65
|
private _completedAt?: number;
|
|
69
66
|
get completedAt(): number | undefined { return this._completedAt; }
|
|
70
67
|
|
|
71
|
-
//
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
68
|
+
// Stats — accumulated via mutation methods, readable via getters
|
|
69
|
+
private _toolUses: number;
|
|
70
|
+
get toolUses(): number { return this._toolUses; }
|
|
71
|
+
|
|
72
|
+
private _lifetimeUsage: LifetimeUsage;
|
|
73
|
+
get lifetimeUsage(): Readonly<LifetimeUsage> { return this._lifetimeUsage; }
|
|
74
|
+
|
|
75
|
+
private _compactionCount: number;
|
|
76
|
+
get compactionCount(): number { return this._compactionCount; }
|
|
77
|
+
|
|
78
|
+
/** AbortController for cancelling this agent. Set at construction; used only by AgentManager. */
|
|
79
|
+
readonly abortController?: AbortController;
|
|
80
|
+
/** Promise for the full agent run (including post-processing). Set once by AgentManager. */
|
|
77
81
|
promise?: Promise<string>;
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
outputFile?: string;
|
|
82
|
+
|
|
83
|
+
// Phase-specific collaborators — each born complete when their info becomes available
|
|
84
|
+
execution?: ExecutionState;
|
|
85
|
+
worktreeState?: WorktreeState;
|
|
86
|
+
notification?: NotificationState;
|
|
84
87
|
|
|
85
88
|
constructor(init: AgentRecordInit) {
|
|
86
89
|
this.id = init.id;
|
|
@@ -94,18 +97,26 @@ export class AgentRecord {
|
|
|
94
97
|
this._startedAt = init.startedAt ?? Date.now();
|
|
95
98
|
this._completedAt = init.completedAt;
|
|
96
99
|
|
|
97
|
-
this.
|
|
98
|
-
this.
|
|
99
|
-
this.
|
|
100
|
+
this._toolUses = 0;
|
|
101
|
+
this._lifetimeUsage = { input: 0, output: 0, cacheWrite: 0 };
|
|
102
|
+
this._compactionCount = 0;
|
|
100
103
|
this.abortController = init.abortController;
|
|
101
|
-
this.session = init.session;
|
|
102
104
|
this.promise = init.promise;
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
this.
|
|
108
|
-
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Increment tool use count. Called by record-observer on tool_execution_end. */
|
|
108
|
+
incrementToolUses(): void {
|
|
109
|
+
this._toolUses++;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Accumulate a usage delta into lifetimeUsage. Called by record-observer on message_end. */
|
|
113
|
+
addUsage(delta: { input: number; output: number; cacheWrite: number }): void {
|
|
114
|
+
addUsage(this._lifetimeUsage, delta);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Increment compaction count. Called by record-observer on compaction_end. */
|
|
118
|
+
incrementCompactions(): void {
|
|
119
|
+
this._compactionCount++;
|
|
109
120
|
}
|
|
110
121
|
|
|
111
122
|
/** Transition to running state. Sets status and startedAt. */
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* execution-state.ts — ExecutionState: execution-phase state for a running agent.
|
|
3
|
+
*
|
|
4
|
+
* Constructed and attached to AgentRecord when onSessionCreated fires inside startAgent().
|
|
5
|
+
* Contains the session and output file — the two fields that become known once the
|
|
6
|
+
* runner creates the session. promise stays as a separate AgentRecord field because
|
|
7
|
+
* it is set at a different moment (after runner.run() returns).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { AgentSession } from "@earendil-works/pi-coding-agent";
|
|
11
|
+
|
|
12
|
+
export interface ExecutionState {
|
|
13
|
+
/** The active agent session — available from the moment the session is created. */
|
|
14
|
+
readonly session: AgentSession;
|
|
15
|
+
/** Path to the agent's session JSONL file, or undefined if not yet available. */
|
|
16
|
+
readonly outputFile: string | undefined;
|
|
17
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -88,7 +88,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
88
88
|
});
|
|
89
89
|
|
|
90
90
|
// Skip notification if result was already consumed via get_subagent_result
|
|
91
|
-
if (record.resultConsumed) {
|
|
91
|
+
if (record.notification?.resultConsumed) {
|
|
92
92
|
notifications.cleanupCompleted(record.id);
|
|
93
93
|
return;
|
|
94
94
|
}
|
|
@@ -215,6 +215,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
215
215
|
getRecord: (id) => manager.getRecord(id),
|
|
216
216
|
emitEvent: (name, data) => pi.events.emit(name, data),
|
|
217
217
|
steerAgent: (session, message) => steerAgent(session, message),
|
|
218
|
+
queueSteer: (id, message) => manager.queueSteer(id, message),
|
|
218
219
|
})));
|
|
219
220
|
|
|
220
221
|
// ---- /agents interactive menu ----
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* notification-state.ts — NotificationState: notification-scoped tracking per background agent.
|
|
3
|
+
*
|
|
4
|
+
* Constructed once when agent-tool assigns the tool call ID (background agents only).
|
|
5
|
+
* Foreground agents never get a NotificationState — record.notification stays undefined.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export class NotificationState {
|
|
9
|
+
/** The tool call ID that spawned this background agent. Used in task-notification XML. */
|
|
10
|
+
readonly toolCallId: string;
|
|
11
|
+
|
|
12
|
+
private _resultConsumed = false;
|
|
13
|
+
|
|
14
|
+
constructor(toolCallId: string) {
|
|
15
|
+
this.toolCallId = toolCallId;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Whether the parent agent has already consumed this result (suppresses duplicate notifications). */
|
|
19
|
+
get resultConsumed(): boolean {
|
|
20
|
+
return this._resultConsumed;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Mark the result as consumed — suppresses the completion notification. */
|
|
24
|
+
markConsumed(): void {
|
|
25
|
+
this._resultConsumed = true;
|
|
26
|
+
}
|
|
27
|
+
}
|
package/src/notification.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { debugLog } from "./debug.js";
|
|
2
2
|
import type { AgentRecord, NotificationDetails } from "./types.js";
|
|
3
|
-
import type {
|
|
3
|
+
import type { AgentActivityTracker } from "./ui/agent-activity-tracker.js";
|
|
4
4
|
import { getLifetimeTotal, getSessionContextPercent } from "./usage.js";
|
|
5
5
|
|
|
6
6
|
// ---- Pure helpers (exported for unit testing) ----
|
|
@@ -31,7 +31,7 @@ export function formatTaskNotification(record: AgentRecord, resultMaxLen: number
|
|
|
31
31
|
const status = getStatusLabel(record.status, record.error);
|
|
32
32
|
const durationMs = record.completedAt ? record.completedAt - record.startedAt : 0;
|
|
33
33
|
const totalTokens = getLifetimeTotal(record.lifetimeUsage);
|
|
34
|
-
const contextPercent = getSessionContextPercent(record.session);
|
|
34
|
+
const contextPercent = getSessionContextPercent(record.execution?.session);
|
|
35
35
|
const ctxXml = contextPercent !== null ? `<context_percent>${Math.round(contextPercent)}</context_percent>` : "";
|
|
36
36
|
const compactXml = record.compactionCount ? `<compactions>${record.compactionCount}</compactions>` : "";
|
|
37
37
|
|
|
@@ -41,11 +41,13 @@ export function formatTaskNotification(record: AgentRecord, resultMaxLen: number
|
|
|
41
41
|
: record.result
|
|
42
42
|
: "No output.";
|
|
43
43
|
|
|
44
|
+
const toolCallId = record.notification?.toolCallId;
|
|
45
|
+
const outputFile = record.execution?.outputFile;
|
|
44
46
|
return [
|
|
45
47
|
"<task-notification>",
|
|
46
48
|
`<task-id>${record.id}</task-id>`,
|
|
47
|
-
|
|
48
|
-
|
|
49
|
+
toolCallId ? `<tool-use-id>${escapeXml(toolCallId)}</tool-use-id>` : null,
|
|
50
|
+
outputFile ? `<output-file>${escapeXml(outputFile)}</output-file>` : null,
|
|
49
51
|
`<status>${escapeXml(status)}</status>`,
|
|
50
52
|
`<summary>Agent "${escapeXml(record.description)}" ${record.status}</summary>`,
|
|
51
53
|
`<result>${escapeXml(resultPreview)}</result>`,
|
|
@@ -60,7 +62,7 @@ export function formatTaskNotification(record: AgentRecord, resultMaxLen: number
|
|
|
60
62
|
export function buildNotificationDetails(
|
|
61
63
|
record: AgentRecord,
|
|
62
64
|
resultMaxLen: number,
|
|
63
|
-
activity?:
|
|
65
|
+
activity?: AgentActivityTracker,
|
|
64
66
|
): NotificationDetails {
|
|
65
67
|
const totalTokens = getLifetimeTotal(record.lifetimeUsage);
|
|
66
68
|
|
|
@@ -73,7 +75,7 @@ export function buildNotificationDetails(
|
|
|
73
75
|
maxTurns: activity?.maxTurns,
|
|
74
76
|
totalTokens,
|
|
75
77
|
durationMs: record.completedAt ? record.completedAt - record.startedAt : 0,
|
|
76
|
-
outputFile: record.outputFile,
|
|
78
|
+
outputFile: record.execution?.outputFile,
|
|
77
79
|
error: record.error,
|
|
78
80
|
resultPreview: record.result
|
|
79
81
|
? record.result.length > resultMaxLen
|
|
@@ -113,7 +115,7 @@ export interface NotificationDeps {
|
|
|
113
115
|
msg: { customType: string; content: string; display: boolean; details?: unknown },
|
|
114
116
|
opts?: { triggerTurn?: boolean; deliverAs?: "steer" | "followUp" | "nextTurn" },
|
|
115
117
|
) => void;
|
|
116
|
-
agentActivity: Map<string,
|
|
118
|
+
agentActivity: Map<string, AgentActivityTracker>;
|
|
117
119
|
markFinished: (id: string) => void;
|
|
118
120
|
updateWidget: () => void;
|
|
119
121
|
}
|
|
@@ -154,10 +156,11 @@ export function createNotificationSystem(deps: NotificationDeps): NotificationSy
|
|
|
154
156
|
}
|
|
155
157
|
|
|
156
158
|
function emitIndividualNudge(record: AgentRecord) {
|
|
157
|
-
if (record.resultConsumed) return;
|
|
159
|
+
if (record.notification?.resultConsumed) return;
|
|
158
160
|
|
|
159
161
|
const notification = formatTaskNotification(record, 500);
|
|
160
|
-
const
|
|
162
|
+
const outputFile = record.execution?.outputFile;
|
|
163
|
+
const footer = outputFile ? `\nFull transcript available at: ${outputFile}` : "";
|
|
161
164
|
|
|
162
165
|
deps.sendMessage(
|
|
163
166
|
{
|
package/src/record-observer.ts
CHANGED
|
@@ -7,7 +7,6 @@
|
|
|
7
7
|
|
|
8
8
|
import type { CompactionInfo } from "./agent-manager.js";
|
|
9
9
|
import type { AgentRecord } from "./agent-record.js";
|
|
10
|
-
import { addUsage } from "./usage.js";
|
|
11
10
|
|
|
12
11
|
/** Narrow session interface — only the subscribe method needed by the observer. */
|
|
13
12
|
interface SubscribableSession {
|
|
@@ -22,9 +21,9 @@ export interface RecordObserverOptions {
|
|
|
22
21
|
* Subscribe to session events and accumulate stats on the agent record.
|
|
23
22
|
*
|
|
24
23
|
* Handles:
|
|
25
|
-
* - `tool_execution_end` → `record.
|
|
26
|
-
* - `message_end` (assistant, with usage) → `addUsage(
|
|
27
|
-
* - `compaction_end` (not aborted) → `record.
|
|
24
|
+
* - `tool_execution_end` → `record.incrementToolUses()`
|
|
25
|
+
* - `message_end` (assistant, with usage) → `record.addUsage(…)`
|
|
26
|
+
* - `compaction_end` (not aborted) → `record.incrementCompactions()`, call `onCompact`
|
|
28
27
|
*
|
|
29
28
|
* @returns An unsubscribe function.
|
|
30
29
|
*/
|
|
@@ -35,13 +34,13 @@ export function subscribeRecordObserver(
|
|
|
35
34
|
): () => void {
|
|
36
35
|
return session.subscribe((event: any) => {
|
|
37
36
|
if (event.type === "tool_execution_end") {
|
|
38
|
-
record.
|
|
37
|
+
record.incrementToolUses();
|
|
39
38
|
}
|
|
40
39
|
|
|
41
40
|
if (event.type === "message_end" && event.message?.role === "assistant") {
|
|
42
41
|
const u = event.message.usage;
|
|
43
42
|
if (u) {
|
|
44
|
-
addUsage(
|
|
43
|
+
record.addUsage({
|
|
45
44
|
input: u.input ?? 0,
|
|
46
45
|
output: u.output ?? 0,
|
|
47
46
|
cacheWrite: u.cacheWrite ?? 0,
|
|
@@ -50,7 +49,7 @@ export function subscribeRecordObserver(
|
|
|
50
49
|
}
|
|
51
50
|
|
|
52
51
|
if (event.type === "compaction_end" && !event.aborted && event.result) {
|
|
53
|
-
record.
|
|
52
|
+
record.incrementCompactions();
|
|
54
53
|
options?.onCompact?.(record, {
|
|
55
54
|
reason: event.reason,
|
|
56
55
|
tokensBefore: event.result.tokensBefore,
|
package/src/runtime.ts
CHANGED
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
* Follows the same pattern as pi-permission-system's ExtensionRuntime.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import type {
|
|
9
|
+
import type { AgentActivityTracker } from "./ui/agent-activity-tracker.js";
|
|
10
|
+
import type { AgentWidget, UICtx } from "./ui/agent-widget.js";
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* Narrow config subset read by AgentManager when constructing RunOptions.
|
|
@@ -31,7 +32,7 @@ export class SubagentRuntime {
|
|
|
31
32
|
* Per-agent live activity state shared across the notification system,
|
|
32
33
|
* widget, and tool handlers. The Map itself is never replaced.
|
|
33
34
|
*/
|
|
34
|
-
readonly agentActivity: Map<string,
|
|
35
|
+
readonly agentActivity: Map<string, AgentActivityTracker> = new Map();
|
|
35
36
|
/**
|
|
36
37
|
* Persistent widget reference. Null until constructed after AgentManager.
|
|
37
38
|
* Delegation methods use optional chaining so callers never need `widget!`.
|
package/src/service-adapter.ts
CHANGED
|
@@ -17,6 +17,7 @@ export interface AgentManagerLike {
|
|
|
17
17
|
abort(id: string): boolean;
|
|
18
18
|
waitForAll(): Promise<void>;
|
|
19
19
|
hasRunning(): boolean;
|
|
20
|
+
queueSteer(id: string, message: string): boolean;
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
/** Dependencies injected into the adapter factory. */
|
|
@@ -85,13 +86,12 @@ export function createSubagentsService(deps: AdapterDeps): SubagentsService {
|
|
|
85
86
|
if (!record || record.status !== "running") {
|
|
86
87
|
return false;
|
|
87
88
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
return true;
|
|
89
|
+
const session = record.execution?.session;
|
|
90
|
+
if (!session) {
|
|
91
|
+
// Session not ready yet — queue via manager for delivery once initialized
|
|
92
|
+
return manager.queueSteer(id, message);
|
|
93
93
|
}
|
|
94
|
-
await
|
|
94
|
+
await session.steer(message);
|
|
95
95
|
return true;
|
|
96
96
|
},
|
|
97
97
|
|
|
@@ -124,7 +124,8 @@ export function toSubagentRecord(record: AgentRecord): SubagentRecord {
|
|
|
124
124
|
if (record.result !== undefined) out.result = record.result;
|
|
125
125
|
if (record.error !== undefined) out.error = record.error;
|
|
126
126
|
if (record.completedAt !== undefined) out.completedAt = record.completedAt;
|
|
127
|
-
|
|
127
|
+
const worktreeResult = record.worktreeState?.cleanupResult;
|
|
128
|
+
if (worktreeResult !== undefined) out.worktreeResult = worktreeResult;
|
|
128
129
|
|
|
129
130
|
return out;
|
|
130
131
|
}
|