@pi-unipi/subagents 2.6.0 → 2.6.2
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/README.md +89 -78
- package/agents/delegate.md +14 -0
- package/agents/oracle.md +78 -0
- package/agents/researcher.md +52 -0
- package/agents/reviewer.md +79 -0
- package/agents/scout.md +50 -0
- package/agents/worker.md +59 -0
- package/package.json +19 -17
- package/prompts/council.md +48 -0
- package/prompts/gather-context-and-clarify.md +13 -0
- package/prompts/parallel-cleanup.md +59 -0
- package/prompts/parallel-research.md +50 -0
- package/prompts/parallel-review.md +54 -0
- package/prompts/review-loop.md +43 -0
- package/skills/council-mode/SKILL.md +230 -0
- package/skills/subagents/SKILL.md +49 -0
- package/skills/subagents/references/constraints-and-recipes.md +259 -0
- package/skills/subagents/references/execution-controls.md +454 -0
- package/skills/subagents/references/management-authoring-rpc.md +161 -0
- package/skills/subagents/references/multi-lane-orchestration.md +39 -0
- package/skills/subagents/references/prompting-and-roles.md +267 -0
- package/src/acceptance.ts +331 -0
- package/src/agent-manager.ts +405 -0
- package/src/agent-memory.ts +300 -0
- package/src/agent-overrides.ts +183 -0
- package/src/agent-runner.ts +265 -0
- package/src/async-runner.ts +417 -0
- package/src/authority-policy.ts +47 -0
- package/src/budgets.ts +285 -0
- package/src/child-safety.ts +140 -0
- package/src/config.ts +226 -0
- package/src/conversation-viewer.ts +281 -0
- package/src/core-compat.ts +94 -0
- package/src/custom-agents.ts +323 -0
- package/src/file-system-retry.ts +50 -0
- package/src/fleet-data.ts +61 -0
- package/src/fleet-view.ts +316 -0
- package/src/foreground-detach.ts +59 -0
- package/src/fork-context.ts +285 -0
- package/src/global.d.ts +7 -0
- package/src/guide.ts +129 -0
- package/src/index.ts +1149 -0
- package/src/mission-state.ts +133 -0
- package/src/mission-store.ts +434 -0
- package/src/model-resolver.ts +79 -0
- package/src/output-limits.ts +142 -0
- package/src/parity-types.ts +315 -0
- package/src/pi-args.ts +161 -0
- package/src/pi-spawn.ts +147 -0
- package/src/result-files.ts +232 -0
- package/src/result-watcher.ts +221 -0
- package/src/retained-children.ts +176 -0
- package/src/run-fanout-budget.ts +356 -0
- package/src/scheduled-runs.ts +351 -0
- package/src/schemas.ts +263 -0
- package/src/slash-commands.ts +77 -0
- package/src/supervisor-channel.ts +249 -0
- package/src/tool-handler.ts +1144 -0
- package/src/types.ts +238 -0
- package/src/widget.ts +447 -0
- package/src/workflow-script.ts +737 -0
- package/src/workflow-worker.ts +384 -0
- package/src/worktree.ts +614 -0
- package/dist/agent-manager.d.ts +0 -81
- package/dist/agent-manager.d.ts.map +0 -1
- package/dist/agent-manager.js +0 -295
- package/dist/agent-manager.js.map +0 -1
- package/dist/agent-runner.d.ts +0 -51
- package/dist/agent-runner.d.ts.map +0 -1
- package/dist/agent-runner.js +0 -262
- package/dist/agent-runner.js.map +0 -1
- package/dist/config.d.ts +0 -24
- package/dist/config.d.ts.map +0 -1
- package/dist/config.js +0 -132
- package/dist/config.js.map +0 -1
- package/dist/conversation-viewer.d.ts +0 -40
- package/dist/conversation-viewer.d.ts.map +0 -1
- package/dist/conversation-viewer.js +0 -276
- package/dist/conversation-viewer.js.map +0 -1
- package/dist/core-compat.d.ts +0 -21
- package/dist/core-compat.d.ts.map +0 -1
- package/dist/core-compat.js +0 -86
- package/dist/core-compat.js.map +0 -1
- package/dist/custom-agents.d.ts +0 -14
- package/dist/custom-agents.d.ts.map +0 -1
- package/dist/custom-agents.js +0 -113
- package/dist/custom-agents.js.map +0 -1
- package/dist/file-lock.d.ts +0 -42
- package/dist/file-lock.d.ts.map +0 -1
- package/dist/file-lock.js +0 -91
- package/dist/file-lock.js.map +0 -1
- package/dist/index.d.ts +0 -10
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -758
- package/dist/index.js.map +0 -1
- package/dist/model-resolver.d.ts +0 -19
- package/dist/model-resolver.d.ts.map +0 -1
- package/dist/model-resolver.js +0 -61
- package/dist/model-resolver.js.map +0 -1
- package/dist/types.d.ts +0 -98
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -47
- package/dist/types.js.map +0 -1
- package/dist/widget.d.ts +0 -56
- package/dist/widget.d.ts.map +0 -1
- package/dist/widget.js +0 -396
- package/dist/widget.js.map +0 -1
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pi-unipi/subagents — Async child process runner (Phase 3 core)
|
|
3
|
+
*
|
|
4
|
+
* Ported from pi-subagents src/runs/background/subagent-runner.ts (the
|
|
5
|
+
* runPiStreaming core). Spawns a child `pi --mode json -p` process, streams
|
|
6
|
+
* JSON events from stdout, collects the final assistant message + usage,
|
|
7
|
+
* enforces the run deadline, propagates abort/stop, and escalates to file
|
|
8
|
+
* task delivery after an unexplained zero-activity SIGKILL (EDR workaround).
|
|
9
|
+
*
|
|
10
|
+
* Artifacts land under OUR temp root: async-subagent-runs/<runId>/
|
|
11
|
+
* (status.json, output.txt, events.jsonl).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { spawn, type ChildProcess } from "node:child_process";
|
|
15
|
+
import { createWriteStream } from "node:fs";
|
|
16
|
+
import * as fs from "node:fs";
|
|
17
|
+
import * as path from "node:path";
|
|
18
|
+
import { randomUUID } from "node:crypto";
|
|
19
|
+
import { getPiSpawnCommand } from "./pi-spawn.js";
|
|
20
|
+
import { buildPiArgs, cleanupTempDir, type SubagentTaskDelivery } from "./pi-args.js";
|
|
21
|
+
import { ASYNC_DIR, ensureDirs, TEMP_ROOT_DIR } from "./parity-types.js";
|
|
22
|
+
import { childDepthEnv, resolveMaxSubagentDepth } from "./child-safety.js";
|
|
23
|
+
import {
|
|
24
|
+
ensureSupervisorChannelDir,
|
|
25
|
+
resolveSupervisorChannelDir,
|
|
26
|
+
SUPERVISOR_CHANNEL_DIR_ENV,
|
|
27
|
+
SUPERVISOR_PARENT_SESSION_ENV,
|
|
28
|
+
} from "./supervisor-channel.js";
|
|
29
|
+
import type { AgentConfig, SubagentsConfig } from "./types.js";
|
|
30
|
+
|
|
31
|
+
// ============================================================================
|
|
32
|
+
// Types
|
|
33
|
+
// ============================================================================
|
|
34
|
+
|
|
35
|
+
/** JSON event lines the child pi emits in --mode json. */
|
|
36
|
+
export interface ChildEvent {
|
|
37
|
+
type: string;
|
|
38
|
+
[key: string]: unknown;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface AsyncRunSpec {
|
|
42
|
+
agent: AgentConfig;
|
|
43
|
+
task: string;
|
|
44
|
+
cwd: string;
|
|
45
|
+
/** Child session file (resume support). When set, the child runs with
|
|
46
|
+
* --session <file> and the run records it for later resume. */
|
|
47
|
+
sessionFile?: string;
|
|
48
|
+
/** Extra pi CLI args (e.g. model/tool flags handled by the caller). */
|
|
49
|
+
baseArgs?: string[];
|
|
50
|
+
model?: string;
|
|
51
|
+
thinking?: string | false;
|
|
52
|
+
tools?: string[];
|
|
53
|
+
extensions?: string[];
|
|
54
|
+
systemPrompt?: string;
|
|
55
|
+
systemPromptMode?: "replace" | "append";
|
|
56
|
+
inheritProjectContext?: boolean;
|
|
57
|
+
inheritSkills?: boolean;
|
|
58
|
+
sessionDir?: string;
|
|
59
|
+
timeoutMs?: number;
|
|
60
|
+
taskDelivery?: SubagentTaskDelivery;
|
|
61
|
+
parentSessionId?: string;
|
|
62
|
+
config?: SubagentsConfig;
|
|
63
|
+
/** Pre-resolved fork session file (context: "fork"). When present the child
|
|
64
|
+
* launches with --session <file>, branching from the parent conversation. */
|
|
65
|
+
forkSessionFile?: string;
|
|
66
|
+
/** Force thinking off for this child (sanitized Anthropic fork). */
|
|
67
|
+
forceThinkingOff?: boolean;
|
|
68
|
+
/** Observation callbacks */
|
|
69
|
+
onEvent?: (event: ChildEvent) => void;
|
|
70
|
+
onOutputLine?: (line: string) => void;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export type AsyncRunStatus =
|
|
74
|
+
| "queued"
|
|
75
|
+
| "running"
|
|
76
|
+
| "completed"
|
|
77
|
+
| "failed"
|
|
78
|
+
| "stopped"
|
|
79
|
+
| "timedOut";
|
|
80
|
+
|
|
81
|
+
export interface AsyncRunResult {
|
|
82
|
+
runId: string;
|
|
83
|
+
status: AsyncRunStatus;
|
|
84
|
+
output: string;
|
|
85
|
+
error?: string;
|
|
86
|
+
/** Process-terminal proof: the child pid and observed exit. */
|
|
87
|
+
pid?: number;
|
|
88
|
+
exitCode?: number | null;
|
|
89
|
+
signal?: NodeJS.Signals | null;
|
|
90
|
+
/** Usage parsed from the child's final event (best effort). */
|
|
91
|
+
usage?: { input?: number; output?: number; total?: number };
|
|
92
|
+
durationMs: number;
|
|
93
|
+
/** True when the run was retried with file task delivery after zero-activity SIGKILL. */
|
|
94
|
+
retriedFileDelivery?: boolean;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ============================================================================
|
|
98
|
+
// Runner
|
|
99
|
+
// ============================================================================
|
|
100
|
+
|
|
101
|
+
const ZERO_ACTIVITY_TIMEOUT_MS = 20_000;
|
|
102
|
+
|
|
103
|
+
/** Format the effective child system prompt (agent prompt + boundary + memory). */
|
|
104
|
+
function effectiveChildPrompt(spec: AsyncRunSpec): string {
|
|
105
|
+
return spec.systemPrompt ?? spec.agent.systemPrompt;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Run one child pi process to completion. */
|
|
109
|
+
async function runChildProcess(
|
|
110
|
+
spec: AsyncRunSpec,
|
|
111
|
+
runDir: string,
|
|
112
|
+
signal: AbortSignal,
|
|
113
|
+
forceFileDelivery: boolean,
|
|
114
|
+
): Promise<AsyncRunResult> {
|
|
115
|
+
const startedAt = Date.now();
|
|
116
|
+
const runId = path.basename(runDir);
|
|
117
|
+
const outputFile = path.join(runDir, "output.txt");
|
|
118
|
+
|
|
119
|
+
const { args, env: childEnv, tempDir } = buildPiArgs({
|
|
120
|
+
baseArgs: spec.baseArgs ?? ["--mode", "json", "-p"],
|
|
121
|
+
task: spec.task,
|
|
122
|
+
sessionFile: spec.forkSessionFile ?? spec.sessionFile,
|
|
123
|
+
sessionDir: spec.forkSessionFile ? undefined : spec.sessionDir,
|
|
124
|
+
model: spec.model,
|
|
125
|
+
thinking: spec.forceThinkingOff ? "off" : spec.thinking,
|
|
126
|
+
tools: spec.tools,
|
|
127
|
+
extensions: spec.extensions,
|
|
128
|
+
systemPrompt: effectiveChildPrompt(spec),
|
|
129
|
+
systemPromptMode: spec.systemPromptMode ?? spec.agent.promptMode,
|
|
130
|
+
promptFileStem: spec.agent.name,
|
|
131
|
+
inheritProjectContext: spec.inheritProjectContext ?? spec.agent.inheritProjectContext,
|
|
132
|
+
inheritSkills: spec.inheritSkills ?? spec.agent.inheritSkills,
|
|
133
|
+
taskDelivery: forceFileDelivery ? "file" : spec.taskDelivery,
|
|
134
|
+
parentSessionId: spec.parentSessionId,
|
|
135
|
+
childAgentName: spec.agent.name,
|
|
136
|
+
runId,
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
const maxDepth = resolveMaxSubagentDepth(spec.agent, spec.config);
|
|
140
|
+
const depthEnv = childDepthEnv(process.env, maxDepth);
|
|
141
|
+
|
|
142
|
+
// Supervisor channel: children with a parent session can contact_supervisor.
|
|
143
|
+
const channelRoot = path.join(TEMP_ROOT_DIR, "supervisor-channels");
|
|
144
|
+
let supervisorEnv: Record<string, string> = {};
|
|
145
|
+
if (spec.parentSessionId) {
|
|
146
|
+
const channelDir = resolveSupervisorChannelDir(channelRoot, runId, spec.agent.name);
|
|
147
|
+
ensureSupervisorChannelDir(channelDir);
|
|
148
|
+
supervisorEnv = {
|
|
149
|
+
[SUPERVISOR_CHANNEL_DIR_ENV]: channelDir,
|
|
150
|
+
[SUPERVISOR_PARENT_SESSION_ENV]: spec.parentSessionId,
|
|
151
|
+
};
|
|
152
|
+
// Record for the parent-side poller.
|
|
153
|
+
try {
|
|
154
|
+
fs.writeFileSync(path.join(runDir, "supervisor-channel.json"), JSON.stringify({ channelDir }), { mode: 0o600 });
|
|
155
|
+
} catch { /* best effort */ }
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const spawnEnv = { ...process.env, ...childEnv, ...depthEnv, ...supervisorEnv };
|
|
159
|
+
|
|
160
|
+
const spawnSpec = getPiSpawnCommand(args, { env: spawnEnv });
|
|
161
|
+
|
|
162
|
+
return await new Promise<AsyncRunResult>((resolve) => {
|
|
163
|
+
const outputStream = fs.createWriteStream(outputFile, { flags: "w" });
|
|
164
|
+
const child: ChildProcess = spawn(spawnSpec.command, spawnSpec.args, {
|
|
165
|
+
cwd: spec.cwd,
|
|
166
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
167
|
+
env: spawnEnv,
|
|
168
|
+
windowsHide: true,
|
|
169
|
+
detached: process.platform !== "win32",
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
let output = "";
|
|
173
|
+
let usage: AsyncRunResult["usage"];
|
|
174
|
+
let error: string | undefined;
|
|
175
|
+
let timedOut = false;
|
|
176
|
+
let stopped = false;
|
|
177
|
+
let zeroActivityTimer: ReturnType<typeof setTimeout> | undefined;
|
|
178
|
+
let sawActivity = false;
|
|
179
|
+
let settled = false;
|
|
180
|
+
|
|
181
|
+
const finish = (result: Omit<AsyncRunResult, "runId" | "durationMs">) => {
|
|
182
|
+
if (settled) return;
|
|
183
|
+
settled = true;
|
|
184
|
+
if (zeroActivityTimer) clearTimeout(zeroActivityTimer);
|
|
185
|
+
signal.removeEventListener("abort", onAbort);
|
|
186
|
+
outputStream.end();
|
|
187
|
+
cleanupTempDir(tempDir);
|
|
188
|
+
resolve({ ...result, runId, durationMs: Date.now() - startedAt });
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
const onAbort = () => {
|
|
192
|
+
stopped = true;
|
|
193
|
+
trySignalChild(child, "SIGTERM");
|
|
194
|
+
// Grace period then hard kill.
|
|
195
|
+
setTimeout(() => trySignalChild(child, "SIGKILL"), 3_000);
|
|
196
|
+
};
|
|
197
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
198
|
+
|
|
199
|
+
// Run deadline.
|
|
200
|
+
const deadlineMs = spec.timeoutMs;
|
|
201
|
+
if (deadlineMs !== undefined && Number.isInteger(deadlineMs) && deadlineMs > 0) {
|
|
202
|
+
setTimeout(() => {
|
|
203
|
+
timedOut = true;
|
|
204
|
+
trySignalChild(child, "SIGTERM");
|
|
205
|
+
setTimeout(() => trySignalChild(child, "SIGKILL"), 3_000);
|
|
206
|
+
}, deadlineMs).unref?.();
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Zero-activity watchdog: escalate a silent child.
|
|
210
|
+
zeroActivityTimer = setTimeout(() => {
|
|
211
|
+
if (!sawActivity && !settled) {
|
|
212
|
+
trySignalChild(child, "SIGKILL");
|
|
213
|
+
}
|
|
214
|
+
}, ZERO_ACTIVITY_TIMEOUT_MS);
|
|
215
|
+
|
|
216
|
+
child.on("error", (err) => {
|
|
217
|
+
error = `Failed to launch child pi process: ${err.message}`;
|
|
218
|
+
finish({ status: "failed", output: "", error });
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
if (typeof child.pid === "number") {
|
|
222
|
+
// Record ownership for the process-terminal proof.
|
|
223
|
+
try {
|
|
224
|
+
fs.writeFileSync(path.join(runDir, "process.json"), JSON.stringify({ pid: child.pid, startedAt }), { mode: 0o600 });
|
|
225
|
+
} catch { /* best effort */ }
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
child.stdout?.on("data", (chunk: Buffer) => {
|
|
229
|
+
sawActivity = true;
|
|
230
|
+
const text = chunk.toString("utf8");
|
|
231
|
+
outputStream.write(text);
|
|
232
|
+
for (const line of text.split("\n")) {
|
|
233
|
+
if (!line.trim()) continue;
|
|
234
|
+
spec.onOutputLine?.(line);
|
|
235
|
+
let event: ChildEvent;
|
|
236
|
+
try {
|
|
237
|
+
event = JSON.parse(line) as ChildEvent;
|
|
238
|
+
} catch {
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
spec.onEvent?.(event);
|
|
242
|
+
// Track the final assistant message + usage from agent_end-style events.
|
|
243
|
+
if (event.type === "message_end" || event.type === "agent_end" || event.type === "agent_settled") {
|
|
244
|
+
const assistantText = extractAssistantText(event);
|
|
245
|
+
if (assistantText !== undefined) output = assistantText;
|
|
246
|
+
const eventUsage = extractUsage(event);
|
|
247
|
+
if (eventUsage) usage = eventUsage;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
child.stderr?.on("data", (chunk: Buffer) => {
|
|
253
|
+
sawActivity = true;
|
|
254
|
+
const text = chunk.toString("utf8");
|
|
255
|
+
outputStream.write(text);
|
|
256
|
+
if (!error && /Error|FATAL/i.test(text)) {
|
|
257
|
+
error = text.trim().split("\n").slice(-3).join("\n").slice(0, 500);
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
child.on("close", (code, signalName) => {
|
|
262
|
+
if (stopped) {
|
|
263
|
+
finish({ status: "stopped", output, pid: child.pid, exitCode: code, signal: signalName });
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
if (timedOut) {
|
|
267
|
+
finish({
|
|
268
|
+
status: "timedOut",
|
|
269
|
+
output,
|
|
270
|
+
error: `Run timed out after ${spec.timeoutMs}ms.`,
|
|
271
|
+
pid: child.pid,
|
|
272
|
+
exitCode: code,
|
|
273
|
+
signal: signalName,
|
|
274
|
+
});
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
// Zero-activity SIGKILL: retry once with file delivery (EDR workaround).
|
|
278
|
+
if (code === null && signalName === "SIGKILL" && !sawActivity) {
|
|
279
|
+
finish({
|
|
280
|
+
status: "failed",
|
|
281
|
+
output,
|
|
282
|
+
error: "Child process was killed before any output (possible EDR/AV interference).",
|
|
283
|
+
pid: child.pid,
|
|
284
|
+
exitCode: code,
|
|
285
|
+
signal: signalName,
|
|
286
|
+
retriedFileDelivery: true,
|
|
287
|
+
});
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
const ok = code === 0;
|
|
291
|
+
finish({
|
|
292
|
+
status: ok ? "completed" : "failed",
|
|
293
|
+
output: output || (ok ? "" : `Child exited with code ${code}.`),
|
|
294
|
+
...(error && !ok ? { error } : {}),
|
|
295
|
+
pid: child.pid,
|
|
296
|
+
exitCode: code,
|
|
297
|
+
signal: signalName,
|
|
298
|
+
...(usage ? { usage } : {}),
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function trySignalChild(child: ChildProcess, signalName: NodeJS.Signals): void {
|
|
305
|
+
try {
|
|
306
|
+
if (child.pid !== undefined) {
|
|
307
|
+
// Negative pid targets the detached process group (kills grandchildren shells too).
|
|
308
|
+
process.kill(-child.pid, signalName);
|
|
309
|
+
}
|
|
310
|
+
} catch {
|
|
311
|
+
try {
|
|
312
|
+
child.kill(signalName);
|
|
313
|
+
} catch {
|
|
314
|
+
// Already dead.
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function extractAssistantText(event: ChildEvent): string | undefined {
|
|
320
|
+
const message = (event as { message?: { role?: string; content?: unknown } }).message;
|
|
321
|
+
if (!message || message.role !== "assistant") return undefined;
|
|
322
|
+
const content = message.content;
|
|
323
|
+
if (typeof content === "string") return content;
|
|
324
|
+
if (Array.isArray(content)) {
|
|
325
|
+
return content
|
|
326
|
+
.filter((p): p is { type: "text"; text: string } => (p as { type?: string }).type === "text")
|
|
327
|
+
.map((p) => p.text)
|
|
328
|
+
.join("");
|
|
329
|
+
}
|
|
330
|
+
return undefined;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function extractUsage(event: ChildEvent): AsyncRunResult["usage"] | undefined {
|
|
334
|
+
const usage = (event as { usage?: Record<string, unknown> }).usage;
|
|
335
|
+
if (!usage || typeof usage !== "object") return undefined;
|
|
336
|
+
const num = (v: unknown): number | undefined => (typeof v === "number" && Number.isFinite(v) ? v : undefined);
|
|
337
|
+
const input = num(usage.input) ?? num(usage.inputTokens);
|
|
338
|
+
const output = num(usage.output) ?? num(usage.outputTokens);
|
|
339
|
+
const total = num(usage.total) ?? num(usage.totalTokens);
|
|
340
|
+
if (input === undefined && output === undefined && total === undefined) return undefined;
|
|
341
|
+
return { input, output, total };
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// ============================================================================
|
|
345
|
+
// Public API — run with artifacts + status lifecycle
|
|
346
|
+
// ============================================================================
|
|
347
|
+
|
|
348
|
+
/** Create the run directory + initial status.json. */
|
|
349
|
+
export function createAsyncRunDir(agentName: string): string {
|
|
350
|
+
ensureDirs();
|
|
351
|
+
const runId = `${agentName.replace(/[^A-Za-z0-9._-]/g, "_")}-${Date.now().toString(36)}-${randomUUID().slice(0, 8)}`;
|
|
352
|
+
const runDir = path.join(ASYNC_DIR, runId);
|
|
353
|
+
fs.mkdirSync(runDir, { recursive: true, mode: 0o700 });
|
|
354
|
+
writeStatus(runDir, { status: "queued", agent: agentName, createdAt: Date.now() });
|
|
355
|
+
return runDir;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
export function writeStatus(runDir: string, status: Record<string, unknown>): void {
|
|
359
|
+
try {
|
|
360
|
+
const existing = readStatus(runDir);
|
|
361
|
+
fs.writeFileSync(
|
|
362
|
+
path.join(runDir, "status.json"),
|
|
363
|
+
`${JSON.stringify({ ...existing, ...status, updatedAt: Date.now() })}\n`,
|
|
364
|
+
{ mode: 0o600 },
|
|
365
|
+
);
|
|
366
|
+
} catch {
|
|
367
|
+
fs.writeFileSync(
|
|
368
|
+
path.join(runDir, "status.json"),
|
|
369
|
+
`${JSON.stringify({ ...status, updatedAt: Date.now() })}\n`,
|
|
370
|
+
{ mode: 0o600 },
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
export function readStatus(runDir: string): Record<string, unknown> {
|
|
376
|
+
try {
|
|
377
|
+
return JSON.parse(fs.readFileSync(path.join(runDir, "status.json"), "utf8")) as Record<string, unknown>;
|
|
378
|
+
} catch {
|
|
379
|
+
return {};
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Launch an async run. Writes status.json transitions (queued → running →
|
|
385
|
+
* terminal), streams events, and returns the terminal result. Zero-activity
|
|
386
|
+
* SIGKILL retries once with forced file task delivery.
|
|
387
|
+
*/
|
|
388
|
+
export async function runAsyncSubagent(
|
|
389
|
+
spec: AsyncRunSpec,
|
|
390
|
+
runDir: string,
|
|
391
|
+
signal: AbortSignal,
|
|
392
|
+
): Promise<AsyncRunResult> {
|
|
393
|
+
writeStatus(runDir, {
|
|
394
|
+
status: "running",
|
|
395
|
+
pid: "pending",
|
|
396
|
+
agent: spec.agent.name,
|
|
397
|
+
...(spec.sessionFile ? { sessionFile: spec.sessionFile } : {}),
|
|
398
|
+
...(spec.forkSessionFile ? { sessionFile: spec.forkSessionFile } : {}),
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
let result = await runChildProcess(spec, runDir, signal, false);
|
|
402
|
+
|
|
403
|
+
// EDR workaround: retry once with file delivery.
|
|
404
|
+
if (result.status === "failed" && result.retriedFileDelivery) {
|
|
405
|
+
writeStatus(runDir, { status: "running", retry: "file-delivery" });
|
|
406
|
+
result = await runChildProcess(spec, runDir, signal, true);
|
|
407
|
+
result.retriedFileDelivery = true;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
writeStatus(runDir, {
|
|
411
|
+
status: result.status,
|
|
412
|
+
error: result.error,
|
|
413
|
+
exitCode: result.exitCode ?? null,
|
|
414
|
+
durationMs: result.durationMs,
|
|
415
|
+
});
|
|
416
|
+
return result;
|
|
417
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pi-unipi/subagents — Authority policy
|
|
3
|
+
*
|
|
4
|
+
* Ported from pi-subagents src/policy/authority.ts. Small fixed policy for
|
|
5
|
+
* operational actions: "confirm" requires explicit user confirmation;
|
|
6
|
+
* "auto" proceeds without asking.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export type AuthorityDecision = "auto" | "confirm" | "forbid";
|
|
10
|
+
|
|
11
|
+
export interface AuthorityPolicyConfig {
|
|
12
|
+
discardWorktree?: "confirm" | "auto";
|
|
13
|
+
destructiveCleanup?: "confirm" | "auto";
|
|
14
|
+
spawnBudgetGrant?: "confirm" | "auto";
|
|
15
|
+
scheduleCreate?: "confirm" | "auto";
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type AuthorityAction = keyof AuthorityPolicyConfig;
|
|
19
|
+
|
|
20
|
+
const DEFAULT_POLICY: Required<AuthorityPolicyConfig> = {
|
|
21
|
+
discardWorktree: "confirm",
|
|
22
|
+
destructiveCleanup: "confirm",
|
|
23
|
+
spawnBudgetGrant: "confirm",
|
|
24
|
+
scheduleCreate: "auto",
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export function resolveAuthorityDecision(input: {
|
|
28
|
+
action: AuthorityAction;
|
|
29
|
+
policy?: AuthorityPolicyConfig;
|
|
30
|
+
confirmed?: boolean;
|
|
31
|
+
}): AuthorityDecision {
|
|
32
|
+
const configured = input.policy?.[input.action] ?? DEFAULT_POLICY[input.action];
|
|
33
|
+
if (configured === "auto") return "auto";
|
|
34
|
+
return input.confirmed ? "auto" : "confirm";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function authorityPolicyFromConfig(raw: unknown): AuthorityPolicyConfig {
|
|
38
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return {};
|
|
39
|
+
const value = raw as Record<string, unknown>;
|
|
40
|
+
const result: AuthorityPolicyConfig = {};
|
|
41
|
+
for (const action of Object.keys(DEFAULT_POLICY) as AuthorityAction[]) {
|
|
42
|
+
if (value[action] === "confirm" || value[action] === "auto") {
|
|
43
|
+
result[action] = value[action] as "confirm" | "auto";
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return result;
|
|
47
|
+
}
|