@gotgenes/pi-subagents 6.14.0 → 6.15.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.
@@ -1,34 +1,31 @@
1
- import type { AgentToolResult, ExtensionContext } from "@earendil-works/pi-coding-agent";
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 { resolveAgentInvocationConfig } from "../invocation-config.js";
8
- import { resolveInvocationModel } from "../model-resolver.js";
6
+ import type { ParentSnapshot } from "../parent-snapshot.js";
9
7
 
10
- import type { AgentInvocation, AgentRecord, SubagentType } from "../types.js";
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: (ctx: ExtensionContext, type: string, prompt: string, opts: AgentSpawnConfig) => string;
31
- spawnAndWait: (ctx: ExtensionContext, type: string, prompt: string, opts: Omit<AgentSpawnConfig, "isBackground">) => Promise<AgentRecord>;
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(deps: AgentToolDeps) {
69
- const typeListText = buildTypeListText(deps.registry, deps.agentDir);
70
- const availableTypesText = deps.registry.getAvailableTypes().join(", ");
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 ${deps.agentDir}/agents/<name>.md (global) are also available.`,
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, deps.registry)
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,71 +286,27 @@ Guidelines:
273
286
  ctx: any,
274
287
  ) => {
275
288
  // Ensure we have UI context for widget rendering
276
- deps.widget.setUICtx(ctx.ui as UICtx);
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
- deps.registry.reload();
280
-
281
- const rawType = params.subagent_type as SubagentType;
282
- const resolved = deps.registry.resolveType(rawType);
283
- const subagentType = resolved ?? "general-purpose";
284
- const fellBack = resolved === undefined;
285
-
286
- const displayName = getDisplayName(subagentType, deps.registry);
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 (resolution.error) return textResult(resolution.error);
301
- const model = resolution.model;
301
+ if ("error" in config) return textResult(config.error);
302
302
 
303
- const thinking = resolvedConfig.thinking;
304
- const inheritContext = resolvedConfig.inheritContext;
305
- const runInBackground = resolvedConfig.runInBackground;
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 = deps.manager.getRecord(params.resume as string);
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.`,
@@ -348,7 +317,7 @@ Guidelines:
348
317
  `Agent "${params.resume}" has no active session to resume.`,
349
318
  );
350
319
  }
