@gotgenes/pi-subagents 7.2.0 → 7.2.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.
@@ -0,0 +1,42 @@
1
+ ---
2
+ issue: 195
3
+ issue_title: "Convert tool factories to classes"
4
+ ---
5
+
6
+ # Retro: #195 — Convert tool factories to classes
7
+
8
+ ## Stage: Planning (2026-05-24T12:00:00Z)
9
+
10
+ ### Session summary
11
+
12
+ Produced a 5-step TDD plan converting `createAgentTool`, `createGetResultTool`, and `createSteerTool` to classes with constructor-injected dependencies.
13
+ Verified both prerequisites (#193, #194) are closed and their effects visible in the current source.
14
+ Designed narrow interfaces (`AgentToolRuntime`, `GetResultToolManager`, `SteerToolManager`, `SteerToolEvents`, etc.) that `SubagentRuntime`, `AgentManager`, and `NotificationManager` satisfy structurally.
15
+
16
+ ### Observations
17
+
18
+ - The conversion is mechanical — no behavioral changes, just structural.
19
+ Existing tests cover all paths; only test helpers need updating.
20
+ - `steerAgent` and `getAgentConversation` are pure functions that can be imported directly by the classes rather than injected — simplifies the constructor signature.
21
+ - `agentDir` doesn't fit neatly on any existing collaborator, so it remains a constructor param for `AgentTool`.
22
+ - The `AgentToolWidget` interface may become redundant once `AgentToolRuntime` replaces it as the type passed to `spawnBackground`/`runForeground`, but this is deferred to implementation.
23
+ - Ordered TDD steps from smallest (SteerTool) to largest (AgentTool) to build confidence incrementally.
24
+
25
+ ## Stage: Implementation — TDD (2026-05-24T21:26:00Z)
26
+
27
+ ### Session summary
28
+
29
+ Completed all 5 planned TDD cycles (SteerTool → GetResultTool → AgentTool → `index.ts` wiring → architecture doc).
30
+ All 854 tests pass; type check and lint clean.
31
+ Total: 5 commits across source + test files, plus 1 cleanup fix commit.
32
+
33
+ ### Observations
34
+
35
+ - `steerAgent` was inlined as `session.steer(message)` directly in `SteerTool.execute()` rather than imported as a module function.
36
+ This eliminated the dep entirely — the mock session's `steer` vi.fn() handles it in tests without `vi.mock`.
37
+ - The `verbose` test in `get-result-tool.test.ts` was upgraded to drive the real `getAgentConversation` function via `createMockSession({ messages: [...] })` overrides, making it a stronger integration test.
38
+ - `AgentToolWidget` was eliminated: `AgentToolRuntime` (a superset) replaced it, and `background-spawner` and `foreground-runner` already define their own narrow `BackgroundWidgetDeps`/`ForegroundWidgetDeps` interfaces.
39
+ - `createToolDeps()` changed shape from `AgentToolDeps` bag to `AgentToolFixture` (`{ manager, runtime, settings, registry, agentDir }`).
40
+ This required updating `background-spawner.test.ts` and `foreground-runner.test.ts` (not listed in the plan) to destructure `{ manager, runtime }` instead of `{ manager, widget, agentActivity }`.
41
+ - Biome flagged an unused `import type { AgentSession }` in `get-result-tool.ts` (left by ESLint's cast removal in step 2) — caught by `pnpm run lint` and fixed in a separate commit.
42
+ - The `ReturnType<typeof vi.fn>` annotation on `makeNotifications()` in the get-result-tool test triggered a TypeScript error; fixed by removing the return type annotation entirely (per testing skill guidance).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gotgenes/pi-subagents",
3
- "version": "7.2.0",
3
+ "version": "7.2.2",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./src/service.ts"
package/src/index.ts CHANGED
@@ -15,7 +15,6 @@ import { join } from "node:path";
15
15
  import {
16
16
  createAgentSession,
17
17
  DefaultResourceLoader,
18
- defineTool,
19
18
  type ExtensionAPI,
20
19
  getAgentDir,
21
20
  SettingsManager as SdkSettingsManager,
@@ -25,7 +24,7 @@ import { AgentTypeRegistry } from "#src/config/agent-types";
25
24
  import { loadCustomAgents } from "#src/config/custom-agents";
26
25
  import { SessionLifecycleHandler, ToolStartHandler } from "#src/handlers/index";
27
26
  import { AgentManager, type AgentManagerObserver } from "#src/lifecycle/agent-manager";
28
- import { createAgentRunner, getAgentConversation, type RunnerIO, steerAgent } from "#src/lifecycle/agent-runner";
27
+ import { createAgentRunner, type RunnerIO } from "#src/lifecycle/agent-runner";
29
28
  import { buildParentSnapshot } from "#src/lifecycle/parent-snapshot";
30
29
  import { GitWorktreeManager } from "#src/lifecycle/worktree";
31
30
  import { buildEventData, type NotificationDetails, NotificationManager } from "#src/observation/notification";
@@ -40,16 +39,13 @@ import { buildAgentPrompt } from "#src/session/prompts";
40
39
  import { deriveSubagentSessionDir } from "#src/session/session-dir";
41
40
  import { preloadSkills } from "#src/session/skill-loader";
42
41
  import { SettingsManager } from "#src/settings";
43
- import { createAgentTool } from "#src/tools/agent-tool";
44
- import { createGetResultTool } from "#src/tools/get-result-tool";
42
+ import { AgentTool } from "#src/tools/agent-tool";
43
+ import { GetResultTool } from "#src/tools/get-result-tool";
45
44
  import { getModelLabelFromConfig } from "#src/tools/helpers";
46
- import { createSteerTool } from "#src/tools/steer-tool";
45
+ import { SteerTool } from "#src/tools/steer-tool";
47
46
  import { FsAgentFileOps } from "#src/ui/agent-file-ops";
48
47
  import { createAgentsMenuHandler } from "#src/ui/agent-menu";
49
- import {
50
- AgentWidget,
51
- type UICtx,
52
- } from "#src/ui/agent-widget";
48
+ import { AgentWidget } from "#src/ui/agent-widget";
53
49
 
54
50
  export default function (pi: ExtensionAPI) {
55
51
  // ---- Register custom notification renderer ----
@@ -67,7 +63,7 @@ export default function (pi: ExtensionAPI) {
67
63
  (msg, opts) => pi.sendMessage(msg, opts),
68
64
  runtime.agentActivity,
69
65
  (id) => runtime.markFinished(id),
70
- () => runtime.updateWidget(),
66
+ () => runtime.update(),
71
67
  );
72
68
 
73
69
  // Settings: owns all three in-memory values and handles load/save/emit.
@@ -185,46 +181,15 @@ export default function (pi: ExtensionAPI) {
185
181
 
186
182
  // ---- Agent tool ----
187
183
 
188
- pi.registerTool(defineTool(createAgentTool({
189
- manager: {
190
- spawn: (snapshot, type, prompt, opts) => manager.spawn(snapshot, type, prompt, opts),
191
- spawnAndWait: (snapshot, type, prompt, opts) => manager.spawnAndWait(snapshot, type, prompt, opts),
192
- resume: (id, prompt, signal) => manager.resume(id, prompt, signal),
193
- getRecord: (id) => manager.getRecord(id),
194
- getMaxConcurrent: () => settings.maxConcurrent,
195
- },
196
- widget: {
197
- setUICtx: (ctx) => runtime.setUICtx(ctx as UICtx),
198
- ensureTimer: () => runtime.ensureTimer(),
199
- update: () => runtime.updateWidget(),
200
- markFinished: (id) => runtime.markFinished(id),
201
- },
202
- agentActivity: runtime.agentActivity,
203
- registry,
204
- agentDir: getAgentDir(),
205
- settings,
206
- buildSnapshot: runtime.buildSnapshot.bind(runtime),
207
- getModelInfo: runtime.getModelInfo.bind(runtime),
208
- getSessionInfo: runtime.getSessionInfo.bind(runtime),
209
- })));
184
+ pi.registerTool(new AgentTool(manager, runtime, settings, registry, getAgentDir()).toToolDefinition());
210
185
 
211
186
  // ---- get_subagent_result tool ----
212
187
 
213
- pi.registerTool(defineTool(createGetResultTool(
214
- (id) => manager.getRecord(id),
215
- (key) => notifications.cancelNudge(key),
216
- (session) => getAgentConversation(session),
217
- registry,
218
- )));
188
+ pi.registerTool(new GetResultTool(manager, notifications, registry).toToolDefinition());
219
189
 
220
190
  // ---- steer_subagent tool ----
221
191
 
222
- pi.registerTool(defineTool(createSteerTool(
223
- (id) => manager.getRecord(id),
224
- (name, data) => pi.events.emit(name, data),
225
- (session, message) => steerAgent(session, message),
226
- (id, message) => manager.queueSteer(id, message),
227
- )));
192
+ pi.registerTool(new SteerTool(manager, pi.events).toToolDefinition());
228
193
 
229
194
  // ---- /agents interactive menu ----
230
195
 
package/src/runtime.ts CHANGED
@@ -109,7 +109,7 @@ export class SubagentRuntime {
109
109
  }
110
110
 
111
111
  /** Delegate to widget.update — no-op when widget is null. */
112
- updateWidget(): void {
112
+ update(): void {
113
113
  this.widget?.update();
114
114
  }
115
115
 
@@ -1,5 +1,6 @@
1
1
  /* eslint-disable @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-base-to-string, @typescript-eslint/restrict-template-expressions -- Pi SDK types are not fully exported; see upstream Pi SDK for type improvements */
2
2
  import type { AgentToolResult } from "@earendil-works/pi-coding-agent";
3
+ import { defineTool } from "@earendil-works/pi-coding-agent";
3
4
  import { Text } from "@earendil-works/pi-tui";
4
5
  import { Type } from "@sinclair/typebox";
5
6
  import { AgentTypeRegistry } from "#src/config/agent-types";
@@ -15,72 +16,149 @@ import { AgentActivityTracker } from "#src/ui/agent-activity-tracker";
15
16
  import { type UICtx } from "#src/ui/agent-widget";
16
17
  import { type AgentDetails, getDisplayName } from "#src/ui/display";
17
18
 
18
- // ---- Deps interface ----
19
-
20
- /** Narrow manager interface — only the methods the Agent tool calls. */
21
- export interface AgentToolManager {
22
- spawn: (snapshot: ParentSnapshot, type: string, prompt: string, opts: AgentSpawnConfig) => string;
23
- spawnAndWait: (snapshot: ParentSnapshot, type: string, prompt: string, opts: Omit<AgentSpawnConfig, "isBackground">) => Promise<AgentRecord>;
24
- resume: (id: string, prompt: string, signal: AbortSignal) => Promise<AgentRecord | undefined>;
25
- getRecord: (id: string) => AgentRecord | undefined;
26
- getMaxConcurrent: () => number;
27
- }
28
-
29
- /** Narrow widget interface — only the methods the Agent tool calls. */
30
- export interface AgentToolWidget {
31
- setUICtx: (ctx: unknown) => void;
32
- ensureTimer: () => void;
33
- update: () => void;
34
- markFinished: (id: string) => void;
35
- }
19
+ // ---- Shared interfaces (also used by background-spawner and foreground-runner) ----
36
20
 
37
21
  /**
38
22
  * Narrow read/write interface for the agent-tool's agentActivity access.
39
23
  * The full Map satisfies this structurally — no wrapper needed.
40
24
  */
41
25
  export interface AgentActivityAccess {
42
- get(id: string): AgentActivityTracker | undefined;
43
- set(id: string, tracker: AgentActivityTracker): void;
44
- delete(id: string): void;
26
+ get(id: string): AgentActivityTracker | undefined;
27
+ set(id: string, tracker: AgentActivityTracker): void;
28
+ delete(id: string): void;
45
29
  }
46
30
 
47
- export interface AgentToolDeps {
48
- manager: AgentToolManager;
49
- widget: AgentToolWidget;
50
- agentActivity: AgentActivityAccess;
51
- registry: AgentTypeRegistry;
52
- agentDir: string;
53
- /** Narrow settings accessor only the default max turns is needed here. */
54
- settings: { readonly defaultMaxTurns: number | undefined };
55
- /** Build a ParentSnapshot from the current session context. */
56
- buildSnapshot: (inheritContext: boolean) => ParentSnapshot;
57
- /** Model info from the current session context. */
58
- getModelInfo: () => ModelInfo;
59
- /** Parent session identity from the current session context. */
60
- getSessionInfo: () => { parentSessionFile: string; parentSessionId: string };
31
+ // ---- Deps interfaces ----
32
+
33
+ /** Narrow manager interface — only the methods the Agent tool calls. */
34
+ export interface AgentToolManager {
35
+ spawn: (snapshot: ParentSnapshot, type: string, prompt: string, opts: AgentSpawnConfig) => string;
36
+ spawnAndWait: (snapshot: ParentSnapshot, type: string, prompt: string, opts: Omit<AgentSpawnConfig, "isBackground">) => Promise<AgentRecord>;
37
+ resume: (id: string, prompt: string, signal: AbortSignal) => Promise<AgentRecord | undefined>;
38
+ getRecord: (id: string) => AgentRecord | undefined;
39
+ }
40
+
41
+ /** Narrow runtime interface the Agent tool's slice of SubagentRuntime. */
42
+ export interface AgentToolRuntime {
43
+ readonly agentActivity: AgentActivityAccess;
44
+ setUICtx(ctx: UICtx): void;
45
+ ensureTimer(): void;
46
+ update(): void;
47
+ markFinished(id: string): void;
48
+ buildSnapshot(inheritContext: boolean): ParentSnapshot;
49
+ getModelInfo(): ModelInfo;
50
+ getSessionInfo(): { parentSessionFile: string; parentSessionId: string };
61
51
  }
62
52
 
63
- // ---- Factory ----
53
+ /** Narrow settings accessor — only the fields the Agent tool reads. */
54
+ export type AgentToolSettings = {
55
+ readonly defaultMaxTurns: number | undefined;
56
+ readonly maxConcurrent: number;
57
+ };
58
+
59
+ // ---- Class ----
60
+
61
+ export class AgentTool {
62
+ private readonly typeListText: string;
63
+ private readonly availableTypesText: string;
64
+
65
+ constructor(
66
+ private readonly manager: AgentToolManager,
67
+ private readonly runtime: AgentToolRuntime,
68
+ private readonly settings: AgentToolSettings,
69
+ private readonly registry: AgentTypeRegistry,
70
+ private readonly agentDir: string,
71
+ ) {
72
+ this.typeListText = buildTypeListText(registry, agentDir);
73
+ this.availableTypesText = registry.getAvailableTypes().join(", ");
74
+ }
75
+
76
+ async execute(
77
+ toolCallId: string,
78
+ params: Record<string, unknown>,
79
+ signal: AbortSignal | undefined,
80
+ onUpdate: ((update: AgentToolResult<any>) => void) | undefined,
81
+ ctx: any,
82
+ ) {
83
+ // Ensure we have UI context for widget rendering
84
+ this.runtime.setUICtx(ctx.ui as UICtx);
64
85
 
65
- /** Create the Agent tool definition (without Pi SDK wrapper). */
66
- export function createAgentTool({
67
- manager,
68
- widget,
69
- agentActivity,
70
- registry,
71
- agentDir,
72
- settings,
73
- buildSnapshot,
74
- getModelInfo,
75
- getSessionInfo,
76
- }: AgentToolDeps) {
77
- const typeListText = buildTypeListText(registry, agentDir);
78
- const availableTypesText = registry.getAvailableTypes().join(", ");
79
- return {
80
- name: "Agent" as const,
81
- label: "Agent",
82
- promptSnippet: "Agent: Launch a specialized agent for complex, multi-step tasks.",
83
- description: `Launch a new agent to handle complex, multi-step tasks autonomously.
86
+ // Reload custom agents so new .pi/agents/*.md files are picked up without restart
87
+ this.registry.reload();
88
+
89
+ // ---- Config resolution (pure) ----
90
+ const config = resolveSpawnConfig(
91
+ params,
92
+ this.registry,
93
+ this.runtime.getModelInfo(),
94
+ this.settings,
95
+ );
96
+ if ("error" in config) return textResult(config.error);
97
+
98
+ // ---- Boundary extraction (after config so inheritContext is resolved) ----
99
+ const snapshot = this.runtime.buildSnapshot(config.execution.inheritContext);
100
+ const { parentSessionFile, parentSessionId } = this.runtime.getSessionInfo();
101
+ const parentSession: ParentSessionInfo = { parentSessionFile, parentSessionId, toolCallId };
102
+
103
+ // ---- Resume existing agent ----
104
+ if (params.resume) {
105
+ const existing = this.manager.getRecord(params.resume as string);
106
+ if (!existing) {
107
+ return textResult(
108
+ `Agent not found: "${params.resume}". It may have been cleaned up.`,
109
+ );
110
+ }
111
+ if (!existing.session) {
112
+ return textResult(
113
+ `Agent "${params.resume}" has no active session to resume.`,
114
+ );
115
+ }
116
+ const record = await this.manager.resume(
117
+ params.resume as string,
118
+ params.prompt as string,
119
+ signal ?? new AbortController().signal,
120
+ );
121
+ if (!record) {
122
+ return textResult(`Failed to resume agent "${params.resume}".`);
123
+ }
124
+ return textResult(
125
+ record.result?.trim() ?? record.error?.trim() ?? "No output.",
126
+ buildDetails(config.presentation.detailBase, record),
127
+ );
128
+ }
129
+
130
+ // ---- Background execution ----
131
+ if (config.execution.runInBackground) {
132
+ return spawnBackground(
133
+ this.manager,
134
+ this.runtime,
135
+ this.runtime.agentActivity,
136
+ { config, snapshot, parentSession, settings: this.settings },
137
+ );
138
+ }
139
+
140
+ // ---- Foreground execution — stream progress via onUpdate ----
141
+ return runForeground(
142
+ this.manager,
143
+ this.runtime,
144
+ this.runtime.agentActivity,
145
+ { config, snapshot, parentSession },
146
+ signal,
147
+ onUpdate,
148
+ );
149
+ }
150
+
151
+ toToolDefinition() {
152
+ const typeListText = this.typeListText;
153
+ const availableTypesText = this.availableTypesText;
154
+ const agentDir = this.agentDir;
155
+ const registry = this.registry;
156
+
157
+ return defineTool({
158
+ name: "Agent" as const,
159
+ label: "Agent",
160
+ promptSnippet: "Agent: Launch a specialized agent for complex, multi-step tasks.",
161
+ description: `Launch a new agent to handle complex, multi-step tasks autonomously.
84
162
 
85
163
  The Agent tool launches specialized agents that autonomously handle complex tasks. Each agent type has specific capabilities and tools available to it.
86
164
 
@@ -101,170 +179,102 @@ Guidelines:
101
179
  - Use thinking to control extended thinking level.
102
180
  - Use inherit_context if the agent needs the parent conversation history.
103
181
  - Use isolation: "worktree" to run the agent in an isolated git worktree (safe parallel file modifications).`,
104
- parameters: Type.Object({
105
- prompt: Type.String({
106
- description: "The task for the agent to perform.",
107
- }),
108
- description: Type.String({
109
- description: "A short (3-5 word) description of the task (shown in UI).",
110
- }),
111
- subagent_type: Type.String({
112
- 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.`,
113
- }),
114
- model: Type.Optional(
115
- Type.String({
116
- description:
117
- 'Optional model override. Accepts "provider/modelId" or fuzzy name (e.g. "haiku", "sonnet"). Omit to use the agent type\'s default.',
118
- }),
119
- ),
120
- thinking: Type.Optional(
121
- Type.String({
122
- description:
123
- "Thinking level: off, minimal, low, medium, high, xhigh. Overrides agent default.",
124
- }),
125
- ),
126
- max_turns: Type.Optional(
127
- Type.Number({
128
- description:
129
- "Maximum number of agentic turns before stopping. Omit for unlimited (default).",
130
- minimum: 1,
131
- }),
132
- ),
133
- run_in_background: Type.Optional(
134
- Type.Boolean({
135
- description:
136
- "Set to true to run in background. Returns agent ID immediately. You will be notified on completion.",
137
- }),
138
- ),
139
- resume: Type.Optional(
140
- Type.String({
141
- description: "Optional agent ID to resume from. Continues from previous context.",
142
- }),
143
- ),
144
- isolated: Type.Optional(
145
- Type.Boolean({
146
- description: "If true, agent gets no extension/MCP tools — only built-in tools.",
147
- }),
148
- ),
149
- inherit_context: Type.Optional(
150
- Type.Boolean({
151
- description:
152
- "If true, fork parent conversation into the agent. Default: false (fresh context).",
153
- }),
154
- ),
155
- isolation: Type.Optional(
156
- Type.Literal("worktree", {
157
- description:
158
- 'Set to "worktree" to run the agent in a temporary git worktree (isolated copy of the repo). Changes are saved to a branch on completion.',
159
- }),
160
- ),
161
- }),
162
-
163
- // ---- Custom rendering: Claude Code style ----
164
-
165
- renderCall(args: Record<string, unknown>, theme: any) {
166
- const displayName = args.subagent_type
167
- ? getDisplayName(args.subagent_type as string, registry)
168
- : "Agent";
169
- const desc = (args.description as string | undefined) ?? "";
170
- return new Text(
171
- "▸ " +
172
- theme.fg("toolTitle", theme.bold(displayName)) +
173
- (desc ? " " + theme.fg("muted", desc) : ""),
174
- 0,
175
- 0,
176
- );
177
- },
178
-
179
- renderResult(result: any, { expanded, isPartial }: any, theme: any) {
180
- const details = result.details as AgentDetails | undefined;
181
- if (!details) {
182
- const text = result.content[0]?.type === "text" ? result.content[0].text : "";
183
- return new Text(text, 0, 0);
184
- }
185
- const resultText = result.content[0]?.type === "text" ? result.content[0].text : "";
186
- return new Text(
187
- renderAgentResult(details, resultText, expanded, isPartial, theme),
188
- 0,
189
- 0,
190
- );
191
- },
192
-
193
- // ---- Execute ----
194
-
195
- execute: async (
196
- toolCallId: string,
197
- params: Record<string, unknown>,
198
- signal: AbortSignal | undefined,
199
- onUpdate: ((update: AgentToolResult<any>) => void) | undefined,
200
- ctx: any,
201
- ) => {
202
- // Ensure we have UI context for widget rendering
203
- widget.setUICtx(ctx.ui as UICtx);
204
-
205
- // Reload custom agents so new .pi/agents/*.md files are picked up without restart
206
- registry.reload();
207
-
208
- // ---- Config resolution (pure) ----
209
- const config = resolveSpawnConfig(
210
- params,
211
- registry,
212
- getModelInfo(),
213
- settings,
214
- );
215
- if ("error" in config) return textResult(config.error);
182
+ parameters: Type.Object({
183
+ prompt: Type.String({
184
+ description: "The task for the agent to perform.",
185
+ }),
186
+ description: Type.String({
187
+ description: "A short (3-5 word) description of the task (shown in UI).",
188
+ }),
189
+ subagent_type: Type.String({
190
+ 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.`,
191
+ }),
192
+ model: Type.Optional(
193
+ Type.String({
194
+ description:
195
+ 'Optional model override. Accepts "provider/modelId" or fuzzy name (e.g. "haiku", "sonnet"). Omit to use the agent type\'s default.',
196
+ }),
197
+ ),
198
+ thinking: Type.Optional(
199
+ Type.String({
200
+ description:
201
+ "Thinking level: off, minimal, low, medium, high, xhigh. Overrides agent default.",
202
+ }),
203
+ ),
204
+ max_turns: Type.Optional(
205
+ Type.Number({
206
+ description:
207
+ "Maximum number of agentic turns before stopping. Omit for unlimited (default).",
208
+ minimum: 1,
209
+ }),
210
+ ),
211
+ run_in_background: Type.Optional(
212
+ Type.Boolean({
213
+ description:
214
+ "Set to true to run in background. Returns agent ID immediately. You will be notified when it completes.",
215
+ }),
216
+ ),
217
+ resume: Type.Optional(
218
+ Type.String({
219
+ description: "Optional agent ID to resume from. Continues from previous context.",
220
+ }),
221
+ ),
222
+ isolated: Type.Optional(
223
+ Type.Boolean({
224
+ description: "If true, agent gets no extension/MCP tools — only built-in tools.",
225
+ }),
226
+ ),
227
+ inherit_context: Type.Optional(
228
+ Type.Boolean({
229
+ description:
230
+ "If true, fork parent conversation into the agent. Default: false (fresh context).",
231
+ }),
232
+ ),
233
+ isolation: Type.Optional(
234
+ Type.Literal("worktree", {
235
+ description:
236
+ 'Set to "worktree" to run the agent in a temporary git worktree (isolated copy of the repo). Changes are saved to a branch on completion.',
237
+ }),
238
+ ),
239
+ }),
216
240
 
217
- // ---- Boundary extraction (after config so inheritContext is resolved) ----
218
- const snapshot = buildSnapshot(config.execution.inheritContext);
219
- const { parentSessionFile, parentSessionId } = getSessionInfo();
220
- const parentSession: ParentSessionInfo = { parentSessionFile, parentSessionId, toolCallId };
241
+ // ---- Custom rendering: Claude Code style ----
221
242
 
222
- // ---- Resume existing agent ----
223
- if (params.resume) {
224
- const existing = manager.getRecord(params.resume as string);
225
- if (!existing) {
226
- return textResult(
227
- `Agent not found: "${params.resume}". It may have been cleaned up.`,
228
- );
229
- }
230
- if (!existing.session) {
231
- return textResult(
232
- `Agent "${params.resume}" has no active session to resume.`,
233
- );
234
- }
235
- const record = await manager.resume(
236
- params.resume as string,
237
- params.prompt as string,
238
- signal ?? new AbortController().signal,
239
- );
240
- if (!record) {
241
- return textResult(`Failed to resume agent "${params.resume}".`);
242
- }
243
- return textResult(
244
- record.result?.trim() ?? record.error?.trim() ?? "No output.",
245
- buildDetails(config.presentation.detailBase, record),
246
- );
247
- }
243
+ renderCall(args: Record<string, unknown>, theme: any) {
244
+ const displayName = args.subagent_type
245
+ ? getDisplayName(args.subagent_type as string, registry)
246
+ : "Agent";
247
+ const desc = (args.description as string | undefined) ?? "";
248
+ return new Text(
249
+ "▸ " +
250
+ theme.fg("toolTitle", theme.bold(displayName)) +
251
+ (desc ? " " + theme.fg("muted", desc) : ""),
252
+ 0,
253
+ 0,
254
+ );
255
+ },
248
256
 
249
- // ---- Background execution ----
250
- if (config.execution.runInBackground) {
251
- return spawnBackground(
252
- manager,
253
- widget,
254
- agentActivity,
255
- { config, snapshot, parentSession },
256
- );
257
- }
257
+ renderResult(result: any, { expanded, isPartial }: any, theme: any) {
258
+ const details = result.details as AgentDetails | undefined;
259
+ if (!details) {
260
+ const text = result.content[0]?.type === "text" ? result.content[0].text : "";
261
+ return new Text(text, 0, 0);
262
+ }
263
+ const resultText = result.content[0]?.type === "text" ? result.content[0].text : "";
264
+ return new Text(
265
+ renderAgentResult(details, resultText, expanded, isPartial, theme),
266
+ 0,
267
+ 0,
268
+ );
269
+ },
258
270
 
259
- // ---- Foreground execution — stream progress via onUpdate ----
260
- return runForeground(
261
- manager,
262
- widget,
263
- agentActivity,
264
- { config, snapshot, parentSession },
265
- signal,
266
- onUpdate,
267
- );
268
- },
269
- };
271
+ execute: (
272
+ toolCallId: string,
273
+ params: Record<string, unknown>,
274
+ signal: AbortSignal | undefined,
275
+ onUpdate: ((update: AgentToolResult<any>) => void) | undefined,
276
+ ctx: any,
277
+ ) => this.execute(toolCallId, params, signal, onUpdate, ctx),
278
+ });
279
+ }
270
280
  }
@@ -11,7 +11,6 @@ import { subscribeUIObserver } from "#src/ui/ui-observer";
11
11
  export interface BackgroundManagerDeps {
12
12
  spawn(snapshot: ParentSnapshot, type: string, prompt: string, opts: AgentSpawnConfig): string;
13
13
  getRecord(id: string): AgentRecord | undefined;
14
- getMaxConcurrent(): number;
15
14
  }
16
15
 
17
16
  /** Narrow widget interface for the background spawner. */
@@ -25,6 +24,7 @@ export interface BackgroundParams {
25
24
  config: ResolvedSpawnConfig;
26
25
  snapshot: ParentSnapshot;
27
26
  parentSession: ParentSessionInfo;
27
+ settings: { readonly maxConcurrent: number };
28
28
  }
29
29
 
30
30
  /**
@@ -77,7 +77,7 @@ export function spawnBackground(
77
77
  `Description: ${execution.description}\n` +
78
78
  (record?.outputFile ? `Output file: ${record.outputFile}\n` : "") +
79
79
  (isQueued
80
- ? `Position: queued (max ${manager.getMaxConcurrent()} concurrent)\n`
80
+ ? `Position: queued (max ${params.settings.maxConcurrent} concurrent)\n`
81
81
  : "") +
82
82
  `\nYou will be notified when this agent completes.\n` +
83
83
  `Use get_subagent_result to retrieve full results, or steer_subagent to send it messages.\n` +