@gotgenes/pi-subagents 6.10.0 → 6.12.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.
@@ -6,26 +6,35 @@ import type { Model } from "@earendil-works/pi-ai";
6
6
  import {
7
7
  type AgentSession,
8
8
  type AgentSessionEvent,
9
- createAgentSession,
10
- DefaultResourceLoader,
11
- getAgentDir,
12
- SessionManager,
13
- SettingsManager,
9
+ type SettingsManager,
14
10
  } from "@earendil-works/pi-coding-agent";
15
11
  import type { AgentConfigLookup } from "./agent-types.js";
16
12
  import { extractText } from "./context.js";
17
- import { detectEnv } from "./env.js";
18
- import { buildMemoryBlock, buildReadOnlyMemoryBlock } from "./memory.js";
13
+ import type { EnvInfo } from "./env.js";
19
14
  import type { ParentSnapshot } from "./parent-snapshot.js";
20
- import { buildAgentPrompt } from "./prompts.js";
21
15
  import { type AssemblerIO, assembleSessionConfig } from "./session-config.js";
22
- import { deriveSubagentSessionDir } from "./session-dir.js";
23
- import { preloadSkills } from "./skill-loader.js";
24
16
  import type { ShellExec, SubagentType, ThinkingLevel } from "./types.js";
25
17
 
26
18
  /** Names of tools registered by this extension that subagents must NOT inherit. */
27
19
  const EXCLUDED_TOOL_NAMES = ["Agent", "get_subagent_result", "steer_subagent"];
28
20
 
21
+ // ── Local message-shape types ───────────────────────────────────────────────
22
+ // The Pi SDK does not export a narrow type for tool-call content variants.
23
+
24
+ /** Tool-call content item — SDK exposes this variant at runtime but doesn’t export the narrow type. */
25
+ interface ToolCallContent {
26
+ type: "toolCall";
27
+ name?: string;
28
+ toolName?: string;
29
+ }
30
+
31
+ /** Extracts the display name from a tool-call content item. */
32
+ function getToolCallName(c: { type: string }): string {
33
+ if (c.type !== "toolCall") return "unknown";
34
+ const tc = c as ToolCallContent;
35
+ return tc.name ?? tc.toolName ?? "unknown";
36
+ }
37
+
29
38
  /**
30
39
  * Filter the session's active tool names according to extension/denylist rules.
31
40
  *
@@ -68,6 +77,64 @@ export function normalizeMaxTurns(n: number | undefined): number | undefined {
68
77
  return Math.max(1, n);
69
78
  }
70
79
 
80
+ // ── IO boundary ───────────────────────────────────────────────────────────────
81
+
82
+ /** Minimal resource-loader contract used by the runner. */
83
+ export interface ResourceLoaderLike {
84
+ reload(): Promise<void>;
85
+ }
86
+
87
+ /** Minimal session-manager contract used by the runner. */
88
+ export interface SessionManagerLike {
89
+ newSession(opts: { parentSession?: string }): void;
90
+ getSessionFile(): string | undefined;
91
+ }
92
+
93
+ /** Options passed to RunnerIO.createResourceLoader. */
94
+ export interface ResourceLoaderOptions {
95
+ cwd: string;
96
+ agentDir: string;
97
+ noExtensions?: boolean;
98
+ noSkills?: boolean;
99
+ noPromptTemplates?: boolean;
100
+ noThemes?: boolean;
101
+ noContextFiles?: boolean;
102
+ systemPromptOverride?: () => string;
103
+ /** Override the append system prompt. Receives the current base value; return the replacement. */
104
+ appendSystemPromptOverride?: (base: string[]) => string[];
105
+ }
106
+
107
+ /** Options passed to RunnerIO.createSession. */
108
+ export interface CreateSessionOptions {
109
+ cwd: string;
110
+ agentDir: string;
111
+ sessionManager: SessionManagerLike;
112
+ settingsManager: SettingsManager;
113
+ modelRegistry: unknown;
114
+ model?: unknown;
115
+ tools: string[];
116
+ resourceLoader: ResourceLoaderLike;
117
+ thinkingLevel?: ThinkingLevel;
118
+ }
119
+
120
+ /**
121
+ * IO boundary injected into runAgent().
122
+ *
123
+ * Decouples the runner from direct Pi SDK imports and sibling-module IO,
124
+ * making it testable via plain stub objects without vi.mock().
125
+ */
126
+ export interface RunnerIO {
127
+ detectEnv: (exec: ShellExec, cwd: string) => Promise<EnvInfo>;
128
+ getAgentDir: () => string;
129
+ createResourceLoader: (opts: ResourceLoaderOptions) => ResourceLoaderLike;
130
+ deriveSessionDir: (parentSessionFile: string | undefined, effectiveCwd: string) => string;
131
+ createSessionManager: (cwd: string, sessionDir: string) => SessionManagerLike;
132
+ createSettingsManager: (cwd: string, agentDir: string) => SettingsManager;
133
+ createSession: (opts: CreateSessionOptions) => Promise<{ session: AgentSession }>;
134
+ assemblerIO: AssemblerIO;
135
+ }
136
+
137
+ // ── Public interfaces ─────────────────────────────────────────────────────────
71
138
 
