@mystilleef/pi-subagent 0.5.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.5.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.75.4",
68
- "@earendil-works/pi-ai": "^0.75.4",
69
- "@earendil-works/pi-coding-agent": "^0.75.4",
70
- "@earendil-works/pi-tui": "^0.75.4",
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
74
  "@types/node": "^25.9.1",
73
- "typebox": "^1.1.38",
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.
@@ -4,22 +4,21 @@ import type {
4
4
  ExtensionContext,
5
5
  } from "@earendil-works/pi-coding-agent";
6
6
  import { type Static, Type } from "typebox";
7
- import { getCachedAgentDiscovery } from "./agent-cache.js";
8
- import {
9
- type AgentConfig,
10
- type AgentScope,
11
- discoverAgents,
12
- type ThinkingLevel,
13
- } from "./agents.js";
14
- import { generateSubagentInstanceName } from "./instance-name.js";
15
- import { runSingleAgent } from "./process.js";
7
+ import { getCachedAgentDiscovery } from "../agent/agent-cache.js";
8
+ import type {
9
+ AgentConfig,
10
+ AgentScope,
11
+ ThinkingLevel,
12
+ } from "../agent/agents.js";
13
+ import { runSingleAgent } from "../child/process.js";
14
+ import { formatSubagentResultForParent } from "../output/summary.js";
16
15
  import {
17
16
  cancelProgressState,
18
17
  createProgressState,
19
18
  failProgressState,
20
19
  finalizeProgressState,
21
20
  getProgressState,
22
- } from "./progress.js";
21
+ } from "../progress/progress.js";
23
22
  import {
24
23
  createSubagentError,
25
24
  getFeedbackSummaryText,
@@ -27,20 +26,20 @@ import {
27
26
  hasSubagentFailed,
28
27
  patchProgressFromDetails,
29
28
  sanitizeDetailsForDisplay,
30
- } from "./result-details.js";
29
+ } from "../progress/result-details.js";
30
+ import { generateSubagentInstanceName } from "../shared/instance-name.js";
31
+ import type {
32
+ OnUpdateCallback,
33
+ SingleResult,
34
+ SubagentDetails,
35
+ SubagentToolResult,
36
+ } from "../shared/types.js";
31
37
  import {
32
38
  listRunJobs,
33
39
  type RunJob,
34
40
  registerRunJob,
35
41
  removeRunJob,
36
42
  } from "./run-registry.js";
37
- import { formatSubagentResultForParent } from "./summary.js";
38
- import type {
39
- OnUpdateCallback,
40
- SingleResult,
41
- SubagentDetails,
42
- SubagentToolResult,
43
- } from "./types.js";
44
43
 
