pi-subagents 0.47.0 → 0.47.1
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 +14 -0
- package/agents/reviewer.md +3 -4
- package/package.json +1 -1
- package/src/extension/config.ts +10 -0
- package/src/extension/index.ts +9 -11
- package/src/extension/rpc.ts +4 -0
- package/src/extension/schemas.ts +2 -2
- package/src/intercom/native-supervisor-channel.ts +102 -4
- package/src/runs/background/active-run-index.ts +42 -0
- package/src/runs/background/async-job-tracker.ts +279 -134
- package/src/runs/background/async-status.ts +27 -4
- package/src/runs/background/control-channel.ts +55 -17
- package/src/runs/background/result-watcher.ts +87 -6
- package/src/runs/background/run-status.ts +18 -0
- package/src/runs/background/scheduled-runs.ts +23 -0
- package/src/runs/background/stale-run-reconciler.ts +10 -3
- package/src/runs/background/subagent-runner.ts +21 -4
- package/src/runs/foreground/async-dismiss-action.ts +85 -0
- package/src/runs/foreground/async-steering-action.ts +6 -7
- package/src/runs/foreground/subagent-executor.ts +34 -8
- package/src/runs/shared/subagent-prompt-runtime.ts +24 -5
- package/src/shared/artifacts.ts +1 -1
- package/src/shared/file-coalescer.ts +9 -0
- package/src/shared/types.ts +5 -1
- package/src/workflows/scripted-workflow.ts +4 -1
|
@@ -53,6 +53,7 @@ import {
|
|
|
53
53
|
} from "../../shared/settings.ts";
|
|
54
54
|
import { discoverAvailableSkills, normalizeSkillInput } from "../../agents/skills.ts";
|
|
55
55
|
import { buildAsyncRunnerSteps, DEFAULT_ASYNC_TIMEOUT_MS, executeAsyncChain, executeAsyncSingle, formatAsyncStartedMessage, isAsyncAvailable, workflowAwaitedAsyncResultPath } from "../background/async-execution.ts";
|
|
56
|
+
import { updateActiveRunIndex } from "../background/active-run-index.ts";
|
|
56
57
|
import { isScheduledRunAction, type ScheduledRunAction } from "../background/scheduled-runs.ts";
|
|
57
58
|
import { enqueueChainAppendRequest, readPendingChainAppendRequests, runnerStepOutputNames } from "../background/chain-append.ts";
|
|
58
59
|
import { ChainOutputValidationError, validateChainOutputBindingsWithContext } from "../shared/chain-outputs.ts";
|
|
@@ -93,6 +94,7 @@ import {
|
|
|
93
94
|
workflowForegroundSteeringLaunchOptions,
|
|
94
95
|
} from "./workflow-foreground-steering.ts";
|
|
95
96
|
import { stopAsyncRun } from "./async-stop-action.ts";
|
|
97
|
+
import { dismissRecoveredWorkflow } from "./async-dismiss-action.ts";
|
|
96
98
|
import { reconcileAsyncRun } from "../background/stale-run-reconciler.ts";
|
|
97
99
|
import { resolveAsyncRootResultPath, waitForImportedAsyncRoot } from "../background/chain-root-attachment.ts";
|
|
98
100
|
import { attachRootChildrenToSteps, createNestedRoute, findNestedControlResult, resolveInheritedNestedRouteFromEnv, resolveNestedAsyncDir, resolveNestedParentAddressFromEnv, snapshotNestedEventFiles, updateForegroundNestedProjection, writeNestedControlRequest, writeNestedEvent, type NestedRunResolutionScope } from "../shared/nested-events.ts";
|
|
@@ -162,7 +164,7 @@ import {
|
|
|
162
164
|
wrapForkTask,
|
|
163
165
|
} from "../../shared/types.ts";
|
|
164
166
|
|
|
165
|
-
const MUTATING_MANAGEMENT_ACTIONS = new Set(["create", "update", "delete", "eject", "disable", "enable", "reset", "grant-spawn-budget", "watchdog.configure", "mission.create", "mission.update", "mission.resolve-decision", "mission.attach-run", "mission.close", "inspector.open", "inspector.close", "project.open", "project.close", "worktree.discard", "refine", "refine.rollback", "schedule.create", "schedule.pause", "schedule.resume", "schedule.run", "schedule.run-due", "schedule.delete"]);
|
|
167
|
+
const MUTATING_MANAGEMENT_ACTIONS = new Set(["create", "update", "delete", "eject", "disable", "enable", "reset", "grant-spawn-budget", "watchdog.configure", "mission.create", "mission.update", "mission.resolve-decision", "mission.attach-run", "mission.close", "inspector.open", "inspector.close", "project.open", "project.close", "worktree.discard", "refine", "refine.rollback", "dismiss", "schedule.create", "schedule.pause", "schedule.resume", "schedule.run", "schedule.run-due", "schedule.delete"]);
|
|
166
168
|
const DESTRUCTIVE_MANAGEMENT_ACTIONS = new Set(["delete", "eject", "disable", "reset", "mission.close", "worktree.discard", "refine.rollback", "inspector.close", "project.close", "stop", "interrupt", "reject-checkpoint", "schedule.delete"]);
|
|
167
169
|
|
|
168
170
|
function editDistance(left: string, right: string): number {
|
|
@@ -4464,9 +4466,14 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
4464
4466
|
workflow: { trace: [], emits: [], console: [] },
|
|
4465
4467
|
};
|
|
4466
4468
|
const appendWorkflowEvent = (event: Record<string, unknown>) => fs.appendFileSync(eventsPath, `${JSON.stringify({ ts: Date.now(), runId: workflowRunId, ...event })}\n`, "utf-8");
|
|
4469
|
+
let indexedState: AsyncStatus["state"] | undefined;
|
|
4467
4470
|
const persist = () => {
|
|
4468
4471
|
status.lastUpdate = Date.now();
|
|
4469
4472
|
writeAtomicJson(statusPath, status);
|
|
4473
|
+
if (indexedState !== status.state) {
|
|
4474
|
+
updateActiveRunIndex(asyncDir, status.state);
|
|
4475
|
+
indexedState = status.state;
|
|
4476
|
+
}
|
|
4470
4477
|
const job = deps.state.asyncJobs.get(workflowRunId);
|
|
4471
4478
|
if (job) {
|
|
4472
4479
|
job.status = status.state;
|
|
@@ -5249,6 +5256,28 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
5249
5256
|
}
|
|
5250
5257
|
return deps.handleScheduledRunAction(paramsWithResolvedCwd, ctx);
|
|
5251
5258
|
}
|
|
5259
|
+
if (deps.allowMutatingManagementActions === false && MUTATING_MANAGEMENT_ACTIONS.has(action)) {
|
|
5260
|
+
return {
|
|
5261
|
+
content: [{ type: "text", text: `Action '${action}' is not available from child-safe subagent fanout mode.` }],
|
|
5262
|
+
isError: true,
|
|
5263
|
+
details: { mode: "management" as const, results: [] },
|
|
5264
|
+
};
|
|
5265
|
+
}
|
|
5266
|
+
if (action === "dismiss") {
|
|
5267
|
+
const targetRunId = paramsWithResolvedCwd.runId ?? paramsWithResolvedCwd.id;
|
|
5268
|
+
if (!targetRunId) return { content: [{ type: "text", text: "action='dismiss' requires id." }], isError: true, details: { mode: "management", results: [] } };
|
|
5269
|
+
let resolved: ResolvedSubagentRunId | undefined;
|
|
5270
|
+
try {
|
|
5271
|
+
resolved = resolveSubagentRunId(targetRunId, omitUndefinedProperties({ state: deps.state, nested: nestedResolutionScopeForExecutor(deps) }));
|
|
5272
|
+
} catch (error) {
|
|
5273
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
5274
|
+
return { content: [{ type: "text", text: message }], isError: true, details: { mode: "management", results: [] } };
|
|
5275
|
+
}
|
|
5276
|
+
if (resolved?.kind !== "async") {
|
|
5277
|
+
return { content: [{ type: "text", text: `Run '${targetRunId}' is not a recovered workflow.` }], isError: true, details: { mode: "management", results: [] } };
|
|
5278
|
+
}
|
|
5279
|
+
return dismissRecoveredWorkflow(deps.state, resolved.location);
|
|
5280
|
+
}
|
|
5252
5281
|
if (action === "stop") {
|
|
5253
5282
|
const targetRunId = paramsWithResolvedCwd.runId ?? paramsWithResolvedCwd.id;
|
|
5254
5283
|
const workflowController = targetRunId ? deps.state.workflowControllers?.get(targetRunId) : undefined;
|
|
@@ -5260,6 +5289,10 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
5260
5289
|
if (paramsWithResolvedCwd.dir) {
|
|
5261
5290
|
try {
|
|
5262
5291
|
const location = resolveAsyncRunLocation(paramsWithResolvedCwd, DIRS.async, DIRS.results);
|
|
5292
|
+
const existingStatus = readStatus(location.asyncDir ?? "");
|
|
5293
|
+
if (existingStatus?.mode === "workflow" && existingStatus.state === "running") {
|
|
5294
|
+
return { content: [{ type: "text", text: `Workflow ${existingStatus.runId} is not controlled by this extension runtime; reload recovery cannot stop it safely.` }], isError: true, details: { mode: "management", results: [] } };
|
|
5295
|
+
}
|
|
5263
5296
|
const stopResult = stopAsyncRun(deps.state, location.resolvedId ?? targetRunId ?? path.basename(location.asyncDir ?? paramsWithResolvedCwd.dir), deps.kill, location);
|
|
5264
5297
|
return stopResult ?? { content: [{ type: "text", text: `No running or queued async run was found for '${targetRunId ?? paramsWithResolvedCwd.dir}'.` }], isError: true, details: { mode: "management", results: [] } };
|
|
5265
5298
|
} catch (error) {
|
|
@@ -5344,13 +5377,6 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
5344
5377
|
details: { mode: "management" as const, results: [] },
|
|
5345
5378
|
};
|
|
5346
5379
|
}
|
|
5347
|
-
if (deps.allowMutatingManagementActions === false && MUTATING_MANAGEMENT_ACTIONS.has(action)) {
|
|
5348
|
-
return {
|
|
5349
|
-
content: [{ type: "text", text: `Action '${action}' is not available from child-safe subagent fanout mode.` }],
|
|
5350
|
-
isError: true,
|
|
5351
|
-
details: { mode: "management" as const, results: [] },
|
|
5352
|
-
};
|
|
5353
|
-
}
|
|
5354
5380
|
return handleManagementAction(action, paramsWithResolvedCwd, {
|
|
5355
5381
|
...ctx,
|
|
5356
5382
|
cwd: requestCwd,
|
|
@@ -31,6 +31,7 @@ const SUBAGENT_INHERIT_PROJECT_CONTEXT_ENV = "PI_SUBAGENT_INHERIT_PROJECT_CONTEX
|
|
|
31
31
|
const SUBAGENT_INHERIT_SKILLS_ENV = "PI_SUBAGENT_INHERIT_SKILLS";
|
|
32
32
|
export const SUBAGENT_INTERCOM_SESSION_NAME_ENV = "PI_SUBAGENT_INTERCOM_SESSION_NAME";
|
|
33
33
|
const STEERING_LEGACY_SETTLE_FALLBACK_MS = 1000;
|
|
34
|
+
const STEERING_SAFETY_POLL_INTERVAL_MS = 5000;
|
|
34
35
|
|
|
35
36
|
const STRUCTURED_OUTPUT_INSTRUCTIONS = [
|
|
36
37
|
"This subagent step has a strict structured output contract.",
|
|
@@ -331,7 +332,13 @@ function registerToolBudget(pi: ExtensionAPI, budget: ResolvedToolBudget | undef
|
|
|
331
332
|
|
|
332
333
|
export function registerSteeringInbox(
|
|
333
334
|
pi: ExtensionAPI,
|
|
334
|
-
deps: {
|
|
335
|
+
deps: {
|
|
336
|
+
watch?: typeof fs.watch;
|
|
337
|
+
nativeRealpath?: (filePath: string) => string;
|
|
338
|
+
legacySettleFallbackMs?: number;
|
|
339
|
+
safetyPollIntervalMs?: number;
|
|
340
|
+
timers?: Pick<typeof globalThis, "setInterval" | "clearInterval">;
|
|
341
|
+
} = {},
|
|
335
342
|
): void {
|
|
336
343
|
const steerInbox = process.env[SUBAGENT_STEER_INBOX_ENV]?.trim();
|
|
337
344
|
if (!steerInbox) return;
|
|
@@ -350,6 +357,7 @@ export function registerSteeringInbox(
|
|
|
350
357
|
let canSteer = typeof sendUserMessage === "function";
|
|
351
358
|
let watcher: fs.FSWatcher | undefined;
|
|
352
359
|
let interval: NodeJS.Timeout | undefined;
|
|
360
|
+
let safetyInterval: NodeJS.Timeout | undefined;
|
|
353
361
|
let settleFallback: NodeJS.Timeout | undefined;
|
|
354
362
|
const legacySettleFallbackMs = deps.legacySettleFallbackMs ?? STEERING_LEGACY_SETTLE_FALLBACK_MS;
|
|
355
363
|
const acknowledge = (request: SteerRequest, state: "delivered" | "queued" | "failed", message: string, deliveryStatus?: SteerDeliveryStatus): void => {
|
|
@@ -431,14 +439,24 @@ export function registerSteeringInbox(
|
|
|
431
439
|
return;
|
|
432
440
|
}
|
|
433
441
|
started = true;
|
|
442
|
+
const startPolling = (): void => {
|
|
443
|
+
if (interval || disposed) return;
|
|
444
|
+
interval = (deps.timers?.setInterval ?? setInterval)(flush, 250) as NodeJS.Timeout;
|
|
445
|
+
interval.unref?.();
|
|
446
|
+
};
|
|
447
|
+
const startSafetyPolling = (): void => {
|
|
448
|
+
if (safetyInterval || disposed) return;
|
|
449
|
+
safetyInterval = (deps.timers?.setInterval ?? setInterval)(flush, deps.safetyPollIntervalMs ?? STEERING_SAFETY_POLL_INTERVAL_MS) as NodeJS.Timeout;
|
|
450
|
+
safetyInterval.unref?.();
|
|
451
|
+
};
|
|
434
452
|
try {
|
|
435
453
|
watcher = (deps.watch ?? fs.watch)(resolveWatchPath(steerInbox, deps.nativeRealpath), () => flush());
|
|
436
|
-
watcher.on("error",
|
|
454
|
+
watcher.on("error", startPolling);
|
|
455
|
+
startSafetyPolling();
|
|
437
456
|
} catch {
|
|
438
457
|
watcher = undefined;
|
|
458
|
+
startPolling();
|
|
439
459
|
}
|
|
440
|
-
interval = setInterval(flush, 250);
|
|
441
|
-
interval.unref?.();
|
|
442
460
|
};
|
|
443
461
|
const activate = (): undefined => {
|
|
444
462
|
start();
|
|
@@ -534,7 +552,8 @@ export function registerSteeringInbox(
|
|
|
534
552
|
disposed = true;
|
|
535
553
|
clearSettleFallback();
|
|
536
554
|
try { watcher?.close(); } catch {}
|
|
537
|
-
if (interval) clearInterval(interval);
|
|
555
|
+
if (interval) (deps.timers?.clearInterval ?? clearInterval)(interval);
|
|
556
|
+
if (safetyInterval) (deps.timers?.clearInterval ?? clearInterval)(safetyInterval);
|
|
538
557
|
});
|
|
539
558
|
}
|
|
540
559
|
|
package/src/shared/artifacts.ts
CHANGED
|
@@ -228,7 +228,7 @@ export function appendJsonl(filePath: string, line: string): void {
|
|
|
228
228
|
}
|
|
229
229
|
|
|
230
230
|
export function cleanupOldArtifacts(dir: string, maxAgeDays: number): void {
|
|
231
|
-
if (!fs.existsSync(dir)) return;
|
|
231
|
+
if (maxAgeDays <= 0 || !fs.existsSync(dir)) return;
|
|
232
232
|
|
|
233
233
|
const markerPath = path.join(dir, CLEANUP_MARKER_FILE);
|
|
234
234
|
const now = Date.now();
|
|
@@ -5,6 +5,7 @@ interface TimerApi {
|
|
|
5
5
|
|
|
6
6
|
interface FileCoalescer {
|
|
7
7
|
schedule(file: string, delayMs?: number): boolean;
|
|
8
|
+
flush(file: string): boolean;
|
|
8
9
|
clear(): void;
|
|
9
10
|
}
|
|
10
11
|
|
|
@@ -30,6 +31,14 @@ export function createFileCoalescer(
|
|
|
30
31
|
pending.set(file, timer);
|
|
31
32
|
return true;
|
|
32
33
|
},
|
|
34
|
+
flush(file: string): boolean {
|
|
35
|
+
const timer = pending.get(file);
|
|
36
|
+
if (timer === undefined) return false;
|
|
37
|
+
timerApi.clearTimeout(timer);
|
|
38
|
+
pending.delete(file);
|
|
39
|
+
handler(file);
|
|
40
|
+
return true;
|
|
41
|
+
},
|
|
33
42
|
clear(): void {
|
|
34
43
|
for (const timer of pending.values()) {
|
|
35
44
|
timerApi.clearTimeout(timer);
|
package/src/shared/types.ts
CHANGED
|
@@ -1290,6 +1290,8 @@ export interface AsyncStatus {
|
|
|
1290
1290
|
context?: "fresh" | "fork" | "mixed";
|
|
1291
1291
|
isNested?: boolean;
|
|
1292
1292
|
state: "queued" | "running" | "complete" | "failed" | "paused" | "stopped" | "rejected";
|
|
1293
|
+
/** Display-only dismissal marker for a reload-orphaned workflow. */
|
|
1294
|
+
displayDismissedAt?: number;
|
|
1293
1295
|
error?: string;
|
|
1294
1296
|
activityState?: ActivityState;
|
|
1295
1297
|
lastActivityAt?: number;
|
|
@@ -1854,6 +1856,8 @@ export interface ExtensionConfig {
|
|
|
1854
1856
|
worktreeBaseDir?: string;
|
|
1855
1857
|
/** Where to store subagent artifact files. Defaults to "project" (cwd/.pi/subagents). Set to "session" for pi session dir, or "temp" for OS temp. */
|
|
1856
1858
|
artifactDir?: ArtifactDirPreference;
|
|
1859
|
+
/** Artifact cleanup retention. Set cleanupDays to 0 to disable cleanup. */
|
|
1860
|
+
artifactConfig?: Pick<ArtifactConfig, "cleanupDays">;
|
|
1857
1861
|
intercomBridge?: IntercomBridgeConfig;
|
|
1858
1862
|
proactiveSkillSubagents?: ProactiveSkillSubagentsConfig | false;
|
|
1859
1863
|
scheduledRuns?: ScheduledRunsConfig;
|
|
@@ -1961,7 +1965,7 @@ export const SLASH_SUBAGENT_CANCEL_EVENT = "subagent:slash:cancel";
|
|
|
1961
1965
|
export const POLL_INTERVAL_MS = 250;
|
|
1962
1966
|
export const MAX_WIDGET_JOBS = 4;
|
|
1963
1967
|
export const DEFAULT_SUBAGENT_MAX_DEPTH = 2;
|
|
1964
|
-
export const SUBAGENT_ACTIONS = ["list", "get", "models", "children.list", "guide", "create", "update", "delete", "eject", "disable", "enable", "reset", "mission.create", "mission.list", "mission.show", "mission.update", "mission.resolve-decision", "mission.attach-run", "mission.close", "worktree.discard", "refine", "refine.show", "refine.rollback", "inspector.open", "inspector.status", "inspector.close", "project.open", "project.status", "project.close", "status", "grant-spawn-budget", "interrupt", "resume", "steer", "stop", "append-step", "approve-checkpoint", "reject-checkpoint", "doctor", "watchdog.status", "watchdog.check", "watchdog.configure", "watchdog.recommend-model", "schedule.create", "schedule.list", "schedule.show", "schedule.history", "schedule.pause", "schedule.resume", "schedule.run", "schedule.run-due", "schedule.delete"] as const;
|
|
1968
|
+
export const SUBAGENT_ACTIONS = ["list", "get", "models", "children.list", "guide", "create", "update", "delete", "eject", "disable", "enable", "reset", "mission.create", "mission.list", "mission.show", "mission.update", "mission.resolve-decision", "mission.attach-run", "mission.close", "worktree.discard", "refine", "refine.show", "refine.rollback", "inspector.open", "inspector.status", "inspector.close", "project.open", "project.status", "project.close", "status", "grant-spawn-budget", "interrupt", "resume", "steer", "stop", "dismiss", "append-step", "approve-checkpoint", "reject-checkpoint", "doctor", "watchdog.status", "watchdog.check", "watchdog.configure", "watchdog.recommend-model", "schedule.create", "schedule.list", "schedule.show", "schedule.history", "schedule.pause", "schedule.resume", "schedule.run", "schedule.run-due", "schedule.delete"] as const;
|
|
1965
1969
|
|
|
1966
1970
|
export const DEFAULT_FORK_PREAMBLE =
|
|
1967
1971
|
"You are a delegated subagent running from a fork of the parent session. " +
|
|
@@ -469,10 +469,13 @@ function validateKey(value: unknown, owner = "runs.run"): string {
|
|
|
469
469
|
return value;
|
|
470
470
|
}
|
|
471
471
|
|
|
472
|
-
function workflowStringMetadata(params: Record<string, unknown>): Pick<WorkflowScriptTraceEntry, "phase" | "label"> {
|
|
472
|
+
function workflowStringMetadata(params: Record<string, unknown>): Pick<WorkflowScriptTraceEntry, "phase" | "label" | "agent"> {
|
|
473
473
|
return {
|
|
474
474
|
...(typeof params.phase === "string" && params.phase.trim() ? { phase: params.phase.trim() } : {}),
|
|
475
475
|
...(typeof params.label === "string" && params.label.trim() ? { label: params.label.trim() } : {}),
|
|
476
|
+
// Requested agent name, so a child is identifiable while it runs. Launch
|
|
477
|
+
// resolution overwrites this with the canonical name on the terminal entry.
|
|
478
|
+
...(typeof params.agent === "string" && params.agent.trim() ? { agent: params.agent.trim() } : {}),
|
|
476
479
|
};
|
|
477
480
|
}
|
|
478
481
|
|