351
- const record = await deps.manager.resume(
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
- { manager: deps.manager, widget: deps.widget, agentActivity: deps.agentActivity },
369
- {
370
- ctx,
371
- subagentType,
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 (synchronous) execution — stream progress via onUpdate
344
+ // ---- Foreground execution — stream progress via onUpdate ----
389
345
  return runForeground(
390
- { manager: deps.manager, widget: deps.widget, agentActivity: deps.agentActivity },
391
- {
392
- ctx,
393
- subagentType,
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 { AgentInvocation, AgentRecord, IsolationMode, ThinkingLevel } from "../types.js";
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(ctx: any, type: string, prompt: string, opts: AgentSpawnConfig): string;
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
- /** 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. */
23
+ /** All values the background spawner needs beyond the resolved config. */
31
24
  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;
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
- deps: BackgroundDeps,
38
+ manager: BackgroundManagerDeps,
39
+ widget: BackgroundWidgetDeps,
40
+ agentActivity: AgentActivityAccess,
60
41
  params: BackgroundParams,
61
42
  ) {
62
- const bgState = new AgentActivityTracker(params.effectiveMaxTurns);
43
+ const { config } = params;
44
+ const bgState = new AgentActivityTracker(config.effectiveMaxTurns);
63
45
 
64
46
  let id: string;
65
47
  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,
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: params.isolation,
77
- invocation: params.agentInvocation,
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 = deps.manager.getRecord(id);
70
+ const record = manager.getRecord(id);
89
71
 
90
- deps.agentActivity.set(id, bgState);
91
- deps.widget.ensureTimer();
92
- deps.widget.update();
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: ${params.displayName}\n` +
99
- `Description: ${params.description}\n` +
80
+ `Type: ${config.displayName}\n` +
81
+ `Description: ${config.description}\n` +
100
82
  (record?.execution?.outputFile ? `Output file: ${record.execution.outputFile}\n` : "") +
101
83
  (isQueued
102
- ? `Position: queued (max ${deps.manager.getMaxConcurrent()} concurrent)\n`
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
- ...params.detailBase,
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 { AgentInvocation, AgentRecord, IsolationMode, ThinkingLevel } from "../types.js";
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
- ctx: any,
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
- /** Injected collaborators for runForeground. */
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
- ctx: {
47
- sessionManager: {
48
- getSessionFile(): string;
49
- getSessionId(): string;
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,21 +49,24 @@ export interface ForegroundParams {
73
49
  * streaming onUpdate callbacks, cleanup, and result formatting.
74
50
  */
75
51
  export async function runForeground(
76
- deps: ForegroundDeps,
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(params.effectiveMaxTurns);
64
+ const fgState = new AgentActivityTracker(config.effectiveMaxTurns);
86
65
  let unsubUI: (() => void) | undefined;
87
66
 
88
67
  const streamUpdate = () => {
89
68
  const details: AgentDetails = {
90
- ...params.detailBase,
69
+ ...config.detailBase,
91
70
  toolUses: fgState.toolUses,
92
71
  tokens: formatLifetimeTokens(fgState),
93
72
  turnCount: fgState.turnCount,
@@ -113,28 +92,28 @@ export async function runForeground(
113
92
 
114
93
  let record: AgentRecord;
115
94
  try {
116
- record = await deps.manager.spawnAndWait(
117
- params.ctx,
118
- params.subagentType,
119
- params.prompt,
95
+ record = await manager.spawnAndWait(
96
+ params.snapshot,
97
+ config.subagentType,
98
+ config.prompt,
120
99
  {
121
- description: params.description,
122
- model: params.model,
123
- maxTurns: params.effectiveMaxTurns,
124
- isolated: params.isolated,
125
- inheritContext: params.inheritContext,
126
- thinkingLevel: params.thinking,
127
- isolation: params.isolation,
128
- invocation: params.agentInvocation,
100
+ description: config.description,
101
+ model: config.model,
102
+ maxTurns: config.effectiveMaxTurns,
103
+ isolated: config.isolated,
104
+ inheritContext: config.inheritContext,
105
+ thinkingLevel: config.thinking,
106
+ isolation: config.isolation,
107
+ invocation: config.agentInvocation,
129
108
  signal,
130
- parentSessionFile: params.ctx.sessionManager.getSessionFile(),
131
- parentSessionId: params.ctx.sessionManager.getSessionId(),
109
+ parentSessionFile: params.parentSessionFile,
110
+ parentSessionId: params.parentSessionId,
132
111
  onSessionCreated: (session, record) => {
133
112
  fgState.setSession(session);
134
113
  unsubUI = subscribeUIObserver(session, fgState, streamUpdate);
135
114
  fgId = record.id;
136
- deps.agentActivity.set(record.id, fgState);
137
- deps.widget.ensureTimer();
115
+ agentActivity.set(record.id, fgState);
116
+ widget.ensureTimer();
138
117
  },
139
118
  },
140
119
  );
@@ -149,15 +128,15 @@ export async function runForeground(
149
128
 
150
129
  // Clean up foreground agent from widget
151
130
  if (fgId) {
152
- deps.agentActivity.delete(fgId);
153
- deps.widget.markFinished(fgId);
131
+ agentActivity.delete(fgId);
132
+ widget.markFinished(fgId);
154
133
  }
155
134
 
156
135
  const tokenText = formatLifetimeTokens(fgState);
157
- const details = buildDetails(params.detailBase, record, fgState, { tokens: tokenText });
136
+ const details = buildDetails(config.detailBase, record, fgState, { tokens: tokenText });
158
137
 
159
- const fallbackNote = params.fellBack
160
- ? `Note: Unknown agent type "${params.rawType}" \u2014 using general-purpose.\n\n`
138
+ const fallbackNote = config.fellBack
139
+ ? `Note: Unknown agent type "${config.rawType}" using general-purpose.\n\n`
161
140
  : "";
162
141
 
163
142
  if (record.status === "error") {