@gotgenes/pi-subagents 10.0.1 → 10.2.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 +41 -0
- package/docs/architecture/architecture.md +78 -159
- package/docs/architecture/history/phase-14-strip-policy.md +49 -0
- package/docs/plans/0227-evolve-agent-record-into-agent.md +322 -0
- package/docs/plans/0228-async-start-agent-dissolve-run-handle.md +288 -0
- package/docs/retro/0227-evolve-agent-record-into-agent.md +80 -0
- package/docs/retro/0228-async-start-agent-dissolve-run-handle.md +42 -0
- package/docs/retro/0239-collapse-filter-active-tools.md +33 -0
- package/package.json +1 -1
- package/src/lifecycle/agent-manager.ts +70 -207
- package/src/lifecycle/{agent-record.ts → agent.ts} +151 -13
- package/src/lifecycle/execution-state.ts +2 -2
- package/src/observation/notification.ts +8 -8
- package/src/observation/record-observer.ts +7 -7
- package/src/service/service-adapter.ts +8 -8
- package/src/tools/agent-tool.ts +4 -4
- package/src/tools/background-spawner.ts +2 -2
- package/src/tools/foreground-runner.ts +4 -4
- package/src/tools/get-result-tool.ts +2 -2
- package/src/tools/steer-tool.ts +4 -5
- package/src/types.ts +1 -1
- package/src/ui/agent-creation-wizard.ts +2 -2
- package/src/ui/agent-menu.ts +5 -5
- package/src/ui/conversation-viewer.ts +3 -3
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* agent
|
|
2
|
+
* agent.ts — Agent class with encapsulated status-transition logic and per-agent behavior.
|
|
3
3
|
*
|
|
4
4
|
* Status transitions (status, result, error, startedAt, completedAt) are owned
|
|
5
5
|
* by the class and exposed via transition methods. External code reads these
|
|
@@ -8,19 +8,25 @@
|
|
|
8
8
|
* Stats (toolUses, lifetimeUsage, compactionCount) are owned by the class and
|
|
9
9
|
* accumulated via mutation methods (incrementToolUses, addUsage, incrementCompactions).
|
|
10
10
|
*
|
|
11
|
+
* Behavior (abort, steer buffering, worktree setup) lives on the agent
|
|
12
|
+
* rather than on AgentManager — each agent manages its own lifecycle concerns.
|
|
13
|
+
*
|
|
11
14
|
* Phase-specific collaborators (execution, worktreeState, notification) are attached
|
|
12
15
|
* after construction as lifecycle information becomes available.
|
|
13
16
|
*/
|
|
14
17
|
|
|
15
18
|
import type { AgentSession } from "@earendil-works/pi-coding-agent";
|
|
19
|
+
import { debugLog } from "#src/debug";
|
|
20
|
+
import type { RunResult } from "#src/lifecycle/agent-runner";
|
|
16
21
|
import type { ExecutionState } from "#src/lifecycle/execution-state";
|
|
17
22
|
import type { LifetimeUsage } from "#src/lifecycle/usage";
|
|
18
23
|
import { addUsage } from "#src/lifecycle/usage";
|
|
19
|
-
import type {
|
|
24
|
+
import type { WorktreeManager } from "#src/lifecycle/worktree";
|
|
25
|
+
import { WorktreeState } from "#src/lifecycle/worktree-state";
|
|
20
26
|
import type { NotificationState } from "#src/observation/notification-state";
|
|
21
|
-
import type { AgentInvocation, SubagentType } from "#src/types";
|
|
27
|
+
import type { AgentInvocation, IsolationMode, SubagentType } from "#src/types";
|
|
22
28
|
|
|
23
|
-
export type
|
|
29
|
+
export type AgentStatus =
|
|
24
30
|
| "queued"
|
|
25
31
|
| "running"
|
|
26
32
|
| "completed"
|
|
@@ -29,21 +35,21 @@ export type AgentRecordStatus =
|
|
|
29
35
|
| "stopped"
|
|
30
36
|
| "error";
|
|
31
37
|
|
|
32
|
-
export interface
|
|
38
|
+
export interface AgentInit {
|
|
33
39
|
id: string;
|
|
34
40
|
type: SubagentType;
|
|
35
41
|
description: string;
|
|
36
|
-
status?:
|
|
42
|
+
status?: AgentStatus;
|
|
37
43
|
startedAt?: number;
|
|
38
44
|
completedAt?: number;
|
|
39
45
|
result?: string;
|
|
40
46
|
error?: string;
|
|
41
47
|
abortController?: AbortController;
|
|
42
48
|
invocation?: AgentInvocation;
|
|
43
|
-
promise?: Promise<
|
|
49
|
+
promise?: Promise<void>;
|
|
44
50
|
}
|
|
45
51
|
|
|
46
|
-
export class
|
|
52
|
+
export class Agent {
|
|
47
53
|
// Identity — set once at construction
|
|
48
54
|
readonly id: string;
|
|
49
55
|
readonly type: SubagentType;
|
|
@@ -51,8 +57,8 @@ export class AgentRecord {
|
|
|
51
57
|
readonly invocation?: AgentInvocation;
|
|
52
58
|
|
|
53
59
|
// Transition state — encapsulated behind getters, mutated only via transition methods
|
|
54
|
-
private _status:
|
|
55
|
-
get status():
|
|
60
|
+
private _status: AgentStatus;
|
|
61
|
+
get status(): AgentStatus { return this._status; }
|
|
56
62
|
|
|
57
63
|
private _result?: string;
|
|
58
64
|
get result(): string | undefined { return this._result; }
|
|
@@ -79,13 +85,36 @@ export class AgentRecord {
|
|
|
79
85
|
/** AbortController for cancelling this agent. Set at construction; used only by AgentManager. */
|
|
80
86
|
readonly abortController?: AbortController;
|
|
81
87
|
/** Promise for the full agent run (including post-processing). Set once by AgentManager. */
|
|
82
|
-
promise?: Promise<
|
|
88
|
+
promise?: Promise<void>;
|
|
83
89
|
|
|
84
90
|
// Phase-specific collaborators — each born complete when their info becomes available
|
|
85
91
|
execution?: ExecutionState;
|
|
86
92
|
worktreeState?: WorktreeState;
|
|
87
93
|
notification?: NotificationState;
|
|
88
94
|
|
|
95
|
+
/**
|
|
96
|
+
* Create a git worktree for isolated execution, set worktreeState, and return the worktree path.
|
|
97
|
+
* Returns undefined if isolation is not "worktree".
|
|
98
|
+
* Throws if worktree creation fails (strict isolation).
|
|
99
|
+
*/
|
|
100
|
+
setupWorktree(worktrees: WorktreeManager, isolation: IsolationMode | undefined): string | undefined {
|
|
101
|
+
if (isolation !== "worktree") return undefined;
|
|
102
|
+
const wt = worktrees.create(this.id);
|
|
103
|
+
if (!wt) {
|
|
104
|
+
throw new Error(
|
|
105
|
+
'Cannot run with isolation: "worktree" — not a git repo, no commits yet, or `git worktree add` failed. ' +
|
|
106
|
+
'Initialize git and commit at least once, or omit `isolation`.',
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
this.worktreeState = new WorktreeState(wt);
|
|
110
|
+
return wt.path;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Steer buffer — messages queued before the session is ready
|
|
114
|
+
private _pendingSteers: string[] = [];
|
|
115
|
+
/** Number of steer messages waiting to be delivered. */
|
|
116
|
+
get pendingSteerCount(): number { return this._pendingSteers.length; }
|
|
117
|
+
|
|
89
118
|
/** The active agent session, or undefined before the session is created. */
|
|
90
119
|
get session(): AgentSession | undefined {
|
|
91
120
|
return this.execution?.session;
|
|
@@ -96,7 +125,7 @@ export class AgentRecord {
|
|
|
96
125
|
return this.execution?.outputFile;
|
|
97
126
|
}
|
|
98
127
|
|
|
99
|
-
constructor(init:
|
|
128
|
+
constructor(init: AgentInit) {
|
|
100
129
|
this.id = init.id;
|
|
101
130
|
this.type = init.type;
|
|
102
131
|
this.description = init.description;
|
|
@@ -190,12 +219,121 @@ export class AgentRecord {
|
|
|
190
219
|
this._completedAt = completedAt ?? Date.now();
|
|
191
220
|
}
|
|
192
221
|
|
|
193
|
-
/**
|
|
222
|
+
/**
|
|
223
|
+
* Abort a running agent: fire AbortController and transition to stopped.
|
|
224
|
+
* Returns false if the agent is not running.
|
|
225
|
+
* Queue removal stays on AgentManager until #230 extracts ConcurrencyQueue.
|
|
226
|
+
*/
|
|
227
|
+
abort(): boolean {
|
|
228
|
+
if (this._status !== "running") return false;
|
|
229
|
+
this.abortController?.abort();
|
|
230
|
+
this.markStopped();
|
|
231
|
+
return true;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Buffer a steer message for delivery once the session is ready.
|
|
236
|
+
* Called when steer is requested before onSessionCreated fires.
|
|
237
|
+
*/
|
|
238
|
+
queueSteer(message: string): void {
|
|
239
|
+
this._pendingSteers.push(message);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Flush all buffered steer messages to the session and clear the buffer.
|
|
244
|
+
* Called from onSessionCreated once the session is available.
|
|
245
|
+
*/
|
|
246
|
+
flushPendingSteers(session: AgentSession): void {
|
|
247
|
+
for (const msg of this._pendingSteers) {
|
|
248
|
+
session.steer(msg).catch(() => {});
|
|
249
|
+
}
|
|
250
|
+
this._pendingSteers = [];
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** Reset for resume: running status, new startedAt, clear completedAt/result/error/listeners. */
|
|
194
254
|
resetForResume(startedAt: number): void {
|
|
195
255
|
this._status = "running";
|
|
196
256
|
this._startedAt = startedAt;
|
|
197
257
|
this._completedAt = undefined;
|
|
198
258
|
this._result = undefined;
|
|
199
259
|
this._error = undefined;
|
|
260
|
+
this.releaseListeners();
|
|
261
|
+
this._onRunFinished = undefined;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// --- Per-run listener state (released on completion or resume reset) ---
|
|
265
|
+
private _unsub?: () => void;
|
|
266
|
+
private _detachFn?: () => void;
|
|
267
|
+
private _onRunFinished?: () => void;
|
|
268
|
+
|
|
269
|
+
/** Wire a parent AbortSignal so it stops this agent when fired. */
|
|
270
|
+
wireSignal(signal: AbortSignal | undefined, onAbort: () => void): void {
|
|
271
|
+
if (!signal) return;
|
|
272
|
+
const listener = () => onAbort();
|
|
273
|
+
signal.addEventListener("abort", listener, { once: true });
|
|
274
|
+
this._detachFn = () => signal.removeEventListener("abort", listener);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** Store the record-observer unsubscribe handle. */
|
|
278
|
+
attachObserver(unsub: () => void): void {
|
|
279
|
+
this._unsub = unsub;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** Release observer + signal listener handles. */
|
|
283
|
+
releaseListeners(): void {
|
|
284
|
+
this._unsub?.();
|
|
285
|
+
this._unsub = undefined;
|
|
286
|
+
this._detachFn?.();
|
|
287
|
+
this._detachFn = undefined;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/** Set the callback fired once when the run finishes (for concurrency drain). */
|
|
291
|
+
setOnRunFinished(fn: (() => void) | undefined): void {
|
|
292
|
+
this._onRunFinished = fn;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** Fire the onRunFinished callback at most once. */
|
|
296
|
+
private fireOnRunFinished(): void {
|
|
297
|
+
const fn = this._onRunFinished;
|
|
298
|
+
this._onRunFinished = undefined;
|
|
299
|
+
fn?.();
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** Complete a run: release listeners, worktree cleanup, status transition, execution update, fire onRunFinished. */
|
|
303
|
+
completeRun(result: RunResult, worktrees: WorktreeManager): void {
|
|
304
|
+
this.releaseListeners();
|
|
305
|
+
|
|
306
|
+
let finalResult = result.responseText;
|
|
307
|
+
if (this.worktreeState) {
|
|
308
|
+
const wtResult = this.worktreeState.performCleanup(worktrees, this.description);
|
|
309
|
+
if (wtResult.hasChanges && wtResult.branch) {
|
|
310
|
+
finalResult += `\n\n---\nChanges saved to branch \`${wtResult.branch}\`. Merge with: \`git merge ${wtResult.branch}\``;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (result.aborted) this.markAborted(finalResult);
|
|
315
|
+
else if (result.steered) this.markSteered(finalResult);
|
|
316
|
+
else this.markCompleted(finalResult);
|
|
317
|
+
|
|
318
|
+
this.execution = {
|
|
319
|
+
session: result.session,
|
|
320
|
+
outputFile: result.sessionFile ?? this.execution?.outputFile,
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
this.fireOnRunFinished();
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/** Fail a run: mark error, release listeners, best-effort worktree cleanup, fire onRunFinished. */
|
|
327
|
+
failRun(err: unknown, worktrees: WorktreeManager): void {
|
|
328
|
+
this.markError(err);
|
|
329
|
+
this.releaseListeners();
|
|
330
|
+
|
|
331
|
+
if (this.worktreeState) {
|
|
332
|
+
try {
|
|
333
|
+
this.worktreeState.performCleanup(worktrees, this.description);
|
|
334
|
+
} catch (cleanupErr) { debugLog("cleanupWorktree on agent error", cleanupErr); }
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
this.fireOnRunFinished();
|
|
200
338
|
}
|
|
201
339
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* execution-state.ts — ExecutionState: execution-phase state for a running agent.
|
|
3
3
|
*
|
|
4
|
-
* Constructed and attached to
|
|
4
|
+
* Constructed and attached to Agent when onSessionCreated fires inside startAgent().
|
|
5
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
|
|
6
|
+
* runner creates the session. promise stays as a separate Agent field because
|
|
7
7
|
* it is set at a different moment (after runner.run() returns).
|
|
8
8
|
*/
|
|
9
9
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { debugLog } from "#src/debug";
|
|
2
2
|
import { getLifetimeTotal, getSessionContextPercent } from "#src/lifecycle/usage";
|
|
3
|
-
import type {
|
|
3
|
+
import type { Agent } from "#src/types";
|
|
4
4
|
import type { AgentActivityTracker } from "#src/ui/agent-activity-tracker";
|
|
5
5
|
|
|
6
6
|
/** Details attached to custom notification messages for visual rendering. */
|
|
@@ -42,7 +42,7 @@ export function getStatusLabel(status: string, error?: string): string {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
/** Format a structured task notification matching Claude Code's <task-notification> XML. */
|
|
45
|
-
export function formatTaskNotification(record:
|
|
45
|
+
export function formatTaskNotification(record: Agent, resultMaxLen: number): string {
|
|
46
46
|
const status = getStatusLabel(record.status, record.error);
|
|
47
47
|
const durationMs = record.completedAt ? record.completedAt - record.startedAt : 0;
|
|
48
48
|
const totalTokens = getLifetimeTotal(record.lifetimeUsage);
|
|
@@ -75,7 +75,7 @@ export function formatTaskNotification(record: AgentRecord, resultMaxLen: number
|
|
|
75
75
|
|
|
76
76
|
/** Build notification details for the custom message renderer. */
|
|
77
77
|
export function buildNotificationDetails(
|
|
78
|
-
record:
|
|
78
|
+
record: Agent,
|
|
79
79
|
resultMaxLen: number,
|
|
80
80
|
activity?: AgentActivityTracker,
|
|
81
81
|
): NotificationDetails {
|
|
@@ -100,8 +100,8 @@ export function buildNotificationDetails(
|
|
|
100
100
|
};
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
-
/** Build event data for lifecycle events from an
|
|
104
|
-
export function buildEventData(record:
|
|
103
|
+
/** Build event data for lifecycle events from an Agent. */
|
|
104
|
+
export function buildEventData(record: Agent) {
|
|
105
105
|
const durationMs = record.completedAt ? record.completedAt - record.startedAt : Date.now() - record.startedAt;
|
|
106
106
|
const u = record.lifetimeUsage;
|
|
107
107
|
const total = getLifetimeTotal(u);
|
|
@@ -126,7 +126,7 @@ export function buildEventData(record: AgentRecord) {
|
|
|
126
126
|
|
|
127
127
|
export interface NotificationSystem {
|
|
128
128
|
cancelNudge: (key: string) => void;
|
|
129
|
-
sendCompletion: (record:
|
|
129
|
+
sendCompletion: (record: Agent) => void;
|
|
130
130
|
cleanupCompleted: (id: string) => void;
|
|
131
131
|
dispose: () => void;
|
|
132
132
|
}
|
|
@@ -154,7 +154,7 @@ export class NotificationManager implements NotificationSystem {
|
|
|
154
154
|
}
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
-
sendCompletion(record:
|
|
157
|
+
sendCompletion(record: Agent): void {
|
|
158
158
|
this.agentActivity.delete(record.id);
|
|
159
159
|
this.markFinished(record.id);
|
|
160
160
|
this.scheduleNudge(record.id, () => this.emitIndividualNudge(record));
|
|
@@ -187,7 +187,7 @@ export class NotificationManager implements NotificationSystem {
|
|
|
187
187
|
);
|
|
188
188
|
}
|
|
189
189
|
|
|
190
|
-
private emitIndividualNudge(record:
|
|
190
|
+
private emitIndividualNudge(record: Agent): void {
|
|
191
191
|
if (record.notification?.resultConsumed) return;
|
|
192
192
|
|
|
193
193
|
const notification = formatTaskNotification(record, 500);
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* record-observer.ts — Subscribes to session events and updates
|
|
2
|
+
* record-observer.ts — Subscribes to session events and updates Agent stats.
|
|
3
3
|
*
|
|
4
4
|
* Replaces the scattered callback-wrapping logic in AgentManager's startAgent()
|
|
5
5
|
* and resume() with a single direct subscription.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import type { Agent } from "#src/lifecycle/agent";
|
|
8
9
|
import type { CompactionInfo } from "#src/lifecycle/agent-manager";
|
|
9
|
-
import type { AgentRecord } from "#src/lifecycle/agent-record";
|
|
10
10
|
import type { SubscribableSession } from "#src/types";
|
|
11
11
|
|
|
12
|
-
export interface
|
|
13
|
-
onCompact?: (record:
|
|
12
|
+
export interface AgentObserverOptions {
|
|
13
|
+
onCompact?: (record: Agent, info: CompactionInfo) => void;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
/**
|
|
@@ -23,10 +23,10 @@ export interface RecordObserverOptions {
|
|
|
23
23
|
*
|
|
24
24
|
* @returns An unsubscribe function.
|
|
25
25
|
*/
|
|
26
|
-
export function
|
|
26
|
+
export function subscribeAgentObserver(
|
|
27
27
|
session: SubscribableSession,
|
|
28
|
-
record:
|
|
29
|
-
options?:
|
|
28
|
+
record: Agent,
|
|
29
|
+
options?: AgentObserverOptions,
|
|
30
30
|
): () => void {
|
|
31
31
|
return session.subscribe((event) => {
|
|
32
32
|
if (event.type === "tool_execution_end") {
|
|
@@ -8,17 +8,16 @@
|
|
|
8
8
|
import type { ParentSnapshot } from "#src/lifecycle/parent-snapshot";
|
|
9
9
|
import type { SpawnOptions, SubagentRecord, SubagentsService } from "#src/service/service";
|
|
10
10
|
import type { ModelRegistry } from "#src/session/model-resolver";
|
|
11
|
-
import type {
|
|
11
|
+
import type { Agent, SessionContext } from "#src/types";
|
|
12
12
|
|
|
13
13
|
/** Narrow interface for the AgentManager — avoids coupling to the concrete class. */
|
|
14
14
|
export interface AgentManagerLike {
|
|
15
15
|
spawn(snapshot: ParentSnapshot, type: string, prompt: string, options: unknown): string;
|
|
16
|
-
getRecord(id: string):
|
|
17
|
-
listAgents():
|
|
16
|
+
getRecord(id: string): Agent | undefined;
|
|
17
|
+
listAgents(): Agent[];
|
|
18
18
|
abort(id: string): boolean;
|
|
19
19
|
waitForAll(): Promise<void>;
|
|
20
20
|
hasRunning(): boolean;
|
|
21
|
-
queueSteer(id: string, message: string): boolean;
|
|
22
21
|
}
|
|
23
22
|
|
|
24
23
|
/**
|
|
@@ -93,8 +92,9 @@ export class SubagentsServiceAdapter implements SubagentsService {
|
|
|
93
92
|
}
|
|
94
93
|
const session = record.session;
|
|
95
94
|
if (!session) {
|
|
96
|
-
// Session not ready yet —
|
|
97
|
-
|
|
95
|
+
// Session not ready yet — buffer on the agent for delivery once initialized
|
|
96
|
+
record.queueSteer(message);
|
|
97
|
+
return true;
|
|
98
98
|
}
|
|
99
99
|
await session.steer(message);
|
|
100
100
|
return true;
|
|
@@ -110,10 +110,10 @@ export class SubagentsServiceAdapter implements SubagentsService {
|
|
|
110
110
|
}
|
|
111
111
|
|
|
112
112
|
/**
|
|
113
|
-
* Convert an internal
|
|
113
|
+
* Convert an internal Agent to a serializable SubagentRecord.
|
|
114
114
|
* Uses an explicit allowlist — new fields must be opted in.
|
|
115
115
|
*/
|
|
116
|
-
export function toSubagentRecord(record:
|
|
116
|
+
export function toSubagentRecord(record: Agent): SubagentRecord {
|
|
117
117
|
const out: SubagentRecord = {
|
|
118
118
|
id: record.id,
|
|
119
119
|
type: record.type,
|
package/src/tools/agent-tool.ts
CHANGED
|
@@ -11,7 +11,7 @@ import { runForeground } from "#src/tools/foreground-runner";
|
|
|
11
11
|
import { buildDetails, buildTypeListText, textResult } from "#src/tools/helpers";
|
|
12
12
|
import { renderAgentResult } from "#src/tools/result-renderer";
|
|
13
13
|
import { type ModelInfo, resolveSpawnConfig } from "#src/tools/spawn-config";
|
|
14
|
-
import type {
|
|
14
|
+
import type { Agent } from "#src/types";
|
|
15
15
|
import { AgentActivityTracker } from "#src/ui/agent-activity-tracker";
|
|
16
16
|
import { type UICtx } from "#src/ui/agent-widget";
|
|
17
17
|
import { type AgentDetails, getDisplayName } from "#src/ui/display";
|
|
@@ -33,9 +33,9 @@ export interface AgentActivityAccess {
|
|
|
33
33
|
/** Narrow manager interface — only the methods the Agent tool calls. */
|
|
34
34
|
export interface AgentToolManager {
|
|
35
35
|
spawn: (snapshot: ParentSnapshot, type: string, prompt: string, opts: AgentSpawnConfig) => string;
|
|
36
|
-
spawnAndWait: (snapshot: ParentSnapshot, type: string, prompt: string, opts: Omit<AgentSpawnConfig, "isBackground">) => Promise<
|
|
37
|
-
resume: (id: string, prompt: string, signal: AbortSignal) => Promise<
|
|
38
|
-
getRecord: (id: string) =>
|
|
36
|
+
spawnAndWait: (snapshot: ParentSnapshot, type: string, prompt: string, opts: Omit<AgentSpawnConfig, "isBackground">) => Promise<Agent>;
|
|
37
|
+
resume: (id: string, prompt: string, signal: AbortSignal) => Promise<Agent | undefined>;
|
|
38
|
+
getRecord: (id: string) => Agent | undefined;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
/** Narrow runtime interface — the Agent tool's slice of SubagentRuntime. */
|
|
@@ -3,14 +3,14 @@ import type { ParentSnapshot } from "#src/lifecycle/parent-snapshot";
|
|
|
3
3
|
import type { AgentActivityAccess } from "#src/tools/agent-tool";
|
|
4
4
|
import { textResult } from "#src/tools/helpers";
|
|
5
5
|
import type { ResolvedSpawnConfig } from "#src/tools/spawn-config";
|
|
6
|
-
import type {
|
|
6
|
+
import type { Agent } from "#src/types";
|
|
7
7
|
import { AgentActivityTracker } from "#src/ui/agent-activity-tracker";
|
|
8
8
|
import { subscribeUIObserver } from "#src/ui/ui-observer";
|
|
9
9
|
|
|
10
10
|
/** Narrow manager interface for the background spawner. */
|
|
11
11
|
export interface BackgroundManagerDeps {
|
|
12
12
|
spawn(snapshot: ParentSnapshot, type: string, prompt: string, opts: AgentSpawnConfig): string;
|
|
13
|
-
getRecord(id: string):
|
|
13
|
+
getRecord(id: string): Agent | undefined;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
/** Narrow widget interface for the background spawner. */
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
textResult,
|
|
10
10
|
} from "#src/tools/helpers";
|
|
11
11
|
import type { ResolvedSpawnConfig } from "#src/tools/spawn-config";
|
|
12
|
-
import type {
|
|
12
|
+
import type { Agent } from "#src/types";
|
|
13
13
|
import { AgentActivityTracker } from "#src/ui/agent-activity-tracker";
|
|
14
14
|
import {
|
|
15
15
|
type AgentDetails,
|
|
@@ -26,7 +26,7 @@ export interface ForegroundManagerDeps {
|
|
|
26
26
|
type: string,
|
|
27
27
|
prompt: string,
|
|
28
28
|
opts: Omit<AgentSpawnConfig, "isBackground">,
|
|
29
|
-
): Promise<
|
|
29
|
+
): Promise<Agent>;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
/** Narrow widget interface for the foreground runner. */
|
|
@@ -62,7 +62,7 @@ export async function runForeground(
|
|
|
62
62
|
|
|
63
63
|
const fgState = new AgentActivityTracker(execution.effectiveMaxTurns);
|
|
64
64
|
let unsubUI: (() => void) | undefined;
|
|
65
|
-
let recordRef:
|
|
65
|
+
let recordRef: Agent | undefined;
|
|
66
66
|
|
|
67
67
|
const streamUpdate = () => {
|
|
68
68
|
const toolUses = recordRef?.toolUses ?? 0;
|
|
@@ -92,7 +92,7 @@ export async function runForeground(
|
|
|
92
92
|
|
|
93
93
|
streamUpdate();
|
|
94
94
|
|
|
95
|
-
let record:
|
|
95
|
+
let record: Agent;
|
|
96
96
|
try {
|
|
97
97
|
record = await manager.spawnAndWait(
|
|
98
98
|
params.snapshot,
|
|
@@ -4,13 +4,13 @@ import type { AgentConfigLookup } from "#src/config/agent-types";
|
|
|
4
4
|
import { getAgentConversation } from "#src/lifecycle/agent-runner";
|
|
5
5
|
import { getSessionContextPercent } from "#src/lifecycle/usage";
|
|
6
6
|
import { formatLifetimeTokens, textResult } from "#src/tools/helpers";
|
|
7
|
-
import type {
|
|
7
|
+
import type { Agent } from "#src/types";
|
|
8
8
|
import { formatDuration, getDisplayName } from "#src/ui/display";
|
|
9
9
|
|
|
10
10
|
// ---- Deps interfaces ----
|
|
11
11
|
|
|
12
12
|
export interface GetResultToolManager {
|
|
13
|
-
getRecord(id: string):
|
|
13
|
+
getRecord(id: string): Agent | undefined;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
export interface GetResultToolNotifications {
|
package/src/tools/steer-tool.ts
CHANGED
|
@@ -2,13 +2,12 @@ import { defineTool } from "@earendil-works/pi-coding-agent";
|
|
|
2
2
|
import { Type } from "@sinclair/typebox";
|
|
3
3
|
import { getSessionContextPercent } from "#src/lifecycle/usage";
|
|
4
4
|
import { formatLifetimeTokens, textResult } from "#src/tools/helpers";
|
|
5
|
-
import type {
|
|
5
|
+
import type { Agent } from "#src/types";
|
|
6
6
|
|
|
7
7
|
// ---- Deps interfaces ----
|
|
8
8
|
|
|
9
9
|
export interface SteerToolManager {
|
|
10
|
-
getRecord(id: string):
|
|
11
|
-
queueSteer(id: string, message: string): boolean;
|
|
10
|
+
getRecord(id: string): Agent | undefined;
|
|
12
11
|
}
|
|
13
12
|
|
|
14
13
|
export interface SteerToolEvents {
|
|
@@ -43,8 +42,8 @@ export class SteerTool {
|
|
|
43
42
|
}
|
|
44
43
|
const session = record.session;
|
|
45
44
|
if (!session) {
|
|
46
|
-
// Session not ready yet —
|
|
47
|
-
|
|
45
|
+
// Session not ready yet — buffer on the agent for delivery once initialized
|
|
46
|
+
record.queueSteer(params.message);
|
|
48
47
|
this.events.emit("subagents:steered", { id: record.id, message: params.message });
|
|
49
48
|
return textResult(
|
|
50
49
|
`Steering message queued for agent ${record.id}. It will be delivered once the session initializes.`,
|
package/src/types.ts
CHANGED
|
@@ -7,7 +7,7 @@ import type { AgentSessionEvent } from "@earendil-works/pi-coding-agent";
|
|
|
7
7
|
import type { ModelRegistry } from "#src/session/model-resolver";
|
|
8
8
|
|
|
9
9
|
|
|
10
|
-
export {
|
|
10
|
+
export { Agent } from "#src/lifecycle/agent";
|
|
11
11
|
export type { AgentSessionEvent, ThinkingLevel };
|
|
12
12
|
|
|
13
13
|
/**
|
|
@@ -9,7 +9,7 @@ import { join } from "node:path";
|
|
|
9
9
|
|
|
10
10
|
import { BUILTIN_TOOL_NAMES } from "#src/config/agent-types";
|
|
11
11
|
import type { ParentSnapshot } from "#src/lifecycle/parent-snapshot";
|
|
12
|
-
import type {
|
|
12
|
+
import type { Agent } from "#src/types";
|
|
13
13
|
import type { AgentFileOps } from "#src/ui/agent-file-ops";
|
|
14
14
|
import { writeAgentFile } from "#src/ui/agent-file-writer";
|
|
15
15
|
import type { MenuUI } from "#src/ui/agent-menu";
|
|
@@ -23,7 +23,7 @@ export interface WizardManager {
|
|
|
23
23
|
type: string,
|
|
24
24
|
prompt: string,
|
|
25
25
|
opts: { description: string; maxTurns: number },
|
|
26
|
-
) => Promise<
|
|
26
|
+
) => Promise<Agent>;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
/** Narrow registry interface for reloading after creation. */
|
package/src/ui/agent-menu.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { AgentTypeRegistry } from "#src/config/agent-types";
|
|
|
4
4
|
import type { ParentSnapshot } from "#src/lifecycle/parent-snapshot";
|
|
5
5
|
import { type ModelRegistry, resolveModel } from "#src/session/model-resolver";
|
|
6
6
|
import { getModelLabelFromConfig } from "#src/tools/helpers";
|
|
7
|
-
import type {
|
|
7
|
+
import type { Agent, AgentConfig } from "#src/types";
|
|
8
8
|
import type { AgentActivityTracker } from "#src/ui/agent-activity-tracker";
|
|
9
9
|
import { AgentConfigEditor } from "#src/ui/agent-config-editor";
|
|
10
10
|
import { AgentCreationWizard } from "#src/ui/agent-creation-wizard";
|
|
@@ -15,15 +15,15 @@ import { formatDuration, getDisplayName } from "#src/ui/display";
|
|
|
15
15
|
|
|
16
16
|
/** Narrow manager interface for menu operations. */
|
|
17
17
|
export interface AgentMenuManager {
|
|
18
|
-
listAgents: () =>
|
|
19
|
-
getRecord: (id: string) =>
|
|
18
|
+
listAgents: () => Agent[];
|
|
19
|
+
getRecord: (id: string) => Agent | undefined;
|
|
20
20
|
/** Used by generate wizard to spawn an agent that writes the .md file. */
|
|
21
21
|
spawnAndWait: (
|
|
22
22
|
parentSnapshot: ParentSnapshot,
|
|
23
23
|
type: string,
|
|
24
24
|
prompt: string,
|
|
25
25
|
opts: { description: string; maxTurns: number },
|
|
26
|
-
) => Promise<
|
|
26
|
+
) => Promise<Agent>;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
/** Narrow settings interface required by the agent menu. */
|
|
@@ -251,7 +251,7 @@ export class AgentsMenuHandler {
|
|
|
251
251
|
await this.showRunningAgents(ui);
|
|
252
252
|
}
|
|
253
253
|
|
|
254
|
-
private async viewAgentConversation(ui: MenuUI, record:
|
|
254
|
+
private async viewAgentConversation(ui: MenuUI, record: Agent): Promise<void> {
|
|
255
255
|
const session = record.session;
|
|
256
256
|
if (!session) {
|
|
257
257
|
ui.notify(
|
|
@@ -9,7 +9,7 @@ import type { AgentSession } from "@earendil-works/pi-coding-agent";
|
|
|
9
9
|
import { type Component, matchesKey, type TUI, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
10
10
|
import type { AgentConfigLookup } from "#src/config/agent-types";
|
|
11
11
|
import { getLifetimeTotal, getSessionContextPercent } from "#src/lifecycle/usage";
|
|
12
|
-
import type {
|
|
12
|
+
import type { Agent } from "#src/types";
|
|
13
13
|
import type { AgentActivityTracker } from "#src/ui/agent-activity-tracker";
|
|
14
14
|
import { buildInvocationTags, formatDuration, formatSessionTokens, getDisplayName, getPromptModeLabel, type Theme } from "#src/ui/display";
|
|
15
15
|
import { formatMessage, formatStreamingIndicator } from "#src/ui/message-formatters";
|
|
@@ -25,7 +25,7 @@ export const VIEWPORT_HEIGHT_PCT = 70;
|
|
|
25
25
|
export interface ConversationViewerOptions {
|
|
26
26
|
tui: TUI;
|
|
27
27
|
session: AgentSession;
|
|
28
|
-
record:
|
|
28
|
+
record: Agent;
|
|
29
29
|
activity: AgentActivityTracker | undefined;
|
|
30
30
|
theme: Theme;
|
|
31
31
|
done: (result: undefined) => void;
|
|
@@ -42,7 +42,7 @@ export class ConversationViewer implements Component {
|
|
|
42
42
|
|
|
43
43
|
private tui: TUI;
|
|
44
44
|
private session: AgentSession;
|
|
45
|
-
private record:
|
|
45
|
+
private record: Agent;
|
|
46
46
|
private activity: AgentActivityTracker | undefined;
|
|
47
47
|
private theme: Theme;
|
|
48
48
|
private done: (result: undefined) => void;
|