@gotgenes/pi-subagents 6.9.0 → 6.9.2
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 +18 -0
- package/docs/architecture/architecture.md +36 -31
- package/docs/plans/0115-decompose-agent-tool.md +337 -0
- package/docs/plans/0116-type-housekeeping.md +351 -0
- package/docs/retro/0114-narrow-agent-tool-menu-deps.md +38 -0
- package/docs/retro/0115-decompose-agent-tool.md +51 -0
- package/package.json +1 -1
- package/src/agent-manager.ts +12 -4
- package/src/agent-runner.ts +2 -1
- package/src/env.ts +7 -1
- package/src/index.ts +2 -4
- package/src/notification.ts +48 -33
- package/src/parent-snapshot.ts +20 -1
- package/src/prompts.ts +3 -2
- package/src/renderer.ts +1 -1
- package/src/session-config.ts +2 -1
- package/src/tools/agent-tool.ts +33 -201
- package/src/tools/background-spawner.ts +116 -0
- package/src/tools/foreground-runner.ts +175 -0
- package/src/tools/helpers.ts +45 -1
- package/src/types.ts +14 -46
- package/src/ui/agent-menu.ts +1 -1
- package/src/ui/conversation-viewer.ts +27 -10
package/src/notification.ts
CHANGED
|
@@ -1,8 +1,23 @@
|
|
|
1
1
|
import { debugLog } from "./debug.js";
|
|
2
|
-
import type { AgentRecord
|
|
2
|
+
import type { AgentRecord } from "./types.js";
|
|
3
3
|
import type { AgentActivityTracker } from "./ui/agent-activity-tracker.js";
|
|
4
4
|
import { getLifetimeTotal, getSessionContextPercent } from "./usage.js";
|
|
5
5
|
|
|
6
|
+
/** Details attached to custom notification messages for visual rendering. */
|
|
7
|
+
export interface NotificationDetails {
|
|
8
|
+
id: string;
|
|
9
|
+
description: string;
|
|
10
|
+
status: string;
|
|
11
|
+
toolUses: number;
|
|
12
|
+
turnCount: number;
|
|
13
|
+
maxTurns?: number;
|
|
14
|
+
totalTokens: number;
|
|
15
|
+
durationMs: number;
|
|
16
|
+
outputFile?: string;
|
|
17
|
+
error?: string;
|
|
18
|
+
resultPreview: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
6
21
|
// ---- Pure helpers (exported for unit testing) ----
|
|
7
22
|
|
|
8
23
|
/** Escape XML special characters to prevent injection in structured notifications. */
|
|
@@ -129,23 +144,43 @@ export interface NotificationSystem {
|
|
|
129
144
|
|
|
130
145
|
const NUDGE_HOLD_MS = 200;
|
|
131
146
|
|
|
132
|
-
export
|
|
133
|
-
|
|
147
|
+
export class NotificationManager implements NotificationSystem {
|
|
148
|
+
private pendingNudges = new Map<string, ReturnType<typeof setTimeout>>();
|
|
134
149
|
|
|
135
|
-
|
|
136
|
-
|
|
150
|
+
constructor(private deps: NotificationDeps) {}
|
|
151
|
+
|
|
152
|
+
cancelNudge(key: string): void {
|
|
153
|
+
const timer = this.pendingNudges.get(key);
|
|
137
154
|
if (timer != null) {
|
|
138
155
|
clearTimeout(timer);
|
|
139
|
-
pendingNudges.delete(key);
|
|
156
|
+
this.pendingNudges.delete(key);
|
|
140
157
|
}
|
|
141
158
|
}
|
|
142
159
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
160
|
+
sendCompletion(record: AgentRecord): void {
|
|
161
|
+
this.deps.agentActivity.delete(record.id);
|
|
162
|
+
this.deps.markFinished(record.id);
|
|
163
|
+
this.scheduleNudge(record.id, () => this.emitIndividualNudge(record));
|
|
164
|
+
this.deps.updateWidget();
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
cleanupCompleted(id: string): void {
|
|
168
|
+
this.deps.agentActivity.delete(id);
|
|
169
|
+
this.deps.markFinished(id);
|
|
170
|
+
this.deps.updateWidget();
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
dispose(): void {
|
|
174
|
+
for (const timer of this.pendingNudges.values()) clearTimeout(timer);
|
|
175
|
+
this.pendingNudges.clear();
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
private scheduleNudge(key: string, send: () => void, delay = NUDGE_HOLD_MS): void {
|
|
179
|
+
this.cancelNudge(key);
|
|
180
|
+
this.pendingNudges.set(
|
|
146
181
|
key,
|
|
147
182
|
setTimeout(() => {
|
|
148
|
-
pendingNudges.delete(key);
|
|
183
|
+
this.pendingNudges.delete(key);
|
|
149
184
|
try {
|
|
150
185
|
send();
|
|
151
186
|
} catch (err) {
|
|
@@ -155,41 +190,21 @@ export function createNotificationSystem(deps: NotificationDeps): NotificationSy
|
|
|
155
190
|
);
|
|
156
191
|
}
|
|
157
192
|
|
|
158
|
-
|
|
193
|
+
private emitIndividualNudge(record: AgentRecord): void {
|
|
159
194
|
if (record.notification?.resultConsumed) return;
|
|
160
195
|
|
|
161
196
|
const notification = formatTaskNotification(record, 500);
|
|
162
197
|
const outputFile = record.execution?.outputFile;
|
|
163
198
|
const footer = outputFile ? `\nFull transcript available at: ${outputFile}` : "";
|
|
164
199
|
|
|
165
|
-
deps.sendMessage(
|
|
200
|
+
this.deps.sendMessage(
|
|
166
201
|
{
|
|
167
202
|
customType: "subagent-notification",
|
|
168
203
|
content: notification + footer,
|
|
169
204
|
display: true,
|
|
170
|
-
details: buildNotificationDetails(record, 500, deps.agentActivity.get(record.id)),
|
|
205
|
+
details: buildNotificationDetails(record, 500, this.deps.agentActivity.get(record.id)),
|
|
171
206
|
},
|
|
172
207
|
{ deliverAs: "followUp", triggerTurn: true },
|
|
173
208
|
);
|
|
174
209
|
}
|
|
175
|
-
|
|
176
|
-
function sendCompletion(record: AgentRecord) {
|
|
177
|
-
deps.agentActivity.delete(record.id);
|
|
178
|
-
deps.markFinished(record.id);
|
|
179
|
-
scheduleNudge(record.id, () => emitIndividualNudge(record));
|
|
180
|
-
deps.updateWidget();
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
function cleanupCompleted(id: string) {
|
|
184
|
-
deps.agentActivity.delete(id);
|
|
185
|
-
deps.markFinished(id);
|
|
186
|
-
deps.updateWidget();
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
function dispose() {
|
|
190
|
-
for (const timer of pendingNudges.values()) clearTimeout(timer);
|
|
191
|
-
pendingNudges.clear();
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
return { cancelNudge, sendCompletion, cleanupCompleted, dispose };
|
|
195
210
|
}
|
package/src/parent-snapshot.ts
CHANGED
|
@@ -4,7 +4,26 @@
|
|
|
4
4
|
|
|
5
5
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
6
6
|
import { buildParentContext } from "./context.js";
|
|
7
|
-
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Plain data snapshot of the parent session state captured at spawn time.
|
|
10
|
+
* Replaces live `ExtensionContext` references so queued agents don't read stale state.
|
|
11
|
+
*/
|
|
12
|
+
export interface ParentSnapshot {
|
|
13
|
+
/** Parent working directory. */
|
|
14
|
+
cwd: string;
|
|
15
|
+
/** Parent's effective system prompt (for append-mode agents). */
|
|
16
|
+
systemPrompt: string;
|
|
17
|
+
/** Parent's current model instance (fallback when agent config has no model). */
|
|
18
|
+
model: unknown;
|
|
19
|
+
/** Model registry for resolving config.model strings and creating sessions. */
|
|
20
|
+
modelRegistry: {
|
|
21
|
+
find(provider: string, modelId: string): unknown;
|
|
22
|
+
getAvailable?(): Array<{ provider: string; id: string }>;
|
|
23
|
+
};
|
|
24
|
+
/** Pre-built parent conversation text (when inheritContext was requested). */
|
|
25
|
+
parentContext?: string;
|
|
26
|
+
}
|
|
8
27
|
|
|
9
28
|
/**
|
|
10
29
|
* Build an immutable snapshot of the parent session state.
|
package/src/prompts.ts
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* prompts.ts — System prompt builder for agents.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import type {
|
|
5
|
+
import type { EnvInfo } from "./env.js";
|
|
6
|
+
import type { AgentPromptConfig } from "./types.js";
|
|
6
7
|
|
|
7
8
|
/** Extra sections to inject into the system prompt (memory, skills, etc.). */
|
|
8
9
|
export interface PromptExtras {
|
|
@@ -27,7 +28,7 @@ export interface PromptExtras {
|
|
|
27
28
|
* @param extras Optional extra sections to inject (memory, preloaded skills).
|
|
28
29
|
*/
|
|
29
30
|
export function buildAgentPrompt(
|
|
30
|
-
config:
|
|
31
|
+
config: AgentPromptConfig,
|
|
31
32
|
cwd: string,
|
|
32
33
|
env: EnvInfo,
|
|
33
34
|
parentSystemPrompt?: string,
|
package/src/renderer.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Text } from "@earendil-works/pi-tui";
|
|
2
|
-
import type { NotificationDetails } from "./
|
|
2
|
+
import type { NotificationDetails } from "./notification.js";
|
|
3
3
|
import { formatMs, formatTokens, formatTurns } from "./ui/agent-widget.js";
|
|
4
4
|
|
|
5
5
|
/** Narrow theme interface — only the methods the renderer actually calls. */
|
package/src/session-config.ts
CHANGED
|
@@ -15,10 +15,11 @@ import {
|
|
|
15
15
|
getMemoryToolNames,
|
|
16
16
|
getReadOnlyMemoryToolNames,
|
|
17
17
|
} from "./agent-types.js";
|
|
18
|
+
import type { EnvInfo } from "./env.js";
|
|
18
19
|
import { buildMemoryBlock, buildReadOnlyMemoryBlock } from "./memory.js";
|
|
19
20
|
import { buildAgentPrompt, type PromptExtras } from "./prompts.js";
|
|
20
21
|
import { preloadSkills } from "./skill-loader.js";
|
|
21
|
-
import type {
|
|
22
|
+
import type { SubagentType, ThinkingLevel } from "./types.js";
|
|
22
23
|
|
|
23
24
|
// ── Public interfaces ────────────────────────────────────────────────────────
|
|
24
25
|
|
package/src/tools/agent-tool.ts
CHANGED
|
@@ -7,13 +7,11 @@ import { AgentTypeRegistry } from "../agent-types.js";
|
|
|
7
7
|
import { resolveAgentInvocationConfig } from "../invocation-config.js";
|
|
8
8
|
import { resolveInvocationModel } from "../model-resolver.js";
|
|
9
9
|
|
|
10
|
-
import { NotificationState } from "../notification-state.js";
|
|
11
10
|
import type { AgentInvocation, AgentRecord, SubagentType } from "../types.js";
|
|
12
11
|
import { AgentActivityTracker } from "../ui/agent-activity-tracker.js";
|
|
13
12
|
import {
|
|
14
13
|
type AgentDetails,
|
|
15
14
|
buildInvocationTags,
|
|
16
|
-
describeActivity,
|
|
17
15
|
formatMs,
|
|
18
16
|
formatTurns,
|
|
19
17
|
getDisplayName,
|
|
@@ -21,55 +19,9 @@ import {
|
|
|
21
19
|
SPINNER,
|
|
22
20
|
type UICtx,
|
|
23
21
|
} from "../ui/agent-widget.js";
|
|
24
|
-
import {
|
|
25
|
-
import
|
|
26
|
-
import { buildTypeListText, formatLifetimeTokens, textResult } from "./helpers.js";
|
|
27
|
-
|
|
28
|
-
// ---- Agent-tool-specific helpers ----
|
|
29
|
-
|
|
30
|
-
/** Parenthetical status note for completed agent result text. */
|
|
31
|
-
export function getStatusNote(status: string): string {
|
|
32
|
-
switch (status) {
|
|
33
|
-
case "aborted":
|
|
34
|
-
return " (aborted — max turns exceeded, output may be incomplete)";
|
|
35
|
-
case "steered":
|
|
36
|
-
return " (wrapped up — reached turn limit)";
|
|
37
|
-
case "stopped":
|
|
38
|
-
return " (stopped by user)";
|
|
39
|
-
default:
|
|
40
|
-
return "";
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/** Build AgentDetails from a base + record-specific fields. */
|
|
45
|
-
export function buildDetails(
|
|
46
|
-
base: Pick<AgentDetails, "displayName" | "description" | "subagentType" | "modelName" | "tags">,
|
|
47
|
-
record: {
|
|
48
|
-
toolUses: number;
|
|
49
|
-
startedAt: number;
|
|
50
|
-
completedAt?: number;
|
|
51
|
-
status: string;
|
|
52
|
-
error?: string;
|
|
53
|
-
id?: string;
|
|
54
|
-
session?: any;
|
|
55
|
-
lifetimeUsage: LifetimeUsage;
|
|
56
|
-
},
|
|
57
|
-
activity?: AgentActivityTracker,
|
|
58
|
-
overrides?: Partial<AgentDetails>,
|
|
59
|
-
): AgentDetails {
|
|
60
|
-
return {
|
|
61
|
-
...base,
|
|
62
|
-
toolUses: record.toolUses,
|
|
63
|
-
tokens: formatLifetimeTokens(record),
|
|
64
|
-
turnCount: activity?.turnCount,
|
|
65
|
-
maxTurns: activity?.maxTurns,
|
|
66
|
-
durationMs: (record.completedAt ?? Date.now()) - record.startedAt,
|
|
67
|
-
status: record.status as AgentDetails["status"],
|
|
68
|
-
agentId: record.id,
|
|
69
|
-
error: record.error,
|
|
70
|
-
...overrides,
|
|
71
|
-
};
|
|
72
|
-
}
|
|
22
|
+
import { spawnBackground } from "./background-spawner.js";
|
|
23
|
+
import { runForeground } from "./foreground-runner.js";
|
|
24
|
+
import { buildDetails, buildTypeListText, formatLifetimeTokens, getStatusNote, textResult } from "./helpers.js";
|
|
73
25
|
|
|
74
26
|
// ---- Deps interface ----
|
|
75
27
|
|
|
@@ -80,7 +32,6 @@ export interface AgentToolManager {
|
|
|
80
32
|
resume: (id: string, prompt: string, signal: AbortSignal) => Promise<AgentRecord | undefined>;
|
|
81
33
|
getRecord: (id: string) => AgentRecord | undefined;
|
|
82
34
|
getMaxConcurrent: () => number;
|
|
83
|
-
listAgents: () => AgentRecord[];
|
|
84
35
|
}
|
|
85
36
|
|
|
86
37
|
/** Narrow widget interface — only the methods the Agent tool calls. */
|
|
@@ -412,167 +363,48 @@ Guidelines:
|
|
|
412
363
|
|
|
413
364
|
// Background execution
|
|
414
365
|
if (runInBackground) {
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
parentSessionFile: ctx.sessionManager.getSessionFile(),
|
|
422
|
-
parentSessionId: ctx.sessionManager.getSessionId(),
|
|
366
|
+
return spawnBackground(
|
|
367
|
+
{ manager: deps.manager, widget: deps.widget, agentActivity: deps.agentActivity },
|
|
368
|
+
{
|
|
369
|
+
ctx,
|
|
370
|
+
subagentType,
|
|
371
|
+
prompt: params.prompt as string,
|
|
423
372
|
description: params.description as string,
|
|
373
|
+
displayName,
|
|
374
|
+
toolCallId,
|
|
375
|
+
detailBase,
|
|
424
376
|
model,
|
|
425
|
-
|
|
377
|
+
effectiveMaxTurns,
|
|
426
378
|
isolated,
|
|
427
379
|
inheritContext,
|
|
428
|
-
|
|
429
|
-
isBackground: true,
|
|
380
|
+
thinking,
|
|
430
381
|
isolation,
|
|
431
|
-
|
|
432
|
-
onSessionCreated: (session: any) => {
|
|
433
|
-
bgState.setSession(session);
|
|
434
|
-
subscribeUIObserver(session, bgState);
|
|
435
|
-
},
|
|
436
|
-
});
|
|
437
|
-
} catch (err) {
|
|
438
|
-
return textResult(err instanceof Error ? err.message : String(err));
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
const record = deps.manager.getRecord(id);
|
|
442
|
-
if (record) {
|
|
443
|
-
// Born complete: notification-state object owns toolCallId + resultConsumed.
|
|
444
|
-
record.notification = new NotificationState(toolCallId);
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
deps.agentActivity.set(id, bgState);
|
|
448
|
-
deps.widget.ensureTimer();
|
|
449
|
-
deps.widget.update();
|
|
450
|
-
|
|
451
|
-
const isQueued = record?.status === "queued";
|
|
452
|
-
return textResult(
|
|
453
|
-
`Agent ${isQueued ? "queued" : "started"} in background.\n` +
|
|
454
|
-
`Agent ID: ${id}\n` +
|
|
455
|
-
`Type: ${displayName}\n` +
|
|
456
|
-
`Description: ${params.description}\n` +
|
|
457
|
-
(record?.execution?.outputFile ? `Output file: ${record.execution.outputFile}\n` : "") +
|
|
458
|
-
(isQueued
|
|
459
|
-
? `Position: queued (max ${deps.manager.getMaxConcurrent()} concurrent)\n`
|
|
460
|
-
: "") +
|
|
461
|
-
`\nYou will be notified when this agent completes.\n` +
|
|
462
|
-
`Use get_subagent_result to retrieve full results, or steer_subagent to send it messages.\n` +
|
|
463
|
-
`Do not duplicate this agent's work.`,
|
|
464
|
-
{
|
|
465
|
-
...detailBase,
|
|
466
|
-
toolUses: 0,
|
|
467
|
-
tokens: "",
|
|
468
|
-
durationMs: 0,
|
|
469
|
-
status: "background" as const,
|
|
470
|
-
agentId: id,
|
|
382
|
+
agentInvocation,
|
|
471
383
|
},
|
|
472
384
|
);
|
|
473
385
|
}
|
|
474
386
|
|
|
475
387
|
// Foreground (synchronous) execution — stream progress via onUpdate
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
const fgState = new AgentActivityTracker(effectiveMaxTurns);
|
|
481
|
-
let unsubUI: (() => void) | undefined;
|
|
482
|
-
|
|
483
|
-
const streamUpdate = () => {
|
|
484
|
-
const details: AgentDetails = {
|
|
485
|
-
...detailBase,
|
|
486
|
-
toolUses: fgState.toolUses,
|
|
487
|
-
tokens: formatLifetimeTokens(fgState),
|
|
488
|
-
turnCount: fgState.turnCount,
|
|
489
|
-
maxTurns: fgState.maxTurns,
|
|
490
|
-
durationMs: Date.now() - startedAt,
|
|
491
|
-
status: "running",
|
|
492
|
-
activity: describeActivity(fgState.activeTools, fgState.responseText),
|
|
493
|
-
spinnerFrame: spinnerFrame % SPINNER.length,
|
|
494
|
-
};
|
|
495
|
-
onUpdate?.({
|
|
496
|
-
content: [{ type: "text", text: `${fgState.toolUses} tool uses...` }],
|
|
497
|
-
details: details as any,
|
|
498
|
-
});
|
|
499
|
-
};
|
|
500
|
-
|
|
501
|
-
// Animate spinner at ~80ms (smooth rotation through 10 braille frames)
|
|
502
|
-
const spinnerInterval = setInterval(() => {
|
|
503
|
-
spinnerFrame++;
|
|
504
|
-
streamUpdate();
|
|
505
|
-
}, 80);
|
|
506
|
-
|
|
507
|
-
streamUpdate();
|
|
508
|
-
|
|
509
|
-
let record: AgentRecord;
|
|
510
|
-
try {
|
|
511
|
-
record = await deps.manager.spawnAndWait(
|
|
388
|
+
return runForeground(
|
|
389
|
+
{ manager: deps.manager, widget: deps.widget, agentActivity: deps.agentActivity },
|
|
390
|
+
{
|
|
512
391
|
ctx,
|
|
513
392
|
subagentType,
|
|
514
|
-
params.prompt as string,
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
unsubUI = subscribeUIObserver(session, fgState, streamUpdate);
|
|
530
|
-
for (const a of deps.manager.listAgents()) {
|
|
531
|
-
if (a.execution?.session === session) {
|
|
532
|
-
fgId = a.id;
|
|
533
|
-
deps.agentActivity.set(a.id, fgState);
|
|
534
|
-
deps.widget.ensureTimer();
|
|
535
|
-
break;
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
|
-
},
|
|
539
|
-
},
|
|
540
|
-
);
|
|
541
|
-
} catch (err) {
|
|
542
|
-
clearInterval(spinnerInterval);
|
|
543
|
-
unsubUI?.();
|
|
544
|
-
return textResult(err instanceof Error ? err.message : String(err));
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
clearInterval(spinnerInterval);
|
|
548
|
-
unsubUI?.();
|
|
549
|
-
|
|
550
|
-
// Clean up foreground agent from widget
|
|
551
|
-
if (fgId) {
|
|
552
|
-
deps.agentActivity.delete(fgId);
|
|
553
|
-
deps.widget.markFinished(fgId);
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
// Get final token count
|
|
557
|
-
const tokenText = formatLifetimeTokens(fgState);
|
|
558
|
-
|
|
559
|
-
const details = buildDetails(detailBase, record, fgState, { tokens: tokenText });
|
|
560
|
-
|
|
561
|
-
const fallbackNote = fellBack
|
|
562
|
-
? `Note: Unknown agent type "${rawType}" — using general-purpose.\n\n`
|
|
563
|
-
: "";
|
|
564
|
-
|
|
565
|
-
if (record.status === "error") {
|
|
566
|
-
return textResult(`${fallbackNote}Agent failed: ${record.error}`, details);
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
const durationMs = (record.completedAt ?? Date.now()) - record.startedAt;
|
|
570
|
-
const statsParts = [`${record.toolUses} tool uses`];
|
|
571
|
-
if (tokenText) statsParts.push(tokenText);
|
|
572
|
-
return textResult(
|
|
573
|
-
`${fallbackNote}Agent completed in ${formatMs(durationMs)} (${statsParts.join(", ")})${getStatusNote(record.status)}.\n\n` +
|
|
574
|
-
(record.result?.trim() || "No output."),
|
|
575
|
-
details,
|
|
393
|
+
prompt: params.prompt as string,
|
|
394
|
+
description: params.description as string,
|
|
395
|
+
detailBase,
|
|
396
|
+
rawType,
|
|
397
|
+
fellBack,
|
|
398
|
+
model,
|
|
399
|
+
effectiveMaxTurns,
|
|
400
|
+
isolated,
|
|
401
|
+
inheritContext,
|
|
402
|
+
thinking,
|
|
403
|
+
isolation,
|
|
404
|
+
agentInvocation,
|
|
405
|
+
},
|
|
406
|
+
signal,
|
|
407
|
+
onUpdate,
|
|
576
408
|
);
|
|
577
409
|
},
|
|
578
410
|
};
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import type { Model } from "@earendil-works/pi-ai";
|
|
2
|
+
import type { AgentSpawnConfig } from "../agent-manager.js";
|
|
3
|
+
import type { AgentInvocation, AgentRecord, IsolationMode, ThinkingLevel } from "../types.js";
|
|
4
|
+
import { AgentActivityTracker } from "../ui/agent-activity-tracker.js";
|
|
5
|
+
import type { AgentDetails } from "../ui/agent-widget.js";
|
|
6
|
+
import { subscribeUIObserver } from "../ui/ui-observer.js";
|
|
7
|
+
import type { AgentActivityAccess } from "./agent-tool.js";
|
|
8
|
+
import { textResult } from "./helpers.js";
|
|
9
|
+
|
|
10
|
+
/** Narrow manager interface for the background spawner. */
|
|
11
|
+
export interface BackgroundManagerDeps {
|
|
12
|
+
spawn(ctx: any, type: string, prompt: string, opts: AgentSpawnConfig): string;
|
|
13
|
+
getRecord(id: string): AgentRecord | undefined;
|
|
14
|
+
getMaxConcurrent(): number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Narrow widget interface for the background spawner. */
|
|
18
|
+
export interface BackgroundWidgetDeps {
|
|
19
|
+
ensureTimer(): void;
|
|
20
|
+
update(): void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Injected collaborators for spawnBackground. */
|
|
24
|
+
export interface BackgroundDeps {
|
|
25
|
+
manager: BackgroundManagerDeps;
|
|
26
|
+
widget: BackgroundWidgetDeps;
|
|
27
|
+
agentActivity: AgentActivityAccess;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** All values the background spawner needs, bundled from shared execute setup. */
|
|
31
|
+
export interface BackgroundParams {
|
|
32
|
+
ctx: {
|
|
33
|
+
sessionManager: {
|
|
34
|
+
getSessionFile(): string;
|
|
35
|
+
getSessionId(): string;
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
subagentType: string;
|
|
39
|
+
prompt: string;
|
|
40
|
+
description: string;
|
|
41
|
+
displayName: string;
|
|
42
|
+
toolCallId: string;
|
|
43
|
+
detailBase: Pick<AgentDetails, "displayName" | "description" | "subagentType" | "modelName" | "tags">;
|
|
44
|
+
model: Model<any> | undefined;
|
|
45
|
+
effectiveMaxTurns: number | undefined;
|
|
46
|
+
isolated: boolean | undefined;
|
|
47
|
+
inheritContext: boolean | undefined;
|
|
48
|
+
thinking: ThinkingLevel | undefined;
|
|
49
|
+
isolation: IsolationMode | undefined;
|
|
50
|
+
agentInvocation: AgentInvocation;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Spawn a background agent and return the tool result immediately.
|
|
55
|
+
* Owns: activity tracker creation, UI observer subscription, activity map
|
|
56
|
+
* registration, widget update, and launch message formatting.
|
|
57
|
+
*/
|
|
58
|
+
export function spawnBackground(
|
|
59
|
+
deps: BackgroundDeps,
|
|
60
|
+
params: BackgroundParams,
|
|
61
|
+
) {
|
|
62
|
+
const bgState = new AgentActivityTracker(params.effectiveMaxTurns);
|
|
63
|
+
|
|
64
|
+
let id: string;
|
|
65
|
+
try {
|
|
66
|
+
id = deps.manager.spawn(params.ctx, params.subagentType, params.prompt, {
|
|
67
|
+
parentSessionFile: params.ctx.sessionManager.getSessionFile(),
|
|
68
|
+
parentSessionId: params.ctx.sessionManager.getSessionId(),
|
|
69
|
+
description: params.description,
|
|
70
|
+
model: params.model,
|
|
71
|
+
maxTurns: params.effectiveMaxTurns,
|
|
72
|
+
isolated: params.isolated,
|
|
73
|
+
inheritContext: params.inheritContext,
|
|
74
|
+
thinkingLevel: params.thinking,
|
|
75
|
+
isBackground: true,
|
|
76
|
+
isolation: params.isolation,
|
|
77
|
+
invocation: params.agentInvocation,
|
|
78
|
+
toolCallId: params.toolCallId,
|
|
79
|
+
onSessionCreated: (session) => {
|
|
80
|
+
bgState.setSession(session);
|
|
81
|
+
subscribeUIObserver(session, bgState);
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
} catch (err) {
|
|
85
|
+
return textResult(err instanceof Error ? err.message : String(err));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const record = deps.manager.getRecord(id);
|
|
89
|
+
|
|
90
|
+
deps.agentActivity.set(id, bgState);
|
|
91
|
+
deps.widget.ensureTimer();
|
|
92
|
+
deps.widget.update();
|
|
93
|
+
|
|
94
|
+
const isQueued = record?.status === "queued";
|
|
95
|
+
return textResult(
|
|
96
|
+
`Agent ${isQueued ? "queued" : "started"} in background.\n` +
|
|
97
|
+
`Agent ID: ${id}\n` +
|
|
98
|
+
`Type: ${params.displayName}\n` +
|
|
99
|
+
`Description: ${params.description}\n` +
|
|
100
|
+
(record?.execution?.outputFile ? `Output file: ${record.execution.outputFile}\n` : "") +
|
|
101
|
+
(isQueued
|
|
102
|
+
? `Position: queued (max ${deps.manager.getMaxConcurrent()} concurrent)\n`
|
|
103
|
+
: "") +
|
|
104
|
+
`\nYou will be notified when this agent completes.\n` +
|
|
105
|
+
`Use get_subagent_result to retrieve full results, or steer_subagent to send it messages.\n` +
|
|
106
|
+
`Do not duplicate this agent's work.`,
|
|
107
|
+
{
|
|
108
|
+
...params.detailBase,
|
|
109
|
+
toolUses: 0,
|
|
110
|
+
tokens: "",
|
|
111
|
+
durationMs: 0,
|
|
112
|
+
status: "background" as const,
|
|
113
|
+
agentId: id,
|
|
114
|
+
},
|
|
115
|
+
);
|
|
116
|
+
}
|