@mystilleef/pi-subagent 0.4.0 → 0.6.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mystilleef/pi-subagent",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "Pi subagent for the SPAE Framework",
5
5
  "author": "Lateef Alabi-Oki <mystilleef@gmail.com>",
6
6
  "license": "MIT",
@@ -47,12 +47,14 @@
47
47
  "access": "public"
48
48
  },
49
49
  "scripts": {
50
- "verify": "bun fix && bun check && bun test",
51
- "coverage": "bun fix && bun check && bun test --coverage",
52
- "check": "biome check . && tsc --noEmit",
50
+ "typecheck": "tsc --noEmit",
51
+ "lint": "biome check --error-on-warnings .",
53
52
  "fix": "biome check --write --unsafe .",
54
- "pack:smoke": "bun scripts/pack-smoke.ts",
55
53
  "migrate": "biome migrate --write",
54
+ "coverage": "bun test --coverage",
55
+ "check": "bun lint && bun typecheck",
56
+ "verify": "bun migrate && bun fix && bun typecheck && bun coverage",
57
+ "pack:smoke": "bun scripts/pack-smoke.ts",
56
58
  "release": "sh -c 'npm version \"$1\" -m \"chore(release): %s\" && git push --follow-tags' --"
57
59
  },
58
60
  "peerDependencies": {
@@ -63,14 +65,14 @@
63
65
  "typebox": "*"
64
66
  },
65
67
  "devDependencies": {
66
- "@biomejs/biome": "^2.4.15",
67
- "@earendil-works/pi-agent-core": "^0.74.1",
68
- "@earendil-works/pi-ai": "^0.74.1",
69
- "@earendil-works/pi-coding-agent": "^0.74.1",
70
- "@earendil-works/pi-tui": "^0.74.1",
68
+ "@biomejs/biome": "^2.4.16",
69
+ "@earendil-works/pi-agent-core": "^0.78.0",
70
+ "@earendil-works/pi-ai": "^0.78.0",
71
+ "@earendil-works/pi-coding-agent": "^0.78.0",
72
+ "@earendil-works/pi-tui": "^0.78.0",
71
73
  "@types/bun": "^1.3.14",
72
- "@types/node": "^25.8.0",
73
- "typebox": "^1.1.38",
74
+ "@types/node": "^25.9.1",
75
+ "typebox": "^1.1.39",
74
76
  "typescript": "^6.0.3"
75
77
  }
76
78
  }
@@ -2,40 +2,40 @@ import path from "node:path";
2
2
  import {
3
3
  type AgentDiscoveryResult,
4
4
  type AgentScope,
5
- discoverAgents,
5
+ discoverAgentsAsync,
6
6
  } from "./agents.js";
7
7
 
8
8
  export type AgentDiscoveryCacheEntry = AgentDiscoveryResult & { ts: number };
9
9
  export type AgentDiscoveryCache = Map<string, AgentDiscoveryCacheEntry>;
10
- export const AGENT_DISCOVERY_CACHE_TTL_MS = 3_000;
10
+ export const AGENT_DISCOVERY_CACHE_TTL_MS = 300_000;
11
11
  const sharedAgentDiscoveryCache: AgentDiscoveryCache = new Map();
12
12
 
13
13
  export function resetAgentDiscoveryCache(): void {
14
14
  sharedAgentDiscoveryCache.clear();
15
15
  }
16
16
 
