@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 +13 -11
- package/src/{agent-cache.ts → agent/agent-cache.ts} +9 -9
- package/src/{agents.ts → agent/agents.ts} +81 -63
- package/src/{process.ts → child/process.ts} +99 -43
- package/src/{prompt-contract.ts → child/prompt-contract.ts} +2 -0
- package/src/index.ts +9 -9
- package/src/{jobs-command.ts → orchestration/jobs-command.ts} +2 -3
- package/src/{run.ts → orchestration/run.ts} +2 -2
- package/src/{subagent-orchestrator.ts → orchestration/subagent-orchestrator.ts} +19 -26
- package/src/{normalize.ts → output/normalize.ts} +0 -21
- package/src/{summary.ts → output/summary.ts} +4 -2
- package/src/{ui.ts → output/ui.ts} +20 -28
- package/src/{progress-state.ts → progress/progress-state.ts} +2 -2
- package/src/{progress.ts → progress/progress.ts} +2 -2
- package/src/{result-details.ts → progress/result-details.ts} +9 -9
- package/src/shared/instance-name.ts +117 -0
- package/src/{types.ts → shared/types.ts} +3 -2
- package/src/{utils.ts → shared/utils.ts} +4 -6
- package/src/instance-name.ts +0 -164
- /package/src/{child-events.ts → child/child-events.ts} +0 -0
- /package/src/{termination.ts → child/termination.ts} +0 -0
- /package/src/{cancel-command.ts → orchestration/cancel-command.ts} +0 -0
- /package/src/{run-command.ts → orchestration/run-command.ts} +0 -0
- /package/src/{run-registry.ts → orchestration/run-registry.ts} +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mystilleef/pi-subagent",
|
|
3
|
-
"version": "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
|
-
"
|
|
51
|
-
"
|
|
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.
|
|
67
|
-
"@earendil-works/pi-agent-core": "^0.
|
|
68
|
-
"@earendil-works/pi-ai": "^0.
|
|
69
|
-
"@earendil-works/pi-coding-agent": "^0.
|
|
70
|
-
"@earendil-works/pi-tui": "^0.
|
|
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.
|
|
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
|
-
|
|
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 =
|
|
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 = { ...
|
|
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
|
-
.
|
|
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
|
|
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
|
|
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 (!
|
|
67
|
-
|
|
68
|
-
}
|
|
69
|
-
let entries: fs.Dirent[];
|
|
124
|
+
if (!(await isDirectoryAsync(dir))) return agents;
|
|
125
|
+
let entries: Dirent[];
|
|
70
126
|
try {
|
|
71
|
-
entries =
|
|
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 =
|
|
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
|
|
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
|
|
147
|
+
async function isDirectoryAsync(p: string): Promise<boolean> {
|
|
129
148
|
try {
|
|
130
|
-
return
|
|
149
|
+
return (await fsPromises.stat(p)).isDirectory();
|
|
131
150
|
} catch {
|
|
132
151
|
return false;
|
|
133
152
|
}
|
|
134
153
|
}
|
|
135
154
|
|
|
136
|
-
function
|
|
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 (
|
|
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
|
|
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 =
|
|
153
|
-
const userAgents =
|
|
154
|
-
scope === "project" ? [] :
|
|
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
|
-
:
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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 "
|
|
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 "
|
|
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
|
|
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
|
|
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
|
|
591
|
-
|
|
592
|
-
|
|
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
|
-
|
|
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 {
|
|
12
|
-
import {
|
|
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:
|
|
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 "
|
|
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
|
|
2
|
-
import {
|
|
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 "
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
} from "
|
|
14
|
-
import {
|
|
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 "
|
|
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 "
|
|
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 =
|
|
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.
|
|
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 "
|
|
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 "
|
|
25
|
-
import { hasSubagentFailed } from "
|
|
26
|
-
import type { SubagentDetails, UsageStats } from "
|
|
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 (
|
|
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
|
|
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 "
|
|
11
|
-
import type { SubagentDetails } from "
|
|
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 "
|
|
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 "
|
|
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 "
|
|
5
|
+
} from "../output/summary.js";
|
|
11
6
|
import type {
|
|
12
7
|
SingleResult,
|
|
13
8
|
SubagentDetails,
|
|
14
9
|
SubagentToolResult,
|
|
15
|
-
} from "
|
|
16
|
-
import { detectMessageError } from "
|
|
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 "
|
|
3
|
-
import type { TerminationMetadata } from "
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
}
|
package/src/instance-name.ts
DELETED
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|