45
44
  const AgentScopeSchema = StringEnum(["user", "project", "both"] as const, {
46
45
  description:
@@ -99,18 +98,15 @@ function sanitizeResultDetails(
99
98
  includeDebugMessages && (options?.includeMessages ?? true);
100
99
  const { messages, termination, progress, stderr, usage, ...core } = result;
101
100
  const { contextWindowTokens, ...usageBase } = usage;
102
-
103
101
  const sanitized: Record<string, unknown> = {
104
102
  ...core,
105
103
  stderr: includeDebugMessages ? stderr : "",
106
104
  usage: { ...usageBase },
107
105
  };
108
-
109
106
  if (contextWindowTokens !== undefined) {
110
107
  (sanitized.usage as Record<string, unknown>).contextWindowTokens =
111
108
  contextWindowTokens;
112
109
  }
113
-
114
110
  if (progress !== undefined) {
115
111
  const { activityText, lastToolPreview, ...progBase } = progress;
116
112
  sanitized.progress = {
@@ -122,14 +118,12 @@ function sanitizeResultDetails(
122
118
  ...(lastToolPreview !== undefined && { lastToolPreview }),
123
119
  };
124
120
  }
125
-
126
121
  if (includeMessages) {
127
122
  sanitized.messages = options?.recentMessages
128
123
  ? [...options.recentMessages]
129
124
  : messages !== undefined
130
125
  ? [...messages]
131
126
  : undefined;
132
-
133
127
  if (includeDebugMessages && termination !== undefined) {
134
128
  const { cancelReason, terminationSignal, fallbackCause, ...termBase } =
135
129
  termination;
@@ -141,7 +135,6 @@ function sanitizeResultDetails(
141
135
  };
142
136
  }
143
137
  }
144
-
145
138
  return sanitized as unknown as SingleResult;
146
139
  }
147
140
 
@@ -323,7 +316,7 @@ export async function startSubagentJob(
323
316
  hostSignal: AbortSignal | undefined,
324
317
  ): Promise<StartJobResult> {
325
318
  const agentScope: AgentScope = params.agentScope ?? "both";
326
- const discovery = getCachedAgentDiscovery(ctx.cwd, agentScope);
319
+ const discovery = await getCachedAgentDiscovery(ctx.cwd, agentScope);
327
320
  const agents = discovery.agents;
328
321
  const debug = params.debug === true;
329
322
  const makeDetails = createDetailsBuilder(
@@ -344,7 +337,7 @@ export async function startSubagentJob(
344
337
  if (!confirmed) return { kind: "cancelled", makeDetails };
345
338
  }
346
339
  if (requested.source === "project") {
347
- const userAgents = discoverAgents(ctx.cwd, "user");
340
+ const userAgents = await getCachedAgentDiscovery(ctx.cwd, "user");
348
341
  const hasUserCollision = userAgents.agents.some(
349
342
  (a) => a.name === requested.name,
350
343
  );
@@ -32,27 +32,6 @@ export function extractSemanticToolTarget(
32
32
  return "";
33
33
  }
34
34
 
35
- export function isTranscriptNoiseLine(line: string): boolean {
36
- return /^(?:(?:hello|hi|hey)(?:[!,.?:;]+|\s|$)|reasoning:|raw log:|apolog(?:y|ies)|sorry\b)/i.test(
37
- line,
38
- );
39
- }
40
-
41
- export function isFailureDiagnosticLine(line: string): boolean {
42
- return /^(?:at\s+|error:|failed:|failure:|exception:|traceback\b|caused by:)/i.test(
43
- line,
44
- );
45
- }
46
-
47
- export function filterOutputLines(output: string): string[] {
48
- return output
49
- .split(/\r?\n/)
50
- .map((l) => l.trim())
51
- .filter(
52
- (l) => l && !isTranscriptNoiseLine(l) && !isFailureDiagnosticLine(l),
53
- );
54
- }
55
-
56
35
  export function stripTerminalStatusPrefixes(value: string): string {
57
36
  return value.replace(/^(?:(?:success|failure):\s*)+/i, "");
58
37
  }
@@ -1,5 +1,5 @@
1
+ import type { SingleResult } from "../shared/types.js";
1
2
  import { normalizeTerminalSentence } from "./normalize.js";
2
- import type { SingleResult } from "./types.js";
3
3
 
4
4
  export const FEEDBACK_UI_SUMMARY_MAX_CHARS = 120;
5
5
 
@@ -20,7 +20,9 @@ const FEEDBACK_UI_LABEL_PATTERN =
20
20
  /^\s*(outcome|project summary|result|summary|status|output|message|error|check):\s*/i;
21
21
 
22
22
  export function formatSubagentResultForParent(result: SingleResult): string {
23
- return result.finalOutput;
23
+ return result.thinkingWarning
24
+ ? `[thinking] ${result.thinkingWarning}\n\n${result.finalOutput}`
25
+ : result.finalOutput;
24
26
  }
25
27
 
26
28
  export function summarizeFeedbackUiFinalOutput(finalOutput: string): string {
@@ -7,11 +7,7 @@ import {
7
7
  type MarkdownTheme,
8
8
  Text,
9
9
  } from "@earendil-works/pi-tui";
10
- import type { AgentScope } from "./agents.js";
11
- import {
12
- extractSemanticToolTarget,
13
- normalizeSummaryValue,
14
- } from "./normalize.js";
10
+ import type { AgentScope } from "../agent/agents.js";
15
11
  import {
16
12
  formatContextPercent,
17
13
  formatElapsed,
@@ -21,9 +17,13 @@ import {
21
17
  STATUS_ICON,
22
18
  type SubagentProgressState,
23
19
  type ThemeBg,
24
- } from "./progress-state.js";
25
- import { hasSubagentFailed } from "./result-details.js";
26
- import type { SubagentDetails, UsageStats } from "./types.js";
20
+ } from "../progress/progress-state.js";
21
+ import { hasSubagentFailed } from "../progress/result-details.js";
22
+ import type { SubagentDetails, UsageStats } from "../shared/types.js";
23
+ import {
24
+ extractSemanticToolTarget,
25
+ normalizeSummaryValue,
26
+ } from "./normalize.js";
27
27
 
28
28
  export type { ThemeBg };
29
29
 
@@ -103,32 +103,16 @@ export function formatUsageStats(
103
103
  return parts.join(" · ");
104
104
  }
105
105
 
106
- /**
107
- * Formats millisecond durations into human-readable time strings.
108
- */
109
- export function formatDuration(ms: number): string {
110
- if (ms < 1000) return `${Math.floor(ms)}ms`;
111
- if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
112
- const minutes = Math.floor(ms / 60000);
113
- const seconds = Math.floor((ms % 60000) / 1000);
114
- return `${minutes}m ${seconds.toString().padStart(2, "0")}s`;
115
- }
116
-
117
106
  /**
118
107
  * Formats the footer for subagent result cards, including model, context, turns, and cost.
119
108
  */
120
- export function formatResultFooter(
121
- usage: UsageStats,
122
- model?: string,
123
- durationMs?: number,
124
- ): string {
109
+ export function formatResultFooter(usage: UsageStats, model?: string): string {
125
110
  const parts: string[] = [];
126
111
  if (model) parts.push(model);
127
- if (usage.contextTokens && usage.contextTokens > 0)
128
- parts.push(`ctx:${formatTokens(usage.contextTokens)}`);
129
112
  if (usage.turns)
130
113
  parts.push(`${usage.turns} turn${usage.turns > 1 ? "s" : ""}`);
131
- if (typeof durationMs === "number") parts.push(formatDuration(durationMs));
114
+ if (usage.contextTokens && usage.contextTokens > 0)
115
+ parts.push(`ctx:${formatTokens(usage.contextTokens)}`);
132
116
  if (usage.cost) parts.push(`$${usage.cost.toFixed(4)}`);
133
117
  return parts.join(" · ");
134
118
  }
@@ -243,12 +227,20 @@ export function renderSubagentResult(
243
227
  const finalOutput = r.finalOutput ?? getFinalOutput(r.messages ?? []);
244
228
  const title = formatSubagentTitle(r.agent, r.instanceName, theme);
245
229
  const bodyText = stripOutcomeLineForResultUi(bodyOverride ?? finalOutput);
246
- const usageStr = formatResultFooter(r.usage, r.model, r.durationMs);
230
+ const toolCount = r.progress?.toolCalls?.length ?? 0;
231
+ const toolLabel = `${toolCount} ${toolCount === 1 ? "tool" : "tools"}`;
232
+ const ctxPercent = formatContextPercent({
233
+ contextTokens: r.usage.contextTokens,
234
+ contextWindowTokens: r.usage.contextWindowTokens,
235
+ } as SubagentProgressState);
236
+ const metadata = `${toolLabel} · ${ctxPercent} ctx · ${formatElapsed(r.durationMs ?? 0)}`;
237
+ const usageStr = formatResultFooter(r.usage, r.model);
247
238
  return renderStatusCard(
248
239
  {
249
240
  status: resultStatus,
250
241
  title,
251
242
  variant: "full",
243
+ metadata,
252
244
  body: bodyText,
253
245
  footer: usageStr,
254
246
  },
@@ -7,8 +7,8 @@ import {
7
7
  normalizeTerminalSentence,
8
8
  TOOL_PREVIEW_MAX_CHARS,
9
9
  truncateText,
10
- } from "./normalize.js";
11
- import type { SubagentDetails } from "./types.js";
10
+ } from "../output/normalize.js";
11
+ import type { SubagentDetails } from "../shared/types.js";
12
12
 
13
13
  export type ThemeBg = "toolPendingBg" | "toolSuccessBg" | "toolErrorBg";
14
14
 
@@ -18,6 +18,7 @@
18
18
 
19
19
  import type { Component } from "@earendil-works/pi-tui";
20
20
  import { Box, Text } from "@earendil-works/pi-tui";
21
+ import { formatSubagentTitle, type SubagentTheme } from "../output/ui.js";
21
22
  import {
22
23
  formatHeaderStats,
23
24
  getProgressState,
@@ -28,9 +29,8 @@ import {
28
29
  type SubagentProgressState,
29
30
  type ThemeBg,
30
31
  } from "./progress-state.js";
31
- import { formatSubagentTitle, type SubagentTheme } from "./ui.js";
32
32
 
33
- export { makeToolPreview } from "./normalize.js";
33
+ export { makeToolPreview } from "../output/normalize.js";
34
34
  export {
35
35
  cancelProgressState,
36
36
  clearProgressState,
@@ -1,19 +1,19 @@
1
- import { TOOL_RESULT_FAILED_MESSAGE } from "./process.js";
2
- import {
3
- extractProgressFromDetails,
4
- getProgressState,
5
- patchProgressState,
6
- } from "./progress.js";
1
+ import { TOOL_RESULT_FAILED_MESSAGE } from "../child/process.js";
7
2
  import {
8
3
  formatSubagentResultForParent,
9
4
  summarizeFeedbackUiFinalOutput,
10
- } from "./summary.js";
5
+ } from "../output/summary.js";
11
6
  import type {
12
7
  SingleResult,
13
8
  SubagentDetails,
14
9
  SubagentToolResult,
15
- } from "./types.js";
16
- import { detectMessageError } from "./utils.js";
10
+ } from "../shared/types.js";
11
+ import { detectMessageError } from "../shared/utils.js";
12
+ import {
13
+ extractProgressFromDetails,
14
+ getProgressState,
15
+ patchProgressState,
16
+ } from "./progress.js";
17
17
 
18
18
  export function hasSubagentFailed(result: SingleResult): boolean {
19
19
  return (
@@ -0,0 +1,117 @@
1
+ const DEFAULT_ADJECTIVES = [
2
+ "able",
3
+ "agile",
4
+ "alert",
5
+ "amber",
6
+ "ample",
7
+ "apt",
8
+ "arctic",
9
+ "avid",
10
+ "bold",
11
+ "brave",
12
+ "bright",
13
+ "brisk",
14
+ "calm",
15
+ "clever",
16
+ "cosmic",
17
+ "crisp",
18
+ "daring",
19
+ "dawn",
20
+ "eager",
21
+ "early",
22
+ "fair",
23
+ "fast",
24
+ "fierce",
25
+ "fine",
26
+ "fresh",
27
+ "gentle",
28
+ "golden",
29
+ "grand",
30
+ "happy",
31
+ "honest",
32
+ "jolly",
33
+ "keen",
34
+ "kind",
35
+ "lively",
36
+ "lucky",
37
+ "merry",
38
+ "mighty",
39
+ "nimble",
40
+ "noble",
41
+ "novel",
42
+ "patient",
43
+ "proud",
44
+ "quick",
45
+ "quiet",
46
+ "rapid",
47
+ "ready",
48
+ "sharp",
49
+ "smart",
50
+ "solid",
51
+ "steady",
52
+ "swift",
53
+ "tidy",
54
+ "vivid",
55
+ "warm",
56
+ "wise",
57
+ ] as const;
58
+
59
+ const DEFAULT_NOUNS = [
60
+ "badger",
61
+ "beacon",
62
+ "bison",
63
+ "brook",
64
+ "cedar",
65
+ "comet",
66
+ "coral",
67
+ "coyote",
68
+ "crane",
69
+ "dolphin",
70
+ "eagle",
71
+ "ember",
72
+ "falcon",
73
+ "finch",
74
+ "forest",
75
+ "fox",
76
+ "gecko",
77
+ "glade",
78
+ "harbor",
79
+ "hawk",
80
+ "heron",
81
+ "island",
82
+ "jaguar",
83
+ "koala",
84
+ "lagoon",
85
+ "lemur",
86
+ "lynx",
87
+ "maple",
88
+ "meadow",
89
+ "otter",
90
+ "panda",
91
+ "panther",
92
+ "pelican",
93
+ "phoenix",
94
+ "puma",
95
+ "raven",
96
+ "reef",
97
+ "river",
98
+ "salmon",
99
+ "sparrow",
100
+ "summit",
101
+ "tiger",
102
+ "valley",
103
+ "violet",
104
+ "walrus",
105
+ "willow",
106
+ "wolf",
107
+ "wren",
108
+ "yak",
109
+ "zephyr",
110
+ ] as const;
111
+
112
+ export function generateSubagentInstanceName(): string {
113
+ const adj =
114
+ DEFAULT_ADJECTIVES[Math.floor(Math.random() * DEFAULT_ADJECTIVES.length)];
115
+ const noun = DEFAULT_NOUNS[Math.floor(Math.random() * DEFAULT_NOUNS.length)];
116
+ return `${adj}-${noun}`;
117
+ }
@@ -1,6 +1,6 @@
1
1
  import type { Message } from "@earendil-works/pi-ai";
2
- import type { AgentScope } from "./agents.js";
3
- import type { TerminationMetadata } from "./termination.js";
2
+ import type { AgentScope } from "../agent/agents.js";
3
+ import type { TerminationMetadata } from "../child/termination.js";
4
4
 
5
5
  export interface UsageStats {
6
6
  input: number;
@@ -40,6 +40,7 @@ export interface SingleResult {
40
40
  progress?: StreamingProgress;
41
41
  messages?: Message[];
42
42
  termination?: TerminationMetadata;
43
+ thinkingWarning?: string;
43
44
  }
44
45
 
45
46
  export interface SubagentDetails {
@@ -5,7 +5,6 @@ import type { Message } from "@earendil-works/pi-ai";
5
5
  import {
6
6
  DefaultResourceLoader,
7
7
  getAgentDir,
8
- withFileMutationQueue,
9
8
  } from "@earendil-works/pi-coding-agent";
10
9
 
11
10
  export const DEFAULT_MAX_OUTPUT_BYTES = 50_000;
@@ -66,11 +65,10 @@ export async function writePromptToTempFile(
66
65
  );
67
66
  const safeName = agentName.replace(/[^\w.-]+/g, "_");
68
67
  const filePath = path.join(tmpDir, `prompt-${safeName}.md`);
69
- await withFileMutationQueue(filePath, async () => {
70
- await fs.promises.writeFile(filePath, prompt, {
71
- encoding: "utf-8",
72
- mode: 0o600,
73
- });
68
+ // mkdtemp guarantees a unique directory per call; no concurrent writer can hold this path.
69
+ await fs.promises.writeFile(filePath, prompt, {
70
+ encoding: "utf-8",
71
+ mode: 0o600,
74
72
  });
75
73
  return { dir: tmpDir, filePath };
76
74
  }
@@ -1,164 +0,0 @@
1
- const DEFAULT_ADJECTIVES = [
2
- "able",
3
- "agile",
4
- "alert",
5
- "amber",
6
- "ample",
7
- "apt",
8
- "arctic",
9
- "avid",
10
- "bold",
11
- "brave",
12
- "bright",
13
- "brisk",
14
- "calm",
15
- "clever",
16
- "cosmic",
17
- "crisp",
18
- "daring",
19
- "dawn",
20
- "eager",
21
- "early",
22
- "fair",
23
- "fast",
24
- "fierce",
25
- "fine",
26
- "fresh",
27
- "gentle",
28
- "golden",
29
- "grand",
30
- "happy",
31
- "honest",
32
- "jolly",
33
- "keen",
34
- "kind",
35
- "lively",
36
- "lucky",
37
- "merry",
38
- "mighty",
39
- "nimble",
40
- "noble",
41
- "novel",
42
- "patient",
43
- "proud",
44
- "quick",
45
- "quiet",
46
- "rapid",
47
- "ready",
48
- "sharp",
49
- "smart",
50
- "solid",
51
- "steady",
52
- "swift",
53
- "tidy",
54
- "vivid",
55
- "warm",
56
- "wise",
57
- ] as const;
58
-
59
- const DEFAULT_NOUNS = [
60
- "badger",
61
- "beacon",
62
- "bison",
63
- "brook",
64
- "cedar",
65
- "comet",
66
- "coral",
67
- "coyote",
68
- "crane",
69
- "dolphin",
70
- "eagle",
71
- "ember",
72
- "falcon",
73
- "finch",
74
- "forest",
75
- "fox",
76
- "gecko",
77
- "glade",
78
- "harbor",
79
- "hawk",
80
- "heron",
81
- "island",
82
- "jaguar",
83
- "koala",
84
- "lagoon",
85
- "lemur",
86
- "lynx",
87
- "maple",
88
- "meadow",
89
- "otter",
90
- "panda",
91
- "panther",
92
- "pelican",
93
- "phoenix",
94
- "puma",
95
- "raven",
96
- "reef",
97
- "river",
98
- "salmon",
99
- "sparrow",
100
- "summit",
101
- "tiger",
102
- "valley",
103
- "violet",
104
- "walrus",
105
- "willow",
106
- "wolf",
107
- "wren",
108
- "yak",
109
- "zephyr",
110
- ] as const;
111
-
112
- const usedInstanceNames = new Set<string>();
113
-
114
- let adjectives: readonly string[] = DEFAULT_ADJECTIVES;
115
- let nouns: readonly string[] = DEFAULT_NOUNS;
116
- let randomSource: () => number = Math.random;
117
-
118
- function normalizeRandomIndex(limit: number): number {
119
- const value = randomSource();
120
- if (!Number.isFinite(value)) return 0;
121
- return Math.min(limit - 1, Math.max(0, Math.floor(value * limit)));
122
- }
123
-
124
- function nameAt(index: number): string {
125
- const adjective = adjectives[Math.floor(index / nouns.length)];
126
- const noun = nouns[index % nouns.length];
127
- return `${adjective}-${noun}`;
128
- }
129
-
130
- export function generateSubagentInstanceName(): string {
131
- const capacity = adjectives.length * nouns.length;
132
- if (usedInstanceNames.size >= capacity) {
133
- throw new Error(
134
- "No unused subagent instance names remain for this session.",
135
- );
136
- }
137
- const start = normalizeRandomIndex(capacity);
138
- for (let offset = 0; offset < capacity; offset += 1) {
139
- const candidate = nameAt((start + offset) % capacity);
140
- if (!usedInstanceNames.has(candidate)) {
141
- usedInstanceNames.add(candidate);
142
- return candidate;
143
- }
144
- }
145
- throw new Error("No unused subagent instance names remain for this session.");
146
- }
147
-
148
- export function resetSubagentInstanceNamesForTest() {
149
- usedInstanceNames.clear();
150
- adjectives = DEFAULT_ADJECTIVES;
151
- nouns = DEFAULT_NOUNS;
152
- randomSource = Math.random;
153
- }
154
-
155
- export function configureSubagentInstanceNamesForTest(options: {
156
- adjectives?: readonly string[];
157
- nouns?: readonly string[];
158
- randomSource?: () => number;
159
- }) {
160
- usedInstanceNames.clear();
161
- adjectives = options.adjectives ?? DEFAULT_ADJECTIVES;
162
- nouns = options.nouns ?? DEFAULT_NOUNS;
163
- randomSource = options.randomSource ?? Math.random;
164
- }
File without changes
File without changes