17
- export function getCachedAgentDiscovery(
17
+ export async function getCachedAgentDiscovery(
18
18
  cwd: string,
19
19
  scope: AgentScope,
20
20
  cache: AgentDiscoveryCache = sharedAgentDiscoveryCache,
21
21
  cacheTtlMs = AGENT_DISCOVERY_CACHE_TTL_MS,
22
- ): AgentDiscoveryCacheEntry {
22
+ ): Promise<AgentDiscoveryCacheEntry> {
23
23
  const key = `${path.resolve(cwd)}\0${scope}`;
24
24
  const now = Date.now();
25
25
  const entry = cache.get(key);
26
26
  if (entry && now - entry.ts <= cacheTtlMs) return entry;
27
- const nextEntry = { ...discoverAgents(cwd, scope), ts: now };
27
+ const nextEntry = { ...(await discoverAgentsAsync(cwd, scope)), ts: now };
28
28
  cache.set(key, nextEntry);
29
29
  return nextEntry;
30
30
  }
31
31
 
32
- export function getCachedAgentCompletions(
32
+ export async function getCachedAgentCompletions(
33
33
  prefix: string,
34
34
  cwd = process.cwd(),
35
35
  cache: AgentDiscoveryCache = sharedAgentDiscoveryCache,
36
36
  cacheTtlMs = AGENT_DISCOVERY_CACHE_TTL_MS,
37
- ): { value: string; label: string }[] {
38
- return getCachedAgentDiscovery(cwd, "both", cache, cacheTtlMs)
39
- .agents.filter((agent) => agent.name.startsWith(prefix))
37
+ ): Promise<{ value: string; label: string }[]> {
38
+ return (await getCachedAgentDiscovery(cwd, "both", cache, cacheTtlMs)).agents
39
+ .filter((agent) => agent.name.startsWith(prefix))
40
40
  .map((agent) => ({ value: agent.name, label: agent.name }));
41
41
  }
@@ -2,7 +2,8 @@
2
2
  * Agent discovery and configuration
3
3
  */
4
4
 
5
- import * as fs from "node:fs";
5
+ import type { Dirent } from "node:fs";
6
+ import * as fsPromises from "node:fs/promises";
6
7
  import * as path from "node:path";
7
8
  import { getAgentDir, parseFrontmatter } from "@earendil-works/pi-coding-agent";
8
9
 
@@ -41,6 +42,17 @@ export interface AgentDiscoveryResult {
41
42
  projectAgentsDir: string | null;
42
43
  }
43
44
 
45
+ function mergeAgentLists(
46
+ userAgents: AgentConfig[],
47
+ projectAgents: AgentConfig[],
48
+ projectAgentsDir: string | null,
49
+ ): AgentDiscoveryResult {
50
+ const agentMap = new Map<string, AgentConfig>();
51
+ for (const agent of userAgents) agentMap.set(agent.name, agent);
52
+ for (const agent of projectAgents) agentMap.set(agent.name, agent);
53
+ return { agents: Array.from(agentMap.values()), projectAgentsDir };
54
+ }
55
+
44
56
  function parseCommaList(raw: unknown): string[] | undefined {
45
57
  if (typeof raw !== "string") return undefined;
46
58
  const items = raw
@@ -58,17 +70,61 @@ function parseThinkingLevel(raw: unknown): ThinkingLevel | undefined {
58
70
  : undefined;
59
71
  }
60
72
 
61
- function loadAgentsFromDir(
73
+ function parseAgentConfig(
74
+ content: string,
75
+ source: "user" | "project",
76
+ filePath: string,
77
+ ): AgentConfig | null {
78
+ let parsed: ReturnType<typeof parseFrontmatter<Record<string, unknown>>>;
79
+ try {
80
+ parsed = parseFrontmatter<Record<string, unknown>>(content);
81
+ } catch {
82
+ return null;
83
+ }
84
+ const { frontmatter, body } = parsed;
85
+ if (
86
+ typeof frontmatter !== "object" ||
87
+ frontmatter === null ||
88
+ Array.isArray(frontmatter)
89
+ )
90
+ return null;
91
+ const {
92
+ name,
93
+ description,
94
+ tools: rawTools,
95
+ skills: rawSkills,
96
+ thinking: rawThinking,
97
+ } = frontmatter;
98
+ if (typeof name !== "string" || typeof description !== "string") return null;
99
+ if (rawTools != null && typeof rawTools !== "string") return null;
100
+ if (rawSkills != null && typeof rawSkills !== "string") return null;
101
+ if (rawThinking != null && typeof rawThinking !== "string") return null;
102
+ const tools = parseCommaList(rawTools);
103
+ const skills = Object.hasOwn(frontmatter, "skills")
104
+ ? (parseCommaList(rawSkills) ?? [])
105
+ : undefined;
106
+ const thinking = parseThinkingLevel(rawThinking);
107
+ return {
108
+ name,
109
+ description,
110
+ tools,
111
+ skills,
112
+ thinking,
113
+ systemPrompt: body,
114
+ source,
115
+ filePath,
116
+ };
117
+ }
118
+
119
+ async function loadAgentsFromDirAsync(
62
120
  dir: string,
63
121
  source: "user" | "project",
64
- ): AgentConfig[] {
122
+ ): Promise<AgentConfig[]> {
65
123
  const agents: AgentConfig[] = [];
66
- if (!fs.existsSync(dir)) {
67
- return agents;
68
- }
69
- let entries: fs.Dirent[];
124
+ if (!(await isDirectoryAsync(dir))) return agents;
125
+ let entries: Dirent[];
70
126
  try {
71
- entries = fs.readdirSync(dir, { withFileTypes: true });
127
+ entries = await fsPromises.readdir(dir, { withFileTypes: true });
72
128
  } catch {
73
129
  return agents;
74
130
  }
@@ -78,88 +134,50 @@ function loadAgentsFromDir(
78
134
  const filePath = path.join(dir, entry.name);
79
135
  let content: string;
80
136
  try {
81
- content = fs.readFileSync(filePath, "utf-8");
82
- } catch {
83
- continue;
84
- }
85
- let parsed: ReturnType<typeof parseFrontmatter<Record<string, unknown>>>;
86
- try {
87
- parsed = parseFrontmatter<Record<string, unknown>>(content);
137
+ content = await fsPromises.readFile(filePath, "utf-8");
88
138
  } catch {
89
139
  continue;
90
140
  }
91
- const { frontmatter, body } = parsed;
92
- if (
93
- typeof frontmatter !== "object" ||
94
- frontmatter === null ||
95
- Array.isArray(frontmatter)
96
- )
97
- continue;
98
- const {
99
- name,
100
- description,
101
- tools: rawTools,
102
- skills: rawSkills,
103
- thinking: rawThinking,
104
- } = frontmatter;
105
- if (typeof name !== "string" || typeof description !== "string") continue;
106
- if (rawTools != null && typeof rawTools !== "string") continue;
107
- if (rawSkills != null && typeof rawSkills !== "string") continue;
108
- if (rawThinking != null && typeof rawThinking !== "string") continue;
109
- const tools = parseCommaList(rawTools);
110
- const skills = Object.hasOwn(frontmatter, "skills")
111
- ? (parseCommaList(rawSkills) ?? [])
112
- : undefined;
113
- const thinking = parseThinkingLevel(rawThinking);
114
- agents.push({
115
- name,
116
- description,
117
- tools,
118
- skills,
119
- thinking,
120
- systemPrompt: body,
121
- source,
122
- filePath,
123
- });
141
+ const agent = parseAgentConfig(content, source, filePath);
142
+ if (agent) agents.push(agent);
124
143
  }
125
144
  return agents;
126
145
  }
127
146
 
128
- function isDirectory(p: string): boolean {
147
+ async function isDirectoryAsync(p: string): Promise<boolean> {
129
148
  try {
130
- return fs.statSync(p).isDirectory();
149
+ return (await fsPromises.stat(p)).isDirectory();
131
150
  } catch {
132
151
  return false;
133
152
  }
134
153
  }
135
154
 
136
- function findNearestProjectAgentsDir(cwd: string): string | null {
155
+ async function findNearestProjectAgentsDirAsync(
156
+ cwd: string,
157
+ ): Promise<string | null> {
137
158
  let currentDir = cwd;
138
159
  while (true) {
139
160
  const candidate = path.join(currentDir, ".pi", "agents");
140
- if (isDirectory(candidate)) return candidate;
161
+ if (await isDirectoryAsync(candidate)) return candidate;
141
162
  const parentDir = path.dirname(currentDir);
142
163
  if (parentDir === currentDir) return null;
143
164
  currentDir = parentDir;
144
165
  }
145
166
  }
146
167
 
147
- export function discoverAgents(
168
+ export async function discoverAgentsAsync(
148
169
  cwd: string,
149
170
  scope: AgentScope,
150
- ): AgentDiscoveryResult {
171
+ ): Promise<AgentDiscoveryResult> {
151
172
  const userDir = path.join(getAgentDir(), "agents");
152
- const projectAgentsDir = findNearestProjectAgentsDir(cwd);
153
- const userAgents =
154
- scope === "project" ? [] : loadAgentsFromDir(userDir, "user");
155
- const projectAgents =
173
+ const projectAgentsDir = await findNearestProjectAgentsDirAsync(cwd);
174
+ const [userAgents, projectAgents] = await Promise.all([
175
+ scope === "project" ? [] : loadAgentsFromDirAsync(userDir, "user"),
156
176
  scope === "user" || !projectAgentsDir
157
177
  ? []
158
- : loadAgentsFromDir(projectAgentsDir, "project");
159
- const agentMap = new Map<string, AgentConfig>();
160
- for (const agent of userAgents) agentMap.set(agent.name, agent);
161
- for (const agent of projectAgents) agentMap.set(agent.name, agent);
162
- return { agents: Array.from(agentMap.values()), projectAgentsDir };
178
+ : loadAgentsFromDirAsync(projectAgentsDir, "project"),
179
+ ]);
180
+ return mergeAgentLists(userAgents, projectAgents, projectAgentsDir);
163
181
  }
164
182
 
165
183
  export function formatAgentList(
@@ -7,23 +7,23 @@
7
7
  import { type ChildProcess, spawn } from "node:child_process";
8
8
  import * as fs from "node:fs";
9
9
  import readline from "node:readline";
10
- import { getModel, type Message } from "@earendil-works/pi-ai";
11
- import type { AgentConfig, ThinkingLevel } from "./agents.js";
12
- import { parseChildEventLine } from "./child-events.js";
13
- import { makeToolPreview } from "./progress.js";
14
- import { isToolCallPart } from "./progress-state.js";
15
- import { appendSubagentResultContract } from "./prompt-contract.js";
16
10
  import {
17
- getProcessTreeSpawnOptions,
18
- terminateChildProcess,
19
- } from "./termination.js";
11
+ clampThinkingLevel,
12
+ getModel,
13
+ getSupportedThinkingLevels,
14
+ type Message,
15
+ type ModelThinkingLevel,
16
+ } from "@earendil-works/pi-ai";
17
+ import type { AgentConfig, ThinkingLevel } from "../agent/agents.js";
18
+ import { getFinalOutput } from "../output/ui.js";
19
+ import { makeToolPreview } from "../progress/progress.js";
20
+ import { isToolCallPart } from "../progress/progress-state.js";
20
21
  import type {
21
22
  OnUpdateCallback,
22
23
  SingleResult,
23
24
  StreamingProgress,
24
25
  SubagentDetails,
25
- } from "./types.js";
26
- import { getFinalOutput } from "./ui.js";
26
+ } from "../shared/types.js";
27
27
  import {
28
28
  detectMessageError,
29
29
  getPiInvocation,
@@ -32,16 +32,62 @@ import {
32
32
  subagentDepthEnv,
33
33
  truncateOutput,
34
34
  writePromptToTempFile,
35
- } from "./utils.js";
35
+ } from "../shared/utils.js";
36
+ import { parseChildEventLine } from "./child-events.js";
37
+ import { appendSubagentResultContract } from "./prompt-contract.js";
38
+ import {
39
+ getProcessTreeSpawnOptions,
40
+ terminateChildProcess,
41
+ } from "./termination.js";
36
42
 
37
43
  const MAX_STDERR_BYTES = 10_000;
38
44
  const AGENT_END_GRACE_MS = 250;
39
45
 
46
+ function thinkingWarningFor(
47
+ requested: ThinkingLevel,
48
+ effective: ThinkingLevel,
49
+ provider: string,
50
+ modelId: string,
51
+ ): string {
52
+ return `Thinking level "${requested}" not supported by model "${provider}/${modelId}"; using "${effective}" instead`;
53
+ }
54
+
55
+ export function resolveThinkingLevel(
56
+ requested: ThinkingLevel,
57
+ provider: string,
58
+ modelId: string,
59
+ ): { level: ThinkingLevel; warning?: string } {
60
+ const model = getModel(provider as never, modelId as never);
61
+ if (!model) return { level: requested };
62
+ if (model.reasoning === false) {
63
+ return {
64
+ level: "off",
65
+ warning: thinkingWarningFor(requested, "off", provider, modelId),
66
+ };
67
+ }
68
+ if (!model.thinkingLevelMap) return { level: requested };
69
+ const supported = getSupportedThinkingLevels(model);
70
+ if (supported.length === 0) return { level: requested };
71
+ const clamped = clampThinkingLevel(
72
+ model,
73
+ requested as ModelThinkingLevel,
74
+ ) as ThinkingLevel;
75
+ if (clamped === requested) return { level: requested };
76
+ return {
77
+ level: clamped,
78
+ warning: thinkingWarningFor(requested, clamped, provider, modelId),
79
+ };
80
+ }
81
+
40
82
  const MAX_SUBAGENT_DEPTH = 1;
41
83
  export const TOOL_RESULT_FAILED_MESSAGE = "Subagent tool result failed.";
42
84
 
43
85
  type RuntimeResult = SingleResult & { messages: Message[] };
44
86
 
87
+ type TempPrompt = { dir: string; filePath: string };
88
+
89
+ type PromptSetupResult = { tmpPrompt: TempPrompt | null } | { error: unknown };
90
+
45
91
  interface SubagentState {
46
92
  result: RuntimeResult;
47
93
  spawnError?: Error;
@@ -72,7 +118,10 @@ function resolveContextWindowTokens(msg: Message): number | undefined {
72
118
  const m = msg as unknown as Record<string, unknown>;
73
119
  if (typeof m.provider !== "string" || typeof m.model !== "string") return;
74
120
  try {
75
- const { contextWindow } = getModel(m.provider as never, m.model as never);
121
+ const contextWindow = getModel(
122
+ m.provider as never,
123
+ m.model as never,
124
+ )?.contextWindow;
76
125
  return Number.isFinite(contextWindow) && contextWindow > 0
77
126
  ? contextWindow
78
127
  : undefined;
@@ -139,26 +188,22 @@ async function waitForSubagentProcess(
139
188
  let exited = false;
140
189
  let settled = false;
141
190
  let idleTimer: NodeJS.Timeout | undefined;
142
-
143
191
  const done = () => {
144
192
  if (settled) return;
145
193
  settled = true;
146
194
  if (idleTimer) clearTimeout(idleTimer);
147
195
  resolve(exitCode);
148
196
  };
149
-
150
197
  const destroyStreams = () => {
151
198
  proc.stdout?.destroy();
152
199
  proc.stderr?.destroy();
153
200
  };
154
-
155
201
  const armIdleTimer = () => {
156
202
  if (!exited) return;
157
203
  if (idleTimer) clearTimeout(idleTimer);
158
204
  idleTimer = setTimeout(destroyStreams, idleMs);
159
205
  idleTimer.unref?.();
160
206
  };
161
-
162
207
  proc.on("close", done);
163
208
  proc.on("error", () => {
164
209
  exitCode = 1;
@@ -217,16 +262,13 @@ function initRuntimeResult(
217
262
  function addMessageToResult(result: RuntimeResult, msg: Message): void {
218
263
  result.messages.push(msg);
219
264
  result.finalOutput = truncateOutput(getFinalOutput(result.messages));
220
-
221
265
  if (msg.role === "toolResult" && msg.isError) {
222
266
  result.errorMessage ||= TOOL_RESULT_FAILED_MESSAGE;
223
267
  } else if (result.errorMessage === TOOL_RESULT_FAILED_MESSAGE) {
224
268
  result.errorMessage = undefined;
225
269
  }
226
-
227
270
  if (msg.role !== "assistant") return;
228
271
  result.usage.turns++;
229
-
230
272
  const { usage } = msg;
231
273
  if (usage) {
232
274
  result.usage.input += usage.input || 0;
@@ -238,7 +280,6 @@ function addMessageToResult(result: RuntimeResult, msg: Message): void {
238
280
  result.usage.contextWindowTokens =
239
281
  resolveContextWindowTokens(msg) ?? result.usage.contextWindowTokens;
240
282
  }
241
-
242
283
  if (!result.model && msg.model) result.model = msg.model;
243
284
  if (msg.stopReason) result.stopReason = msg.stopReason;
244
285
  if (msg.errorMessage) result.errorMessage = msg.errorMessage;
@@ -304,10 +345,7 @@ function errorForDepthLimit(
304
345
  );
305
346
  }
306
347
 
307
- async function cleanupTempPrompt(tmpPrompt: {
308
- dir: string;
309
- filePath: string;
310
- }): Promise<void> {
348
+ async function cleanupTempPrompt(tmpPrompt: TempPrompt): Promise<void> {
311
349
  try {
312
350
  await fs.promises.unlink(tmpPrompt.filePath);
313
351
  await fs.promises.rmdir(tmpPrompt.dir);
@@ -316,6 +354,22 @@ async function cleanupTempPrompt(tmpPrompt: {
316
354
  }
317
355
  }
318
356
 
357
+ function beginPromptSetup(agent: AgentConfig): Promise<PromptSetupResult> {
358
+ if (!agent.systemPrompt.trim()) return Promise.resolve({ tmpPrompt: null });
359
+ return writePromptToTempFile(agent.name, agent.systemPrompt).then(
360
+ (tmpPrompt) => ({ tmpPrompt }),
361
+ (error: unknown) => ({ error }),
362
+ );
363
+ }
364
+
365
+ async function cleanupPromptSetupResult(
366
+ setup: PromptSetupResult,
367
+ ): Promise<void> {
368
+ if ("tmpPrompt" in setup && setup.tmpPrompt) {
369
+ await cleanupTempPrompt(setup.tmpPrompt);
370
+ }
371
+ }
372
+
319
373
  function findRecentMessagesAnchor(messages: Message[]): number {
320
374
  for (let i = messages.length - 1; i >= 0; i--) {
321
375
  const msg = messages[i];
@@ -423,7 +477,6 @@ function processEventLine(
423
477
  const parseResult = parseChildEventLine(line);
424
478
  if (parseResult.kind !== "known") return;
425
479
  const { event } = parseResult;
426
-
427
480
  if (
428
481
  (event.type === "message_end" || event.type === "tool_result_end") &&
429
482
  event.message
@@ -431,16 +484,13 @@ function processEventLine(
431
484
  addMessageToResult(state.result, event.message as Message);
432
485
  emitUpdate();
433
486
  }
434
-
435
487
  if (event.type !== "agent_end") return;
436
-
437
488
  if (state.result.messages.length === 0 && Array.isArray(event.messages)) {
438
489
  for (const msg of event.messages as Message[]) {
439
490
  addMessageToResult(state.result, msg);
440
491
  }
441
492
  emitUpdate();
442
493
  }
443
-
444
494
  if (state.agentEndGraceTimer || state.terminationPromise) return;
445
495
  state.agentEndGraceTimer = setTimeout(() => {
446
496
  state.agentEndGraceTimer = undefined;
@@ -579,18 +629,28 @@ export async function runSingleAgent(
579
629
  ): Promise<SingleResult> {
580
630
  const agent = agents.find((a) => a.name === agentName);
581
631
  if (!agent) return errorForUnknownAgent(agentName, agents, task);
582
-
583
632
  const depth = getSubagentDepth();
584
633
  if (depth >= MAX_SUBAGENT_DEPTH) {
585
634
  return errorForDepthLimit(agentName, agent.source, task, depth);
586
635
  }
587
-
588
- const thinking = agent.thinking ?? parentThinking;
636
+ const requestedThinking = agent.thinking ?? parentThinking;
637
+ const { level: thinking, warning: thinkingWarning } = parentModel
638
+ ? resolveThinkingLevel(
639
+ requestedThinking,
640
+ parentModel.provider,
641
+ parentModel.id,
642
+ )
643
+ : { level: requestedThinking };
589
644
  const modelDisplay = buildModelDisplay(parentModel, thinking);
590
- const resolvedSkills = agent.skills
591
- ? await resolveAgentSkillArgs(defaultCwd, agent.skills)
592
- : { args: [] };
645
+ const resolvedSkillsPromise: Promise<{ args: string[] } | { error: string }> =
646
+ agent.skills
647
+ ? resolveAgentSkillArgs(defaultCwd, agent.skills)
648
+ : Promise.resolve({ args: [] });
649
+ const promptSetupPromise = beginPromptSetup(agent);
650
+ const resolvedSkills = await resolvedSkillsPromise;
593
651
  if ("error" in resolvedSkills) {
652
+ const promptSetup = await promptSetupPromise;
653
+ await cleanupPromptSetupResult(promptSetup);
594
654
  return createErrorResult(
595
655
  agentName,
596
656
  agent.source,
@@ -599,18 +659,16 @@ export async function runSingleAgent(
599
659
  modelDisplay,
600
660
  );
601
661
  }
602
-
662
+ const promptSetup = await promptSetupPromise;
663
+ if ("error" in promptSetup) throw promptSetup.error;
603
664
  const startedAt = Date.now();
604
665
  const state: SubagentState = {
605
666
  result: initRuntimeResult(agentName, agent.source, task, modelDisplay),
606
667
  wasAborted: false,
607
668
  };
608
-
609
- let tmpPrompt: { dir: string; filePath: string } | null = null;
669
+ if (thinkingWarning) state.result.thinkingWarning = thinkingWarning;
670
+ const tmpPrompt = promptSetup.tmpPrompt;
610
671
  try {
611
- tmpPrompt = agent.systemPrompt.trim()
612
- ? await writePromptToTempFile(agent.name, agent.systemPrompt)
613
- : null;
614
672
  const args = buildPiArgs(
615
673
  agent,
616
674
  task,
@@ -632,7 +690,6 @@ export async function runSingleAgent(
632
690
  env: { ...process.env, ...subagentDepthEnv() },
633
691
  ...getProcessTreeSpawnOptions(terminateOptions.tree),
634
692
  });
635
-
636
693
  const processDone = waitForSubagentProcess(proc);
637
694
  const emitUpdate = makeEmitUpdate(state.result, onUpdate, makeDetails);
638
695
  const requestTermination = makeRequestTerminator(
@@ -641,7 +698,6 @@ export async function runSingleAgent(
641
698
  state,
642
699
  );
643
700
  setupChildProcess(proc, state, emitUpdate, requestTermination);
644
-
645
701
  const onAbort = setupAbortHandler(
646
702
  signal,
647
703
  state,
@@ -3,6 +3,8 @@ export const SUBAGENT_RESULT_CONTRACT = `
3
3
  - End your final response with exactly one line:
4
4
  - Outcome: <short, single, compact lower-case sentence>.
5
5
  - Outcome summarizes the result of your task in a single sentence.
6
+ - The outcome line is for internal use by the agent.
7
+ - Don't present outcome line in the main agent's response.
6
8
  `;
7
9
 
8
10
  export function appendSubagentResultContract(prompt: string): string {
package/src/index.ts CHANGED
@@ -5,18 +5,18 @@ import type {
5
5
  import {
6
6
  getCachedAgentCompletions,
7
7
  resetAgentDiscoveryCache,
8
- } from "./agent-cache.js";
9
- import { cancelSubagentCommandHandler } from "./cancel-command.js";
10
- import { jobsCommandHandler } from "./jobs-command.js";
11
- import { renderSubagentProgress } from "./progress.js";
12
- import { renderSubagentResultMessage } from "./run.js";
13
- import { runCommandHandler } from "./run-command.js";
8
+ } from "./agent/agent-cache.js";
9
+ import { cancelSubagentCommandHandler } from "./orchestration/cancel-command.js";
10
+ import { jobsCommandHandler } from "./orchestration/jobs-command.js";
11
+ import { renderSubagentResultMessage } from "./orchestration/run.js";
12
+ import { runCommandHandler } from "./orchestration/run-command.js";
14
13
  import {
15
14
  formatStartJobStatus,
16
15
  SubagentParams,
17
16
  startSubagentJob,
18
- } from "./subagent-orchestrator.js";
19
- import { renderSubagentCall, renderSubagentResult } from "./ui.js";
17
+ } from "./orchestration/subagent-orchestrator.js";
18
+ import { renderSubagentCall, renderSubagentResult } from "./output/ui.js";
19
+ import { renderSubagentProgress } from "./progress/progress.js";
20
20
 
21
21
  export { SubagentParams };
22
22
 
@@ -29,7 +29,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI) {
29
29
  pi.registerMessageRenderer("subagent-result", renderSubagentResultMessage);
30
30
  pi.registerCommand("run", {
31
31
  description: "Run a subagent directly: /run <agent> [task]",
32
- getArgumentCompletions: async (prefix: string) =>
32
+ getArgumentCompletions: (prefix: string) =>
33
33
  getCachedAgentCompletions(prefix),
34
34
  handler: async (args, ctx) =>
35
35
  runCommandHandler(pi, ctx as ExtensionContext, args),
@@ -1,11 +1,10 @@
1
1
  import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
2
-
2
+ import { renderRunsBoard } from "../output/ui.js";
3
3
  import {
4
4
  getAllProgressStates,
5
5
  type SubagentProgressState,
6
- } from "./progress-state.js";
6
+ } from "../progress/progress-state.js";
7
7
  import { listRunJobs } from "./run-registry.js";
8
- import { renderRunsBoard } from "./ui.js";
9
8
 
10
9
  export async function jobsCommandHandler(
11
10
  ctx: ExtensionCommandContext,
@@ -1,5 +1,5 @@
1
- import type { SubagentDetails } from "./types.js";
2
- import { renderSubagentResult, type SubagentTheme } from "./ui.js";
1
+ import { renderSubagentResult, type SubagentTheme } from "../output/ui.js";
2
+ import type { SubagentDetails } from "../shared/types.js";
3
3
 
4
4
  /**
5
5
  * Pi message renderer adapter for `"subagent-result"` messages.