pi-subagents 0.50.0 → 0.51.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/CHANGELOG.md +62 -0
- package/agents/gpt-pro.md +17 -0
- package/async-retention-discovery-worker.mjs +180 -0
- package/docs/agents.md +35 -2
- package/docs/configuration.md +30 -12
- package/docs/extension-api.md +42 -1
- package/docs/observability.md +4 -4
- package/docs/tool-reference.md +38 -39
- package/docs/workflows.md +169 -3
- package/package.json +4 -2
- package/skills/pi-subagents/SKILL.md +5 -4
- package/skills/pi-subagents/references/constraints-and-recipes.md +6 -4
- package/skills/pi-subagents/references/execution-controls.md +18 -15
- package/skills/pi-subagents/references/management-authoring-rpc.md +3 -3
- package/skills/pi-subagents/references/prompting-and-roles.md +2 -2
- package/src/agents/agent-management.ts +100 -345
- package/src/agents/agents.ts +125 -25
- package/src/api/external-job-provider.ts +185 -0
- package/src/api/preflight.ts +31 -6
- package/src/api/shared-types.ts +2 -0
- package/src/extension/config.ts +3 -3
- package/src/extension/doctor.ts +3 -6
- package/src/extension/fanout-child.ts +2 -2
- package/src/extension/index.ts +165 -88
- package/src/extension/public-execution.ts +28 -1
- package/src/extension/schemas.ts +12 -35
- package/src/extension/tool-description.ts +35 -24
- package/src/inspectors/herdr/actions.ts +2 -2
- package/src/inspectors/herdr/inspector-runner.ts +2 -1
- package/src/inspectors/herdr/project-panes.ts +2 -2
- package/src/intercom/native-supervisor-channel.ts +30 -9
- package/src/missions/lifecycle.ts +6 -1
- package/src/missions/store.ts +4 -9
- package/src/profiles/profiles.ts +3 -1
- package/src/runs/background/active-run-index.ts +31 -8
- package/src/runs/background/async-execution.ts +49 -31
- package/src/runs/background/async-job-tracker.ts +20 -4
- package/src/runs/background/async-resume.ts +28 -13
- package/src/runs/background/async-retention.ts +888 -0
- package/src/runs/background/async-status.ts +39 -53
- package/src/runs/background/chain-append.ts +3 -33
- package/src/runs/background/control-channel.ts +14 -68
- package/src/runs/background/index-segment.ts +59 -0
- package/src/runs/background/notify.ts +3 -1
- package/src/runs/background/result-files.ts +158 -90
- package/src/runs/background/result-watcher.ts +72 -20
- package/src/runs/background/retained-children.ts +13 -3
- package/src/runs/background/run-id-query.ts +7 -0
- package/src/runs/background/run-id-resolver.ts +11 -9
- package/src/runs/background/run-status.ts +27 -18
- package/src/runs/background/scheduled-runs.ts +22 -4
- package/src/runs/background/stale-run-reconciler.ts +6 -3
- package/src/runs/background/steering.ts +11 -1
- package/src/runs/background/subagent-runner.ts +250 -138
- package/src/runs/background/subagent-wait.ts +7 -7
- package/src/runs/background/terminal-run-index.ts +129 -0
- package/src/runs/background/wait-completions.ts +21 -4
- package/src/runs/background/wait-subscriptions.ts +80 -1
- package/src/runs/foreground/async-steering-action.ts +21 -14
- package/src/runs/foreground/execution.ts +2 -0
- package/src/runs/foreground/subagent-executor.ts +460 -1536
- package/src/runs/foreground/workflow-foreground-steering.ts +6 -5
- package/src/runs/shared/chain-outputs.ts +1 -3
- package/src/runs/shared/external-job-bridge.ts +450 -0
- package/src/runs/shared/external-job-runner.ts +286 -0
- package/src/runs/shared/mcp-direct-tool-allowlist.ts +14 -0
- package/src/runs/shared/model-fallback.ts +22 -5
- package/src/runs/shared/orca-progress-tabs.ts +84 -22
- package/src/runs/shared/parallel-handoff.ts +46 -4
- package/src/runs/shared/parallel-utils.ts +4 -15
- package/src/runs/shared/permissions.ts +5 -1
- package/src/runs/shared/pi-args.ts +8 -1
- package/src/runs/shared/subagent-control.ts +26 -4
- package/src/runs/shared/subagent-prompt-runtime.ts +12 -6
- package/src/runs/shared/workflow-graph.ts +1 -23
- package/src/runs/shared/worktree.ts +12 -1
- package/src/shared/atomic-json.ts +22 -2
- package/src/shared/capacity-resilient-json.ts +102 -0
- package/src/shared/completion-owner.ts +14 -0
- package/src/shared/file-system-retry.ts +49 -1
- package/src/shared/fork-context.ts +42 -0
- package/src/shared/prompt-resources.ts +0 -40
- package/src/shared/settings.ts +3 -27
- package/src/shared/types.ts +60 -26
- package/src/shared/utils.ts +8 -0
- package/src/shared/watch-strategy.ts +10 -0
- package/src/slash/slash-commands.ts +11 -3
- package/src/tui/fleet.ts +63 -15
- package/src/workflows/chat-progress.ts +1 -1
- package/src/workflows/scripted-workflow.ts +207 -65
- package/src/runs/foreground/chain-clarify.ts +0 -1354
- package/src/runs/foreground/chain-execution.ts +0 -1581
|
@@ -13,9 +13,10 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
|
13
13
|
import type { AgentConfig } from "../../agents/agents.ts";
|
|
14
14
|
import { appendAgentRefinementOverlay } from "../../agents/agent-refinements.ts";
|
|
15
15
|
import { writePrivateAtomicJson } from "../../shared/atomic-json.ts";
|
|
16
|
+
import { currentCompletionOwnerId } from "../../shared/completion-owner.ts";
|
|
16
17
|
import { applyThinkingSuffix, projectLaunchResolvedChildExtensions, resolvePiLaunchToolPlan } from "../shared/pi-args.ts";
|
|
17
18
|
import { injectOutputPathSystemPrompt, injectSingleOutputInstruction, normalizeSingleOutputOverride, resolveSingleOutputPath, validateFileOnlyOutputMode } from "../shared/single-output.ts";
|
|
18
|
-
import { buildChainInstructions,
|
|
19
|
+
import { buildChainInstructions, isDynamicParallelStep, isParallelStep, resolveChainPath, resolveExistingReadPaths, resolveStepBehavior, suppressProgressForReadOnlyTask, writeInitialProgressFile, type ChainStep, type ResolvedStepBehavior, type SequentialStep, type StepOverrides } from "../../shared/settings.ts";
|
|
19
20
|
import type { RunnerStep } from "../shared/parallel-utils.ts";
|
|
20
21
|
import type { ContextMode } from "../shared/context-mode.ts";
|
|
21
22
|
import { resolvePiPackageRoot } from "../shared/pi-spawn.ts";
|
|
@@ -125,6 +126,7 @@ interface AsyncExecutionContext {
|
|
|
125
126
|
pi: ExtensionAPI;
|
|
126
127
|
cwd: string;
|
|
127
128
|
currentSessionId: string;
|
|
129
|
+
completionOwnerId?: string;
|
|
128
130
|
/** Parent session id used by permission-system ask forwarding. */
|
|
129
131
|
parentSessionId?: string;
|
|
130
132
|
permissions?: PermissionConfig;
|
|
@@ -144,7 +146,7 @@ interface AsyncChainParams {
|
|
|
144
146
|
/** Raw caller-facing goal used only by the started event. */
|
|
145
147
|
goal?: string;
|
|
146
148
|
attachRoot?: ImportedAsyncRoot & { agent: string; outputName?: string; label?: string };
|
|
147
|
-
resultMode?:
|
|
149
|
+
resultMode?: SubagentRunMode;
|
|
148
150
|
agents: AgentConfig[];
|
|
149
151
|
ctx: AsyncExecutionContext;
|
|
150
152
|
availableModels?: AvailableModelInfo[];
|
|
@@ -225,6 +227,7 @@ interface AsyncSingleParams {
|
|
|
225
227
|
worktreeSetupHook?: string;
|
|
226
228
|
worktreeSetupHookTimeoutMs?: number;
|
|
227
229
|
worktreeBaseDir?: string;
|
|
230
|
+
worktree?: boolean;
|
|
228
231
|
controlConfig?: ResolvedControlConfig;
|
|
229
232
|
intercomBridge?: IntercomBridgeConfig;
|
|
230
233
|
controlIntercomTarget?: string;
|
|
@@ -428,7 +431,7 @@ function terminateRunnerBeforeProceed(pid: number): boolean {
|
|
|
428
431
|
return !runnerIsAlive(pid);
|
|
429
432
|
}
|
|
430
433
|
|
|
431
|
-
function persistPreProceedStartupFailure(asyncDir: string, runId: string, runnerProcessInstanceId: string, sessionId: string | undefined, message: string): void {
|
|
434
|
+
function persistPreProceedStartupFailure(asyncDir: string, runId: string, runnerProcessInstanceId: string, sessionId: string | undefined, completionOwnerId: string | undefined, message: string): void {
|
|
432
435
|
const now = Date.now();
|
|
433
436
|
try {
|
|
434
437
|
const statusPath = path.join(asyncDir, "status.json");
|
|
@@ -440,6 +443,7 @@ function persistPreProceedStartupFailure(asyncDir: string, runId: string, runner
|
|
|
440
443
|
...status,
|
|
441
444
|
runId,
|
|
442
445
|
...(sessionId ? { sessionId } : {}),
|
|
446
|
+
...(completionOwnerId ? { completionOwnerId } : {}),
|
|
443
447
|
state: "failed",
|
|
444
448
|
lastUpdate: now,
|
|
445
449
|
error: message,
|
|
@@ -491,10 +495,11 @@ function spawnRunner(cfg: object, suffix: string, cwd: string, onProcessTerminal
|
|
|
491
495
|
fs.writeFileSync(cfgPath, JSON.stringify(launchConfig));
|
|
492
496
|
const runner = path.join(path.dirname(fileURLToPath(import.meta.url)), "subagent-runner.ts");
|
|
493
497
|
const nodeCommand = resolveNodeExecutable();
|
|
494
|
-
const launchForStartup = launchConfig as typeof launchConfig & { asyncDir?: unknown; id?: unknown; sessionId?: unknown; revivalLease?: unknown };
|
|
498
|
+
const launchForStartup = launchConfig as typeof launchConfig & { asyncDir?: unknown; id?: unknown; sessionId?: unknown; completionOwnerId?: unknown; revivalLease?: unknown };
|
|
495
499
|
const launchAsyncDir = typeof launchForStartup.asyncDir === "string" ? launchForStartup.asyncDir : undefined;
|
|
496
500
|
const launchRunId = typeof launchForStartup.id === "string" ? launchForStartup.id : suffix;
|
|
497
501
|
const launchSessionId = typeof launchForStartup.sessionId === "string" ? launchForStartup.sessionId : undefined;
|
|
502
|
+
const launchCompletionOwnerId = typeof launchForStartup.completionOwnerId === "string" ? launchForStartup.completionOwnerId : undefined;
|
|
498
503
|
const startupPath = typeof launchForStartup.revivalLease === "object" && launchAsyncDir
|
|
499
504
|
? path.join(launchAsyncDir, "runner-startup.json")
|
|
500
505
|
: undefined;
|
|
@@ -584,7 +589,7 @@ function spawnRunner(cfg: object, suffix: string, cwd: string, onProcessTerminal
|
|
|
584
589
|
proc.unref();
|
|
585
590
|
if (startupPath && startupAckPath && startupProceedPath) {
|
|
586
591
|
const persistStartupFailure = (message: string) => {
|
|
587
|
-
if (launchAsyncDir) persistPreProceedStartupFailure(launchAsyncDir, launchRunId, runnerProcessInstanceId, launchSessionId, message);
|
|
592
|
+
if (launchAsyncDir) persistPreProceedStartupFailure(launchAsyncDir, launchRunId, runnerProcessInstanceId, launchSessionId, launchCompletionOwnerId, message);
|
|
588
593
|
};
|
|
589
594
|
const ready = waitForRunnerStartup(startupPath, "ready", RUNNER_STARTUP_TIMEOUT_MS);
|
|
590
595
|
if (ready.ok === false) {
|
|
@@ -669,13 +674,11 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
|
|
|
669
674
|
: chain;
|
|
670
675
|
const firstStep = chain[0];
|
|
671
676
|
const originalTask = params.task ?? (firstStep
|
|
672
|
-
? (
|
|
673
|
-
?
|
|
674
|
-
:
|
|
675
|
-
? firstStep.parallel
|
|
676
|
-
:
|
|
677
|
-
? firstStep.parallel.task
|
|
678
|
-
: (firstStep as SequentialStep).task)
|
|
677
|
+
? (isParallelStep(firstStep)
|
|
678
|
+
? firstStep.parallel[0]?.task
|
|
679
|
+
: isDynamicParallelStep(firstStep)
|
|
680
|
+
? firstStep.parallel.task
|
|
681
|
+
: (firstStep as SequentialStep).task)
|
|
679
682
|
: undefined);
|
|
680
683
|
try {
|
|
681
684
|
if (params.validateOutputBindings !== false) {
|
|
@@ -688,13 +691,11 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
|
|
|
688
691
|
const workflowGraph = buildWorkflowGraphSnapshot({ runId: id, mode: resultMode, steps: graphChain });
|
|
689
692
|
|
|
690
693
|
for (const s of chain) {
|
|
691
|
-
const stepAgents =
|
|
692
|
-
?
|
|
693
|
-
:
|
|
694
|
-
? s.parallel.
|
|
695
|
-
:
|
|
696
|
-
? [s.parallel.agent]
|
|
697
|
-
: [(s as SequentialStep).agent];
|
|
694
|
+
const stepAgents = isParallelStep(s)
|
|
695
|
+
? s.parallel.map((t) => t.agent)
|
|
696
|
+
: isDynamicParallelStep(s)
|
|
697
|
+
? [s.parallel.agent]
|
|
698
|
+
: [(s as SequentialStep).agent];
|
|
698
699
|
for (const agentName of stepAgents) {
|
|
699
700
|
if (!agents.find((x) => x.name === agentName)) {
|
|
700
701
|
return { error: `Unknown agent: ${agentName}` };
|
|
@@ -716,7 +717,8 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
|
|
|
716
717
|
};
|
|
717
718
|
const buildSeqStep = (s: SequentialStep, sessionFile?: string, behaviorCwd?: string, progressPrecreated = false, resolvedBehavior?: ResolvedStepBehavior, flatIndex?: number, parallelOutputNamespace?: { stepIndex: number; taskIndex?: number }, runFanoutPath?: string) => {
|
|
718
719
|
const a = agents.find((x) => x.name === s.agent)!;
|
|
719
|
-
const externalRunner = a.runner?.type === "external-cli";
|
|
720
|
+
const externalRunner = a.runner?.type === "external-cli" || a.runner?.type === "external-job";
|
|
721
|
+
const externalRunnerType = a.runner?.type;
|
|
720
722
|
if (externalRunner) {
|
|
721
723
|
const unsupported: string[] = [];
|
|
722
724
|
if (s.model !== undefined) unsupported.push("model override");
|
|
@@ -724,7 +726,7 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
|
|
|
724
726
|
if (s.acceptance !== undefined || params.agentContract !== undefined || s.agentContract !== undefined) unsupported.push("acceptance/agent contract");
|
|
725
727
|
if (s.toolBudget !== undefined || params.toolBudget !== undefined || a.toolBudget !== undefined || params.configToolBudget !== undefined) unsupported.push("tool budget");
|
|
726
728
|
if (params.contextForAgent?.(s.agent) === "fork") unsupported.push("fork context");
|
|
727
|
-
if (unsupported.length > 0) throw new AsyncStartValidationError(`Agent '${a.name}' uses runner.type='
|
|
729
|
+
if (unsupported.length > 0) throw new AsyncStartValidationError(`Agent '${a.name}' uses runner.type='${externalRunnerType}' and does not support: ${unsupported.join(", ")}.`);
|
|
728
730
|
}
|
|
729
731
|
try {
|
|
730
732
|
assertAgentAllowedByCapabilityCeiling(a.name, intersectSubagentCapabilityCeilings(params.capabilityCeiling, decodeSubagentCapabilityCeiling(process.env[SUBAGENT_CAPABILITY_CEILING_ENV])));
|
|
@@ -815,7 +817,7 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
|
|
|
815
817
|
const launchResolvedExtensions = externalRunner ? undefined : projectLaunchResolvedChildExtensions(toolPlan);
|
|
816
818
|
const permissionRules = resolvePermissionRules(ctx.permissions, a.permissions);
|
|
817
819
|
if (externalRunner && permissionRules) {
|
|
818
|
-
throw new AsyncStartValidationError(`Agent '${a.name}' uses runner.type='
|
|
820
|
+
throw new AsyncStartValidationError(`Agent '${a.name}' uses runner.type='${externalRunnerType}', which cannot enforce native Pi child permission rules.`);
|
|
819
821
|
}
|
|
820
822
|
return {
|
|
821
823
|
parentSessionId: ctx.parentSessionId ?? ctx.currentSessionId,
|
|
@@ -872,6 +874,7 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
|
|
|
872
874
|
...(s.outputSchema ? { structuredOutputSchema: s.outputSchema } : {}),
|
|
873
875
|
...(s.outputSchema ? { structuredOutput: createStructuredOutputRuntime(s.outputSchema, path.join(asyncDir, "structured-output")) } : {}),
|
|
874
876
|
...(resolvedToolBudget.budget ? { toolBudget: resolvedToolBudget.budget } : {}),
|
|
877
|
+
...(s.worktree ? { worktree: true } : {}),
|
|
875
878
|
};
|
|
876
879
|
};
|
|
877
880
|
|
|
@@ -890,9 +893,6 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
|
|
|
890
893
|
|
|
891
894
|
try {
|
|
892
895
|
const builtSteps = chain.map((s, stepIndex) => {
|
|
893
|
-
if (isCheckpointStep(s)) {
|
|
894
|
-
return { checkpoint: s.checkpoint, ...(s.message ? { message: s.message } : {}), phase: s.phase, label: s.label };
|
|
895
|
-
}
|
|
896
896
|
if (isParallelStep(s)) {
|
|
897
897
|
const parallelBehaviors = s.parallel.map((task) => {
|
|
898
898
|
const agent = agents.find((candidate) => candidate.name === task.agent)!;
|
|
@@ -958,8 +958,17 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
|
|
|
958
958
|
...(s.gateOn ? { gateOn: s.gateOn } : {}),
|
|
959
959
|
};
|
|
960
960
|
}
|
|
961
|
+
const sequential = s as SequentialStep;
|
|
962
|
+
let behaviorCwd: string | undefined;
|
|
963
|
+
if (sequential.worktree) {
|
|
964
|
+
try {
|
|
965
|
+
behaviorCwd = resolveExpectedWorktreeAgentCwd(runnerCwd, `${id}-s${stepIndex}`, 0, worktreeBaseDir);
|
|
966
|
+
} catch {
|
|
967
|
+
behaviorCwd = undefined;
|
|
968
|
+
}
|
|
969
|
+
}
|
|
961
970
|
const staticStep = nextFlatStep();
|
|
962
|
-
return buildSeqStep(
|
|
971
|
+
return buildSeqStep(sequential, staticStep.sessionFile, behaviorCwd, false, undefined, staticStep.index, undefined, `chain[${stepIndex}]`);
|
|
963
972
|
});
|
|
964
973
|
const steps = params.attachRoot
|
|
965
974
|
? [{
|
|
@@ -1028,7 +1037,6 @@ export function executeAsyncChain(
|
|
|
1028
1037
|
const resultMode = params.resultMode ?? "chain";
|
|
1029
1038
|
const acceptanceErrors = validateExecutionAcceptance({
|
|
1030
1039
|
chain: chain.map((step) => {
|
|
1031
|
-
if (isCheckpointStep(step)) return {};
|
|
1032
1040
|
if (isParallelStep(step)) return { parallel: step.parallel };
|
|
1033
1041
|
if (isDynamicParallelStep(step)) return { acceptance: step.acceptance, parallel: step.parallel };
|
|
1034
1042
|
return { acceptance: step.acceptance };
|
|
@@ -1127,6 +1135,7 @@ export function executeAsyncChain(
|
|
|
1127
1135
|
sessionDir: sessionRoot ? path.join(sessionRoot, `async-${id}`) : undefined,
|
|
1128
1136
|
asyncDir,
|
|
1129
1137
|
sessionId: ctx.currentSessionId,
|
|
1138
|
+
completionOwnerId: ctx.completionOwnerId ?? currentCompletionOwnerId(),
|
|
1130
1139
|
...(capabilityCeiling ? { capabilityCeiling } : {}),
|
|
1131
1140
|
piPackageRoot,
|
|
1132
1141
|
piArgv1: process.argv[1],
|
|
@@ -1253,6 +1262,7 @@ export function executeAsyncChain(
|
|
|
1253
1262
|
id,
|
|
1254
1263
|
pid: spawnResult.pid,
|
|
1255
1264
|
sessionId: ctx.currentSessionId,
|
|
1265
|
+
completionOwnerId: ctx.completionOwnerId ?? currentCompletionOwnerId(),
|
|
1256
1266
|
mode: resultMode,
|
|
1257
1267
|
agent: firstAgents[0],
|
|
1258
1268
|
agents: flatAgents,
|
|
@@ -1323,7 +1333,8 @@ export function executeAsyncSingle(
|
|
|
1323
1333
|
const task = params.task ?? "";
|
|
1324
1334
|
const acceptanceErrors = validateAcceptanceInput(params.acceptance);
|
|
1325
1335
|
if (acceptanceErrors.length > 0) return formatAsyncStartError("single", acceptanceErrors.join(" "));
|
|
1326
|
-
const externalRunner = agentConfig.runner?.type === "external-cli";
|
|
1336
|
+
const externalRunner = agentConfig.runner?.type === "external-cli" || agentConfig.runner?.type === "external-job";
|
|
1337
|
+
const externalRunnerType = agentConfig.runner?.type;
|
|
1327
1338
|
const permissionRules = resolvePermissionRules(ctx.permissions, agentConfig.permissions);
|
|
1328
1339
|
if (externalRunner) {
|
|
1329
1340
|
const unsupported: string[] = [];
|
|
@@ -1335,7 +1346,7 @@ export function executeAsyncSingle(
|
|
|
1335
1346
|
if (params.context === "fork") unsupported.push("fork context");
|
|
1336
1347
|
if ((params.skills?.length ?? 0) > 0) unsupported.push("skills");
|
|
1337
1348
|
if (permissionRules) unsupported.push("native Pi child permissions");
|
|
1338
|
-
if (unsupported.length > 0) return formatAsyncStartError("single", `Agent '${agentConfig.name}' uses runner.type='
|
|
1349
|
+
if (unsupported.length > 0) return formatAsyncStartError("single", `Agent '${agentConfig.name}' uses runner.type='${externalRunnerType}' and does not support: ${unsupported.join(", ")}.`);
|
|
1339
1350
|
}
|
|
1340
1351
|
const capabilityCeiling = intersectSubagentCapabilityCeilings(params.capabilityCeiling ?? resolveCurrentSubagentCapabilityCeiling(ctx.currentSessionId), decodeSubagentCapabilityCeiling(process.env[SUBAGENT_CAPABILITY_CEILING_ENV]));
|
|
1341
1352
|
try {
|
|
@@ -1344,6 +1355,10 @@ export function executeAsyncSingle(
|
|
|
1344
1355
|
return formatAsyncStartError("single", error instanceof Error ? error.message : String(error));
|
|
1345
1356
|
}
|
|
1346
1357
|
const runnerCwd = resolveChildCwd(ctx.cwd, cwd);
|
|
1358
|
+
const instructionCwd = params.worktree === true
|
|
1359
|
+
? resolveExpectedWorktreeAgentCwd(runnerCwd, `${id}-s0`, 0, worktreeBaseDir)
|
|
1360
|
+
: runnerCwd;
|
|
1361
|
+
const readExistenceCwd = params.worktree === true ? runnerCwd : instructionCwd;
|
|
1347
1362
|
const skillNames = params.skills ?? agentConfig.skills ?? [];
|
|
1348
1363
|
const availableModels = params.availableModels;
|
|
1349
1364
|
const { resolved: resolvedSkills, missing: missingSkills } = resolveSkillsWithFallback(
|
|
@@ -1385,7 +1400,7 @@ export function executeAsyncSingle(
|
|
|
1385
1400
|
}
|
|
1386
1401
|
|
|
1387
1402
|
const effectiveOutput = normalizeSingleOutputOverride(params.output, agentConfig.output);
|
|
1388
|
-
const outputPath = resolveSingleOutputPath(effectiveOutput, ctx.cwd,
|
|
1403
|
+
const outputPath = resolveSingleOutputPath(effectiveOutput, ctx.cwd, instructionCwd, params.outputBaseDir ?? (artifactsDir ? path.join(artifactsDir, "outputs", id) : undefined));
|
|
1389
1404
|
systemPrompt = injectOutputPathSystemPrompt(systemPrompt, outputPath, agentConfig);
|
|
1390
1405
|
const outputMode = params.outputMode ?? "inline";
|
|
1391
1406
|
const validationError = validateFileOnlyOutputMode(outputMode, outputPath, `Async single run (${agent})`);
|
|
@@ -1394,7 +1409,7 @@ export function executeAsyncSingle(
|
|
|
1394
1409
|
// Reads: caller override > agent defaultReads > none. `~`/`~/` expand to home;
|
|
1395
1410
|
// absolute paths pass through; relative paths resolve against the child cwd.
|
|
1396
1411
|
const reads = params.reads !== undefined ? params.reads : agentConfig.defaultReads ?? false;
|
|
1397
|
-
const readPaths = Array.isArray(reads) ? resolveExistingReadPaths(reads,
|
|
1412
|
+
const readPaths = Array.isArray(reads) ? resolveExistingReadPaths(reads, readExistenceCwd) : [];
|
|
1398
1413
|
const readsInstruction = readPaths.length > 0
|
|
1399
1414
|
? `[Read from: ${readPaths.join(", ")}]\n\n`
|
|
1400
1415
|
: "";
|
|
@@ -1570,6 +1585,7 @@ export function executeAsyncSingle(
|
|
|
1570
1585
|
...(structuredOutput ? { structuredOutput } : {}),
|
|
1571
1586
|
...(params.structuredOutputSchema ? { structuredOutputSchema: params.structuredOutputSchema } : {}),
|
|
1572
1587
|
...(resolvedToolBudget.budget ? { toolBudget: resolvedToolBudget.budget } : {}),
|
|
1588
|
+
...(params.worktree === true ? { worktree: true } : {}),
|
|
1573
1589
|
},
|
|
1574
1590
|
],
|
|
1575
1591
|
resultPath: params.parentWorkflowRunId !== undefined && params.revivalLease !== undefined
|
|
@@ -1584,6 +1600,7 @@ export function executeAsyncSingle(
|
|
|
1584
1600
|
sessionDir: resolvedSessionDir,
|
|
1585
1601
|
asyncDir,
|
|
1586
1602
|
sessionId: ctx.currentSessionId,
|
|
1603
|
+
completionOwnerId: ctx.completionOwnerId ?? currentCompletionOwnerId(),
|
|
1587
1604
|
...(capabilityCeiling ? { capabilityCeiling } : {}),
|
|
1588
1605
|
piPackageRoot,
|
|
1589
1606
|
piArgv1: process.argv[1],
|
|
@@ -1677,6 +1694,7 @@ export function executeAsyncSingle(
|
|
|
1677
1694
|
id,
|
|
1678
1695
|
pid: spawnResult.pid,
|
|
1679
1696
|
sessionId: ctx.currentSessionId,
|
|
1697
|
+
completionOwnerId: ctx.completionOwnerId ?? currentCompletionOwnerId(),
|
|
1680
1698
|
mode: "single",
|
|
1681
1699
|
agent,
|
|
1682
1700
|
task: task?.trim() ? PROMPT_REDACTED : undefined,
|
|
@@ -20,6 +20,8 @@ import { normalizeParallelGroups } from "./parallel-groups.ts";
|
|
|
20
20
|
import { reconcileAsyncRun, reconcileNestedAsyncDescendants } from "./stale-run-reconciler.ts";
|
|
21
21
|
import { findNestedRouteForRootId, hasLiveNestedDescendants, updateAsyncJobNestedProjection } from "../shared/nested-events.ts";
|
|
22
22
|
import { listAsyncRuns, type AsyncRunSummary } from "./async-status.ts";
|
|
23
|
+
import { EXTERNAL_JOB_BRIDGE_REQUEST_DIR, serviceExternalJobBridgeRequests } from "../shared/external-job-bridge.ts";
|
|
24
|
+
import { shouldUseNativeFsWatch } from "../../shared/watch-strategy.ts";
|
|
23
25
|
|
|
24
26
|
interface AsyncJobTrackerOptions {
|
|
25
27
|
completionRetentionMs?: number;
|
|
@@ -27,6 +29,9 @@ interface AsyncJobTrackerOptions {
|
|
|
27
29
|
pollIntervalMs?: number;
|
|
28
30
|
resultsDir?: string;
|
|
29
31
|
widgetEnabled?: boolean;
|
|
32
|
+
platform?: NodeJS.Platform;
|
|
33
|
+
onJobTerminal?: () => void;
|
|
34
|
+
watch?: typeof fs.watch;
|
|
30
35
|
kill?: (pid: number, signal?: NodeJS.Signals | 0) => boolean;
|
|
31
36
|
now?: () => number;
|
|
32
37
|
}
|
|
@@ -66,6 +71,9 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
66
71
|
const runningJobIds = new Set<string>();
|
|
67
72
|
let rootWatcher: fs.FSWatcher | undefined;
|
|
68
73
|
let nextLivenessAt = Date.now() + livenessIntervalMs;
|
|
74
|
+
const watch = options.watch ?? fs.watch;
|
|
75
|
+
const useNativeWatcher = () => shouldUseNativeFsWatch("async-job-tracker", options.platform);
|
|
76
|
+
const terminalStatus = (status: string) => status === "complete" || status === "failed" || status === "paused" || status === "stopped";
|
|
69
77
|
const rerenderWidget = (ctx: ExtensionContext, jobs = Array.from(state.asyncJobs.values())) => {
|
|
70
78
|
if (state.widgetsSuspended) return;
|
|
71
79
|
renderWidget(ctx, options.widgetEnabled === false ? [] : jobs);
|
|
@@ -139,6 +147,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
139
147
|
mode: run.mode,
|
|
140
148
|
context: run.context,
|
|
141
149
|
cwd: run.cwd,
|
|
150
|
+
sessionRoot: run.sessionRoot,
|
|
142
151
|
agents: visibleSteps.map((step) => step.agent),
|
|
143
152
|
currentStep: run.currentStep,
|
|
144
153
|
chainStepCount: run.chainStepCount,
|
|
@@ -327,6 +336,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
327
336
|
};
|
|
328
337
|
try {
|
|
329
338
|
emitNewControlEvents(job);
|
|
339
|
+
serviceExternalJobBridgeRequests(job.asyncDir);
|
|
330
340
|
try {
|
|
331
341
|
if (job.nestedRoute) reconcileNestedAsyncDescendants(job.nestedRoute, { resultsDir, kill: options.kill, now: options.now });
|
|
332
342
|
} catch (error) {
|
|
@@ -342,6 +352,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
342
352
|
runId: job.asyncId,
|
|
343
353
|
pid: job.pid,
|
|
344
354
|
sessionId: job.sessionId,
|
|
355
|
+
completionOwnerId: job.completionOwnerId,
|
|
345
356
|
mode: job.mode,
|
|
346
357
|
agents: job.agents,
|
|
347
358
|
chainStepCount: job.chainStepCount,
|
|
@@ -404,7 +415,8 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
404
415
|
job.turnBudgetExceeded = status.turnBudgetExceeded ?? job.turnBudgetExceeded;
|
|
405
416
|
job.wrapUpRequested = status.wrapUpRequested ?? job.wrapUpRequested;
|
|
406
417
|
job.sessionFile = status.sessionFile ?? job.sessionFile;
|
|
407
|
-
if (job.status
|
|
418
|
+
if (terminalStatus(job.status)) {
|
|
419
|
+
if (!terminalStatus(previousStatus)) options.onJobTerminal?.();
|
|
408
420
|
rememberFleetJob(state, job);
|
|
409
421
|
if (!nestedRefreshFailed && !hasLiveNestedDescendants(job.nestedChildren) && (previousStatus !== job.status || !state.cleanupTimers.has(job.asyncId))) {
|
|
410
422
|
scheduleCleanup(job.asyncId);
|
|
@@ -442,6 +454,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
442
454
|
};
|
|
443
455
|
|
|
444
456
|
const watchJob = (job: AsyncJobState) => {
|
|
457
|
+
if (!useNativeWatcher()) return;
|
|
445
458
|
const watched = jobWatchers.get(job.asyncId) ?? { watchers: new Map<string, fs.FSWatcher>() };
|
|
446
459
|
const active = job.status === "queued" || job.status === "running";
|
|
447
460
|
const watchPath = (watchPath: string): boolean => {
|
|
@@ -449,11 +462,11 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
449
462
|
const nestedEventSink = job.nestedRoute?.eventSink === watchPath;
|
|
450
463
|
let watcher: fs.FSWatcher;
|
|
451
464
|
try {
|
|
452
|
-
watcher =
|
|
465
|
+
watcher = watch(resolveWatchPath(watchPath), (_event, file) => {
|
|
453
466
|
const rawFileName = file?.toString();
|
|
454
467
|
const fileName = rawFileName ?? path.basename(watchPath);
|
|
455
468
|
const nestedEventFile = nestedEventSink && (!rawFileName || rawFileName.endsWith(".json") || rawFileName.endsWith(".jsonl"));
|
|
456
|
-
if (fileName === "status.json" || fileName === "events.jsonl" || nestedEventFile) {
|
|
469
|
+
if (fileName === "status.json" || fileName === "events.jsonl" || fileName === EXTERNAL_JOB_BRIDGE_REQUEST_DIR || fileName.endsWith(".json") || nestedEventFile) {
|
|
457
470
|
scheduleJobRefresh(job.asyncId);
|
|
458
471
|
}
|
|
459
472
|
if (watchPath !== job.asyncDir && !nestedEventSink) {
|
|
@@ -480,6 +493,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
480
493
|
const statusWatched = watchPath(statusPath);
|
|
481
494
|
if (statusWatched && !hadStatusWatcher) scheduleJobRefresh(job.asyncId);
|
|
482
495
|
watchPath(path.join(job.asyncDir, "events.jsonl"));
|
|
496
|
+
watchPath(path.join(job.asyncDir, EXTERNAL_JOB_BRIDGE_REQUEST_DIR));
|
|
483
497
|
if (job.nestedRoute) watchPath(job.nestedRoute.eventSink);
|
|
484
498
|
if (!statusWatched && active && !watched.retryTimer) {
|
|
485
499
|
watched.retryTimer = setTimeout(() => {
|
|
@@ -497,9 +511,10 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
497
511
|
};
|
|
498
512
|
|
|
499
513
|
const watchAsyncRoot = () => {
|
|
514
|
+
if (!useNativeWatcher()) return;
|
|
500
515
|
if (rootWatcher) return;
|
|
501
516
|
try {
|
|
502
|
-
rootWatcher =
|
|
517
|
+
rootWatcher = watch(resolveWatchPath(asyncDirRoot), (_event, file) => {
|
|
503
518
|
const runDirName = file?.toString();
|
|
504
519
|
for (const job of state.asyncJobs.values()) {
|
|
505
520
|
if (jobWatchers.has(job.asyncId) || (runDirName && path.basename(job.asyncDir) !== runDirName)) continue;
|
|
@@ -568,6 +583,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
568
583
|
status: "queued",
|
|
569
584
|
pid: typeof info.pid === "number" ? info.pid : undefined,
|
|
570
585
|
...(typeof info.sessionId === "string" ? { sessionId: info.sessionId } : {}),
|
|
586
|
+
...(typeof info.completionOwnerId === "string" ? { completionOwnerId: info.completionOwnerId } : {}),
|
|
571
587
|
mode: info.mode ?? (info.chain ? "chain" : "single"),
|
|
572
588
|
description: info.goal ?? info.task,
|
|
573
589
|
agents,
|
|
@@ -8,7 +8,9 @@ import { intersectSubagentCapabilityCeilings, parseSubagentCapabilityCeiling, ty
|
|
|
8
8
|
import { validateRunFanoutBudgetDescriptor } from "../shared/run-fanout-budget.ts";
|
|
9
9
|
import { resolveTurnBudgetConfig } from "../shared/turn-budget.ts";
|
|
10
10
|
import { reconcileAsyncRun } from "./stale-run-reconciler.ts";
|
|
11
|
-
import { resultFilePath } from "./result-files.ts";
|
|
11
|
+
import { resultFilePath, resultPayloadPathForIndexedRun } from "./result-files.ts";
|
|
12
|
+
import { canScanAsyncRunPrefix, MIN_SAFE_ASYNC_RUN_PREFIX_LENGTH } from "./run-id-query.ts";
|
|
13
|
+
import { parallelHandoffPath, resolveRetainedWorktreeCwd } from "../shared/parallel-handoff.ts";
|
|
12
14
|
|
|
13
15
|
export interface AsyncResumeParams {
|
|
14
16
|
id?: string;
|
|
@@ -173,6 +175,8 @@ function prefixedRunIds(dir: string, prefix: string, suffix = ""): string[] {
|
|
|
173
175
|
}
|
|
174
176
|
|
|
175
177
|
function exactResultPath(resultsDir: string, runId: string): string | null {
|
|
178
|
+
const indexed = resultPayloadPathForIndexedRun(resultsDir, runId);
|
|
179
|
+
if (indexed) return indexed;
|
|
176
180
|
const resultPath = resultFilePath(resultsDir, runId);
|
|
177
181
|
assertInsideRoot(resultsDir, resultPath, "Async result file");
|
|
178
182
|
return fs.existsSync(resultPath) ? resultPath : null;
|
|
@@ -181,6 +185,7 @@ function exactResultPath(resultsDir: string, runId: string): string | null {
|
|
|
181
185
|
export function findAsyncRunPrefixMatches(prefix: string, asyncDirRoot: string, resultsDir: string): Array<{ id: string; location: AsyncRunLocation }> {
|
|
182
186
|
const requestedId = assertRunId(prefix, "id");
|
|
183
187
|
if (!requestedId) return [];
|
|
188
|
+
if (!canScanAsyncRunPrefix(requestedId)) return [];
|
|
184
189
|
const asyncRoot = path.resolve(asyncDirRoot);
|
|
185
190
|
const resultRoot = path.resolve(resultsDir);
|
|
186
191
|
const matchingIds = prefixedRunIds(asyncRoot, requestedId).sort();
|
|
@@ -223,6 +228,10 @@ export function resolveAsyncRunLocation(params: AsyncResumeParams, asyncDirRoot:
|
|
|
223
228
|
resolvedId: requestedId,
|
|
224
229
|
};
|
|
225
230
|
}
|
|
231
|
+
if (requestedId.length < MIN_SAFE_ASYNC_RUN_PREFIX_LENGTH) {
|
|
232
|
+
throw new Error(`Async run id prefix '${requestedId}' is too short. Provide at least ${MIN_SAFE_ASYNC_RUN_PREFIX_LENGTH} characters.`);
|
|
233
|
+
}
|
|
234
|
+
if (!canScanAsyncRunPrefix(requestedId)) return { asyncDir: null, resultPath: null, resolvedId: requestedId };
|
|
226
235
|
|
|
227
236
|
const matching = findAsyncRunPrefixMatches(requestedId, asyncRoot, resultRoot);
|
|
228
237
|
if (matching.length === 0) return { asyncDir: null, resultPath: null, resolvedId: requestedId };
|
|
@@ -261,12 +270,7 @@ function validateStatusForResume(status: AsyncStatus | null, source: string): vo
|
|
|
261
270
|
}
|
|
262
271
|
}
|
|
263
272
|
|
|
264
|
-
function normalizeRecoveryAcceptance(value: unknown, descriptorPath: string): AcceptanceInput
|
|
265
|
-
if (value && typeof value === "object" && !Array.isArray(value) && ("explicit" in value || "inferredReason" in value)) {
|
|
266
|
-
const { explicit, inferredReason: _inferredReason, ...publicAcceptance } = value as Record<string, unknown>;
|
|
267
|
-
if (explicit === false) return undefined;
|
|
268
|
-
value = publicAcceptance;
|
|
269
|
-
}
|
|
273
|
+
function normalizeRecoveryAcceptance(value: unknown, descriptorPath: string): AcceptanceInput {
|
|
270
274
|
const errors = validateAcceptanceInput(value, "recoveryDescriptor.acceptance");
|
|
271
275
|
if (errors.length) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': ${errors.join(" ")}`);
|
|
272
276
|
return value as AcceptanceInput;
|
|
@@ -395,11 +399,7 @@ export function readAsyncRecoveryDescriptor(asyncDir: string | undefined): Steer
|
|
|
395
399
|
if (!Array.isArray(control.notifyOn) || control.notifyOn.some((item) => item !== "active_long_running" && item !== "needs_attention")) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': controlConfig.notifyOn is invalid.`);
|
|
396
400
|
if (!Array.isArray(control.notifyChannels) || control.notifyChannels.some((item) => item !== "event" && item !== "async" && item !== "intercom")) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': controlConfig.notifyChannels is invalid.`);
|
|
397
401
|
}
|
|
398
|
-
if (parsed.acceptance !== undefined)
|
|
399
|
-
const acceptance = normalizeRecoveryAcceptance(parsed.acceptance, descriptorPath);
|
|
400
|
-
if (acceptance === undefined) delete parsed.acceptance;
|
|
401
|
-
else parsed.acceptance = acceptance;
|
|
402
|
-
}
|
|
402
|
+
if (parsed.acceptance !== undefined) parsed.acceptance = normalizeRecoveryAcceptance(parsed.acceptance, descriptorPath);
|
|
403
403
|
return parsed as unknown as SteeringRecoveryDescriptor;
|
|
404
404
|
}
|
|
405
405
|
|
|
@@ -410,6 +410,17 @@ function validateResumeSessionFile(runId: string, sessionFile: string): string {
|
|
|
410
410
|
return resolved;
|
|
411
411
|
}
|
|
412
412
|
|
|
413
|
+
function validateResumeCwd(runId: string, cwd: string | undefined): string | undefined {
|
|
414
|
+
if (!cwd) return undefined;
|
|
415
|
+
const resolved = path.resolve(cwd);
|
|
416
|
+
try {
|
|
417
|
+
if (!fs.statSync(resolved).isDirectory()) throw new Error("path is not a directory");
|
|
418
|
+
} catch (error) {
|
|
419
|
+
throw new Error(`Async run '${runId}' required cwd does not exist: ${cwd}`, { cause: error instanceof Error ? error : undefined });
|
|
420
|
+
}
|
|
421
|
+
return resolved;
|
|
422
|
+
}
|
|
423
|
+
|
|
413
424
|
export function resolveAsyncResumeTarget(params: AsyncResumeParams, deps: AsyncResumeDeps = {}, options: AsyncResumeOptions = {}): AsyncResumeTarget {
|
|
414
425
|
const asyncDirRoot = deps.asyncDirRoot ?? DIRS.async;
|
|
415
426
|
const resultsDir = deps.resultsDir ?? DIRS.results;
|
|
@@ -510,6 +521,10 @@ export function resolveAsyncResumeTarget(params: AsyncResumeParams, deps: AsyncR
|
|
|
510
521
|
const stepModel = statusSteps[index]?.model ?? resultSteps[index]?.model ?? (stepCount === 1 ? result?.model : undefined);
|
|
511
522
|
const stepThinking = statusSteps[index]?.thinking ?? resultSteps[index]?.thinking ?? (stepCount === 1 ? result?.thinking : undefined);
|
|
512
523
|
const capabilityCeiling = intersectSubagentCapabilityCeilings(status?.capabilityCeiling, statusSteps[index]?.capabilityCeiling, result?.capabilityCeiling, resultSteps[index]?.capabilityCeiling);
|
|
524
|
+
const managedWorktreeCwd = location.asyncDir
|
|
525
|
+
? resolveRetainedWorktreeCwd(parallelHandoffPath(location.asyncDir), runId, index)
|
|
526
|
+
: undefined;
|
|
527
|
+
const resumeCwd = validateResumeCwd(runId, managedWorktreeCwd ?? status?.cwd ?? result?.cwd ?? recoveryDescriptor?.cwd);
|
|
513
528
|
|
|
514
529
|
return {
|
|
515
530
|
kind: "revive",
|
|
@@ -518,7 +533,7 @@ export function resolveAsyncResumeTarget(params: AsyncResumeParams, deps: AsyncR
|
|
|
518
533
|
state,
|
|
519
534
|
agent,
|
|
520
535
|
index,
|
|
521
|
-
cwd:
|
|
536
|
+
...(resumeCwd ? { cwd: resumeCwd } : {}),
|
|
522
537
|
...(resolvedSessionFile ? { sessionFile: resolvedSessionFile } : {}),
|
|
523
538
|
...(stepModel ? { model: stepModel } : {}),
|
|
524
539
|
...(stepThinking ? { thinking: stepThinking } : {}),
|