72
139
  export interface RunOptions {
73
140
  /** Shell-exec callback for detectEnv — injected from pi.exec(). */
@@ -125,6 +192,20 @@ export interface AgentRunner {
125
192
  resume(session: AgentSession, prompt: string, options?: ResumeOptions): Promise<string>;
126
193
  }
127
194
 
195
+ /**
196
+ * Create an AgentRunner backed by the given IO boundary.
197
+ *
198
+ * Captures io at construction time so AgentManager remains IO-unaware.
199
+ */
200
+ export function createAgentRunner(io: RunnerIO): AgentRunner {
201
+ return {
202
+ run: (snapshot, type, prompt, options) => runAgent(snapshot, type, prompt, options, io),
203
+ resume: resumeAgent,
204
+ };
205
+ }
206
+
207
+ // ── Private helpers ───────────────────────────────────────────────────────────
208
+
128
209
  /**
129
210
  * Subscribe to a session and collect the last assistant message text.
130
211
  * Returns an object with a `getText()` getter and an `unsubscribe` function.
@@ -170,23 +251,20 @@ function forwardAbortSignal(
170
251
  return () => signal.removeEventListener("abort", onAbort);
171
252
  }
172
253
 
254
+ // ── Public functions ──────────────────────────────────────────────────────────
255
+
173
256
  export async function runAgent(
174
257
  snapshot: ParentSnapshot,
175
258
  type: SubagentType,
176
259
  prompt: string,
177
260
  options: RunOptions,
261
+ io: RunnerIO,
178
262
  ): Promise<RunResult> {
179
263
  // Resolve working directory upfront — needed for detectEnv before assembly.
180
264
  const effectiveCwd = options.cwd ?? snapshot.cwd;
181
- const env = await detectEnv(options.exec, effectiveCwd);
265
+ const env = await io.detectEnv(options.exec, effectiveCwd);
182
266
 
183
267
  // Assemble session configuration (synchronous, no SDK objects).
184
- const io: AssemblerIO = {
185
- preloadSkills,
186
- buildMemoryBlock,
187
- buildReadOnlyMemoryBlock,
188
- buildAgentPrompt,
189
- };
190
268
  const cfg = assembleSessionConfig(
191
269
  type,
192
270
  {
@@ -203,10 +281,10 @@ export async function runAgent(
203
281
  },
204
282
  env,
205
283
  options.registry,
206
- io,
284
+ io.assemblerIO,
207
285
  );
208
286
 
209
- const agentDir = getAgentDir();
287
+ const agentDir = io.getAgentDir();
210
288
 
211
289
  // Load extensions/skills: true or string[] → load; false → don't.
212
290
  // Suppress AGENTS.md/CLAUDE.md and APPEND_SYSTEM.md — upstream's
@@ -214,7 +292,7 @@ export async function runAgent(
214
292
  // would defeat prompt_mode: replace and isolated: true. Parent context, if
215
293
  // wanted, reaches the subagent via prompt_mode: append (parentSystemPrompt
216
294
  // is embedded in systemPromptOverride) or inherit_context (conversation).
217
- const loader = new DefaultResourceLoader({
295
+ const loader = io.createResourceLoader({
218
296
  cwd: cfg.effectiveCwd,
219
297
  agentDir,
220
298
  noExtensions: cfg.extensions === false,
@@ -230,25 +308,21 @@ export async function runAgent(
230
308
  // Create a persisted SessionManager so transcripts are written in Pi's
231
309
  // official JSONL format. Falls back to a temp directory when the parent
232
310
  // session is not persisted (e.g. headless/API mode).
233
- const sessionDir = deriveSubagentSessionDir(options.parentSessionFile, cfg.effectiveCwd);
234
- const sessionManager = SessionManager.create(cfg.effectiveCwd, sessionDir);
311
+ const sessionDir = io.deriveSessionDir(options.parentSessionFile, cfg.effectiveCwd);
312
+ const sessionManager = io.createSessionManager(cfg.effectiveCwd, sessionDir);
235
313
  sessionManager.newSession({ parentSession: options.parentSessionId });
236
314
 
237
- const sessionOpts: Parameters<typeof createAgentSession>[0] = {
315
+ const { session } = await io.createSession({
238
316
  cwd: cfg.effectiveCwd,
239
317
  agentDir,
240
318
  sessionManager,
241
- settingsManager: SettingsManager.create(cfg.effectiveCwd, agentDir),
242
- modelRegistry: snapshot.modelRegistry as any,
243
- model: cfg.model as Model<any> | undefined,
319
+ settingsManager: io.createSettingsManager(cfg.effectiveCwd, agentDir),
320
+ modelRegistry: snapshot.modelRegistry,
321
+ model: cfg.model,
244
322
  tools: cfg.toolNames,
245
323
  resourceLoader: loader,
246
- };
247
- if (cfg.thinkingLevel) {
248
- sessionOpts.thinkingLevel = cfg.thinkingLevel;
249
- }
250
-
251
- const { session } = await createAgentSession(sessionOpts);
324
+ thinkingLevel: cfg.thinkingLevel,
325
+ });
252
326
 
253
327
  // Filter active tools: remove our own tools to prevent nesting,
254
328
  // apply extension allowlist if specified, and apply disallowedTools denylist.
@@ -391,9 +465,7 @@ export function getAgentConversation(session: AgentSession): string {
391
465
  for (const c of msg.content) {
392
466
  if (c.type === "text" && c.text) textParts.push(c.text);
393
467
  else if (c.type === "toolCall")
394
- toolCalls.push(
395
- ` Tool: ${(c as any).name ?? (c as any).toolName ?? "unknown"}`,
396
- );
468
+ toolCalls.push(` Tool: ${getToolCallName(c)}`);
397
469
  }
398
470
  if (textParts.length > 0)
399
471
  parts.push(`[Assistant]: ${textParts.join("\n")}`);
package/src/index.ts CHANGED
@@ -11,19 +11,32 @@
11
11
  */
12
12
 
13
13
  import { join } from "node:path";
14
- import { defineTool, type ExtensionAPI, getAgentDir } from "@earendil-works/pi-coding-agent";
14
+ import {
15
+ createAgentSession,
16
+ DefaultResourceLoader,
17
+ defineTool,
18
+ type ExtensionAPI,
19
+ getAgentDir,
20
+ SettingsManager as SdkSettingsManager,
21
+ SessionManager,
22
+ } from "@earendil-works/pi-coding-agent";
15
23
  import { AgentManager, type AgentManagerObserver } from "./agent-manager.js";
16
- import { getAgentConversation, resumeAgent, runAgent, steerAgent } from "./agent-runner.js";
24
+ import { createAgentRunner, getAgentConversation, type RunnerIO, steerAgent } from "./agent-runner.js";
17
25
  import { AgentTypeRegistry } from "./agent-types.js";
18
26
  import { loadCustomAgents } from "./custom-agents.js";
27
+ import { detectEnv } from "./env.js";
19
28
  import { SessionLifecycleHandler, ToolStartHandler } from "./handlers/index.js";
29
+ import { buildMemoryBlock, buildReadOnlyMemoryBlock } from "./memory.js";
20
30
  import { type ModelRegistry, resolveModel } from "./model-resolver.js";
21
31
  import { buildEventData, type NotificationDetails, NotificationManager } from "./notification.js";
32
+ import { buildAgentPrompt } from "./prompts.js";
22
33
  import { createNotificationRenderer } from "./renderer.js";
23
34
  import { createSubagentRuntime } from "./runtime.js";
24
35
  import { publishSubagentsService, unpublishSubagentsService } from "./service.js";
25
36
  import { createSubagentsService } from "./service-adapter.js";
37
+ import { deriveSubagentSessionDir } from "./session-dir.js";
26
38
  import { SettingsManager } from "./settings.js";
39
+ import { preloadSkills } from "./skill-loader.js";
27
40
  import { createAgentTool } from "./tools/agent-tool.js";
28
41
  import { createGetResultTool } from "./tools/get-result-tool.js";
29
42
  import { getModelLabelFromConfig } from "./tools/helpers.js";
@@ -120,8 +133,24 @@ export default function (pi: ExtensionAPI) {
120
133
  },
121
134
  };
