@mystilleef/pi-subagent 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +14 -12
- 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} +21 -45
- 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
|
-
"@types/node": "^25.
|
|
73
|
-
"typebox": "^1.1.
|
|
74
|
+
"@types/node": "^25.9.1",
|
|
75
|
+
"typebox": "^1.1.39",
|
|
74
76
|
"typescript": "^6.0.3"
|
|
75
77
|
}
|
|
76
78
|
}
|
|
@@ -2,40 +2,40 @@ import path from "node:path";
|
|
|
2
2
|
import {
|
|
3
3
|
type AgentDiscoveryResult,
|
|
4
4
|
type AgentScope,
|
|
5
|
-
|
|
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.
|