@agwab/pi-workflow 0.2.1 → 0.3.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/dist/compiler.js +6 -8
- package/dist/dynamic-decision.d.ts +0 -1
- package/dist/dynamic-decision.js +0 -7
- package/dist/dynamic-profiles.d.ts +0 -1
- package/dist/dynamic-profiles.js +0 -3
- package/dist/engine-run-graph.d.ts +1 -0
- package/dist/engine-run-graph.js +142 -2
- package/dist/engine.d.ts +5 -0
- package/dist/engine.js +112 -27
- package/dist/extension.d.ts +2 -1
- package/dist/extension.js +27 -6
- package/dist/index.d.ts +3 -3
- package/dist/index.js +2 -1
- package/dist/store.js +55 -11
- package/dist/subagent-backend.js +155 -29
- package/dist/types.d.ts +6 -0
- package/dist/workflow-runtime.js +10 -1
- package/dist/workflow-view.js +3 -1
- package/dist/workflow-web-source-extension.js +167 -48
- package/dist/workflow-web-source.d.ts +2 -1
- package/dist/workflow-web-source.js +84 -19
- package/node_modules/@agwab/pi-subagent/README.md +3 -3
- package/node_modules/@agwab/pi-subagent/api.mjs +1 -0
- package/node_modules/@agwab/pi-subagent/docs/usage.md +63 -12
- package/node_modules/@agwab/pi-subagent/package.json +2 -2
- package/node_modules/@agwab/pi-subagent/src/api.ts +54 -1
- package/node_modules/@agwab/pi-subagent/src/artifacts/registry.ts +9 -4
- package/node_modules/@agwab/pi-subagent/src/artifacts/result.ts +8 -0
- package/node_modules/@agwab/pi-subagent/src/core/constants.ts +9 -0
- package/node_modules/@agwab/pi-subagent/src/core/validation.ts +21 -0
- package/node_modules/@agwab/pi-subagent/src/index.ts +995 -573
- package/node_modules/@agwab/pi-subagent/src/orchestrate/async.ts +279 -156
- package/node_modules/@agwab/pi-subagent/src/orchestrate/interrupt.ts +165 -89
- package/node_modules/@agwab/pi-subagent/src/orchestrate/reconcile.ts +111 -65
- package/node_modules/@agwab/pi-subagent/src/orchestrate/run-ref.ts +219 -0
- package/node_modules/@agwab/pi-subagent/src/orchestrate/run.ts +88 -8
- package/node_modules/@agwab/pi-subagent/src/orchestrate/status.ts +614 -298
- package/node_modules/@agwab/pi-subagent/src/panel.ts +1352 -560
- package/node_modules/@agwab/pi-subagent/src/runners/headless-model.ts +53 -5
- package/node_modules/@agwab/pi-subagent/src/runners/tmux.ts +13 -6
- package/package.json +2 -2
- package/src/compiler.ts +14 -9
- package/src/dynamic-decision.ts +0 -11
- package/src/dynamic-profiles.ts +0 -4
- package/src/engine-run-graph.ts +185 -2
- package/src/engine.ts +145 -24
- package/src/extension.ts +33 -4
- package/src/index.ts +3 -1
- package/src/store.ts +74 -11
- package/src/subagent-backend.ts +201 -28
- package/src/types.ts +6 -0
- package/src/workflow-runtime.ts +18 -2
- package/src/workflow-view.ts +2 -1
- package/src/workflow-web-source-extension.ts +621 -228
- package/src/workflow-web-source.ts +118 -28
- package/workflows/deep-research/helpers/claim-evidence-gate.mjs +56 -16
- package/workflows/deep-research/helpers/final-audit-packet.mjs +1 -4
- package/workflows/deep-research/helpers/normalize-input-packet.mjs +1 -1
- package/workflows/deep-research/helpers/render-executive.mjs +8 -21
- package/workflows/deep-research/helpers/sanitize-verification-candidates.mjs +89 -15
- package/workflows/deep-research/schemas/deep-research-executive-render-control.schema.json +0 -1
- package/workflows/deep-research/schemas/deep-research-verify-claims-control.schema.json +4 -1
- package/workflows/impact-review/spec.json +3 -3
- package/workflows/spec-review/helpers/spec-review-pipeline.mjs +1 -8
- package/dist/dynamic-loader.d.ts +0 -25
- package/dist/dynamic-loader.js +0 -13
- package/src/dynamic-loader.ts +0 -49
- package/workflows/impact-review/schemas/docs-release-impact-control.schema.json +0 -42
- package/workflows/impact-review/schemas/security-performance-impact-control.schema.json +0 -42
- package/workflows/impact-review/schemas/state-data-impact-control.schema.json +0 -42
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { discoverAgents, loadAgentByName, parseAgentMarkdown, } from "./agents.js";
|
|
2
|
-
export { formatLogs, formatRunDetails, formatRunStatus, formatStatus, refreshRun, resumeRun, resumeSupervisors, runDynamicTask, runWorkflow, runWorkflowSpec, waitForRun, } from "./engine.js";
|
|
3
|
-
export type { ResumeRunSummary } from "./engine.js";
|
|
2
|
+
export { formatLogs, formatRunDetails, formatRunStatus, formatStatus, refreshRun, resumeRun, resumeSupervisors, runDynamicTask, stopRun, runWorkflow, runWorkflowSpec, waitForRun, } from "./engine.js";
|
|
3
|
+
export type { ResumeRunSummary, StopRunSummary } from "./engine.js";
|
|
4
4
|
export { listWorkflows, resolveWorkflowRef } from "./workflow-specs.js";
|
|
5
5
|
export type { ResolvedWorkflowSpecRef, WorkflowSpecRecord, } from "./workflow-specs.js";
|
|
6
6
|
export { compileRole, extractMarkdownSections } from "./roles.js";
|
|
@@ -11,4 +11,4 @@ export { WorkflowValidationError } from "./types.js";
|
|
|
11
11
|
export { runDynamicDecisionLoop } from "./dynamic-decision-loop.js";
|
|
12
12
|
export type { DynamicDecisionLoopControllerContext, DynamicDecisionLoopResult, DynamicDecisionLoopRunResult, RunDynamicDecisionLoopOptions, } from "./dynamic-decision-loop.js";
|
|
13
13
|
export declare const WORKFLOW_COMMAND = "workflow";
|
|
14
|
-
export declare const WORKFLOW_HELP = "pi-workflow\n\nUsage:\n /workflow [run-id]\n /workflow help\n /workflow validate <workflow-name-or-path>\n /workflow roles <workflow-name-or-path>\n /workflow agents\n /workflow list\n /workflow run [--model MODEL] [--thinking LEVEL] <workflow-name-or-path> \"<task>\" [--detach]\n /workflow dynamic [--model MODEL] [--thinking LEVEL] \"<task>\" [--detach]\n /workflow status [run-id]\n /workflow show <run-id-or-workflow-name>\n /workflow logs <run-id> [task-id] [lines]\n /workflow wait <run-id> [timeout-ms]\n /workflow resume <run-id>\n\n/workflow opens the read-only workflow board TUI.\n/workflow <run-id> opens the board focused on that run.\n/workflow dynamic starts a spec-less direct dynamic run: no workflow name,\nuser-selected spec, or generated workflow spec is required.\n\nWith --detach, a standalone supervisor process (pi-workflow supervise) keeps\nthe run progressing after this session exits.\n";
|
|
14
|
+
export declare const WORKFLOW_HELP = "pi-workflow\n\nUsage:\n /workflow [run-id]\n /workflow help\n /workflow validate <workflow-name-or-path>\n /workflow roles <workflow-name-or-path>\n /workflow agents\n /workflow list\n /workflow run [--model MODEL] [--thinking LEVEL] <workflow-name-or-path> \"<task>\" [--detach]\n /workflow dynamic [--model MODEL] [--thinking LEVEL] \"<task>\" [--detach]\n /workflow status [run-id]\n /workflow show <run-id-or-workflow-name>\n /workflow logs <run-id> [task-id] [lines]\n /workflow wait <run-id> [timeout-ms]\n /workflow resume <run-id>\n /workflow stop <run-id>\n\n/workflow opens the read-only workflow board TUI.\n/workflow <run-id> opens the board focused on that run.\n/workflow dynamic starts a spec-less direct dynamic run: no workflow name,\nuser-selected spec, or generated workflow spec is required.\n\nWith --detach, a standalone supervisor process (pi-workflow supervise) keeps\nthe run progressing after this session exits.\n";
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { discoverAgents, loadAgentByName, parseAgentMarkdown, } from "./agents.js";
|
|
2
|
-
export { formatLogs, formatRunDetails, formatRunStatus, formatStatus, refreshRun, resumeRun, resumeSupervisors, runDynamicTask, runWorkflow, runWorkflowSpec, waitForRun, } from "./engine.js";
|
|
2
|
+
export { formatLogs, formatRunDetails, formatRunStatus, formatStatus, refreshRun, resumeRun, resumeSupervisors, runDynamicTask, stopRun, runWorkflow, runWorkflowSpec, waitForRun, } from "./engine.js";
|
|
3
3
|
export { listWorkflows, resolveWorkflowRef } from "./workflow-specs.js";
|
|
4
4
|
export { compileRole, extractMarkdownSections } from "./roles.js";
|
|
5
5
|
export { loadWorkflow, loadWorkflowSpec, parseWorkflow } from "./schema.js";
|
|
@@ -23,6 +23,7 @@ Usage:
|
|
|
23
23
|
/workflow logs <run-id> [task-id] [lines]
|
|
24
24
|
/workflow wait <run-id> [timeout-ms]
|
|
25
25
|
/workflow resume <run-id>
|
|
26
|
+
/workflow stop <run-id>
|
|
26
27
|
|
|
27
28
|
/workflow opens the read-only workflow board TUI.
|
|
28
29
|
/workflow <run-id> opens the board focused on that run.
|
package/dist/store.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
-
import { cp, mkdir, open, readdir, readFile, realpath, rename, stat, unlink, utimes, writeFile, } from "node:fs/promises";
|
|
2
|
+
import { cp, link, mkdir, open, readdir, readFile, realpath, rename, stat, unlink, utimes, writeFile, } from "node:fs/promises";
|
|
3
3
|
import { basename, dirname, isAbsolute, join, normalize, relative, resolve, sep, } from "node:path";
|
|
4
4
|
import { randomBytes } from "node:crypto";
|
|
5
5
|
import { parseWorkflow } from "./schema.js";
|
|
6
6
|
import { WORKFLOW_RUN_TYPE, } from "./types.js";
|
|
7
7
|
const TERMINAL_INDEX_LIMIT = 50;
|
|
8
8
|
const LEASE_STALE_MS = 30_000;
|
|
9
|
+
const LEASE_ABSOLUTE_STALE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
9
10
|
const INDEX_LOCK_WAIT_MS = 5_000;
|
|
10
11
|
const INDEX_LOCK_RETRY_MS = 50;
|
|
11
12
|
const DEFAULT_INDEX_UPDATE_DEBOUNCE_MS = 500;
|
|
@@ -142,34 +143,74 @@ async function reclaimStaleLock(lockFile) {
|
|
|
142
143
|
const snapshot = await readLockSnapshot(lockFile);
|
|
143
144
|
if (!snapshot)
|
|
144
145
|
return true;
|
|
145
|
-
if (
|
|
146
|
+
if (!isReclaimableLockSnapshot(snapshot))
|
|
146
147
|
return false;
|
|
147
|
-
|
|
148
|
+
const reclaimFile = `${lockFile}.reclaim-${process.pid}-${randomBytes(3).toString("hex")}`;
|
|
149
|
+
try {
|
|
150
|
+
await rename(lockFile, reclaimFile);
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
if (error.code === "ENOENT")
|
|
154
|
+
return true;
|
|
148
155
|
return false;
|
|
149
|
-
|
|
150
|
-
|
|
156
|
+
}
|
|
157
|
+
const claimed = await readLockSnapshot(reclaimFile);
|
|
158
|
+
if (!claimed)
|
|
151
159
|
return true;
|
|
152
|
-
if (
|
|
160
|
+
if (!sameLockOwnerSnapshot(snapshot, claimed)) {
|
|
161
|
+
await restoreReclaimFile(reclaimFile, lockFile);
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
if (!isReclaimableLockSnapshot(claimed)) {
|
|
165
|
+
await restoreReclaimFile(reclaimFile, lockFile);
|
|
153
166
|
return false;
|
|
154
|
-
|
|
167
|
+
}
|
|
168
|
+
await unlink(reclaimFile).catch(() => undefined);
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
async function restoreReclaimFile(reclaimFile, lockFile) {
|
|
172
|
+
try {
|
|
173
|
+
await link(reclaimFile, lockFile);
|
|
174
|
+
}
|
|
175
|
+
catch (error) {
|
|
176
|
+
if (error.code !== "EEXIST")
|
|
177
|
+
throw error;
|
|
178
|
+
}
|
|
179
|
+
finally {
|
|
180
|
+
await unlink(reclaimFile).catch(() => undefined);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
function isReclaimableLockSnapshot(snapshot) {
|
|
184
|
+
const now = Date.now();
|
|
185
|
+
const leaseStale = now - snapshot.mtimeMs > LEASE_STALE_MS;
|
|
186
|
+
const absoluteStale = now - (snapshot.createdAtMs ?? snapshot.mtimeMs) > LEASE_ABSOLUTE_STALE_MS;
|
|
187
|
+
if (!leaseStale && !absoluteStale)
|
|
155
188
|
return false;
|
|
156
|
-
if (
|
|
189
|
+
if (snapshot.pid !== undefined &&
|
|
190
|
+
isProcessAlive(snapshot.pid) &&
|
|
191
|
+
!absoluteStale)
|
|
157
192
|
return false;
|
|
158
|
-
await unlink(lockFile).catch(() => undefined);
|
|
159
193
|
return true;
|
|
160
194
|
}
|
|
195
|
+
function sameLockOwnerSnapshot(left, right) {
|
|
196
|
+
return (left.ownerId === right.ownerId &&
|
|
197
|
+
left.pid === right.pid &&
|
|
198
|
+
left.createdAtMs === right.createdAtMs);
|
|
199
|
+
}
|
|
161
200
|
async function readLockSnapshot(lockFile) {
|
|
162
201
|
try {
|
|
163
202
|
const [fileStat, text] = await Promise.all([
|
|
164
203
|
stat(lockFile),
|
|
165
204
|
readFile(lockFile, "utf8"),
|
|
166
205
|
]);
|
|
167
|
-
const [ownerId = "", pidText] = text.split(/\r?\n/);
|
|
206
|
+
const [ownerId = "", pidText, createdAtText] = text.split(/\r?\n/);
|
|
168
207
|
const pid = Number.parseInt(pidText ?? "", 10);
|
|
208
|
+
const createdAtMs = Date.parse(createdAtText ?? "");
|
|
169
209
|
return {
|
|
170
210
|
ownerId,
|
|
171
211
|
pid: Number.isFinite(pid) ? pid : undefined,
|
|
172
212
|
mtimeMs: fileStat.mtimeMs,
|
|
213
|
+
createdAtMs: Number.isFinite(createdAtMs) ? createdAtMs : undefined,
|
|
173
214
|
};
|
|
174
215
|
}
|
|
175
216
|
catch (error) {
|
|
@@ -996,8 +1037,10 @@ export function deriveWorkflowStatus(summary) {
|
|
|
996
1037
|
return "running";
|
|
997
1038
|
if (summary.total > 0 && summary.completed === summary.total)
|
|
998
1039
|
return "completed";
|
|
999
|
-
if (summary.failed > 0
|
|
1040
|
+
if (summary.failed > 0)
|
|
1000
1041
|
return "failed";
|
|
1042
|
+
if (summary.interrupted > 0)
|
|
1043
|
+
return "interrupted";
|
|
1001
1044
|
return "interrupted";
|
|
1002
1045
|
}
|
|
1003
1046
|
export function isTerminalWorkflowStatus(status) {
|
|
@@ -1176,6 +1219,7 @@ export function createTaskRunRecord(cwd, runId, task, index) {
|
|
|
1176
1219
|
dependsOn: task.dependsOn,
|
|
1177
1220
|
artifactGraph: taskArtifactGraph,
|
|
1178
1221
|
dynamicGenerated: task.dynamicGenerated,
|
|
1222
|
+
foreachGenerated: task.foreachGenerated,
|
|
1179
1223
|
files,
|
|
1180
1224
|
lastMessage: blocked ? task.safety.permission.reason : undefined,
|
|
1181
1225
|
};
|
package/dist/subagent-backend.js
CHANGED
|
@@ -17,7 +17,12 @@ const LEGACY_FETCH_CACHE_ENV = "PI_WORKFLOW_FETCH_CACHE";
|
|
|
17
17
|
const DEFAULT_TRANSIENT_MODEL_FAILURE_RETRIES = 5;
|
|
18
18
|
const DEFAULT_ARTIFACT_OUTPUT_RETRIES = 2;
|
|
19
19
|
const MAX_CONCURRENT_LAUNCHES_ENV = "PI_WORKFLOW_MAX_CONCURRENT_LAUNCHES";
|
|
20
|
+
const PARENT_SUBAGENT_CWD_ENV = "PI_WORKFLOW_PARENT_SUBAGENT_CWD";
|
|
21
|
+
const PARENT_SUBAGENT_RUNS_DIR_ENV = "PI_WORKFLOW_PARENT_SUBAGENT_RUNS_DIR";
|
|
22
|
+
const PARENT_SUBAGENT_RUN_ID_ENV = "PI_WORKFLOW_PARENT_SUBAGENT_RUN_ID";
|
|
23
|
+
const PARENT_SUBAGENT_ATTEMPT_ID_ENV = "PI_WORKFLOW_PARENT_SUBAGENT_ATTEMPT_ID";
|
|
20
24
|
const DEFAULT_LAUNCH_SLOT_RELEASE_DELAY_MS = 3_000;
|
|
25
|
+
const STALE_LAUNCH_CLAIM_GRACE_MS = 30_000;
|
|
21
26
|
const MIN_TRANSIENT_RETRY_JITTER_MS = 1_000;
|
|
22
27
|
const MAX_TRANSIENT_RETRY_JITTER_MS = 5_000;
|
|
23
28
|
const MODULE_PATH = fileURLToPath(import.meta.url);
|
|
@@ -39,6 +44,12 @@ function bundledNodeModulePath(packageName, ...parts) {
|
|
|
39
44
|
];
|
|
40
45
|
return (candidates.find((candidate) => existsSync(candidate)) ?? candidates[0]);
|
|
41
46
|
}
|
|
47
|
+
const GENERIC_TASK_STATUS_DETAILS = new Set([
|
|
48
|
+
"completed",
|
|
49
|
+
"failed",
|
|
50
|
+
"interrupted",
|
|
51
|
+
"running",
|
|
52
|
+
]);
|
|
42
53
|
const subagentApiSpecifier = "@agwab/pi-subagent/api";
|
|
43
54
|
let cachedSubagentApi;
|
|
44
55
|
let injectedSubagentApi;
|
|
@@ -52,6 +63,67 @@ async function loadSubagentApi() {
|
|
|
52
63
|
cachedSubagentApi ??= import(subagentApiSpecifier).then((mod) => mod);
|
|
53
64
|
return cachedSubagentApi;
|
|
54
65
|
}
|
|
66
|
+
function nonEmptyEnv(env, key) {
|
|
67
|
+
const value = env[key]?.trim();
|
|
68
|
+
return value ? value : undefined;
|
|
69
|
+
}
|
|
70
|
+
function parentSubagentRefFromEnv(env = process.env) {
|
|
71
|
+
const cwd = nonEmptyEnv(env, PARENT_SUBAGENT_CWD_ENV);
|
|
72
|
+
const runsDir = nonEmptyEnv(env, PARENT_SUBAGENT_RUNS_DIR_ENV);
|
|
73
|
+
const runId = nonEmptyEnv(env, PARENT_SUBAGENT_RUN_ID_ENV);
|
|
74
|
+
if (!cwd || !runsDir || !runId)
|
|
75
|
+
return undefined;
|
|
76
|
+
const attemptId = nonEmptyEnv(env, PARENT_SUBAGENT_ATTEMPT_ID_ENV);
|
|
77
|
+
return { cwd, runsDir, runId, ...(attemptId ? { attemptId } : {}) };
|
|
78
|
+
}
|
|
79
|
+
function terminalChildEventForTaskStatus(status) {
|
|
80
|
+
if (status === "completed")
|
|
81
|
+
return "completed";
|
|
82
|
+
if (status === "failed")
|
|
83
|
+
return "failed";
|
|
84
|
+
if (status === "interrupted")
|
|
85
|
+
return "cancelled";
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
async function recordParentSubagentChildEvent(options) {
|
|
89
|
+
const parent = parentSubagentRefFromEnv();
|
|
90
|
+
if (!parent)
|
|
91
|
+
return;
|
|
92
|
+
const api = await loadSubagentApi().catch(() => undefined);
|
|
93
|
+
if (!api?.recordSubagentChildEvent)
|
|
94
|
+
return;
|
|
95
|
+
await api
|
|
96
|
+
.recordSubagentChildEvent({
|
|
97
|
+
...parent,
|
|
98
|
+
event: options.event,
|
|
99
|
+
childRunId: options.childRunId,
|
|
100
|
+
workflowRunId: options.run.runId,
|
|
101
|
+
childTaskId: options.task.taskId,
|
|
102
|
+
...(options.failureKind === undefined
|
|
103
|
+
? {}
|
|
104
|
+
: { failureKind: options.failureKind }),
|
|
105
|
+
...(options.message === undefined ? {} : { message: options.message }),
|
|
106
|
+
})
|
|
107
|
+
.catch(() => undefined);
|
|
108
|
+
}
|
|
109
|
+
async function recordTerminalParentSubagentChildEvent(run, task, snapshot) {
|
|
110
|
+
const event = terminalChildEventForTaskStatus(task.status);
|
|
111
|
+
if (!event)
|
|
112
|
+
return;
|
|
113
|
+
const taskFailureKind = task.statusDetail && !GENERIC_TASK_STATUS_DETAILS.has(task.statusDetail)
|
|
114
|
+
? task.statusDetail
|
|
115
|
+
: undefined;
|
|
116
|
+
await recordParentSubagentChildEvent({
|
|
117
|
+
event,
|
|
118
|
+
childRunId: snapshot.runId,
|
|
119
|
+
run,
|
|
120
|
+
task,
|
|
121
|
+
failureKind: event === "completed"
|
|
122
|
+
? undefined
|
|
123
|
+
: (snapshot.failureKind ?? taskFailureKind ?? task.statusDetail),
|
|
124
|
+
message: task.lastMessage,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
55
127
|
let launchSlotReleaseDelayMs = DEFAULT_LAUNCH_SLOT_RELEASE_DELAY_MS;
|
|
56
128
|
let transientRetryJitterForTests;
|
|
57
129
|
const launchWaitQueue = [];
|
|
@@ -87,8 +159,7 @@ function releaseLaunchSlotAfterDelay(delayMs, release) {
|
|
|
87
159
|
release();
|
|
88
160
|
return;
|
|
89
161
|
}
|
|
90
|
-
|
|
91
|
-
timer.unref?.();
|
|
162
|
+
setTimeout(release, delayMs);
|
|
92
163
|
}
|
|
93
164
|
async function runWithLaunchSlot(action) {
|
|
94
165
|
const release = await acquireLaunchSlot();
|
|
@@ -129,8 +200,6 @@ export function setSubagentLaunchControlsForTests(options) {
|
|
|
129
200
|
}
|
|
130
201
|
export async function cleanupSubagentRun(_cwd, run) {
|
|
131
202
|
for (const task of run.tasks) {
|
|
132
|
-
if (isTerminalTaskStatus(task.status))
|
|
133
|
-
continue;
|
|
134
203
|
const handle = getSubagentHandle(task);
|
|
135
204
|
if (!handle)
|
|
136
205
|
continue;
|
|
@@ -240,6 +309,13 @@ export async function launchSubagentTask(cwd, run, task, compiledTask) {
|
|
|
240
309
|
task.statusDetail = "running";
|
|
241
310
|
task.lastMessage = "launched via pi-subagent/headless";
|
|
242
311
|
await writeRunRecord(cwd, run).catch(() => undefined);
|
|
312
|
+
await recordParentSubagentChildEvent({
|
|
313
|
+
event: "started",
|
|
314
|
+
childRunId: launched.runId,
|
|
315
|
+
run,
|
|
316
|
+
task,
|
|
317
|
+
message: task.lastMessage,
|
|
318
|
+
});
|
|
243
319
|
return { kind: "launched" };
|
|
244
320
|
}
|
|
245
321
|
export async function refreshRunFromSubagentArtifacts(cwd, run) {
|
|
@@ -266,8 +342,13 @@ export async function refreshRunFromSubagentArtifacts(cwd, run) {
|
|
|
266
342
|
}
|
|
267
343
|
}
|
|
268
344
|
if (!handle) {
|
|
345
|
+
if (isStaleLaunchClaim(task)) {
|
|
346
|
+
resetStaleLaunchClaim(task);
|
|
347
|
+
changed = true;
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
269
350
|
if (isTaskTimedOut(task)) {
|
|
270
|
-
|
|
351
|
+
markSubagentTaskTimedOut(task);
|
|
271
352
|
changed = true;
|
|
272
353
|
}
|
|
273
354
|
continue;
|
|
@@ -290,16 +371,8 @@ export async function refreshRunFromSubagentArtifacts(cwd, run) {
|
|
|
290
371
|
.catch(() => null);
|
|
291
372
|
if (snapshot === null) {
|
|
292
373
|
if (isTaskTimedOut(task)) {
|
|
293
|
-
await api
|
|
294
|
-
|
|
295
|
-
cwd: handle.cwd,
|
|
296
|
-
runsDir: handle.runsDir,
|
|
297
|
-
runId: handle.runId,
|
|
298
|
-
attemptId: handle.attemptId,
|
|
299
|
-
reason: "workflow timeout",
|
|
300
|
-
})
|
|
301
|
-
.catch(() => undefined);
|
|
302
|
-
markTaskTimedOut(task);
|
|
374
|
+
await interruptTimedOutSubagent(api, handle);
|
|
375
|
+
markSubagentTaskTimedOut(task);
|
|
303
376
|
changed = true;
|
|
304
377
|
}
|
|
305
378
|
continue;
|
|
@@ -312,16 +385,8 @@ export async function refreshRunFromSubagentArtifacts(cwd, run) {
|
|
|
312
385
|
? `pi-subagent heartbeat ${activeAttempt.heartbeatAt}`
|
|
313
386
|
: "pi-subagent running";
|
|
314
387
|
if (isTaskTimedOut(task)) {
|
|
315
|
-
await api
|
|
316
|
-
|
|
317
|
-
cwd: handle.cwd,
|
|
318
|
-
runsDir: handle.runsDir,
|
|
319
|
-
runId: handle.runId,
|
|
320
|
-
attemptId: handle.attemptId,
|
|
321
|
-
reason: "workflow timeout",
|
|
322
|
-
})
|
|
323
|
-
.catch(() => undefined);
|
|
324
|
-
markTaskTimedOut(task);
|
|
388
|
+
await interruptTimedOutSubagent(api, handle);
|
|
389
|
+
markSubagentTaskTimedOut(task);
|
|
325
390
|
changed = true;
|
|
326
391
|
}
|
|
327
392
|
continue;
|
|
@@ -333,6 +398,40 @@ export async function refreshRunFromSubagentArtifacts(cwd, run) {
|
|
|
333
398
|
await writeRunRecord(cwd, run);
|
|
334
399
|
return run;
|
|
335
400
|
}
|
|
401
|
+
async function interruptTimedOutSubagent(api, handle) {
|
|
402
|
+
await api
|
|
403
|
+
.interruptSubagent({
|
|
404
|
+
cwd: handle.cwd,
|
|
405
|
+
runsDir: handle.runsDir,
|
|
406
|
+
runId: handle.runId,
|
|
407
|
+
attemptId: handle.attemptId,
|
|
408
|
+
reason: "workflow timeout",
|
|
409
|
+
})
|
|
410
|
+
.catch(() => undefined);
|
|
411
|
+
}
|
|
412
|
+
function markSubagentTaskTimedOut(task) {
|
|
413
|
+
markTaskTimedOut(task);
|
|
414
|
+
task.backendHandle = undefined;
|
|
415
|
+
task.backendTaskId = task.taskId;
|
|
416
|
+
task.pid = undefined;
|
|
417
|
+
}
|
|
418
|
+
function isStaleLaunchClaim(task) {
|
|
419
|
+
if (task.statusDetail !== "launching" || !task.startedAt)
|
|
420
|
+
return false;
|
|
421
|
+
const startedAtMs = Date.parse(task.startedAt);
|
|
422
|
+
return (Number.isFinite(startedAtMs) &&
|
|
423
|
+
Date.now() - startedAtMs > STALE_LAUNCH_CLAIM_GRACE_MS);
|
|
424
|
+
}
|
|
425
|
+
function resetStaleLaunchClaim(task) {
|
|
426
|
+
task.status = "pending";
|
|
427
|
+
task.statusDetail = "pending";
|
|
428
|
+
task.startedAt = undefined;
|
|
429
|
+
task.backendHandle = undefined;
|
|
430
|
+
task.backendFiles = undefined;
|
|
431
|
+
task.backendTaskId = task.taskId;
|
|
432
|
+
task.pid = undefined;
|
|
433
|
+
task.lastMessage = "stale pi-subagent launch claim reset";
|
|
434
|
+
}
|
|
336
435
|
async function materializeTerminalSubagentResult(cwd, run, task, snapshot) {
|
|
337
436
|
const outputRef = findLog(snapshot, "output");
|
|
338
437
|
const stderrRef = findLog(snapshot, "stderr");
|
|
@@ -386,7 +485,7 @@ async function materializeTerminalSubagentResult(cwd, run, task, snapshot) {
|
|
|
386
485
|
const contextLengthExceeded = Boolean(subagentResult?.metadata?.contextLengthExceeded ??
|
|
387
486
|
snapshot.metadata?.contextLengthExceeded);
|
|
388
487
|
if (task.artifactGraph?.enabled && statusInfo.status === "completed") {
|
|
389
|
-
|
|
488
|
+
const changed = await materializeTerminalArtifactGraphResult(cwd, run, task, {
|
|
390
489
|
outputFile,
|
|
391
490
|
stderrFile,
|
|
392
491
|
resultFile,
|
|
@@ -395,6 +494,8 @@ async function materializeTerminalSubagentResult(cwd, run, task, snapshot) {
|
|
|
395
494
|
exitCode,
|
|
396
495
|
subagentResult,
|
|
397
496
|
});
|
|
497
|
+
await recordTerminalParentSubagentChildEvent(run, task, snapshot);
|
|
498
|
+
return changed;
|
|
398
499
|
}
|
|
399
500
|
if (shouldAttemptArtifactGraphSalvage({
|
|
400
501
|
task,
|
|
@@ -406,7 +507,7 @@ async function materializeTerminalSubagentResult(cwd, run, task, snapshot) {
|
|
|
406
507
|
subagentResult,
|
|
407
508
|
snapshot,
|
|
408
509
|
})) {
|
|
409
|
-
|
|
510
|
+
const changed = await materializeTerminalArtifactGraphResult(cwd, run, task, {
|
|
410
511
|
outputFile,
|
|
411
512
|
stderrFile,
|
|
412
513
|
resultFile,
|
|
@@ -420,6 +521,8 @@ async function materializeTerminalSubagentResult(cwd, run, task, snapshot) {
|
|
|
420
521
|
subagentFailureKind: snapshot.failureKind,
|
|
421
522
|
},
|
|
422
523
|
});
|
|
524
|
+
await recordTerminalParentSubagentChildEvent(run, task, snapshot);
|
|
525
|
+
return changed;
|
|
423
526
|
}
|
|
424
527
|
const workflowResult = {
|
|
425
528
|
status: statusInfo.status,
|
|
@@ -447,10 +550,12 @@ async function materializeTerminalSubagentResult(cwd, run, task, snapshot) {
|
|
|
447
550
|
};
|
|
448
551
|
if (shouldRetryTransientModelFailure(statusInfo, workflowResult, outputBytes)) {
|
|
449
552
|
await writeJson(transientFailureAttemptPath(resultFile, (task.launchRetry?.attempts ?? 0) + 1), workflowResult);
|
|
450
|
-
|
|
553
|
+
const changed = retryOrFailTransientSubagentFailure(task, {
|
|
451
554
|
reason: statusInfo.failureKind ?? "model",
|
|
452
555
|
message: errorMessage ?? "pi-subagent run failed before producing output",
|
|
453
556
|
});
|
|
557
|
+
await recordTerminalParentSubagentChildEvent(run, task, snapshot);
|
|
558
|
+
return changed;
|
|
454
559
|
}
|
|
455
560
|
await writeJson(resultFile, workflowResult);
|
|
456
561
|
const completedAfterTimeout = resultCompletedAfterTimeout(task, completedAt);
|
|
@@ -464,6 +569,7 @@ async function materializeTerminalSubagentResult(cwd, run, task, snapshot) {
|
|
|
464
569
|
delete task.backendHandle;
|
|
465
570
|
delete task.backendFiles;
|
|
466
571
|
}
|
|
572
|
+
await recordTerminalParentSubagentChildEvent(run, task, snapshot);
|
|
467
573
|
return changed;
|
|
468
574
|
}
|
|
469
575
|
function artifactGraphRetrySession(run, task, subagentResult, attempt) {
|
|
@@ -1145,6 +1251,7 @@ async function recoverSubagentHandle(run, task) {
|
|
|
1145
1251
|
const runsDir = subagentRunsDir(run, task);
|
|
1146
1252
|
const absoluteRunsDir = resolve(task.cwd, runsDir);
|
|
1147
1253
|
const expectedCorrelationId = `${run.runId}:${task.taskId}`;
|
|
1254
|
+
const claimStartedAtMs = timestampMs(task.startedAt);
|
|
1148
1255
|
const entries = await readdir(absoluteRunsDir, { withFileTypes: true }).catch(() => []);
|
|
1149
1256
|
const candidates = [];
|
|
1150
1257
|
for (const entry of entries) {
|
|
@@ -1153,6 +1260,8 @@ async function recoverSubagentHandle(run, task) {
|
|
|
1153
1260
|
const record = await readJsonLoose(join(absoluteRunsDir, entry.name, "run.json"));
|
|
1154
1261
|
if (!record || record.correlationId !== expectedCorrelationId)
|
|
1155
1262
|
continue;
|
|
1263
|
+
if (isPreClaimSubagentRecord(record, claimStartedAtMs))
|
|
1264
|
+
continue;
|
|
1156
1265
|
const attemptId = record.activeAttemptId ??
|
|
1157
1266
|
record.latestAttemptId ??
|
|
1158
1267
|
record.attempts?.at(-1)?.attemptId;
|
|
@@ -1170,6 +1279,14 @@ async function recoverSubagentHandle(run, task) {
|
|
|
1170
1279
|
candidates.sort((left, right) => right.updatedAtMs - left.updatedAtMs);
|
|
1171
1280
|
return candidates[0]?.handle;
|
|
1172
1281
|
}
|
|
1282
|
+
function isPreClaimSubagentRecord(record, claimStartedAtMs) {
|
|
1283
|
+
if (claimStartedAtMs === undefined)
|
|
1284
|
+
return false;
|
|
1285
|
+
const recordStartedAtMs = timestampMs(record.startedAt) ??
|
|
1286
|
+
timestampMs(record.attempts?.[0]?.startedAt) ??
|
|
1287
|
+
timestampMs(record.updatedAt);
|
|
1288
|
+
return (recordStartedAtMs !== undefined && recordStartedAtMs < claimStartedAtMs);
|
|
1289
|
+
}
|
|
1173
1290
|
function timestampMs(value) {
|
|
1174
1291
|
if (value === undefined)
|
|
1175
1292
|
return undefined;
|
|
@@ -1213,7 +1330,16 @@ function subagentRunsDir(run, task) {
|
|
|
1213
1330
|
function subagentSessionId(run, task) {
|
|
1214
1331
|
if (!task.artifactGraph?.enabled)
|
|
1215
1332
|
return undefined;
|
|
1216
|
-
|
|
1333
|
+
const baseSessionId = baseSubagentSessionId(run, task);
|
|
1334
|
+
if (task.outputRetry?.sessionId)
|
|
1335
|
+
return task.outputRetry.sessionId;
|
|
1336
|
+
const launchAttempt = task.launchRetry?.attempts ?? 0;
|
|
1337
|
+
if (launchAttempt > 0)
|
|
1338
|
+
return `${baseSessionId}:launch-retry-${launchAttempt}`;
|
|
1339
|
+
const resumeAttempt = task.resumeEvents?.length ?? 0;
|
|
1340
|
+
if (resumeAttempt > 0)
|
|
1341
|
+
return `${baseSessionId}:resume-${resumeAttempt}`;
|
|
1342
|
+
return baseSessionId;
|
|
1217
1343
|
}
|
|
1218
1344
|
function baseSubagentSessionId(run, task) {
|
|
1219
1345
|
return `pi-workflow.${run.runId}.${task.taskId}`.replace(/[^A-Za-z0-9._-]/g, "-");
|
package/dist/types.d.ts
CHANGED
|
@@ -484,6 +484,9 @@ export interface CompiledTask {
|
|
|
484
484
|
branchId?: string;
|
|
485
485
|
outputProfile?: string;
|
|
486
486
|
};
|
|
487
|
+
foreachGenerated?: {
|
|
488
|
+
placeholderSpecId: string;
|
|
489
|
+
};
|
|
487
490
|
loopChild?: CompiledLoopChildTaskRef;
|
|
488
491
|
loopPlaceholder?: {
|
|
489
492
|
loopId: string;
|
|
@@ -562,6 +565,9 @@ export interface WorkflowTaskRunRecord {
|
|
|
562
565
|
branchId?: string;
|
|
563
566
|
outputProfile?: string;
|
|
564
567
|
};
|
|
568
|
+
foreachGenerated?: {
|
|
569
|
+
placeholderSpecId: string;
|
|
570
|
+
};
|
|
565
571
|
launchRetry?: {
|
|
566
572
|
attempts: number;
|
|
567
573
|
maxAttempts?: number;
|
package/dist/workflow-runtime.js
CHANGED
|
@@ -193,9 +193,18 @@ export function readSimpleJsonPath(value, path) {
|
|
|
193
193
|
const parts = path.slice(2).split(".").filter(Boolean);
|
|
194
194
|
let current = value;
|
|
195
195
|
for (const part of parts) {
|
|
196
|
-
if (current
|
|
196
|
+
if (!canReadJsonPathPart(current, part))
|
|
197
197
|
return undefined;
|
|
198
198
|
current = current[part];
|
|
199
199
|
}
|
|
200
200
|
return current;
|
|
201
201
|
}
|
|
202
|
+
function canReadJsonPathPart(value, part) {
|
|
203
|
+
return (isSafeJsonPathPart(part) && isRecord(value) && Object.hasOwn(value, part));
|
|
204
|
+
}
|
|
205
|
+
function isSafeJsonPathPart(part) {
|
|
206
|
+
return part !== "__proto__" && part !== "prototype" && part !== "constructor";
|
|
207
|
+
}
|
|
208
|
+
function isRecord(value) {
|
|
209
|
+
return typeof value === "object" && value !== null;
|
|
210
|
+
}
|
package/dist/workflow-view.js
CHANGED
|
@@ -953,12 +953,14 @@ function statusForSummary(summary) {
|
|
|
953
953
|
return "running";
|
|
954
954
|
if (summary.blocked > 0)
|
|
955
955
|
return "blocked";
|
|
956
|
-
if (summary.failed > 0
|
|
956
|
+
if (summary.failed > 0)
|
|
957
957
|
return "failed";
|
|
958
958
|
if (summary.pending > 0)
|
|
959
959
|
return "pending";
|
|
960
960
|
if (summary.total > 0 && summary.completed === summary.total)
|
|
961
961
|
return "completed";
|
|
962
|
+
if (summary.interrupted > 0)
|
|
963
|
+
return "interrupted";
|
|
962
964
|
return "interrupted";
|
|
963
965
|
}
|
|
964
966
|
function taskElapsed(task) {
|