122
135
 
136
+ const runnerIO: RunnerIO = {
137
+ detectEnv,
138
+ getAgentDir,
139
+ createResourceLoader: (opts) => new DefaultResourceLoader(opts),
140
+ deriveSessionDir: deriveSubagentSessionDir,
141
+ createSessionManager: (cwd, dir) => SessionManager.create(cwd, dir),
142
+ createSettingsManager: (cwd, dir) => SdkSettingsManager.create(cwd, dir),
143
+ createSession: (opts) => createAgentSession(opts as any),
144
+ assemblerIO: {
145
+ preloadSkills,
146
+ buildMemoryBlock,
147
+ buildReadOnlyMemoryBlock,
148
+ buildAgentPrompt,
149
+ },
150
+ };
151
+
123
152
  const manager = new AgentManager({
124
- runner: { run: runAgent, resume: resumeAgent },
153
+ runner: createAgentRunner(runnerIO),
125
154
  worktrees: new GitWorktreeManager(process.cwd()),
126
155
  exec: (cmd, args, opts) => pi.exec(cmd, args, opts),
127
156
  registry,
package/src/runtime.ts CHANGED
@@ -7,7 +7,19 @@
7
7
  */
8
8
 
9
9
  import type { AgentActivityTracker } from "./ui/agent-activity-tracker.js";
10
- import type { AgentWidget, UICtx } from "./ui/agent-widget.js";
10
+ import type { UICtx } from "./ui/agent-widget.js";
11
+
12
+ /**
13
+ * Narrow widget interface consumed by SubagentRuntime delegation methods.
14
+ * AgentWidget satisfies this structurally; tests use plain stubs.
15
+ */
16
+ export interface WidgetLike {
17
+ setUICtx(ctx: UICtx): void;
18
+ onTurnStart(): void;
19
+ markFinished(id: string): void;
20
+ update(): void;
21
+ ensureTimer(): void;
22
+ }
11
23
 
12
24
  /**
13
25
  * Narrow config subset read by AgentManager when constructing RunOptions.
@@ -37,7 +49,7 @@ export class SubagentRuntime {
37
49
  * Persistent widget reference. Null until constructed after AgentManager.
38
50
  * Delegation methods use optional chaining so callers never need `widget!`.
39
51
  */
40
- widget: AgentWidget | null = null;
52
+ widget: WidgetLike | null = null;
41
53
 
42
54
  // ── Session-context methods ──────────────────────────────────────────────
43
55
 
@@ -21,7 +21,7 @@ import {
21
21
  } from "../ui/agent-widget.js";
22
22
  import { spawnBackground } from "./background-spawner.js";
23
23
  import { runForeground } from "./foreground-runner.js";
24
- import { buildDetails, buildTypeListText, formatLifetimeTokens, getStatusNote, textResult } from "./helpers.js";
24
+ import { buildDetails, buildTypeListText, textResult } from "./helpers.js";
25
25
 
26
26
  // ---- Deps interface ----
27
27
 
@@ -48,7 +48,7 @@ export function buildDetails(
48
48
 
49
49
  /** Tool execute return value for a text response. */
50
50
  export function textResult(msg: string, details?: unknown) {
51
- return { content: [{ type: "text" as const, text: msg }], details: details as any };
51
+ return { content: [{ type: "text" as const, text: msg }], details };
52
52
  }
53
53
 
54
54
  /** Format an agent's lifetime token total, or "" when zero. */
@@ -14,6 +14,37 @@ import { getLifetimeTotal, getSessionContextPercent } from "../usage.js";
14
14
  import type { AgentActivityTracker } from "./agent-activity-tracker.js";
15
15
  import { buildInvocationTags, describeActivity, formatDuration, formatSessionTokens, getDisplayName, getPromptModeLabel, type Theme } from "./agent-widget.js";
16
16
 
17
+ // ── Local message-shape types ───────────────────────────────────────────────
18
+ // The Pi SDK does not export narrow types for all message content variants.
19
+ // These file-local types document the runtime shapes this module handles.
20
+
21
+ /** Tool-call content item — SDK exposes this variant at runtime but doesn't export the narrow type. */
22
+ interface ToolCallContent {
23
+ type: "toolCall";
24
+ name?: string;
25
+ toolName?: string;
26
+ }
27
+
28
+ /** Extracts the tool name from a content item, falling back to 'unknown'. */
29
+ function getToolCallName(c: { type: string }): string {
30
+ if (c.type !== "toolCall") return "unknown";
31
+ const tc = c as ToolCallContent;
32
+ return tc.name ?? tc.toolName ?? "unknown";
33
+ }
34
+
35
+ /** Bash execution message — 'bashExecution' role is not in the SDK's AgentSession message role union. */
36
+ interface BashExecutionMessage {
37
+ role: "bashExecution";
38
+ command: string;
39
+ output?: string;
40
+ }
41
+
42
+ function isBashExecution(msg: { role: string }): msg is BashExecutionMessage {
43
+ return msg.role === "bashExecution";
44
+ }
45
+
46
+ // ─────────────────────────────────────────────────────────────────────────────
47
+
17
48
  /** Base lines consumed by chrome: top border + header + header sep + footer sep + footer + bottom border. */
18
49
  const CHROME_LINES_BASE = 6;
19
50
  const MIN_VIEWPORT = 3;
@@ -228,7 +259,7 @@ export class ConversationViewer implements Component {
228
259
  for (const c of msg.content) {
229
260
  if (c.type === "text" && c.text) textParts.push(c.text);
230
261
  else if (c.type === "toolCall") {
231
- toolCalls.push((c as any).name ?? (c as any).toolName ?? "unknown");
262
+ toolCalls.push(getToolCallName(c));
232
263
  }
233
264
  }
234
265
  if (needsSeparator) lines.push(th.fg("dim", "───"));
@@ -250,14 +281,13 @@ export class ConversationViewer implements Component {
250
281
  for (const line of wrapTextWithAnsi(truncated.trim(), width)) {
251
282
  lines.push(th.fg("dim", line));
252
283
  }
253
- } else if ((msg as any).role === "bashExecution") {
254
- const bash = msg as any;
284
+ } else if (isBashExecution(msg)) {
255
285
  if (needsSeparator) lines.push(th.fg("dim", "───"));
256
- lines.push(truncateToWidth(th.fg("muted", ` $ ${bash.command}`), width));
257
- if (bash.output?.trim()) {
258
- const out = bash.output.length > 500
259
- ? bash.output.slice(0, 500) + "... (truncated)"
260
- : bash.output;
286
+ lines.push(truncateToWidth(th.fg("muted", ` $ ${msg.command}`), width));
287
+ if (msg.output?.trim()) {
288
+ const out = msg.output.length > 500
289
+ ? msg.output.slice(0, 500) + "... (truncated)"
290
+ : msg.output;
261
291
  for (const line of wrapTextWithAnsi(out.trim(), width)) {
262
292
  lines.push(th.fg("dim", line));
263
293
  }