@gotgenes/pi-subagents 6.14.1 → 6.16.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 +28 -0
- package/docs/architecture/architecture.md +14 -30
- package/docs/plans/0144-consolidate-observation-model.md +263 -0
- package/docs/plans/0145-decompose-execute-push-ctx-to-boundary.md +290 -0
- package/docs/retro/0145-decompose-execute-push-ctx-to-boundary.md +56 -0
- package/package.json +1 -1
- package/src/agent-manager.ts +7 -9
- package/src/agent-record.ts +11 -0
- package/src/index.ts +27 -13
- package/src/notification.ts +21 -24
- package/src/service-adapter.ts +19 -17
- package/src/tools/agent-tool.ts +56 -113
- package/src/tools/background-spawner.ts +34 -52
- package/src/tools/foreground-runner.ts +43 -61
- package/src/tools/get-result-tool.ts +3 -3
- package/src/tools/spawn-config.ts +146 -0
- package/src/tools/steer-tool.ts +1 -1
- package/src/ui/agent-activity-tracker.ts +3 -27
- package/src/ui/agent-menu.ts +1 -1
- package/src/ui/agent-widget.ts +3 -4
- package/src/ui/conversation-viewer.ts +3 -3
- package/src/ui/ui-observer.ts +1 -12
package/src/service-adapter.ts
CHANGED
|
@@ -5,13 +5,16 @@
|
|
|
5
5
|
* (stripping non-serializable fields), and session gating.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
8
9
|
import type { ModelRegistry } from "./model-resolver.js";
|
|
10
|
+
import type { ParentSnapshot } from "./parent-snapshot.js";
|
|
11
|
+
import { buildParentSnapshot } from "./parent-snapshot.js";
|
|
9
12
|
import type { SubagentRecord, SubagentsService } from "./service.js";
|
|
10
13
|
import type { AgentRecord } from "./types.js";
|
|
11
14
|
|
|
12
15
|
/** Narrow interface for the AgentManager — avoids coupling to the concrete class. */
|
|
13
16
|
export interface AgentManagerLike {
|
|
14
|
-
spawn(
|
|
17
|
+
spawn(snapshot: ParentSnapshot, type: string, prompt: string, options: unknown): string;
|
|
15
18
|
getRecord(id: string): AgentRecord | undefined;
|
|
16
19
|
listAgents(): AgentRecord[];
|
|
17
20
|
abort(id: string): boolean;
|
|
@@ -20,32 +23,27 @@ export interface AgentManagerLike {
|
|
|
20
23
|
queueSteer(id: string, message: string): boolean;
|
|
21
24
|
}
|
|
22
25
|
|
|
23
|
-
/** Dependencies injected into the adapter factory. */
|
|
24
|
-
export interface AdapterDeps {
|
|
25
|
-
manager: AgentManagerLike;
|
|
26
|
-
resolveModel: (input: string, registry: ModelRegistry) => unknown | string;
|
|
27
|
-
getCtx: () => { pi: unknown; ctx: unknown } | undefined;
|
|
28
|
-
getModelRegistry: () => ModelRegistry | undefined;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
26
|
/** Create a SubagentsService backed by the given dependencies. */
|
|
32
|
-
export function createSubagentsService(
|
|
33
|
-
|
|
34
|
-
|
|
27
|
+
export function createSubagentsService(
|
|
28
|
+
manager: AgentManagerLike,
|
|
29
|
+
resolveModel: (input: string, registry: ModelRegistry) => unknown | string,
|
|
30
|
+
getCtx: () => { pi: unknown; ctx: unknown } | undefined,
|
|
31
|
+
getModelRegistry: () => ModelRegistry | undefined,
|
|
32
|
+
): SubagentsService {
|
|
35
33
|
return {
|
|
36
34
|
spawn(type: string, prompt: string, options?) {
|
|
37
|
-
const session =
|
|
35
|
+
const session = getCtx();
|
|
38
36
|
if (!session) {
|
|
39
37
|
throw new Error("No active session — cannot spawn agents outside a session.");
|
|
40
38
|
}
|
|
41
39
|
|
|
42
40
|
let model: unknown;
|
|
43
41
|
if (options?.model) {
|
|
44
|
-
const registry =
|
|
42
|
+
const registry = getModelRegistry();
|
|
45
43
|
if (!registry) {
|
|
46
44
|
throw new Error("No model registry available.");
|
|
47
45
|
}
|
|
48
|
-
const resolved =
|
|
46
|
+
const resolved = resolveModel(options.model, registry);
|
|
49
47
|
if (typeof resolved === "string") {
|
|
50
48
|
throw new Error(resolved);
|
|
51
49
|
}
|
|
@@ -55,7 +53,11 @@ export function createSubagentsService(deps: AdapterDeps): SubagentsService {
|
|
|
55
53
|
const description = options?.description ?? prompt.slice(0, 80);
|
|
56
54
|
const isBackground = !(options?.foreground ?? false);
|
|
57
55
|
|
|
58
|
-
|
|
56
|
+
const snapshot = buildParentSnapshot(
|
|
57
|
+
session.ctx as ExtensionContext,
|
|
58
|
+
options?.inheritContext,
|
|
59
|
+
);
|
|
60
|
+
return manager.spawn(snapshot, type, prompt, {
|
|
59
61
|
description,
|
|
60
62
|
model,
|
|
61
63
|
maxTurns: options?.maxTurns,
|
|
@@ -86,7 +88,7 @@ export function createSubagentsService(deps: AdapterDeps): SubagentsService {
|
|
|
86
88
|
if (!record || record.status !== "running") {
|
|
87
89
|
return false;
|
|
88
90
|
}
|
|
89
|
-
const session = record.
|
|
91
|
+
const session = record.session;
|
|
90
92
|
if (!session) {
|
|
91
93
|
// Session not ready yet — queue via manager for delivery once initialized
|
|
92
94
|
return manager.queueSteer(id, message);
|
package/src/tools/agent-tool.ts
CHANGED
|
@@ -1,34 +1,31 @@
|
|
|
1
|
-
import type { AgentToolResult
|
|
1
|
+
import type { AgentToolResult } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { Text } from "@earendil-works/pi-tui";
|
|
3
3
|
import { Type } from "@sinclair/typebox";
|
|
4
4
|
import type { AgentSpawnConfig } from "../agent-manager.js";
|
|
5
|
-
import { normalizeMaxTurns } from "../agent-runner.js";
|
|
6
5
|
import { AgentTypeRegistry } from "../agent-types.js";
|
|
7
|
-
import {
|
|
8
|
-
import { resolveInvocationModel } from "../model-resolver.js";
|
|
6
|
+
import type { ParentSnapshot } from "../parent-snapshot.js";
|
|
9
7
|
|
|
10
|
-
import type {
|
|
8
|
+
import type { AgentRecord } from "../types.js";
|
|
11
9
|
import { AgentActivityTracker } from "../ui/agent-activity-tracker.js";
|
|
12
10
|
import { type UICtx } from "../ui/agent-widget.js";
|
|
13
11
|
import {
|
|
14
12
|
type AgentDetails,
|
|
15
|
-
buildInvocationTags,
|
|
16
13
|
formatMs,
|
|
17
14
|
formatTurns,
|
|
18
15
|
getDisplayName,
|
|
19
|
-
getPromptModeLabel,
|
|
20
16
|
SPINNER,
|
|
21
17
|
} from "../ui/display.js";
|
|
22
18
|
import { spawnBackground } from "./background-spawner.js";
|
|
23
19
|
import { runForeground } from "./foreground-runner.js";
|
|
24
20
|
import { buildDetails, buildTypeListText, textResult } from "./helpers.js";
|
|
21
|
+
import { type ModelInfo, resolveSpawnConfig } from "./spawn-config.js";
|
|
25
22
|
|
|
26
23
|
// ---- Deps interface ----
|
|
27
24
|
|
|
28
25
|
/** Narrow manager interface — only the methods the Agent tool calls. */
|
|
29
26
|
export interface AgentToolManager {
|
|
30
|
-
spawn: (
|
|
31
|
-
spawnAndWait: (
|
|
27
|
+
spawn: (snapshot: ParentSnapshot, type: string, prompt: string, opts: AgentSpawnConfig) => string;
|
|
28
|
+
spawnAndWait: (snapshot: ParentSnapshot, type: string, prompt: string, opts: Omit<AgentSpawnConfig, "isBackground">) => Promise<AgentRecord>;
|
|
32
29
|
resume: (id: string, prompt: string, signal: AbortSignal) => Promise<AgentRecord | undefined>;
|
|
33
30
|
getRecord: (id: string) => AgentRecord | undefined;
|
|
34
31
|
getMaxConcurrent: () => number;
|
|
@@ -60,14 +57,30 @@ export interface AgentToolDeps {
|
|
|
60
57
|
agentDir: string;
|
|
61
58
|
/** Narrow settings accessor — only the default max turns is needed here. */
|
|
62
59
|
settings: { readonly defaultMaxTurns: number | undefined };
|
|
60
|
+
/** Build a ParentSnapshot from the current session context. */
|
|
61
|
+
buildSnapshot: (inheritContext: boolean) => ParentSnapshot;
|
|
62
|
+
/** Model info from the current session context. */
|
|
63
|
+
getModelInfo: () => ModelInfo;
|
|
64
|
+
/** Parent session identity from the current session context. */
|
|
65
|
+
getSessionInfo: () => { parentSessionFile: string; parentSessionId: string };
|
|
63
66
|
}
|
|
64
67
|
|
|
65
68
|
// ---- Factory ----
|
|
66
69
|
|
|
67
70
|
/** Create the Agent tool definition (without Pi SDK wrapper). */
|
|
68
|
-
export function createAgentTool(
|
|
69
|
-
|
|
70
|
-
|
|
71
|
+
export function createAgentTool({
|
|
72
|
+
manager,
|
|
73
|
+
widget,
|
|
74
|
+
agentActivity,
|
|
75
|
+
registry,
|
|
76
|
+
agentDir,
|
|
77
|
+
settings,
|
|
78
|
+
buildSnapshot,
|
|
79
|
+
getModelInfo,
|
|
80
|
+
getSessionInfo,
|
|
81
|
+
}: AgentToolDeps) {
|
|
82
|
+
const typeListText = buildTypeListText(registry, agentDir);
|
|
83
|
+
const availableTypesText = registry.getAvailableTypes().join(", ");
|
|
71
84
|
return {
|
|
72
85
|
name: "Agent" as const,
|
|
73
86
|
label: "Agent",
|
|
@@ -101,7 +114,7 @@ Guidelines:
|
|
|
101
114
|
description: "A short (3-5 word) description of the task (shown in UI).",
|
|
102
115
|
}),
|
|
103
116
|
subagent_type: Type.String({
|
|
104
|
-
description: `The type of specialized agent to use. Available types: ${availableTypesText}. Custom agents from .pi/agents/<name>.md (project) or ${
|
|
117
|
+
description: `The type of specialized agent to use. Available types: ${availableTypesText}. Custom agents from .pi/agents/<name>.md (project) or ${agentDir}/agents/<name>.md (global) are also available.`,
|
|
105
118
|
}),
|
|
106
119
|
model: Type.Optional(
|
|
107
120
|
Type.String({
|
|
@@ -156,7 +169,7 @@ Guidelines:
|
|
|
156
169
|
|
|
157
170
|
renderCall(args: Record<string, unknown>, theme: any) {
|
|
158
171
|
const displayName = args.subagent_type
|
|
159
|
-
? getDisplayName(args.subagent_type as string,
|
|
172
|
+
? getDisplayName(args.subagent_type as string, registry)
|
|
160
173
|
: "Agent";
|
|
161
174
|
const desc = (args.description as string) ?? "";
|
|
162
175
|
return new Text(
|
|
@@ -273,82 +286,38 @@ Guidelines:
|
|
|
273
286
|
ctx: any,
|
|
274
287
|
) => {
|
|
275
288
|
// Ensure we have UI context for widget rendering
|
|
276
|
-
|
|
289
|
+
widget.setUICtx(ctx.ui as UICtx);
|
|
277
290
|
|
|
278
291
|
// Reload custom agents so new .pi/agents/*.md files are picked up without restart
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
const
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
// Get agent config for invocation resolution
|
|
289
|
-
const customConfig = deps.registry.resolveAgentConfig(subagentType);
|
|
290
|
-
|
|
291
|
-
const resolvedConfig = resolveAgentInvocationConfig(customConfig, params);
|
|
292
|
-
|
|
293
|
-
// Resolve model from agent config first; tool-call params only fill gaps.
|
|
294
|
-
const resolution = resolveInvocationModel(
|
|
295
|
-
ctx.model,
|
|
296
|
-
resolvedConfig.modelInput,
|
|
297
|
-
resolvedConfig.modelFromParams,
|
|
298
|
-
ctx.modelRegistry,
|
|
292
|
+
registry.reload();
|
|
293
|
+
|
|
294
|
+
// ---- Config resolution (pure) ----
|
|
295
|
+
const config = resolveSpawnConfig(
|
|
296
|
+
params,
|
|
297
|
+
registry,
|
|
298
|
+
getModelInfo(),
|
|
299
|
+
settings,
|
|
299
300
|
);
|
|
300
|
-
if (
|
|
301
|
-
const model = resolution.model;
|
|
301
|
+
if ("error" in config) return textResult(config.error);
|
|
302
302
|
|
|
303
|
-
|
|
304
|
-
const
|
|
305
|
-
const
|
|
306
|
-
const isolated = resolvedConfig.isolated;
|
|
307
|
-
const isolation = resolvedConfig.isolation;
|
|
308
|
-
|
|
309
|
-
const parentModelId = ctx.model?.id;
|
|
310
|
-
const effectiveModelId = model?.id;
|
|
311
|
-
const modelName =
|
|
312
|
-
effectiveModelId && effectiveModelId !== parentModelId
|
|
313
|
-
? (model?.name ?? effectiveModelId).replace(/^Claude\s+/i, "").toLowerCase()
|
|
314
|
-
: undefined;
|
|
315
|
-
const effectiveMaxTurns = normalizeMaxTurns(
|
|
316
|
-
resolvedConfig.maxTurns ?? deps.settings.defaultMaxTurns,
|
|
317
|
-
);
|
|
318
|
-
const agentInvocation: AgentInvocation = {
|
|
319
|
-
modelName,
|
|
320
|
-
thinking,
|
|
321
|
-
maxTurns: normalizeMaxTurns(resolvedConfig.maxTurns),
|
|
322
|
-
isolated,
|
|
323
|
-
inheritContext,
|
|
324
|
-
runInBackground,
|
|
325
|
-
isolation,
|
|
326
|
-
};
|
|
327
|
-
const modeLabel = getPromptModeLabel(subagentType, deps.registry);
|
|
328
|
-
const { tags: invocationTags } = buildInvocationTags(agentInvocation);
|
|
329
|
-
const agentTags = modeLabel ? [modeLabel, ...invocationTags] : invocationTags;
|
|
330
|
-
const detailBase = {
|
|
331
|
-
displayName,
|
|
332
|
-
description: params.description as string,
|
|
333
|
-
subagentType,
|
|
334
|
-
modelName,
|
|
335
|
-
tags: agentTags.length > 0 ? agentTags : undefined,
|
|
336
|
-
};
|
|
303
|
+
// ---- Boundary extraction (after config so inheritContext is resolved) ----
|
|
304
|
+
const snapshot = buildSnapshot(config.inheritContext);
|
|
305
|
+
const { parentSessionFile, parentSessionId } = getSessionInfo();
|
|
337
306
|
|
|
338
|
-
// Resume existing agent
|
|
307
|
+
// ---- Resume existing agent ----
|
|
339
308
|
if (params.resume) {
|
|
340
|
-
const existing =
|
|
309
|
+
const existing = manager.getRecord(params.resume as string);
|
|
341
310
|
if (!existing) {
|
|
342
311
|
return textResult(
|
|
343
312
|
`Agent not found: "${params.resume}". It may have been cleaned up.`,
|
|
344
313
|
);
|
|
345
314
|
}
|
|
346
|
-
if (!existing.
|
|
315
|
+
if (!existing.session) {
|
|
347
316
|
return textResult(
|
|
348
317
|
`Agent "${params.resume}" has no active session to resume.`,
|
|
349
318
|
);
|
|
350
319
|
}
|
|
351
|
-
const record = await
|
|
320
|
+
const record = await manager.resume(
|
|
352
321
|
params.resume as string,
|
|
353
322
|
params.prompt as string,
|
|
354
323
|
signal ?? new AbortController().signal,
|
|
@@ -358,52 +327,26 @@ Guidelines:
|
|
|
358
327
|
}
|
|
359
328
|
return textResult(
|
|
360
329
|
record.result?.trim() || record.error?.trim() || "No output.",
|
|
361
|
-
buildDetails(detailBase, record),
|
|
330
|
+
buildDetails(config.detailBase, record),
|
|
362
331
|
);
|
|
363
332
|
}
|
|
364
333
|
|
|
365
|
-
// Background execution
|
|
366
|
-
if (runInBackground) {
|
|
334
|
+
// ---- Background execution ----
|
|
335
|
+
if (config.runInBackground) {
|
|
367
336
|
return spawnBackground(
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
prompt: params.prompt as string,
|
|
373
|
-
description: params.description as string,
|
|
374
|
-
displayName,
|
|
375
|
-
toolCallId,
|
|
376
|
-
detailBase,
|
|
377
|
-
model,
|
|
378
|
-
effectiveMaxTurns,
|
|
379
|
-
isolated,
|
|
380
|
-
inheritContext,
|
|
381
|
-
thinking,
|
|
382
|
-
isolation,
|
|
383
|
-
agentInvocation,
|
|
384
|
-
},
|
|
337
|
+
manager,
|
|
338
|
+
widget,
|
|
339
|
+
agentActivity,
|
|
340
|
+
{ config, snapshot, parentSessionFile, parentSessionId, toolCallId },
|
|
385
341
|
);
|
|
386
342
|
}
|
|
387
343
|
|
|
388
|
-
// Foreground
|
|
344
|
+
// ---- Foreground execution — stream progress via onUpdate ----
|
|
389
345
|
return runForeground(
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
prompt: params.prompt as string,
|
|
395
|
-
description: params.description as string,
|
|
396
|
-
detailBase,
|
|
397
|
-
rawType,
|
|
398
|
-
fellBack,
|
|
399
|
-
model,
|
|
400
|
-
effectiveMaxTurns,
|
|
401
|
-
isolated,
|
|
402
|
-
inheritContext,
|
|
403
|
-
thinking,
|
|
404
|
-
isolation,
|
|
405
|
-
agentInvocation,
|
|
406
|
-
},
|
|
346
|
+
manager,
|
|
347
|
+
widget,
|
|
348
|
+
agentActivity,
|
|
349
|
+
{ config, snapshot, parentSessionFile, parentSessionId },
|
|
407
350
|
signal,
|
|
408
351
|
onUpdate,
|
|
409
352
|
);
|
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import type { Model } from "@earendil-works/pi-ai";
|
|
2
1
|
import type { AgentSpawnConfig } from "../agent-manager.js";
|
|
3
|
-
import type {
|
|
2
|
+
import type { ParentSnapshot } from "../parent-snapshot.js";
|
|
3
|
+
import type { AgentRecord } from "../types.js";
|
|
4
4
|
import { AgentActivityTracker } from "../ui/agent-activity-tracker.js";
|
|
5
|
-
import type { AgentDetails } from "../ui/display.js";
|
|
6
5
|
import { subscribeUIObserver } from "../ui/ui-observer.js";
|
|
7
6
|
import type { AgentActivityAccess } from "./agent-tool.js";
|
|
8
7
|
import { textResult } from "./helpers.js";
|
|
8
|
+
import type { ResolvedSpawnConfig } from "./spawn-config.js";
|
|
9
9
|
|
|
10
10
|
/** Narrow manager interface for the background spawner. */
|
|
11
11
|
export interface BackgroundManagerDeps {
|
|
12
|
-
spawn(
|
|
12
|
+
spawn(snapshot: ParentSnapshot, type: string, prompt: string, opts: AgentSpawnConfig): string;
|
|
13
13
|
getRecord(id: string): AgentRecord | undefined;
|
|
14
14
|
getMaxConcurrent(): number;
|
|
15
15
|
}
|
|
@@ -20,34 +20,13 @@ export interface BackgroundWidgetDeps {
|
|
|
20
20
|
update(): void;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
/**
|
|
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. */
|
|
23
|
+
/** All values the background spawner needs beyond the resolved config. */
|
|
31
24
|
export interface BackgroundParams {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
};
|
|
37
|
-
};
|
|
38
|
-
subagentType: string;
|
|
39
|
-
prompt: string;
|
|
40
|
-
description: string;
|
|
41
|
-
displayName: string;
|
|
25
|
+
config: ResolvedSpawnConfig;
|
|
26
|
+
snapshot: ParentSnapshot;
|
|
27
|
+
parentSessionFile: string;
|
|
28
|
+
parentSessionId: string;
|
|
42
29
|
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
30
|
}
|
|
52
31
|
|
|
53
32
|
/**
|
|
@@ -56,25 +35,28 @@ export interface BackgroundParams {
|
|
|
56
35
|
* registration, widget update, and launch message formatting.
|
|
57
36
|
*/
|
|
58
37
|
export function spawnBackground(
|
|
59
|
-
|
|
38
|
+
manager: BackgroundManagerDeps,
|
|
39
|
+
widget: BackgroundWidgetDeps,
|
|
40
|
+
agentActivity: AgentActivityAccess,
|
|
60
41
|
params: BackgroundParams,
|
|
61
42
|
) {
|
|
62
|
-
const
|
|
43
|
+
const { config } = params;
|
|
44
|
+
const bgState = new AgentActivityTracker(config.effectiveMaxTurns);
|
|
63
45
|
|
|
64
46
|
let id: string;
|
|
65
47
|
try {
|
|
66
|
-
id =
|
|
67
|
-
parentSessionFile: params.
|
|
68
|
-
parentSessionId: params.
|
|
69
|
-
description:
|
|
70
|
-
model:
|
|
71
|
-
maxTurns:
|
|
72
|
-
isolated:
|
|
73
|
-
inheritContext:
|
|
74
|
-
thinkingLevel:
|
|
48
|
+
id = manager.spawn(params.snapshot, config.subagentType, config.prompt, {
|
|
49
|
+
parentSessionFile: params.parentSessionFile,
|
|
50
|
+
parentSessionId: params.parentSessionId,
|
|
51
|
+
description: config.description,
|
|
52
|
+
model: config.model,
|
|
53
|
+
maxTurns: config.effectiveMaxTurns,
|
|
54
|
+
isolated: config.isolated,
|
|
55
|
+
inheritContext: config.inheritContext,
|
|
56
|
+
thinkingLevel: config.thinking,
|
|
75
57
|
isBackground: true,
|
|
76
|
-
isolation:
|
|
77
|
-
invocation:
|
|
58
|
+
isolation: config.isolation,
|
|
59
|
+
invocation: config.agentInvocation,
|
|
78
60
|
toolCallId: params.toolCallId,
|
|
79
61
|
onSessionCreated: (session) => {
|
|
80
62
|
bgState.setSession(session);
|
|
@@ -85,27 +67,27 @@ export function spawnBackground(
|
|
|
85
67
|
return textResult(err instanceof Error ? err.message : String(err));
|
|
86
68
|
}
|
|
87
69
|
|
|
88
|
-
const record =
|
|
70
|
+
const record = manager.getRecord(id);
|
|
89
71
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
72
|
+
agentActivity.set(id, bgState);
|
|
73
|
+
widget.ensureTimer();
|
|
74
|
+
widget.update();
|
|
93
75
|
|
|
94
76
|
const isQueued = record?.status === "queued";
|
|
95
77
|
return textResult(
|
|
96
78
|
`Agent ${isQueued ? "queued" : "started"} in background.\n` +
|
|
97
79
|
`Agent ID: ${id}\n` +
|
|
98
|
-
`Type: ${
|
|
99
|
-
`Description: ${
|
|
100
|
-
(record?.
|
|
80
|
+
`Type: ${config.displayName}\n` +
|
|
81
|
+
`Description: ${config.description}\n` +
|
|
82
|
+
(record?.outputFile ? `Output file: ${record.outputFile}\n` : "") +
|
|
101
83
|
(isQueued
|
|
102
|
-
? `Position: queued (max ${
|
|
84
|
+
? `Position: queued (max ${manager.getMaxConcurrent()} concurrent)\n`
|
|
103
85
|
: "") +
|
|
104
86
|
`\nYou will be notified when this agent completes.\n` +
|
|
105
87
|
`Use get_subagent_result to retrieve full results, or steer_subagent to send it messages.\n` +
|
|
106
88
|
`Do not duplicate this agent's work.`,
|
|
107
89
|
{
|
|
108
|
-
...
|
|
90
|
+
...config.detailBase,
|
|
109
91
|
toolUses: 0,
|
|
110
92
|
tokens: "",
|
|
111
93
|
durationMs: 0,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type { Model } from "@earendil-works/pi-ai";
|
|
2
1
|
import type { AgentToolResult } from "@earendil-works/pi-coding-agent";
|
|
3
2
|
import type { AgentSpawnConfig } from "../agent-manager.js";
|
|
4
|
-
import type {
|
|
3
|
+
import type { ParentSnapshot } from "../parent-snapshot.js";
|
|
4
|
+
import type { AgentRecord } from "../types.js";
|
|
5
5
|
import { AgentActivityTracker } from "../ui/agent-activity-tracker.js";
|
|
6
6
|
import {
|
|
7
7
|
type AgentDetails,
|
|
@@ -17,11 +17,12 @@ import {
|
|
|
17
17
|
getStatusNote,
|
|
18
18
|
textResult,
|
|
19
19
|
} from "./helpers.js";
|
|
20
|
+
import type { ResolvedSpawnConfig } from "./spawn-config.js";
|
|
20
21
|
|
|
21
22
|
/** Narrow manager interface for the foreground runner. */
|
|
22
23
|
export interface ForegroundManagerDeps {
|
|
23
24
|
spawnAndWait(
|
|
24
|
-
|
|
25
|
+
snapshot: ParentSnapshot,
|
|
25
26
|
type: string,
|
|
26
27
|
prompt: string,
|
|
27
28
|
opts: Omit<AgentSpawnConfig, "isBackground">,
|
|
@@ -34,37 +35,12 @@ export interface ForegroundWidgetDeps {
|
|
|
34
35
|
markFinished(id: string): void;
|
|
35
36
|
}
|
|
36
37
|
|
|
37
|
-
/**
|
|
38
|
-
export interface ForegroundDeps {
|
|
39
|
-
manager: ForegroundManagerDeps;
|
|
40
|
-
widget: ForegroundWidgetDeps;
|
|
41
|
-
agentActivity: AgentActivityAccess;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/** All values the foreground runner needs, bundled from shared execute setup. */
|
|
38
|
+
/** All values the foreground runner needs beyond the resolved config. */
|
|
45
39
|
export interface ForegroundParams {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
};
|
|
51
|
-
};
|
|
52
|
-
subagentType: string;
|
|
53
|
-
prompt: string;
|
|
54
|
-
description: string;
|
|
55
|
-
detailBase: Pick<
|
|
56
|
-
AgentDetails,
|
|
57
|
-
"displayName" | "description" | "subagentType" | "modelName" | "tags"
|
|
58
|
-
>;
|
|
59
|
-
rawType: string;
|
|
60
|
-
fellBack: boolean;
|
|
61
|
-
model: Model<any> | undefined;
|
|
62
|
-
effectiveMaxTurns: number | undefined;
|
|
63
|
-
isolated: boolean | undefined;
|
|
64
|
-
inheritContext: boolean | undefined;
|
|
65
|
-
thinking: ThinkingLevel | undefined;
|
|
66
|
-
isolation: IsolationMode | undefined;
|
|
67
|
-
agentInvocation: AgentInvocation;
|
|
40
|
+
config: ResolvedSpawnConfig;
|
|
41
|
+
snapshot: ParentSnapshot;
|
|
42
|
+
parentSessionFile: string;
|
|
43
|
+
parentSessionId: string;
|
|
68
44
|
}
|
|
69
45
|
|
|
70
46
|
/**
|
|
@@ -73,23 +49,28 @@ export interface ForegroundParams {
|
|
|
73
49
|
* streaming onUpdate callbacks, cleanup, and result formatting.
|
|
74
50
|
*/
|
|
75
51
|
export async function runForeground(
|
|
76
|
-
|
|
52
|
+
manager: ForegroundManagerDeps,
|
|
53
|
+
widget: ForegroundWidgetDeps,
|
|
54
|
+
agentActivity: AgentActivityAccess,
|
|
77
55
|
params: ForegroundParams,
|
|
78
56
|
signal: AbortSignal | undefined,
|
|
79
57
|
onUpdate: ((update: AgentToolResult<any>) => void) | undefined,
|
|
80
58
|
) {
|
|
59
|
+
const { config } = params;
|
|
81
60
|
let spinnerFrame = 0;
|
|
82
61
|
const startedAt = Date.now();
|
|
83
62
|
let fgId: string | undefined;
|
|
84
63
|
|
|
85
|
-
const fgState = new AgentActivityTracker(
|
|
64
|
+
const fgState = new AgentActivityTracker(config.effectiveMaxTurns);
|
|
86
65
|
let unsubUI: (() => void) | undefined;
|
|
66
|
+
let recordRef: AgentRecord | undefined;
|
|
87
67
|
|
|
88
68
|
const streamUpdate = () => {
|
|
69
|
+
const toolUses = recordRef?.toolUses ?? 0;
|
|
89
70
|
const details: AgentDetails = {
|
|
90
|
-
...
|
|
91
|
-
toolUses
|
|
92
|
-
tokens: formatLifetimeTokens(
|
|
71
|
+
...config.detailBase,
|
|
72
|
+
toolUses,
|
|
73
|
+
tokens: recordRef ? formatLifetimeTokens(recordRef) : "",
|
|
93
74
|
turnCount: fgState.turnCount,
|
|
94
75
|
maxTurns: fgState.maxTurns,
|
|
95
76
|
durationMs: Date.now() - startedAt,
|
|
@@ -98,7 +79,7 @@ export async function runForeground(
|
|
|
98
79
|
spinnerFrame: spinnerFrame % SPINNER.length,
|
|
99
80
|
};
|
|
100
81
|
onUpdate?.({
|
|
101
|
-
content: [{ type: "text", text: `${
|
|
82
|
+
content: [{ type: "text", text: `${toolUses} tool uses...` }],
|
|
102
83
|
details: details as any,
|
|
103
84
|
});
|
|
104
85
|
};
|
|
@@ -113,28 +94,29 @@ export async function runForeground(
|
|
|
113
94
|
|
|
114
95
|
let record: AgentRecord;
|
|
115
96
|
try {
|
|
116
|
-
record = await
|
|
117
|
-
params.
|
|
118
|
-
|
|
119
|
-
|
|
97
|
+
record = await manager.spawnAndWait(
|
|
98
|
+
params.snapshot,
|
|
99
|
+
config.subagentType,
|
|
100
|
+
config.prompt,
|
|
120
101
|
{
|
|
121
|
-
description:
|
|
122
|
-
model:
|
|
123
|
-
maxTurns:
|
|
124
|
-
isolated:
|
|
125
|
-
inheritContext:
|
|
126
|
-
thinkingLevel:
|
|
127
|
-
isolation:
|
|
128
|
-
invocation:
|
|
102
|
+
description: config.description,
|
|
103
|
+
model: config.model,
|
|
104
|
+
maxTurns: config.effectiveMaxTurns,
|
|
105
|
+
isolated: config.isolated,
|
|
106
|
+
inheritContext: config.inheritContext,
|
|
107
|
+
thinkingLevel: config.thinking,
|
|
108
|
+
isolation: config.isolation,
|
|
109
|
+
invocation: config.agentInvocation,
|
|
129
110
|
signal,
|
|
130
|
-
parentSessionFile: params.
|
|
131
|
-
parentSessionId: params.
|
|
111
|
+
parentSessionFile: params.parentSessionFile,
|
|
112
|
+
parentSessionId: params.parentSessionId,
|
|
132
113
|
onSessionCreated: (session, record) => {
|
|
133
114
|
fgState.setSession(session);
|
|
115
|
+
recordRef = record;
|
|
134
116
|
unsubUI = subscribeUIObserver(session, fgState, streamUpdate);
|
|
135
117
|
fgId = record.id;
|
|
136
|
-
|
|
137
|
-
|
|
118
|
+
agentActivity.set(record.id, fgState);
|
|
119
|
+
widget.ensureTimer();
|
|
138
120
|
},
|
|
139
121
|
},
|
|
140
122
|
);
|
|
@@ -149,15 +131,15 @@ export async function runForeground(
|
|
|
149
131
|
|
|
150
132
|
// Clean up foreground agent from widget
|
|
151
133
|
if (fgId) {
|
|
152
|
-
|
|
153
|
-
|
|
134
|
+
agentActivity.delete(fgId);
|
|
135
|
+
widget.markFinished(fgId);
|
|
154
136
|
}
|
|
155
137
|
|
|
156
|
-
const tokenText = formatLifetimeTokens(
|
|
157
|
-
const details = buildDetails(
|
|
138
|
+
const tokenText = formatLifetimeTokens(record);
|
|
139
|
+
const details = buildDetails(config.detailBase, record, fgState, { tokens: tokenText });
|
|
158
140
|
|
|
159
|
-
const fallbackNote =
|
|
160
|
-
? `Note: Unknown agent type "${
|
|
141
|
+
const fallbackNote = config.fellBack
|
|
142
|
+
? `Note: Unknown agent type "${config.rawType}" — using general-purpose.\n\n`
|
|
161
143
|
: "";
|
|
162
144
|
|
|
163
145
|
if (record.status === "error") {
|
|
@@ -65,7 +65,7 @@ export function createGetResultTool(deps: GetResultDeps) {
|
|
|
65
65
|
const displayName = getDisplayName(record.type, deps.registry);
|
|
66
66
|
const duration = formatDuration(record.startedAt, record.completedAt);
|
|
67
67
|
const tokens = formatLifetimeTokens(record);
|
|
68
|
-
const contextPercent = getSessionContextPercent(record.
|
|
68
|
+
const contextPercent = getSessionContextPercent(record.session);
|
|
69
69
|
const statsParts = [`Tool uses: ${record.toolUses}`];
|
|
70
70
|
if (tokens) statsParts.push(tokens);
|
|
71
71
|
if (contextPercent !== null) statsParts.push(`Context: ${Math.round(contextPercent)}%`);
|
|
@@ -92,8 +92,8 @@ export function createGetResultTool(deps: GetResultDeps) {
|
|
|
92
92
|
}
|
|
93
93
|
|
|
94
94
|
// Verbose: include full conversation
|
|
95
|
-
if (params.verbose && record.
|
|
96
|
-
const conversation = deps.getConversation(record.
|
|
95
|
+
if (params.verbose && record.session) {
|
|
96
|
+
const conversation = deps.getConversation(record.session);
|
|
97
97
|
if (conversation) {
|
|
98
98
|
output += `\n\n--- Agent Conversation ---\n${conversation}`;
|
|
99
99
|
}
|