pi-subagents 0.46.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 +37 -0
- package/agents/reviewer.md +3 -4
- package/docs/agents.md +1 -1
- package/docs/configuration.md +14 -6
- package/docs/extension-api.md +1 -1
- package/docs/missions.md +5 -3
- package/docs/models.md +3 -1
- package/docs/observability.md +3 -3
- package/docs/tool-reference.md +2 -2
- package/docs/watchdog.md +1 -1
- package/package.json +1 -1
- package/skills/pi-subagents/references/execution-controls.md +4 -4
- package/skills/pi-subagents/references/management-authoring-rpc.md +1 -1
- package/src/extension/config.ts +13 -0
- package/src/extension/fanout-child.ts +5 -4
- package/src/extension/index.ts +39 -14
- package/src/extension/rpc.ts +7 -6
- package/src/extension/schemas.ts +27 -7
- package/src/extension/tool-description.ts +26 -8
- package/src/inspectors/herdr/project-panes.ts +2 -1
- package/src/intercom/native-supervisor-channel.ts +102 -4
- package/src/missions/store.ts +2 -1
- package/src/missions/workflow-state.ts +19 -13
- package/src/runs/background/active-run-index.ts +42 -0
- package/src/runs/background/async-execution.ts +10 -5
- package/src/runs/background/async-job-tracker.ts +293 -133
- package/src/runs/background/async-resume.ts +19 -3
- package/src/runs/background/async-status.ts +33 -5
- package/src/runs/background/control-channel.ts +91 -17
- package/src/runs/background/result-watcher.ts +102 -7
- package/src/runs/background/run-status.ts +18 -0
- package/src/runs/background/scheduled-runs.ts +25 -1
- package/src/runs/background/stale-run-reconciler.ts +12 -24
- package/src/runs/background/subagent-runner.ts +68 -10
- package/src/runs/foreground/async-dismiss-action.ts +85 -0
- package/src/runs/foreground/async-steering-action.ts +6 -7
- package/src/runs/foreground/chain-execution.ts +3 -0
- package/src/runs/foreground/execution.ts +3 -0
- package/src/runs/foreground/subagent-executor.ts +129 -20
- package/src/runs/foreground/workflow-foreground-steering.ts +187 -0
- package/src/runs/shared/dynamic-fanout.ts +1 -1
- package/src/runs/shared/model-fallback.ts +8 -4
- package/src/runs/shared/model-scope.ts +12 -2
- package/src/runs/shared/parallel-utils.ts +1 -0
- package/src/runs/shared/subagent-prompt-runtime.ts +24 -5
- package/src/runs/shared/worktree.ts +3 -2
- package/src/shared/artifacts.ts +15 -15
- package/src/shared/display-text.ts +100 -0
- package/src/shared/file-coalescer.ts +9 -0
- package/src/shared/formatters.ts +4 -6
- package/src/shared/settings.ts +15 -2
- package/src/shared/types.ts +16 -2
- package/src/shared/utils.ts +43 -33
- package/src/slash/slash-commands.ts +3 -1
- package/src/tui/fleet-status.ts +14 -10
- package/src/tui/render.ts +30 -26
- package/src/watchdog/change-signature.ts +4 -3
- package/src/workflows/scripted-workflow.ts +4 -1
|
@@ -5,6 +5,8 @@ import * as path from "node:path";
|
|
|
5
5
|
import { pathToFileURL } from "node:url";
|
|
6
6
|
import type { Message } from "@earendil-works/pi-ai";
|
|
7
7
|
import { writeAtomicJson } from "../../shared/atomic-json.ts";
|
|
8
|
+
import { createFileCoalescer } from "../../shared/file-coalescer.ts";
|
|
9
|
+
import { isActiveAsyncState, updateActiveRunIndex } from "./active-run-index.ts";
|
|
8
10
|
import { createChildTranscriptWriter, type ChildTranscriptWriter } from "../../shared/child-transcript.ts";
|
|
9
11
|
import { closeSteerInbox, consumeInterruptRequest, consumeSteerRequests, deliverInterruptRequest, deliverStopRequest, deliverTimeoutRequest, enqueueStepSteer, steerAcksDir, steerCapabilityPath, stepSteerInboxDir, watchAsyncControlInbox, type SteerAck, type SteerCapability, type SteerRequest } from "./control-channel.ts";
|
|
10
12
|
import { appendJsonl as appendRawJsonl, formatOutputArtifactContent, getArtifactPaths, writeArtifact, writeMetadata } from "../../shared/artifacts.ts";
|
|
@@ -1955,6 +1957,47 @@ function combinedAbortSignal(signals: Array<AbortSignal | undefined>): AbortSign
|
|
|
1955
1957
|
return controller.signal;
|
|
1956
1958
|
}
|
|
1957
1959
|
|
|
1960
|
+
async function runSingleStepWithTimeout(
|
|
1961
|
+
step: SubagentStep,
|
|
1962
|
+
ctx: SingleStepContext,
|
|
1963
|
+
parentDeadlineAt?: number,
|
|
1964
|
+
): Promise<SingleStepResult> {
|
|
1965
|
+
if (step.timeoutMs === undefined) return runSingleStep(step, ctx);
|
|
1966
|
+
|
|
1967
|
+
const parentRemainingMs = parentDeadlineAt === undefined ? undefined : Math.max(0, parentDeadlineAt - Date.now());
|
|
1968
|
+
const timeoutMs = parentRemainingMs === undefined ? step.timeoutMs : Math.min(step.timeoutMs, parentRemainingMs);
|
|
1969
|
+
const timeoutMessage = parentRemainingMs !== undefined && parentRemainingMs <= step.timeoutMs
|
|
1970
|
+
? ctx.timeoutMessage
|
|
1971
|
+
: `Subagent timed out after ${step.timeoutMs}ms.`;
|
|
1972
|
+
const timeoutController = new AbortController();
|
|
1973
|
+
let timeoutAction: (() => void) | undefined;
|
|
1974
|
+
let timeoutTriggered = false;
|
|
1975
|
+
const triggerTimeout = (): void => {
|
|
1976
|
+
if (timeoutTriggered) return;
|
|
1977
|
+
timeoutTriggered = true;
|
|
1978
|
+
timeoutController.abort();
|
|
1979
|
+
timeoutAction?.();
|
|
1980
|
+
};
|
|
1981
|
+
const registerTimeout = (action: (() => void) | undefined): void => {
|
|
1982
|
+
timeoutAction = action;
|
|
1983
|
+
ctx.registerTimeout?.(action ? triggerTimeout : undefined);
|
|
1984
|
+
if (action && timeoutTriggered) action();
|
|
1985
|
+
};
|
|
1986
|
+
const timer = setTimeout(triggerTimeout, timeoutMs);
|
|
1987
|
+
timer.unref?.();
|
|
1988
|
+
try {
|
|
1989
|
+
return await runSingleStep(step, {
|
|
1990
|
+
...ctx,
|
|
1991
|
+
registerTimeout,
|
|
1992
|
+
timeoutSignal: combinedAbortSignal([ctx.timeoutSignal, timeoutController.signal]),
|
|
1993
|
+
timeoutMessage,
|
|
1994
|
+
});
|
|
1995
|
+
} finally {
|
|
1996
|
+
clearTimeout(timer);
|
|
1997
|
+
ctx.registerTimeout?.(undefined);
|
|
1998
|
+
}
|
|
1999
|
+
}
|
|
2000
|
+
|
|
1958
2001
|
async function runSubagent(
|
|
1959
2002
|
config: SubagentRunConfig,
|
|
1960
2003
|
onWriterProcess?: (writer: { state: "none" | "spawning" } | { state: "running"; pid: number }) => void,
|
|
@@ -2139,6 +2182,7 @@ async function runSubagent(
|
|
|
2139
2182
|
|
|
2140
2183
|
fs.mkdirSync(asyncDir, { recursive: true });
|
|
2141
2184
|
writeAtomicJson(statusPath, statusPayload);
|
|
2185
|
+
updateActiveRunIndex(asyncDir, statusPayload.state);
|
|
2142
2186
|
let pendingParallelUsageCost: CostSummary = { inputTokens: 0, outputTokens: 0, costUsd: 0 };
|
|
2143
2187
|
const currentUsageTotals = (): CostSummary => {
|
|
2144
2188
|
const cost = results.reduce<CostSummary>((sum, result) => ({
|
|
@@ -2211,11 +2255,25 @@ async function runSubagent(
|
|
|
2211
2255
|
for (const node of graph.nodes) updateNode(node);
|
|
2212
2256
|
statusPayload.workflowGraph = graph;
|
|
2213
2257
|
};
|
|
2214
|
-
|
|
2258
|
+
let lastIndexedActiveState = isActiveAsyncState(statusPayload.state);
|
|
2259
|
+
const writeStatusPayloadNow = (): void => {
|
|
2215
2260
|
refreshWorkflowGraph();
|
|
2216
2261
|
writeAtomicJson(statusPath, statusPayload);
|
|
2262
|
+
const activeState = isActiveAsyncState(statusPayload.state);
|
|
2263
|
+
if (activeState !== lastIndexedActiveState) {
|
|
2264
|
+
updateActiveRunIndex(asyncDir, statusPayload.state);
|
|
2265
|
+
lastIndexedActiveState = activeState;
|
|
2266
|
+
}
|
|
2217
2267
|
emitNestedSelfEvent(statusPayload.state === "running" || statusPayload.state === "queued" ? "subagent.nested.updated" : "subagent.nested.completed");
|
|
2218
2268
|
};
|
|
2269
|
+
const statusWriteCoalescer = createFileCoalescer(writeStatusPayloadNow, 100);
|
|
2270
|
+
const writeStatusPayload = (immediate = true): void => {
|
|
2271
|
+
if (immediate || statusPayload.state !== "running" || statusPayload.activityState !== undefined) {
|
|
2272
|
+
if (!statusWriteCoalescer.flush(statusPath)) writeStatusPayloadNow();
|
|
2273
|
+
return;
|
|
2274
|
+
}
|
|
2275
|
+
statusWriteCoalescer.schedule(statusPath);
|
|
2276
|
+
};
|
|
2219
2277
|
const updateExternalProcess = (index: number, process: ExternalProcessStatus): void => {
|
|
2220
2278
|
requiredStatusStep(statusPayload, index).externalProcess = process;
|
|
2221
2279
|
statusPayload.lastUpdate = Date.now();
|
|
@@ -2742,7 +2800,7 @@ async function runSubagent(
|
|
|
2742
2800
|
step.lastActivityAt = now;
|
|
2743
2801
|
statusPayload.lastActivityAt = now;
|
|
2744
2802
|
statusPayload.lastUpdate = now;
|
|
2745
|
-
writeStatusPayload();
|
|
2803
|
+
writeStatusPayload(false);
|
|
2746
2804
|
return;
|
|
2747
2805
|
}
|
|
2748
2806
|
if (event.type === "tool_execution_start" && event.toolName) {
|
|
@@ -2878,7 +2936,7 @@ async function runSubagent(
|
|
|
2878
2936
|
statusPayload.lastActivityAt = now;
|
|
2879
2937
|
statusPayload.lastUpdate = now;
|
|
2880
2938
|
maybeEmitActiveLongRunning(flatIndex, now);
|
|
2881
|
-
writeStatusPayload();
|
|
2939
|
+
writeStatusPayload(false);
|
|
2882
2940
|
};
|
|
2883
2941
|
const updateRunnerActivityState = (now: number): boolean => {
|
|
2884
2942
|
if (!controlConfig.enabled) return false;
|
|
@@ -3449,7 +3507,7 @@ async function runSubagent(
|
|
|
3449
3507
|
writeStatusPayload();
|
|
3450
3508
|
appendJsonl(eventsPath, JSON.stringify({ type: "subagent.step.started", ts: taskStartTime, runId: id, stepIndex: fi, agent: task.agent }));
|
|
3451
3509
|
flushPendingStepSteers(fi);
|
|
3452
|
-
const singleResult = await
|
|
3510
|
+
const singleResult = await runSingleStepWithTimeout(task, compactOptional<SingleStepContext>({
|
|
3453
3511
|
previousOutput, placeholder, cwd, sessionEnabled,
|
|
3454
3512
|
outputs,
|
|
3455
3513
|
sessionDir: config.sessionDir ? path.join(config.sessionDir, `dynamic-${stepIndex}-${taskIdx}`) : undefined,
|
|
@@ -3479,7 +3537,7 @@ async function runSubagent(
|
|
|
3479
3537
|
onWriterProcess,
|
|
3480
3538
|
onExternalProcess: (process) => updateExternalProcess(fi, process),
|
|
3481
3539
|
skipAcceptance: () => timedOut || stopped,
|
|
3482
|
-
}));
|
|
3540
|
+
}), config.deadlineAt);
|
|
3483
3541
|
const taskEndTime = Date.now();
|
|
3484
3542
|
const childInterrupted = singleResult.interrupted === true;
|
|
3485
3543
|
const childStopped = singleResult.stopped === true;
|
|
@@ -3831,7 +3889,7 @@ async function runSubagent(
|
|
|
3831
3889
|
const { taskForRun, taskCwd } = prepareParallelTaskRun(task, cwd, worktreeSetup, taskIdx);
|
|
3832
3890
|
flushPendingStepSteers(fi);
|
|
3833
3891
|
|
|
3834
|
-
const singleResult = await
|
|
3892
|
+
const singleResult = await runSingleStepWithTimeout(taskForRun, compactOptional<SingleStepContext>({
|
|
3835
3893
|
previousOutput, placeholder, cwd: taskCwd, sessionEnabled,
|
|
3836
3894
|
outputs,
|
|
3837
3895
|
sessionDir: taskSessionDir,
|
|
@@ -3861,7 +3919,7 @@ async function runSubagent(
|
|
|
3861
3919
|
onWriterProcess,
|
|
3862
3920
|
onExternalProcess: (process) => updateExternalProcess(fi, process),
|
|
3863
3921
|
skipAcceptance: () => timedOut || stopped,
|
|
3864
|
-
}));
|
|
3922
|
+
}), config.deadlineAt);
|
|
3865
3923
|
if (task.sessionFile) {
|
|
3866
3924
|
latestSessionFile = task.sessionFile;
|
|
3867
3925
|
}
|
|
@@ -4120,7 +4178,7 @@ async function runSubagent(
|
|
|
4120
4178
|
}));
|
|
4121
4179
|
|
|
4122
4180
|
flushPendingStepSteers(flatIndex);
|
|
4123
|
-
const singleResult = await
|
|
4181
|
+
const singleResult = await runSingleStepWithTimeout(seqStep, compactOptional<SingleStepContext>({
|
|
4124
4182
|
previousOutput, placeholder, cwd, sessionEnabled,
|
|
4125
4183
|
outputs: statusPayload.mode === "single" ? undefined : outputs,
|
|
4126
4184
|
sessionDir: config.sessionDir,
|
|
@@ -4150,7 +4208,7 @@ async function runSubagent(
|
|
|
4150
4208
|
onWriterProcess,
|
|
4151
4209
|
onExternalProcess: (process) => updateExternalProcess(flatIndex, process),
|
|
4152
4210
|
skipAcceptance: () => timedOut || stopped,
|
|
4153
|
-
}));
|
|
4211
|
+
}), config.deadlineAt);
|
|
4154
4212
|
if (seqStep.sessionFile) {
|
|
4155
4213
|
latestSessionFile = seqStep.sessionFile;
|
|
4156
4214
|
}
|
|
@@ -4439,6 +4497,7 @@ async function runSubagent(
|
|
|
4439
4497
|
statusPayload.error = `Step failed: ${failedStep.agent}`;
|
|
4440
4498
|
}
|
|
4441
4499
|
}
|
|
4500
|
+
writeStatusPayload();
|
|
4442
4501
|
try {
|
|
4443
4502
|
writeAtomicJson(resultPath, {
|
|
4444
4503
|
lifecycleArtifactVersion: SUBAGENT_LIFECYCLE_ARTIFACT_VERSION,
|
|
@@ -4592,7 +4651,6 @@ async function runSubagent(
|
|
|
4592
4651
|
console.error(`Failed to write process-terminal candidate for '${id}':`, error);
|
|
4593
4652
|
}
|
|
4594
4653
|
}
|
|
4595
|
-
writeStatusPayload();
|
|
4596
4654
|
}
|
|
4597
4655
|
|
|
4598
4656
|
async function waitForStartupControl(
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
4
|
+
import { writeAtomicJson } from "../../shared/atomic-json.ts";
|
|
5
|
+
import { DIRS, type Details, type SubagentState } from "../../shared/types.ts";
|
|
6
|
+
import { readStatus } from "../../shared/utils.ts";
|
|
7
|
+
import { updateActiveRunIndex } from "../background/active-run-index.ts";
|
|
8
|
+
import { reconcileAsyncRun } from "../background/stale-run-reconciler.ts";
|
|
9
|
+
|
|
10
|
+
export function dismissRecoveredWorkflow(
|
|
11
|
+
state: SubagentState,
|
|
12
|
+
location: { asyncDir: string | null; resolvedId?: string },
|
|
13
|
+
): AgentToolResult<Details> {
|
|
14
|
+
const asyncDir = location.asyncDir;
|
|
15
|
+
const runId = location.resolvedId ?? (asyncDir ? path.basename(asyncDir) : "requested");
|
|
16
|
+
if (!asyncDir) {
|
|
17
|
+
return {
|
|
18
|
+
content: [{ type: "text", text: `Recovered workflow '${runId}' has no disk status to dismiss.` }],
|
|
19
|
+
isError: true,
|
|
20
|
+
details: { mode: "management", results: [] },
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
const status = readStatus(asyncDir);
|
|
24
|
+
if (!status || status.mode !== "workflow") {
|
|
25
|
+
return {
|
|
26
|
+
content: [{ type: "text", text: `Run '${runId}' is not a recovered workflow.` }],
|
|
27
|
+
isError: true,
|
|
28
|
+
details: { mode: "management", results: [] },
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
if (!state.currentSessionId || status.sessionId !== state.currentSessionId) {
|
|
32
|
+
return {
|
|
33
|
+
content: [{ type: "text", text: `Recovered workflow '${runId}' was not found in the active session.` }],
|
|
34
|
+
isError: true,
|
|
35
|
+
details: { mode: "management", results: [] },
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
if (state.workflowControllers?.has(status.runId) || state.workflowControllers?.has(runId)) {
|
|
39
|
+
return {
|
|
40
|
+
content: [{ type: "text", text: `Workflow '${runId}' still has a live controller and cannot be dismissed.` }],
|
|
41
|
+
isError: true,
|
|
42
|
+
details: { mode: "management", results: [] },
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
if (status.state !== "running") {
|
|
46
|
+
return {
|
|
47
|
+
content: [{ type: "text", text: `Recovered workflow '${runId}' is ${status.state}, not running.` }],
|
|
48
|
+
isError: true,
|
|
49
|
+
details: { mode: "management", results: [] },
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
let latestStatus = status;
|
|
53
|
+
const resultPath = path.join(DIRS.results, `${status.runId}.json`);
|
|
54
|
+
if (fs.existsSync(resultPath)) {
|
|
55
|
+
const reconciled = reconcileAsyncRun(asyncDir).status;
|
|
56
|
+
if (reconciled && reconciled.state !== "running") {
|
|
57
|
+
return {
|
|
58
|
+
content: [{ type: "text", text: `Recovered workflow '${runId}' is ${reconciled.state}, not running.` }],
|
|
59
|
+
isError: true,
|
|
60
|
+
details: { mode: "management", results: [] },
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
if (reconciled) latestStatus = reconciled;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const dismissed = { ...latestStatus, displayDismissedAt: Date.now() };
|
|
67
|
+
writeAtomicJson(path.join(asyncDir, "status.json"), dismissed);
|
|
68
|
+
const repaired = reconcileAsyncRun(asyncDir).status;
|
|
69
|
+
if (repaired && repaired.state !== "running") {
|
|
70
|
+
return {
|
|
71
|
+
content: [{ type: "text", text: `Recovered workflow '${runId}' is ${repaired.state}, not running.` }],
|
|
72
|
+
isError: true,
|
|
73
|
+
details: { mode: "management", results: [] },
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
updateActiveRunIndex(asyncDir, "complete");
|
|
77
|
+
state.asyncJobs.delete(status.runId);
|
|
78
|
+
state.asyncJobs.delete(runId);
|
|
79
|
+
state.fleetJobs?.delete(status.runId);
|
|
80
|
+
state.fleetJobs?.delete(runId);
|
|
81
|
+
return {
|
|
82
|
+
content: [{ type: "text", text: `Dismissed recovered workflow ${status.runId} from the display. No running work was terminated.` }],
|
|
83
|
+
details: { mode: "management", results: [] },
|
|
84
|
+
};
|
|
85
|
+
}
|
|
@@ -219,15 +219,14 @@ export async function steerAsyncRun(input: {
|
|
|
219
219
|
const limits = remainingSteeringRecoveryLimits(recoveryTarget.recoveryDescriptor, paused);
|
|
220
220
|
const revived = await input.recover(limits);
|
|
221
221
|
if (revived.isError || !revived.details.asyncId) throw new Error(revived.content[0]?.type === "text" ? revived.content[0].text : "Replacement launch failed; source run remains paused.");
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
const stepSteering = sourceStatus.steps?.[targetIndex]?.steering;
|
|
222
|
+
const targetIndex = input.index ?? paused.steering?.recent.find((request) => request.id === requestId)?.targets[0]?.index ?? status.steps?.findIndex((step) => step.status === "running") ?? -1;
|
|
223
|
+
if (paused.steering && targetIndex >= 0) {
|
|
224
|
+
updateSteeringTarget(paused.steering, requestId, targetIndex, "recovered", Date.now(), { replacementRunId: revived.details.asyncId });
|
|
225
|
+
const stepSteering = paused.steps?.[targetIndex]?.steering;
|
|
227
226
|
if (stepSteering) updateSteeringTarget(stepSteering, requestId, targetIndex, "recovered", Date.now(), { replacementRunId: revived.details.asyncId });
|
|
228
|
-
writeAtomicJson(path.join(asyncDir, "status.json"),
|
|
227
|
+
writeAtomicJson(path.join(asyncDir, "status.json"), paused);
|
|
229
228
|
}
|
|
230
|
-
const recovered =
|
|
229
|
+
const recovered = paused.steering ? actionResultFromSteeringStatus(paused.steering, status.runId, requestId, revived.details.asyncId) : undefined;
|
|
231
230
|
appendSteeringNotice("recovered", `Steering recovered for run ${status.runId}; replacement ${revived.details.asyncId} launched.`);
|
|
232
231
|
return { content: [{ type: "text", text: `Steering recovered for async run ${status.runId}; replacement ${revived.details.asyncId} launched after the source paused.` }], details: { mode: "management", results: [], steering: recovered ?? { requestId, state: "recovered", sourceRunId: status.runId, replacementRunId: revived.details.asyncId, deliveryStatus: "delivered", targets: [{ index: input.index ?? 0, state: "recovered" }] } } };
|
|
233
232
|
} catch (error) {
|
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
import { discoverAvailableSkills, normalizeSkillInput } from "../../agents/skills.ts";
|
|
35
35
|
import { INTERCOM_BRIDGE_MARKER } from "../../intercom/intercom-bridge.ts";
|
|
36
36
|
import { runSync } from "./execution.ts";
|
|
37
|
+
import { workflowForegroundSteeringLaunchOptions } from "./workflow-foreground-steering.ts";
|
|
37
38
|
import {
|
|
38
39
|
beginForegroundChild,
|
|
39
40
|
finishForegroundChild,
|
|
@@ -387,6 +388,7 @@ async function runParallelChainTasks(input: ParallelChainRunInput): Promise<Sing
|
|
|
387
388
|
result = await runSync(input.ctx.cwd, input.agents, task.agent, taskStr, {
|
|
388
389
|
permissions: input.permissions,
|
|
389
390
|
parentSessionId: input.ctx.sessionManager.getSessionId() ?? undefined,
|
|
391
|
+
...workflowForegroundSteeringLaunchOptions(input.foregroundControl, childIndex),
|
|
390
392
|
capabilityCeiling: input.capabilityCeiling,
|
|
391
393
|
context: input.contextForAgent?.(task.agent),
|
|
392
394
|
cwd: taskCwd,
|
|
@@ -1367,6 +1369,7 @@ ${step.message}` : ""}` }],
|
|
|
1367
1369
|
r = await runSync(ctx.cwd, agents, seqStep.agent, stepTask, {
|
|
1368
1370
|
permissions: params.permissions,
|
|
1369
1371
|
parentSessionId: ctx.sessionManager.getSessionId() ?? undefined,
|
|
1372
|
+
...workflowForegroundSteeringLaunchOptions(params.foregroundControl, childIndex),
|
|
1370
1373
|
capabilityCeiling: params.capabilityCeiling,
|
|
1371
1374
|
context: params.contextForAgent?.(seqStep.agent),
|
|
1372
1375
|
cwd: resolveChildCwd(cwd ?? ctx.cwd, seqStep.cwd),
|
|
@@ -338,6 +338,9 @@ async function runSingleAttempt(
|
|
|
338
338
|
parentRootRunId: options.nestedRoute?.rootRunId,
|
|
339
339
|
parentCapabilityToken: options.nestedRoute?.capabilityToken,
|
|
340
340
|
parentSessionId: options.parentSessionId,
|
|
341
|
+
steerInboxDir: options.steerInboxDir,
|
|
342
|
+
steerCapabilityPath: options.steerCapabilityPath,
|
|
343
|
+
steerAckDir: options.steerAckDir,
|
|
341
344
|
structuredOutput: options.structuredOutput,
|
|
342
345
|
toolBudget: options.toolBudget,
|
|
343
346
|
allowZeroToolBudget: options.allowZeroToolBudget,
|
|
@@ -4,7 +4,7 @@ import * as path from "node:path";
|
|
|
4
4
|
import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
5
5
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
6
6
|
import { resolveAgentName, type AgentConfig, type AgentScope } from "../../agents/agents.ts";
|
|
7
|
-
import { getArtifactsDir, getChainRunsDir, getProjectArtifactPackagingWarning } from "../../shared/artifacts.ts";
|
|
7
|
+
import { getArtifactsDir, getChainRunsDir, getProjectArtifactPackagingWarning, getProjectSubagentsDir } from "../../shared/artifacts.ts";
|
|
8
8
|
import { writeAtomicJson } from "../../shared/atomic-json.ts";
|
|
9
9
|
import { ChainClarifyComponent, type ChainClarifyResult } from "./chain-clarify.ts";
|
|
10
10
|
import { resolveEffectiveThinking, toModelInfo, type ModelInfo } from "../../shared/model-info.ts";
|
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
isParallelStep,
|
|
40
40
|
isDynamicParallelStep,
|
|
41
41
|
resolveChainPath,
|
|
42
|
+
resolveExistingReadPaths,
|
|
42
43
|
resolveStepBehavior,
|
|
43
44
|
suppressProgressForReadOnlyTask,
|
|
44
45
|
taskDisallowsFileUpdates,
|
|
@@ -51,7 +52,8 @@ import {
|
|
|
51
52
|
type StepOverrides,
|
|
52
53
|
} from "../../shared/settings.ts";
|
|
53
54
|
import { discoverAvailableSkills, normalizeSkillInput } from "../../agents/skills.ts";
|
|
54
|
-
import { buildAsyncRunnerSteps, executeAsyncChain, executeAsyncSingle, formatAsyncStartedMessage, isAsyncAvailable, workflowAwaitedAsyncResultPath } from "../background/async-execution.ts";
|
|
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";
|
|
55
57
|
import { isScheduledRunAction, type ScheduledRunAction } from "../background/scheduled-runs.ts";
|
|
56
58
|
import { enqueueChainAppendRequest, readPendingChainAppendRequests, runnerStepOutputNames } from "../background/chain-append.ts";
|
|
57
59
|
import { ChainOutputValidationError, validateChainOutputBindingsWithContext } from "../shared/chain-outputs.ts";
|
|
@@ -84,7 +86,15 @@ import { applySteeringRecoveryAgentConfig, buildRevivedAsyncTask, resolveAsyncRe
|
|
|
84
86
|
import { deliverCheckpointDecisionRequest, deliverInterruptRequest, readRevivalBriefs, requestAsyncSteer, type SteerDeliveryMode } from "../background/control-channel.ts";
|
|
85
87
|
import { updateSteeringTarget, waitForSteeringAction } from "../background/steering.ts";
|
|
86
88
|
import { steerAsyncRun } from "./async-steering-action.ts";
|
|
89
|
+
import {
|
|
90
|
+
removeWorkflowForegroundSteeringRoute,
|
|
91
|
+
resolveWorkflowForegroundSteeringTarget,
|
|
92
|
+
steerWorkflowForegroundTarget,
|
|
93
|
+
workflowForegroundSteeringDir,
|
|
94
|
+
workflowForegroundSteeringLaunchOptions,
|
|
95
|
+
} from "./workflow-foreground-steering.ts";
|
|
87
96
|
import { stopAsyncRun } from "./async-stop-action.ts";
|
|
97
|
+
import { dismissRecoveredWorkflow } from "./async-dismiss-action.ts";
|
|
88
98
|
import { reconcileAsyncRun } from "../background/stale-run-reconciler.ts";
|
|
89
99
|
import { resolveAsyncRootResultPath, waitForImportedAsyncRoot } from "../background/chain-root-attachment.ts";
|
|
90
100
|
import { attachRootChildrenToSteps, createNestedRoute, findNestedControlResult, resolveInheritedNestedRouteFromEnv, resolveNestedAsyncDir, resolveNestedParentAddressFromEnv, snapshotNestedEventFiles, updateForegroundNestedProjection, writeNestedControlRequest, writeNestedEvent, type NestedRunResolutionScope } from "../shared/nested-events.ts";
|
|
@@ -154,7 +164,7 @@ import {
|
|
|
154
164
|
wrapForkTask,
|
|
155
165
|
} from "../../shared/types.ts";
|
|
156
166
|
|
|
157
|
-
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"]);
|
|
158
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"]);
|
|
159
169
|
|
|
160
170
|
function editDistance(left: string, right: string): number {
|
|
@@ -403,6 +413,7 @@ function resolveRequestedCwd(runtimeCwd: string, requestedCwd: string | undefine
|
|
|
403
413
|
function removeForegroundControlIfIdle(state: SubagentState, runId: string): boolean {
|
|
404
414
|
const control = state.foregroundControls.get(runId);
|
|
405
415
|
if (control && (!foregroundSchedulingSettled(control) || (control.activeChildren?.size ?? 0) > 0)) return false;
|
|
416
|
+
if (control) removeWorkflowForegroundSteeringRoute(control);
|
|
406
417
|
state.foregroundControls.delete(runId);
|
|
407
418
|
if (state.lastForegroundControlId === runId) state.lastForegroundControlId = null;
|
|
408
419
|
return true;
|
|
@@ -1450,7 +1461,7 @@ async function resumeAsyncRun(input: {
|
|
|
1450
1461
|
inheritProjectContext: recoveryDescriptor.inheritProjectContext,
|
|
1451
1462
|
inheritSkills: recoveryDescriptor.inheritSkills,
|
|
1452
1463
|
source: "project",
|
|
1453
|
-
filePath: recoveryDescriptor.agentFilePath ?? path.join(recoveryDescriptor.cwd, "
|
|
1464
|
+
filePath: recoveryDescriptor.agentFilePath ?? path.join(getProjectSubagentsDir(recoveryDescriptor.cwd), "recovery-agent"),
|
|
1454
1465
|
} : undefined);
|
|
1455
1466
|
if (!agentConfig) {
|
|
1456
1467
|
return {
|
|
@@ -2096,7 +2107,17 @@ function applySingleAgentLaunchDefaults(params: SubagentParamsLike, agents: Agen
|
|
|
2096
2107
|
|
|
2097
2108
|
export const DEFAULT_FOREGROUND_TIMEOUT_MS = 30 * 60 * 1000;
|
|
2098
2109
|
|
|
2099
|
-
|
|
2110
|
+
// Async single-agent runs also need a wall-clock backstop: a child whose bash
|
|
2111
|
+
// tool blocks forever (e.g. a background process inheriting the terminal with
|
|
2112
|
+
// no bash `timeout` arg) would otherwise hang the parent indefinitely with
|
|
2113
|
+
// zero signal. Same generous default as foreground; explicit timeoutMs/
|
|
2114
|
+
// maxRuntimeMs and agent-level defaultTimeoutMs remain authoritative.
|
|
2115
|
+
//
|
|
2116
|
+
// Deliberately NOT applied at the workflow level: async scripted workflows
|
|
2117
|
+
// stay unbounded as a whole, while each runner child has its own deadline.
|
|
2118
|
+
export { DEFAULT_ASYNC_TIMEOUT_MS };
|
|
2119
|
+
|
|
2120
|
+
export function resolveForegroundTimeout(params: SubagentParamsLike, defaultTimeoutMs?: number): { timeoutMs?: number; error?: string } {
|
|
2100
2121
|
const rawTimeout = params.timeoutMs;
|
|
2101
2122
|
const rawMaxRuntime = params.maxRuntimeMs;
|
|
2102
2123
|
if (rawTimeout === undefined && rawMaxRuntime === undefined) {
|
|
@@ -2115,6 +2136,21 @@ function resolveForegroundTimeout(params: SubagentParamsLike, defaultTimeoutMs?:
|
|
|
2115
2136
|
return timeoutMs === undefined ? {} : { timeoutMs };
|
|
2116
2137
|
}
|
|
2117
2138
|
|
|
2139
|
+
/**
|
|
2140
|
+
* Resolve the effective launch timeout for a single-agent run, applying the
|
|
2141
|
+
* async/foreground default when neither the caller nor the agent set one.
|
|
2142
|
+
*
|
|
2143
|
+
* The async default is deliberately applied only to plain single-agent
|
|
2144
|
+
* launches. Composite launches keep their top-level execution unbounded when
|
|
2145
|
+
* no timeout is set; their runner children resolve separate deadlines.
|
|
2146
|
+
* Exported so the executor wiring is directly testable.
|
|
2147
|
+
*/
|
|
2148
|
+
export function resolveSingleAgentLaunchTimeout(params: SubagentParamsLike, async: boolean): { timeoutMs?: number; error?: string } {
|
|
2149
|
+
const isComposite = (params.chain?.length ?? 0) > 0 || (params.tasks?.length ?? 0) > 0 || params.workflowScript !== undefined;
|
|
2150
|
+
const defaultTimeoutMs = !async ? DEFAULT_FOREGROUND_TIMEOUT_MS : isComposite ? undefined : DEFAULT_ASYNC_TIMEOUT_MS;
|
|
2151
|
+
return resolveForegroundTimeout(params, defaultTimeoutMs);
|
|
2152
|
+
}
|
|
2153
|
+
|
|
2118
2154
|
function resolveToolBudget(
|
|
2119
2155
|
raw: unknown,
|
|
2120
2156
|
label = "toolBudget",
|
|
@@ -3155,6 +3191,7 @@ async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Pr
|
|
|
3155
3191
|
const result = await runSync(input.ctx.cwd, input.agents, task.agent, taskText, compactOptional<Parameters<typeof runSync>[4]>({
|
|
3156
3192
|
permissions: input.permissions,
|
|
3157
3193
|
parentSessionId: input.ctx.sessionManager.getSessionId() ?? undefined,
|
|
3194
|
+
...workflowForegroundSteeringLaunchOptions(input.foregroundControl, index),
|
|
3158
3195
|
context: input.contextPolicy.contextForAgent(task.agent),
|
|
3159
3196
|
cwd: taskCwd,
|
|
3160
3197
|
signal: input.signal,
|
|
@@ -3830,8 +3867,9 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
|
|
|
3830
3867
|
// Reads: caller override > agent defaultReads > none. `~`/`~/` expand to home;
|
|
3831
3868
|
// absolute paths pass through; relative paths resolve against the child cwd.
|
|
3832
3869
|
const reads = readsOverride !== undefined ? readsOverride : agentConfig.defaultReads ?? false;
|
|
3833
|
-
const
|
|
3834
|
-
|
|
3870
|
+
const readPaths = Array.isArray(reads) ? resolveExistingReadPaths(reads, effectiveCwd) : [];
|
|
3871
|
+
const readsInstruction = readPaths.length > 0
|
|
3872
|
+
? `[Read from: ${readPaths.join(", ")}]\n\n`
|
|
3835
3873
|
: "";
|
|
3836
3874
|
task = readsInstruction + task;
|
|
3837
3875
|
task = injectSingleOutputInstruction(task, outputPath, agentConfig);
|
|
@@ -3875,6 +3913,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
|
|
|
3875
3913
|
r = await runSync(ctx.cwd, agents, params.agent!, task, compactOptional<Parameters<typeof runSync>[4]>({
|
|
3876
3914
|
permissions: deps.config.permissions,
|
|
3877
3915
|
parentSessionId: ctx.sessionManager.getSessionId() ?? undefined,
|
|
3916
|
+
...workflowForegroundSteeringLaunchOptions(foregroundControl, 0),
|
|
3878
3917
|
context: data.contextPolicy.contextForAgent(params.agent!),
|
|
3879
3918
|
cwd: effectiveCwd,
|
|
3880
3919
|
signal,
|
|
@@ -4427,9 +4466,14 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
4427
4466
|
workflow: { trace: [], emits: [], console: [] },
|
|
4428
4467
|
};
|
|
4429
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;
|
|
4430
4470
|
const persist = () => {
|
|
4431
4471
|
status.lastUpdate = Date.now();
|
|
4432
4472
|
writeAtomicJson(statusPath, status);
|
|
4473
|
+
if (indexedState !== status.state) {
|
|
4474
|
+
updateActiveRunIndex(asyncDir, status.state);
|
|
4475
|
+
indexedState = status.state;
|
|
4476
|
+
}
|
|
4433
4477
|
const job = deps.state.asyncJobs.get(workflowRunId);
|
|
4434
4478
|
if (job) {
|
|
4435
4479
|
job.status = status.state;
|
|
@@ -4483,10 +4527,24 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
4483
4527
|
void Promise.resolve().then(async () => {
|
|
4484
4528
|
const workflowResults: SingleResult[] = [];
|
|
4485
4529
|
const { action: _action, agent: _agent, task: _task, resume: _resume, tasks: _tasks, chain: _chain, concurrency: _concurrency, foregroundOnly: _foregroundOnly, clarify: _clarify, timeoutMs: _timeoutMs, maxRuntimeMs: _maxRuntimeMs, usageBudget: _usageBudget, missionId: _missionId, mission: _mission, ...workflowChildDefaults } = workflowRequest;
|
|
4530
|
+
const workflowSteps = new Map<string, NonNullable<AsyncStatus["steps"]>[number]>();
|
|
4531
|
+
let projectedTraceLength = 0;
|
|
4532
|
+
let projectedTraceTail: NonNullable<Details["workflow"]>["trace"][number] | undefined;
|
|
4486
4533
|
const updateTrace = (trace: NonNullable<Details["workflow"]>["trace"]) => {
|
|
4487
4534
|
status.workflow = { ...(status.workflow ?? { emits: [], console: [] }), trace };
|
|
4488
|
-
|
|
4489
|
-
|
|
4535
|
+
const rebuild = trace.length < projectedTraceLength
|
|
4536
|
+
|| (projectedTraceLength > 0 && trace[projectedTraceLength - 1] !== projectedTraceTail);
|
|
4537
|
+
if (rebuild) {
|
|
4538
|
+
workflowSteps.clear();
|
|
4539
|
+
for (const step of status.steps ?? []) {
|
|
4540
|
+
if (step.workflowKey) workflowSteps.set(step.workflowKey, step);
|
|
4541
|
+
}
|
|
4542
|
+
projectedTraceLength = 0;
|
|
4543
|
+
}
|
|
4544
|
+
for (let index = projectedTraceLength; index < trace.length; index += 1) {
|
|
4545
|
+
const entry = trace[index]!;
|
|
4546
|
+
if (entry.operation !== "run") continue;
|
|
4547
|
+
const existing = workflowSteps.get(entry.key);
|
|
4490
4548
|
if (entry.state === "reused" && existing) continue;
|
|
4491
4549
|
const mapped = entry.state === "started" || entry.state === "reused" ? "running" : entry.state === "completed" ? "completed" : "failed";
|
|
4492
4550
|
if (existing) {
|
|
@@ -4497,9 +4555,13 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
4497
4555
|
if (entry.durationMs === undefined) delete existing.durationMs;
|
|
4498
4556
|
else existing.durationMs = entry.durationMs;
|
|
4499
4557
|
} else {
|
|
4500
|
-
|
|
4558
|
+
const step: NonNullable<AsyncStatus["steps"]>[number] = { agent: entry.agent ?? entry.key, label: entry.key, workflowKey: entry.key, parentWorkflowRunId: workflowRunId, status: mapped, startedAt: Date.now() };
|
|
4559
|
+
status.steps?.push(step);
|
|
4560
|
+
workflowSteps.set(entry.key, step);
|
|
4501
4561
|
}
|
|
4502
4562
|
}
|
|
4563
|
+
projectedTraceLength = trace.length;
|
|
4564
|
+
projectedTraceTail = trace.at(-1);
|
|
4503
4565
|
projectWorkflowActivity();
|
|
4504
4566
|
persist();
|
|
4505
4567
|
appendWorkflowEvent({ type: "subagent.workflow.trace", trace });
|
|
@@ -5089,6 +5151,12 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
5089
5151
|
try {
|
|
5090
5152
|
const location = resolveAsyncRunLocation(paramsWithResolvedCwd, DIRS.async, DIRS.results);
|
|
5091
5153
|
const runId = location.resolvedId ?? targetRunId ?? path.basename(location.asyncDir ?? paramsWithResolvedCwd.dir);
|
|
5154
|
+
const directoryStatus = location.asyncDir ? readStatus(location.asyncDir) : null;
|
|
5155
|
+
if (directoryStatus?.mode === "workflow") {
|
|
5156
|
+
const route = resolveWorkflowForegroundSteeringTarget({ state: deps.state, workflowRunId: directoryStatus.runId || runId, asyncDirRoot: DIRS.async });
|
|
5157
|
+
if (!route.ok) return { content: [{ type: "text", text: route.message }], isError: true, details: { mode: "management", results: [] } };
|
|
5158
|
+
return steerWorkflowForegroundTarget({ target: route.target, message, mode: paramsWithResolvedCwd.mode, index: paramsWithResolvedCwd.index, signal });
|
|
5159
|
+
}
|
|
5092
5160
|
if (location.asyncDir) {
|
|
5093
5161
|
const unsupported = externalRunnerControlError(location.asyncDir, "steer");
|
|
5094
5162
|
if (unsupported) return unsupported;
|
|
@@ -5124,8 +5192,18 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
5124
5192
|
return { content: [{ type: "text", text }], isError: true, details: { mode: "management", results: [] } };
|
|
5125
5193
|
}
|
|
5126
5194
|
if (resolved?.kind === "nested") return steerNestedRun(omitUndefinedProperties({ target: resolved, message, mode: paramsWithResolvedCwd.mode, index: paramsWithResolvedCwd.index, signal }));
|
|
5127
|
-
if (resolved?.kind === "foreground")
|
|
5195
|
+
if (resolved?.kind === "foreground") {
|
|
5196
|
+
const route = resolveWorkflowForegroundSteeringTarget({ state: deps.state, childRunId: resolved.id, asyncDirRoot: DIRS.async });
|
|
5197
|
+
if (!route.ok) return { content: [{ type: "text", text: route.message }], isError: true, details: { mode: "management", results: [] } };
|
|
5198
|
+
return steerWorkflowForegroundTarget({ target: route.target, message, mode: paramsWithResolvedCwd.mode, index: paramsWithResolvedCwd.index, signal });
|
|
5199
|
+
}
|
|
5128
5200
|
if (resolved?.kind !== "async") return { content: [{ type: "text", text: `No async run found for '${targetRunId}'.` }], isError: true, details: { mode: "management", results: [] } };
|
|
5201
|
+
const resolvedStatus = resolved.location.asyncDir ? readStatus(resolved.location.asyncDir) : null;
|
|
5202
|
+
if (resolvedStatus?.mode === "workflow") {
|
|
5203
|
+
const route = resolveWorkflowForegroundSteeringTarget({ state: deps.state, workflowRunId: resolvedStatus.runId || resolved.id, asyncDirRoot: DIRS.async });
|
|
5204
|
+
if (!route.ok) return { content: [{ type: "text", text: route.message }], isError: true, details: { mode: "management", results: [] } };
|
|
5205
|
+
return steerWorkflowForegroundTarget({ target: route.target, message, mode: paramsWithResolvedCwd.mode, index: paramsWithResolvedCwd.index, signal });
|
|
5206
|
+
}
|
|
5129
5207
|
if (resolved.location.asyncDir) {
|
|
5130
5208
|
const unsupported = externalRunnerControlError(resolved.location.asyncDir, "steer");
|
|
5131
5209
|
if (unsupported) return unsupported;
|
|
@@ -5178,6 +5256,28 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
5178
5256
|
}
|
|
5179
5257
|
return deps.handleScheduledRunAction(paramsWithResolvedCwd, ctx);
|
|
5180
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
|
+
}
|
|
5181
5281
|
if (action === "stop") {
|
|
5182
5282
|
const targetRunId = paramsWithResolvedCwd.runId ?? paramsWithResolvedCwd.id;
|
|
5183
5283
|
const workflowController = targetRunId ? deps.state.workflowControllers?.get(targetRunId) : undefined;
|
|
@@ -5189,6 +5289,10 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
5189
5289
|
if (paramsWithResolvedCwd.dir) {
|
|
5190
5290
|
try {
|
|
5191
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
|
+
}
|
|
5192
5296
|
const stopResult = stopAsyncRun(deps.state, location.resolvedId ?? targetRunId ?? path.basename(location.asyncDir ?? paramsWithResolvedCwd.dir), deps.kill, location);
|
|
5193
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: [] } };
|
|
5194
5298
|
} catch (error) {
|
|
@@ -5273,13 +5377,6 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
5273
5377
|
details: { mode: "management" as const, results: [] },
|
|
5274
5378
|
};
|
|
5275
5379
|
}
|
|
5276
|
-
if (deps.allowMutatingManagementActions === false && MUTATING_MANAGEMENT_ACTIONS.has(action)) {
|
|
5277
|
-
return {
|
|
5278
|
-
content: [{ type: "text", text: `Action '${action}' is not available from child-safe subagent fanout mode.` }],
|
|
5279
|
-
isError: true,
|
|
5280
|
-
details: { mode: "management" as const, results: [] },
|
|
5281
|
-
};
|
|
5282
|
-
}
|
|
5283
5380
|
return handleManagementAction(action, paramsWithResolvedCwd, {
|
|
5284
5381
|
...ctx,
|
|
5285
5382
|
cwd: requestCwd,
|
|
@@ -5432,9 +5529,9 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
5432
5529
|
if (externalAgent && (!effectiveAsync || effectiveParams.foregroundOnly === true)) {
|
|
5433
5530
|
return buildRequestedModeError(effectiveParams, `Agent '${externalAgent.name}' uses runner.type='external-cli', which currently supports async/background execution only. Omit async or pass async:true; clarify and foregroundOnly are unsupported.`);
|
|
5434
5531
|
}
|
|
5435
|
-
const foregroundTimeout =
|
|
5532
|
+
const foregroundTimeout = resolveSingleAgentLaunchTimeout(
|
|
5436
5533
|
effectiveParams,
|
|
5437
|
-
effectiveAsync
|
|
5534
|
+
effectiveAsync,
|
|
5438
5535
|
);
|
|
5439
5536
|
if (foregroundTimeout.error) return buildRequestedModeError(effectiveParams, foregroundTimeout.error);
|
|
5440
5537
|
const controlConfig = resolveControlConfig(deps.config.control, effectiveParams.control);
|
|
@@ -5584,6 +5681,17 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
5584
5681
|
const foregroundDescription = effectiveParams.task?.trim()
|
|
5585
5682
|
|| effectiveParams.tasks?.[0]?.task?.trim()
|
|
5586
5683
|
|| (effectiveParams.chain ? firstRawChainTask(effectiveParams.chain)?.trim() : undefined);
|
|
5684
|
+
const parentWorkflowStatus = effectiveParams.workflowParentRunId
|
|
5685
|
+
? readStatus(path.join(DIRS.async, effectiveParams.workflowParentRunId))
|
|
5686
|
+
: null;
|
|
5687
|
+
const workflowSteeringDir = effectiveParams.workflowParentRunId
|
|
5688
|
+
&& requestSessionId
|
|
5689
|
+
&& deps.state.workflowControllers?.has(effectiveParams.workflowParentRunId)
|
|
5690
|
+
&& parentWorkflowStatus?.mode === "workflow"
|
|
5691
|
+
&& (parentWorkflowStatus.state === "running" || parentWorkflowStatus.state === "queued")
|
|
5692
|
+
&& parentWorkflowStatus.sessionId === requestSessionId
|
|
5693
|
+
? workflowForegroundSteeringDir(DIRS.async, effectiveParams.workflowParentRunId, runId)
|
|
5694
|
+
: undefined;
|
|
5587
5695
|
const foregroundControl: ForegroundRunControl | undefined = effectiveAsync
|
|
5588
5696
|
? undefined
|
|
5589
5697
|
: compactOptional<ForegroundRunControl>({
|
|
@@ -5592,6 +5700,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
5592
5700
|
mode: foregroundMode,
|
|
5593
5701
|
...(effectiveParams.workflowParentRunId ? { parentWorkflowRunId: effectiveParams.workflowParentRunId } : {}),
|
|
5594
5702
|
...(effectiveParams.workflowKey ? { workflowKey: effectiveParams.workflowKey } : {}),
|
|
5703
|
+
workflowSteeringDir,
|
|
5595
5704
|
startedAt: Date.now(),
|
|
5596
5705
|
updatedAt: Date.now(),
|
|
5597
5706
|
cwd: effectiveCwd,
|