@duckmind/dm-darwin-x64 0.13.2 → 0.13.6
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/CHANGELOG.md +49 -2
- package/README.md +5 -5
- package/dm +0 -0
- package/docs/settings.md +1 -0
- package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/custom-provider-qwen-cli/package.json +1 -1
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +1 -1
- package/extensions/.dm-extensions.json +2 -2
- package/extensions/dm-multicodex/README.md +3 -1
- package/extensions/dm-multicodex/account-manager.test.ts +3 -1
- package/extensions/dm-multicodex/account-manager.ts +27 -1
- package/extensions/dm-multicodex/commands.test.ts +1 -0
- package/extensions/dm-multicodex/commands.ts +81 -1
- package/extensions/dm-multicodex/index.ts +7 -0
- package/extensions/dm-multicodex/node_modules/.package-lock.json +28 -28
- package/extensions/dm-multicodex/package-lock.json +9633 -9633
- package/extensions/dm-multicodex/package.json +56 -56
- package/extensions/dm-multicodex/storage.ts +2 -2
- package/extensions/dm-multicodex/sync.test.ts +114 -0
- package/extensions/dm-multicodex/sync.ts +249 -0
- package/extensions/dm-multicodex/tsconfig.json +17 -0
- package/extensions/dm-subagents/artifacts.ts +11 -5
- package/extensions/dm-subagents/async-execution.ts +6 -1
- package/extensions/dm-subagents/execution.ts +1 -1
- package/extensions/dm-subagents/index.ts +1 -1
- package/extensions/dm-subagents/intercom-bridge.ts +8 -0
- package/extensions/dm-subagents/package.json +1 -1
- package/extensions/dm-subagents/schemas.ts +1 -1
- package/extensions/dm-subagents/settings.ts +6 -4
- package/extensions/dm-subagents/skills.ts +117 -25
- package/extensions/dm-subagents/subagent-executor.ts +2 -6
- package/extensions/dm-subagents/subagent-runner.ts +176 -51
- package/extensions/dm-subagents/types.ts +62 -2
- package/extensions/dm-subagents/worktree.ts +27 -9
- package/extensions/dm-thinking-timer/README.md +1 -1
- package/extensions/dm-ultrathink/src/naming.ts +151 -10
- package/package.json +1 -1
- package/theme/duckmind.json +77 -65
|
@@ -3,12 +3,10 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import * as fs from "node:fs";
|
|
6
|
-
import * as os from "node:os";
|
|
7
6
|
import * as path from "node:path";
|
|
8
7
|
import type { AgentConfig } from "./agents.ts";
|
|
9
8
|
import { normalizeSkillInput } from "./skills.ts";
|
|
10
|
-
|
|
11
|
-
const CHAIN_RUNS_DIR = path.join(os.tmpdir(), "dm-chain-runs");
|
|
9
|
+
import { CHAIN_RUNS_DIR } from "./types.ts";
|
|
12
10
|
const CHAIN_DIR_MAX_AGE_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
13
11
|
|
|
14
12
|
// =============================================================================
|
|
@@ -100,7 +98,9 @@ export function createChainDir(runId: string, baseDir?: string): string {
|
|
|
100
98
|
export function removeChainDir(chainDir: string): void {
|
|
101
99
|
try {
|
|
102
100
|
fs.rmSync(chainDir, { recursive: true });
|
|
103
|
-
} catch {
|
|
101
|
+
} catch {
|
|
102
|
+
// Chain cleanup is best-effort. Runs can already have cleaned their temp dir.
|
|
103
|
+
}
|
|
104
104
|
}
|
|
105
105
|
|
|
106
106
|
export function cleanupOldChainDirs(): void {
|
|
@@ -110,6 +110,8 @@ export function cleanupOldChainDirs(): void {
|
|
|
110
110
|
try {
|
|
111
111
|
dirs = fs.readdirSync(CHAIN_RUNS_DIR);
|
|
112
112
|
} catch {
|
|
113
|
+
// Startup cleanup is best-effort. If the scoped temp root is unreadable,
|
|
114
|
+
// skip cleanup instead of failing extension startup.
|
|
113
115
|
return;
|
|
114
116
|
}
|
|
115
117
|
|
|
@@ -6,7 +6,6 @@ import { execSync } from "node:child_process";
|
|
|
6
6
|
import * as fs from "node:fs";
|
|
7
7
|
import * as os from "node:os";
|
|
8
8
|
import * as path from "node:path";
|
|
9
|
-
import { loadSkills, type Skill } from "@mariozechner/pi-coding-agent";
|
|
10
9
|
|
|
11
10
|
export type SkillSource =
|
|
12
11
|
| "project"
|
|
@@ -86,6 +85,7 @@ function getPackageSkillPaths(packageRoot: string): string[] {
|
|
|
86
85
|
.filter((s: unknown) => typeof s === "string")
|
|
87
86
|
.map((s: string) => path.resolve(packageRoot, s));
|
|
88
87
|
} catch {
|
|
88
|
+
// Package scanning is opportunistic; ignore malformed/missing package metadata.
|
|
89
89
|
return [];
|
|
90
90
|
}
|
|
91
91
|
}
|
|
@@ -98,6 +98,7 @@ function getGlobalNpmRoot(): string | null {
|
|
|
98
98
|
cachedGlobalNpmRoot = execSync("npm root -g", { encoding: "utf-8", timeout: 5000 }).trim();
|
|
99
99
|
return cachedGlobalNpmRoot;
|
|
100
100
|
} catch {
|
|
101
|
+
// Global npm root is optional in constrained environments.
|
|
101
102
|
cachedGlobalNpmRoot = ""; // Empty string means "tried but failed"
|
|
102
103
|
return null;
|
|
103
104
|
}
|
|
@@ -123,6 +124,7 @@ function collectPackageSkillPaths(cwd: string): string[] {
|
|
|
123
124
|
try {
|
|
124
125
|
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
125
126
|
} catch {
|
|
127
|
+
// Ignore unreadable package roots and continue scanning other roots.
|
|
126
128
|
continue;
|
|
127
129
|
}
|
|
128
130
|
|
|
@@ -136,6 +138,7 @@ function collectPackageSkillPaths(cwd: string): string[] {
|
|
|
136
138
|
try {
|
|
137
139
|
scopeEntries = fs.readdirSync(scopeDir, { withFileTypes: true });
|
|
138
140
|
} catch {
|
|
141
|
+
// Ignore unreadable scoped package directories and continue.
|
|
139
142
|
continue;
|
|
140
143
|
}
|
|
141
144
|
for (const scopeEntry of scopeEntries) {
|
|
@@ -178,7 +181,9 @@ function collectSettingsSkillPaths(cwd: string): string[] {
|
|
|
178
181
|
}
|
|
179
182
|
results.push(resolved);
|
|
180
183
|
}
|
|
181
|
-
} catch {
|
|
184
|
+
} catch {
|
|
185
|
+
// Settings-provided skills are optional; ignore malformed or missing settings files.
|
|
186
|
+
}
|
|
182
187
|
}
|
|
183
188
|
|
|
184
189
|
return results;
|
|
@@ -196,21 +201,22 @@ function buildSkillPaths(cwd: string): string[] {
|
|
|
196
201
|
return [...new Set([...defaultSkillPaths, ...packagePaths, ...settingsPaths])];
|
|
197
202
|
}
|
|
198
203
|
|
|
199
|
-
function inferSkillSource(
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
+
function inferSkillSource(filePath: string, cwd: string): SkillSource {
|
|
205
|
+
const projectConfigRoot = path.resolve(cwd, CONFIG_DIR);
|
|
206
|
+
const projectSkillsRoot = path.resolve(cwd, CONFIG_DIR, "skills");
|
|
207
|
+
const projectPackagesRoot = path.resolve(cwd, CONFIG_DIR, "npm", "node_modules");
|
|
208
|
+
const projectAgentsRoot = path.resolve(cwd, ".agents");
|
|
209
|
+
const userSkillsRoot = path.resolve(AGENT_DIR, "skills");
|
|
210
|
+
const userPackagesRoot = path.resolve(AGENT_DIR, "npm", "node_modules");
|
|
211
|
+
const userAgentsRoot = path.resolve(os.homedir(), ".agents");
|
|
204
212
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
const altProjectRoot = path.resolve(cwd, ".agents");
|
|
209
|
-
const isProjectScoped = isWithinPath(filePath, projectRoot) || isWithinPath(filePath, altProjectRoot);
|
|
210
|
-
if (isProjectScoped) return "project";
|
|
213
|
+
if (isWithinPath(filePath, projectPackagesRoot)) return "project-package";
|
|
214
|
+
if (isWithinPath(filePath, projectSkillsRoot) || isWithinPath(filePath, projectAgentsRoot)) return "project";
|
|
215
|
+
if (isWithinPath(filePath, projectConfigRoot)) return "project-settings";
|
|
211
216
|
|
|
212
|
-
|
|
213
|
-
if (
|
|
217
|
+
if (isWithinPath(filePath, userPackagesRoot)) return "user-package";
|
|
218
|
+
if (isWithinPath(filePath, userSkillsRoot) || isWithinPath(filePath, userAgentsRoot)) return "user";
|
|
219
|
+
if (isWithinPath(filePath, AGENT_DIR)) return "user-settings";
|
|
214
220
|
|
|
215
221
|
const globalRoot = getGlobalNpmRoot();
|
|
216
222
|
if (globalRoot && isWithinPath(filePath, globalRoot)) return "user-package";
|
|
@@ -227,6 +233,99 @@ function chooseHigherPrioritySkill(existing: CachedSkillEntry | undefined, candi
|
|
|
227
233
|
return candidate.order < existing.order ? candidate : existing;
|
|
228
234
|
}
|
|
229
235
|
|
|
236
|
+
function maybeReadSkillDescription(filePath: string): string | undefined {
|
|
237
|
+
try {
|
|
238
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
239
|
+
const normalized = content.replace(/\r\n/g, "\n");
|
|
240
|
+
if (!normalized.startsWith("---")) return undefined;
|
|
241
|
+
|
|
242
|
+
const endIndex = normalized.indexOf("\n---", 3);
|
|
243
|
+
if (endIndex === -1) return undefined;
|
|
244
|
+
|
|
245
|
+
const frontmatter = normalized.slice(3, endIndex).trim();
|
|
246
|
+
const match = frontmatter.match(/^description:\s*(.+)$/m);
|
|
247
|
+
if (!match) return undefined;
|
|
248
|
+
return match[1]?.trim().replace(/^['\"]|['\"]$/g, "");
|
|
249
|
+
} catch {
|
|
250
|
+
// Description parsing is best-effort metadata extraction.
|
|
251
|
+
return undefined;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function collectFilesystemSkills(cwd: string, skillPaths: string[]): CachedSkillEntry[] {
|
|
256
|
+
const entries: CachedSkillEntry[] = [];
|
|
257
|
+
const seen = new Set<string>();
|
|
258
|
+
let order = 0;
|
|
259
|
+
|
|
260
|
+
const pushEntry = (name: string, filePath: string) => {
|
|
261
|
+
const resolvedFile = path.resolve(filePath);
|
|
262
|
+
if (seen.has(resolvedFile)) return;
|
|
263
|
+
if (!fs.existsSync(resolvedFile)) return;
|
|
264
|
+
seen.add(resolvedFile);
|
|
265
|
+
entries.push({
|
|
266
|
+
name,
|
|
267
|
+
filePath: resolvedFile,
|
|
268
|
+
source: inferSkillSource(resolvedFile, cwd),
|
|
269
|
+
description: maybeReadSkillDescription(resolvedFile),
|
|
270
|
+
order: order++,
|
|
271
|
+
});
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
for (const skillPath of skillPaths) {
|
|
275
|
+
if (!fs.existsSync(skillPath)) continue;
|
|
276
|
+
|
|
277
|
+
let stat: fs.Stats;
|
|
278
|
+
try {
|
|
279
|
+
stat = fs.statSync(skillPath);
|
|
280
|
+
} catch {
|
|
281
|
+
// Ignore paths that disappear or become unreadable during discovery.
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (stat.isFile()) {
|
|
286
|
+
const fileName = path.basename(skillPath);
|
|
287
|
+
if (!fileName.toLowerCase().endsWith(".md")) continue;
|
|
288
|
+
const skillName = fileName.toLowerCase() === "skill.md"
|
|
289
|
+
? path.basename(path.dirname(skillPath))
|
|
290
|
+
: path.basename(fileName, path.extname(fileName));
|
|
291
|
+
pushEntry(skillName, skillPath);
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (!stat.isDirectory()) continue;
|
|
296
|
+
|
|
297
|
+
const rootSkillFile = path.join(skillPath, "SKILL.md");
|
|
298
|
+
if (fs.existsSync(rootSkillFile)) {
|
|
299
|
+
pushEntry(path.basename(skillPath), rootSkillFile);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
let childEntries: fs.Dirent[];
|
|
303
|
+
try {
|
|
304
|
+
childEntries = fs.readdirSync(skillPath, { withFileTypes: true });
|
|
305
|
+
} catch {
|
|
306
|
+
// Ignore unreadable skill directories and continue scanning.
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
for (const child of childEntries) {
|
|
311
|
+
if (child.name.startsWith(".")) continue;
|
|
312
|
+
const childPath = path.join(skillPath, child.name);
|
|
313
|
+
if (child.isDirectory() || child.isSymbolicLink()) {
|
|
314
|
+
const nestedSkillPath = path.join(childPath, "SKILL.md");
|
|
315
|
+
if (fs.existsSync(nestedSkillPath)) {
|
|
316
|
+
pushEntry(child.name, nestedSkillPath);
|
|
317
|
+
}
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
if (child.isFile() && child.name.toLowerCase().endsWith(".md")) {
|
|
321
|
+
pushEntry(path.basename(child.name, path.extname(child.name)), childPath);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return entries;
|
|
327
|
+
}
|
|
328
|
+
|
|
230
329
|
function getCachedSkills(cwd: string): CachedSkillEntry[] {
|
|
231
330
|
const now = Date.now();
|
|
232
331
|
if (loadSkillsCache && loadSkillsCache.cwd === cwd && now - loadSkillsCache.timestamp < LOAD_SKILLS_CACHE_TTL_MS) {
|
|
@@ -234,18 +333,10 @@ function getCachedSkills(cwd: string): CachedSkillEntry[] {
|
|
|
234
333
|
}
|
|
235
334
|
|
|
236
335
|
const skillPaths = buildSkillPaths(cwd);
|
|
237
|
-
const loaded =
|
|
336
|
+
const loaded = collectFilesystemSkills(cwd, skillPaths);
|
|
238
337
|
const dedupedByName = new Map<string, CachedSkillEntry>();
|
|
239
338
|
|
|
240
|
-
for (
|
|
241
|
-
const skill = loaded.skills[i] as Skill;
|
|
242
|
-
const entry: CachedSkillEntry = {
|
|
243
|
-
name: skill.name,
|
|
244
|
-
filePath: skill.filePath,
|
|
245
|
-
source: inferSkillSource(skill.sourceInfo, skill.filePath, cwd),
|
|
246
|
-
description: skill.description,
|
|
247
|
-
order: i,
|
|
248
|
-
};
|
|
339
|
+
for (const entry of loaded) {
|
|
249
340
|
const current = dedupedByName.get(entry.name);
|
|
250
341
|
dedupedByName.set(entry.name, chooseHigherPrioritySkill(current, entry));
|
|
251
342
|
}
|
|
@@ -294,6 +385,7 @@ export function readSkill(
|
|
|
294
385
|
|
|
295
386
|
return skill;
|
|
296
387
|
} catch {
|
|
388
|
+
// Treat unreadable skill files as unresolved so callers can surface as missing.
|
|
297
389
|
return undefined;
|
|
298
390
|
}
|
|
299
391
|
}
|
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
import { discoverAvailableSkills, normalizeSkillInput } from "./skills.ts";
|
|
23
23
|
import { executeAsyncChain, executeAsyncSingle, isAsyncAvailable } from "./async-execution.ts";
|
|
24
24
|
import { createForkContextResolver } from "./fork-context.ts";
|
|
25
|
-
import { applyIntercomBridgeToAgent, resolveIntercomBridge } from "./intercom-bridge.ts";
|
|
25
|
+
import { applyIntercomBridgeToAgent, resolveIntercomBridge, resolveIntercomSessionTarget } from "./intercom-bridge.ts";
|
|
26
26
|
import { finalizeSingleOutput, injectSingleOutputInstruction, resolveSingleOutputPath } from "./single-output.ts";
|
|
27
27
|
import { getSingleResultOutput, mapConcurrent } from "./utils.ts";
|
|
28
28
|
import {
|
|
@@ -1117,11 +1117,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
1117
1117
|
const parentSessionFile = ctx.sessionManager.getSessionFile() ?? null;
|
|
1118
1118
|
deps.state.currentSessionId = parentSessionFile ?? `session-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
1119
1119
|
const discoveredAgents = deps.discoverAgents(ctx.cwd, scope).agents;
|
|
1120
|
-
|
|
1121
|
-
if (!sessionName) {
|
|
1122
|
-
sessionName = `session-${ctx.sessionManager.getSessionId().slice(0, 8)}`;
|
|
1123
|
-
deps.pi.setSessionName(sessionName);
|
|
1124
|
-
}
|
|
1120
|
+
const sessionName = resolveIntercomSessionTarget(deps.pi.getSessionName(), ctx.sessionManager.getSessionId());
|
|
1125
1121
|
const intercomBridge = resolveIntercomBridge({
|
|
1126
1122
|
config: deps.config.intercomBridge,
|
|
1127
1123
|
context: normalizedParams.context,
|
|
@@ -3,6 +3,7 @@ import * as fs from "node:fs";
|
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
4
|
import * as path from "node:path";
|
|
5
5
|
import { pathToFileURL } from "node:url";
|
|
6
|
+
import type { Message } from "@mariozechner/pi-ai";
|
|
6
7
|
import { appendJsonl, getArtifactPaths } from "./artifacts.ts";
|
|
7
8
|
import { getPiSpawnCommand } from "./pi-spawn.ts";
|
|
8
9
|
import { captureSingleOutputSnapshot, resolveSingleOutput } from "./single-output.ts";
|
|
@@ -27,6 +28,7 @@ import {
|
|
|
27
28
|
} from "./parallel-utils.ts";
|
|
28
29
|
import { buildPiArgs, cleanupTempDir } from "./pi-args.ts";
|
|
29
30
|
import { formatModelAttemptNote, isRetryableModelFailure } from "./model-fallback.ts";
|
|
31
|
+
import { detectSubagentError, extractTextFromContent, extractToolArgsPreview, getFinalOutput } from "./utils.ts";
|
|
30
32
|
import {
|
|
31
33
|
cleanupWorktrees,
|
|
32
34
|
createWorktrees,
|
|
@@ -53,6 +55,7 @@ interface SubagentRunConfig {
|
|
|
53
55
|
asyncDir: string;
|
|
54
56
|
sessionId?: string | null;
|
|
55
57
|
piPackageRoot?: string;
|
|
58
|
+
piArgv1?: string;
|
|
56
59
|
worktreeSetupHook?: string;
|
|
57
60
|
worktreeSetupHookTimeoutMs?: number;
|
|
58
61
|
}
|
|
@@ -122,32 +125,44 @@ function emptyUsage(): Usage {
|
|
|
122
125
|
return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 };
|
|
123
126
|
}
|
|
124
127
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
128
|
+
interface ChildEventContext {
|
|
129
|
+
eventsPath: string;
|
|
130
|
+
runId: string;
|
|
131
|
+
stepIndex: number;
|
|
132
|
+
agent: string;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
interface ChildUsage {
|
|
136
|
+
input?: number;
|
|
137
|
+
inputTokens?: number;
|
|
138
|
+
output?: number;
|
|
139
|
+
outputTokens?: number;
|
|
140
|
+
cacheRead?: number;
|
|
141
|
+
cacheWrite?: number;
|
|
142
|
+
cost?: { total?: number };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
type ChildMessage = Message & {
|
|
146
|
+
model?: string;
|
|
147
|
+
errorMessage?: string;
|
|
148
|
+
usage?: ChildUsage;
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
interface ChildEvent {
|
|
152
|
+
type?: string;
|
|
153
|
+
message?: ChildMessage;
|
|
154
|
+
toolName?: string;
|
|
155
|
+
args?: Record<string, unknown>;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
interface RunPiStreamingResult {
|
|
159
|
+
stderr: string;
|
|
160
|
+
exitCode: number | null;
|
|
161
|
+
messages: Message[];
|
|
162
|
+
usage: Usage;
|
|
163
|
+
model?: string;
|
|
164
|
+
error?: string;
|
|
165
|
+
finalOutput: string;
|
|
151
166
|
}
|
|
152
167
|
|
|
153
168
|
function runPiStreaming(
|
|
@@ -156,36 +171,131 @@ function runPiStreaming(
|
|
|
156
171
|
outputFile: string,
|
|
157
172
|
env?: Record<string, string | undefined>,
|
|
158
173
|
piPackageRoot?: string,
|
|
174
|
+
piArgv1?: string,
|
|
159
175
|
maxSubagentDepth?: number,
|
|
160
|
-
|
|
176
|
+
childEventContext?: ChildEventContext,
|
|
177
|
+
): Promise<RunPiStreamingResult> {
|
|
161
178
|
return new Promise((resolve) => {
|
|
162
179
|
const outputStream = fs.createWriteStream(outputFile, { flags: "w" });
|
|
163
180
|
const spawnEnv = { ...process.env, ...(env ?? {}), ...getSubagentDepthEnv(maxSubagentDepth) };
|
|
164
|
-
const spawnSpec = getPiSpawnCommand(args,
|
|
181
|
+
const spawnSpec = getPiSpawnCommand(args, {
|
|
182
|
+
...(piPackageRoot ? { piPackageRoot } : {}),
|
|
183
|
+
...(piArgv1 ? { argv1: piArgv1 } : {}),
|
|
184
|
+
});
|
|
165
185
|
const child = spawn(spawnSpec.command, spawnSpec.args, { cwd, stdio: ["ignore", "pipe", "pipe"], env: spawnEnv });
|
|
166
|
-
let stdout = "";
|
|
167
186
|
let stderr = "";
|
|
187
|
+
let stdoutBuf = "";
|
|
188
|
+
let stderrBuf = "";
|
|
189
|
+
const messages: Message[] = [];
|
|
190
|
+
const usage = emptyUsage();
|
|
191
|
+
let model: string | undefined;
|
|
192
|
+
let error: string | undefined;
|
|
193
|
+
const rawStdoutLines: string[] = [];
|
|
194
|
+
|
|
195
|
+
const writeOutputLine = (line: string) => {
|
|
196
|
+
if (!line.trim()) return;
|
|
197
|
+
outputStream.write(`${line}\n`);
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
const writeOutputText = (text: string) => {
|
|
201
|
+
for (const line of text.split("\n")) {
|
|
202
|
+
writeOutputLine(line);
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
const appendChildEvent = (event: Record<string, unknown>) => {
|
|
207
|
+
if (!childEventContext) return;
|
|
208
|
+
appendJsonl(childEventContext.eventsPath, JSON.stringify({
|
|
209
|
+
...event,
|
|
210
|
+
subagentSource: "child",
|
|
211
|
+
subagentRunId: childEventContext.runId,
|
|
212
|
+
subagentStepIndex: childEventContext.stepIndex,
|
|
213
|
+
subagentAgent: childEventContext.agent,
|
|
214
|
+
observedAt: Date.now(),
|
|
215
|
+
}));
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const appendChildLine = (type: "subagent.child.stdout" | "subagent.child.stderr", line: string) => {
|
|
219
|
+
appendChildEvent({ type, line });
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
const processStdoutLine = (line: string) => {
|
|
223
|
+
if (!line.trim()) return;
|
|
224
|
+
let event: ChildEvent;
|
|
225
|
+
try {
|
|
226
|
+
event = JSON.parse(line) as ChildEvent;
|
|
227
|
+
} catch {
|
|
228
|
+
rawStdoutLines.push(line);
|
|
229
|
+
writeOutputLine(line);
|
|
230
|
+
appendChildLine("subagent.child.stdout", line);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
appendChildEvent(event);
|
|
235
|
+
|
|
236
|
+
if (event.type === "tool_execution_start" && event.toolName) {
|
|
237
|
+
const toolArgs = extractToolArgsPreview(event.args ?? {});
|
|
238
|
+
writeOutputLine(toolArgs ? `${event.toolName}: ${toolArgs}` : event.toolName);
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if ((event.type === "message_end" || event.type === "tool_result_end") && event.message) {
|
|
243
|
+
messages.push(event.message);
|
|
244
|
+
const text = extractTextFromContent(event.message.content);
|
|
245
|
+
if (text) writeOutputText(text);
|
|
246
|
+
|
|
247
|
+
if (event.type !== "message_end" || event.message.role !== "assistant") return;
|
|
248
|
+
if (event.message.model) model = event.message.model;
|
|
249
|
+
if (event.message.errorMessage) error = event.message.errorMessage;
|
|
250
|
+
const eventUsage = event.message.usage;
|
|
251
|
+
if (!eventUsage) return;
|
|
252
|
+
usage.turns++;
|
|
253
|
+
usage.input += eventUsage.input ?? eventUsage.inputTokens ?? 0;
|
|
254
|
+
usage.output += eventUsage.output ?? eventUsage.outputTokens ?? 0;
|
|
255
|
+
usage.cacheRead += eventUsage.cacheRead ?? 0;
|
|
256
|
+
usage.cacheWrite += eventUsage.cacheWrite ?? 0;
|
|
257
|
+
usage.cost += eventUsage.cost?.total ?? 0;
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
const processStderrText = (text: string) => {
|
|
262
|
+
stderr += text;
|
|
263
|
+
stderrBuf += text;
|
|
264
|
+
outputStream.write(text);
|
|
265
|
+
if (!childEventContext) return;
|
|
266
|
+
const lines = stderrBuf.split("\n");
|
|
267
|
+
stderrBuf = lines.pop() || "";
|
|
268
|
+
for (const line of lines) {
|
|
269
|
+
if (!line.trim()) continue;
|
|
270
|
+
appendChildLine("subagent.child.stderr", line);
|
|
271
|
+
}
|
|
272
|
+
};
|
|
168
273
|
|
|
169
274
|
child.stdout.on("data", (chunk: Buffer) => {
|
|
170
275
|
const text = chunk.toString();
|
|
171
|
-
|
|
172
|
-
|
|
276
|
+
stdoutBuf += text;
|
|
277
|
+
const lines = stdoutBuf.split("\n");
|
|
278
|
+
stdoutBuf = lines.pop() || "";
|
|
279
|
+
for (const line of lines) processStdoutLine(line);
|
|
173
280
|
});
|
|
174
281
|
|
|
175
282
|
child.stderr.on("data", (chunk: Buffer) => {
|
|
176
|
-
|
|
177
|
-
stderr += text;
|
|
178
|
-
outputStream.write(text);
|
|
283
|
+
processStderrText(chunk.toString());
|
|
179
284
|
});
|
|
180
285
|
|
|
181
286
|
child.on("close", (exitCode) => {
|
|
287
|
+
if (stdoutBuf.trim()) processStdoutLine(stdoutBuf);
|
|
288
|
+
if (stderrBuf.trim()) appendChildLine("subagent.child.stderr", stderrBuf);
|
|
182
289
|
outputStream.end();
|
|
183
|
-
|
|
290
|
+
const finalOutput = getFinalOutput(messages) || rawStdoutLines.join("\n").trim();
|
|
291
|
+
resolve({ stderr, exitCode, messages, usage, model, error, finalOutput });
|
|
184
292
|
});
|
|
185
293
|
|
|
186
|
-
child.on("error", () => {
|
|
294
|
+
child.on("error", (spawnError) => {
|
|
187
295
|
outputStream.end();
|
|
188
|
-
|
|
296
|
+
const finalOutput = getFinalOutput(messages) || rawStdoutLines.join("\n").trim();
|
|
297
|
+
const spawnErrorMessage = spawnError instanceof Error ? spawnError.message : String(spawnError);
|
|
298
|
+
resolve({ stderr, exitCode: 1, messages, usage, model, error: error ?? spawnErrorMessage, finalOutput });
|
|
189
299
|
});
|
|
190
300
|
});
|
|
191
301
|
}
|
|
@@ -349,6 +459,7 @@ interface SingleStepContext {
|
|
|
349
459
|
flatStepCount: number;
|
|
350
460
|
outputFile: string;
|
|
351
461
|
piPackageRoot?: string;
|
|
462
|
+
piArgv1?: string;
|
|
352
463
|
}
|
|
353
464
|
|
|
354
465
|
/** Run a single pi agent step, returning output and metadata */
|
|
@@ -389,14 +500,13 @@ async function runSingleStep(
|
|
|
389
500
|
const attemptedModels: string[] = [];
|
|
390
501
|
const modelAttempts: ModelAttempt[] = [];
|
|
391
502
|
const attemptNotes: string[] = [];
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
| undefined;
|
|
503
|
+
const eventsPath = path.join(path.dirname(ctx.outputFile), "events.jsonl");
|
|
504
|
+
let finalResult: RunPiStreamingResult | undefined;
|
|
395
505
|
|
|
396
506
|
for (let index = 0; index < candidates.length; index++) {
|
|
397
507
|
const candidate = candidates[index];
|
|
398
508
|
const { args, env, tempDir } = buildPiArgs({
|
|
399
|
-
baseArgs: ["-p"],
|
|
509
|
+
baseArgs: ["--mode", "json", "-p"],
|
|
400
510
|
task,
|
|
401
511
|
sessionEnabled,
|
|
402
512
|
sessionDir,
|
|
@@ -409,28 +519,41 @@ async function runSingleStep(
|
|
|
409
519
|
mcpDirectTools: step.mcpDirectTools,
|
|
410
520
|
promptFileStem: step.agent,
|
|
411
521
|
});
|
|
412
|
-
const
|
|
413
|
-
|
|
522
|
+
const run = await runPiStreaming(
|
|
523
|
+
args,
|
|
524
|
+
step.cwd ?? ctx.cwd,
|
|
525
|
+
ctx.outputFile,
|
|
526
|
+
env,
|
|
527
|
+
ctx.piPackageRoot,
|
|
528
|
+
ctx.piArgv1,
|
|
529
|
+
step.maxSubagentDepth,
|
|
530
|
+
{ eventsPath, runId: ctx.id, stepIndex: ctx.flatIndex, agent: step.agent },
|
|
531
|
+
);
|
|
414
532
|
cleanupTempDir(tempDir);
|
|
415
533
|
|
|
416
|
-
const
|
|
417
|
-
const
|
|
534
|
+
const hiddenError = run.exitCode === 0 && !run.error ? detectSubagentError(run.messages) : null;
|
|
535
|
+
const effectiveExitCode = hiddenError?.hasError ? (hiddenError.exitCode ?? 1) : run.exitCode;
|
|
536
|
+
const error = hiddenError?.hasError
|
|
537
|
+
? hiddenError.details
|
|
538
|
+
? `${hiddenError.errorType} failed (exit ${effectiveExitCode}): ${hiddenError.details}`
|
|
539
|
+
: `${hiddenError.errorType} failed with exit code ${effectiveExitCode}`
|
|
540
|
+
: run.error || (run.exitCode !== 0 && run.stderr.trim() ? run.stderr.trim() : undefined);
|
|
418
541
|
const attempt: ModelAttempt = {
|
|
419
|
-
model: candidate ??
|
|
420
|
-
success:
|
|
421
|
-
exitCode:
|
|
542
|
+
model: candidate ?? run.model ?? step.model ?? "default",
|
|
543
|
+
success: effectiveExitCode === 0 && !error,
|
|
544
|
+
exitCode: effectiveExitCode,
|
|
422
545
|
error,
|
|
423
|
-
usage:
|
|
546
|
+
usage: run.usage,
|
|
424
547
|
};
|
|
425
548
|
modelAttempts.push(attempt);
|
|
426
549
|
if (candidate) attemptedModels.push(candidate);
|
|
427
|
-
finalResult = { ...run,
|
|
550
|
+
finalResult = { ...run, exitCode: effectiveExitCode, model: candidate ?? run.model, error };
|
|
428
551
|
if (attempt.success) break;
|
|
429
552
|
if (!isRetryableModelFailure(error) || index === candidates.length - 1) break;
|
|
430
553
|
attemptNotes.push(formatModelAttemptNote(attempt, candidates[index + 1]));
|
|
431
554
|
}
|
|
432
555
|
|
|
433
|
-
const rawOutput =
|
|
556
|
+
const rawOutput = finalResult?.finalOutput ?? "";
|
|
434
557
|
const resolvedOutput = step.outputPath && finalResult?.exitCode === 0
|
|
435
558
|
? resolveSingleOutput(step.outputPath, rawOutput, outputSnapshot)
|
|
436
559
|
: { fullOutput: rawOutput };
|
|
@@ -768,6 +891,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
768
891
|
flatIndex: fi, flatStepCount: flatSteps.length,
|
|
769
892
|
outputFile: path.join(asyncDir, `output-${fi}.log`),
|
|
770
893
|
piPackageRoot: config.piPackageRoot,
|
|
894
|
+
piArgv1: config.piArgv1,
|
|
771
895
|
});
|
|
772
896
|
if (task.sessionFile) {
|
|
773
897
|
latestSessionFile = task.sessionFile;
|
|
@@ -888,6 +1012,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
888
1012
|
flatIndex, flatStepCount: flatSteps.length,
|
|
889
1013
|
outputFile: path.join(asyncDir, `output-${flatIndex}.log`),
|
|
890
1014
|
piPackageRoot: config.piPackageRoot,
|
|
1015
|
+
piArgv1: config.piArgv1,
|
|
891
1016
|
});
|
|
892
1017
|
if (seqStep.sessionFile) {
|
|
893
1018
|
latestSessionFile = seqStep.sessionFile;
|
|
@@ -309,10 +309,66 @@ export const DEFAULT_ARTIFACT_CONFIG: ArtifactConfig = {
|
|
|
309
309
|
cleanupDays: 7,
|
|
310
310
|
};
|
|
311
311
|
|
|
312
|
+
function sanitizeTempScopeSegment(value: string): string {
|
|
313
|
+
const sanitized = value
|
|
314
|
+
.trim()
|
|
315
|
+
.replace(/[^A-Za-z0-9._-]+/g, "-")
|
|
316
|
+
.replace(/^-+|-+$/g, "");
|
|
317
|
+
return sanitized || "unknown";
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
export function resolveTempScopeId(options?: {
|
|
321
|
+
env?: NodeJS.ProcessEnv;
|
|
322
|
+
getuid?: (() => number) | undefined;
|
|
323
|
+
userInfo?: (() => { username?: string | null }) | undefined;
|
|
324
|
+
homedir?: (() => string) | undefined;
|
|
325
|
+
}): string {
|
|
326
|
+
const env = options?.env ?? process.env;
|
|
327
|
+
const getuid = options && Object.hasOwn(options, "getuid")
|
|
328
|
+
? options.getuid
|
|
329
|
+
: process.getuid?.bind(process);
|
|
330
|
+
if (typeof getuid === "function") {
|
|
331
|
+
return `uid-${getuid()}`;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
for (const key of ["USERNAME", "USER", "LOGNAME"] as const) {
|
|
335
|
+
const value = env[key];
|
|
336
|
+
if (value) return `user-${sanitizeTempScopeSegment(value)}`;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const userInfo = options && Object.hasOwn(options, "userInfo")
|
|
340
|
+
? options.userInfo
|
|
341
|
+
: os.userInfo;
|
|
342
|
+
try {
|
|
343
|
+
const username = userInfo?.().username;
|
|
344
|
+
if (username) return `user-${sanitizeTempScopeSegment(username)}`;
|
|
345
|
+
} catch {
|
|
346
|
+
// Fall through to home-directory-based scoping.
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const homedir = env.USERPROFILE ?? env.HOME;
|
|
350
|
+
if (homedir) return `home-${sanitizeTempScopeSegment(homedir)}`;
|
|
351
|
+
|
|
352
|
+
const resolveHomedir = options && Object.hasOwn(options, "homedir")
|
|
353
|
+
? options.homedir
|
|
354
|
+
: os.homedir;
|
|
355
|
+
try {
|
|
356
|
+
const fallbackHomedir = resolveHomedir?.();
|
|
357
|
+
if (fallbackHomedir) return `home-${sanitizeTempScopeSegment(fallbackHomedir)}`;
|
|
358
|
+
} catch {
|
|
359
|
+
// Fall through to the last-resort shared scope.
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return "shared";
|
|
363
|
+
}
|
|
364
|
+
|
|
312
365
|
export const MAX_PARALLEL = 8;
|
|
313
366
|
export const MAX_CONCURRENCY = 4;
|
|
314
|
-
export const
|
|
315
|
-
export const
|
|
367
|
+
export const TEMP_ROOT_DIR = path.join(os.tmpdir(), `pi-subagents-${resolveTempScopeId()}`);
|
|
368
|
+
export const RESULTS_DIR = path.join(TEMP_ROOT_DIR, "async-subagent-results");
|
|
369
|
+
export const ASYNC_DIR = path.join(TEMP_ROOT_DIR, "async-subagent-runs");
|
|
370
|
+
export const CHAIN_RUNS_DIR = path.join(TEMP_ROOT_DIR, "chain-runs");
|
|
371
|
+
export const TEMP_ARTIFACTS_DIR = path.join(TEMP_ROOT_DIR, "artifacts");
|
|
316
372
|
export const WIDGET_KEY = "subagent-async";
|
|
317
373
|
export const SLASH_RESULT_TYPE = "subagent-slash-result";
|
|
318
374
|
export const SLASH_SUBAGENT_REQUEST_EVENT = "subagent:slash:request";
|
|
@@ -329,6 +385,10 @@ export const DEFAULT_FORK_PREAMBLE =
|
|
|
329
385
|
"Your sole job is to execute the task below. Do not continue or respond to the prior conversation " +
|
|
330
386
|
"— focus exclusively on completing this task using your tools.";
|
|
331
387
|
|
|
388
|
+
export function getAsyncConfigPath(suffix: string): string {
|
|
389
|
+
return path.join(TEMP_ROOT_DIR, `async-cfg-${suffix}.json`);
|
|
390
|
+
}
|
|
391
|
+
|
|
332
392
|
export function wrapForkTask(task: string, preamble?: string | false): string {
|
|
333
393
|
if (preamble === false) return task;
|
|
334
394
|
const effectivePreamble = preamble ?? DEFAULT_FORK_PREAMBLE;
|