@agwab/pi-workflow 0.2.1 → 0.4.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/README.md +3 -1
- package/dist/artifact-graph-runtime.d.ts +1 -1
- package/dist/artifact-graph-runtime.js +10 -5
- package/dist/artifact-graph-schema.js +127 -5
- package/dist/compiler.js +52 -19
- package/dist/dynamic-generated-task-runtime.js +3 -1
- package/dist/dynamic-profiles.d.ts +1 -1
- package/dist/engine-run-graph.d.ts +3 -0
- package/dist/engine-run-graph.js +194 -4
- package/dist/engine.d.ts +5 -0
- package/dist/engine.js +389 -41
- package/dist/extension.d.ts +2 -1
- package/dist/extension.js +30 -8
- package/dist/index.d.ts +11 -3
- package/dist/index.js +6 -1
- package/dist/prompt-json.d.ts +7 -0
- package/dist/prompt-json.js +13 -0
- package/dist/roles.d.ts +1 -1
- package/dist/roles.js +5 -8
- package/dist/store.d.ts +20 -1
- package/dist/store.js +139 -35
- package/dist/strings.d.ts +11 -0
- package/dist/strings.js +24 -0
- package/dist/subagent-backend.js +710 -40
- package/dist/types.d.ts +107 -1
- package/dist/verification-ontology.d.ts +31 -0
- package/dist/verification-ontology.js +66 -0
- package/dist/workflow-artifact-tool.js +5 -6
- package/dist/workflow-artifacts.d.ts +7 -0
- package/dist/workflow-artifacts.js +55 -4
- package/dist/workflow-fetch-cache-extension.d.ts +1 -0
- package/dist/workflow-fetch-cache-extension.js +57 -9
- package/dist/workflow-metrics.d.ts +113 -0
- package/dist/workflow-metrics.js +272 -0
- package/dist/workflow-output-artifacts.js +5 -3
- package/dist/workflow-partial-output.d.ts +45 -0
- package/dist/workflow-partial-output.js +205 -0
- package/dist/workflow-progress-health.js +42 -10
- package/dist/workflow-runtime.js +10 -1
- package/dist/workflow-view.js +3 -1
- package/dist/workflow-web-source-extension.js +194 -52
- package/dist/workflow-web-source.d.ts +2 -1
- package/dist/workflow-web-source.js +109 -30
- package/docs/usage.md +76 -29
- 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 +1046 -576
- 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 +1356 -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/skills/workflow-guide/SKILL.md +1 -0
- package/src/artifact-graph-runtime.ts +19 -13
- package/src/artifact-graph-schema.ts +143 -3
- package/src/cli.mjs +52 -0
- package/src/compiler.ts +63 -18
- package/src/dynamic-generated-task-runtime.ts +3 -1
- package/src/dynamic-profiles.ts +1 -1
- package/src/engine-run-graph.ts +246 -4
- package/src/engine.ts +545 -38
- package/src/extension.ts +36 -6
- package/src/index.ts +52 -1
- package/src/prompt-json.ts +13 -0
- package/src/roles.ts +6 -9
- package/src/store.ts +194 -42
- package/src/strings.ts +38 -0
- package/src/subagent-backend.ts +921 -62
- package/src/types.ts +116 -2
- package/src/verification-ontology.ts +88 -0
- package/src/workflow-artifact-tool.ts +5 -7
- package/src/workflow-artifacts.ts +83 -3
- package/src/workflow-fetch-cache-extension.ts +78 -13
- package/src/workflow-metrics.ts +478 -0
- package/src/workflow-output-artifacts.ts +5 -3
- package/src/workflow-partial-output.ts +299 -0
- package/src/workflow-progress-health.ts +47 -15
- package/src/workflow-runtime.ts +18 -2
- package/src/workflow-view.ts +2 -1
- package/src/workflow-web-source-extension.ts +654 -232
- package/src/workflow-web-source.ts +153 -39
- package/workflows/README.md +7 -25
- package/workflows/deep-research/batched-verification.spec.json +253 -0
- package/workflows/deep-research/helpers/batch-verification-candidates.mjs +136 -0
- package/workflows/deep-research/helpers/claim-evidence-gate.mjs +229 -36
- package/workflows/deep-research/helpers/final-audit-packet.mjs +1 -4
- package/workflows/deep-research/helpers/normalize-input-packet.mjs +81 -2
- package/workflows/deep-research/helpers/render-executive.mjs +40 -26
- package/workflows/deep-research/helpers/sanitize-verification-candidates.mjs +89 -15
- package/workflows/deep-research/helpers/shadow-select-verification.mjs +229 -0
- package/workflows/deep-research/helpers/verification-ontology.mjs +77 -0
- package/workflows/deep-research/schemas/deep-research-executive-render-control.schema.json +3 -3
- package/workflows/deep-research/schemas/deep-research-research-questions-control.schema.json +38 -0
- package/workflows/deep-research/schemas/deep-research-sanitize-claims-control.schema.json +63 -0
- package/workflows/deep-research/schemas/deep-research-verify-claims-batch-control.schema.json +47 -0
- package/workflows/deep-research/schemas/deep-research-verify-claims-control.schema.json +13 -3
- package/workflows/deep-research/spec.json +32 -12
- 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/skills/workflow-guide/scaffolds/dag-required-reads/spec.json.validate.stderr +0 -0
- package/skills/workflow-guide/scaffolds/dag-required-reads/spec.json.validate.stdout +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/subagent-backend.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import { existsSync } from "node:fs";
|
|
2
3
|
import { copyFile, mkdir, readFile, readdir, rm, writeFile, } from "node:fs/promises";
|
|
3
4
|
import { delimiter, dirname, extname, isAbsolute, join, relative, resolve, sep, } from "node:path";
|
|
@@ -10,14 +11,24 @@ import { writeWorkflowFetchCacheExtensionWrapper } from "./workflow-fetch-cache-
|
|
|
10
11
|
import { writeWorkflowWebSourceExtensionWrapper } from "./workflow-web-source-extension.js";
|
|
11
12
|
import { isWorkflowWebSourceTool } from "./workflow-web-source.js";
|
|
12
13
|
import { buildWorkflowOutputRetryInstructions, parseWorkflowOutputForBundle, writeWorkflowTaskArtifactBundle, } from "./workflow-output-artifacts.js";
|
|
14
|
+
import { writeWorkflowPartialOutputLedgerFromFile } from "./workflow-partial-output.js";
|
|
13
15
|
const DEFAULT_SUBAGENT_RUNS_ROOT = ".pi/workflow-subagents";
|
|
16
|
+
const MAX_SUBAGENT_SESSION_ID_LENGTH = 64;
|
|
14
17
|
const EXTRA_SUBAGENT_EXTENSIONS_ENV = "PI_WORKFLOW_SUBAGENT_EXTRA_EXTENSIONS";
|
|
15
18
|
const FETCH_CONTENT_CACHE_ENV = "PI_WORKFLOW_FETCH_CONTENT_CACHE";
|
|
16
19
|
const LEGACY_FETCH_CACHE_ENV = "PI_WORKFLOW_FETCH_CACHE";
|
|
20
|
+
const FETCH_CONTENT_INLINE_CHARS_ENV = "PI_WORKFLOW_FETCH_CONTENT_INLINE_CHARS";
|
|
21
|
+
const DEFAULT_WORKFLOW_FETCH_CONTENT_INLINE_CHARS = 12_000;
|
|
17
22
|
const DEFAULT_TRANSIENT_MODEL_FAILURE_RETRIES = 5;
|
|
18
23
|
const DEFAULT_ARTIFACT_OUTPUT_RETRIES = 2;
|
|
19
24
|
const MAX_CONCURRENT_LAUNCHES_ENV = "PI_WORKFLOW_MAX_CONCURRENT_LAUNCHES";
|
|
25
|
+
const LAUNCH_SLOT_RELEASE_DELAY_MS_ENV = "PI_WORKFLOW_LAUNCH_SLOT_RELEASE_DELAY_MS";
|
|
26
|
+
const PARENT_SUBAGENT_CWD_ENV = "PI_WORKFLOW_PARENT_SUBAGENT_CWD";
|
|
27
|
+
const PARENT_SUBAGENT_RUNS_DIR_ENV = "PI_WORKFLOW_PARENT_SUBAGENT_RUNS_DIR";
|
|
28
|
+
const PARENT_SUBAGENT_RUN_ID_ENV = "PI_WORKFLOW_PARENT_SUBAGENT_RUN_ID";
|
|
29
|
+
const PARENT_SUBAGENT_ATTEMPT_ID_ENV = "PI_WORKFLOW_PARENT_SUBAGENT_ATTEMPT_ID";
|
|
20
30
|
const DEFAULT_LAUNCH_SLOT_RELEASE_DELAY_MS = 3_000;
|
|
31
|
+
const STALE_LAUNCH_CLAIM_GRACE_MS = 30_000;
|
|
21
32
|
const MIN_TRANSIENT_RETRY_JITTER_MS = 1_000;
|
|
22
33
|
const MAX_TRANSIENT_RETRY_JITTER_MS = 5_000;
|
|
23
34
|
const MODULE_PATH = fileURLToPath(import.meta.url);
|
|
@@ -39,6 +50,12 @@ function bundledNodeModulePath(packageName, ...parts) {
|
|
|
39
50
|
];
|
|
40
51
|
return (candidates.find((candidate) => existsSync(candidate)) ?? candidates[0]);
|
|
41
52
|
}
|
|
53
|
+
const GENERIC_TASK_STATUS_DETAILS = new Set([
|
|
54
|
+
"completed",
|
|
55
|
+
"failed",
|
|
56
|
+
"interrupted",
|
|
57
|
+
"running",
|
|
58
|
+
]);
|
|
42
59
|
const subagentApiSpecifier = "@agwab/pi-subagent/api";
|
|
43
60
|
let cachedSubagentApi;
|
|
44
61
|
let injectedSubagentApi;
|
|
@@ -52,7 +69,68 @@ async function loadSubagentApi() {
|
|
|
52
69
|
cachedSubagentApi ??= import(subagentApiSpecifier).then((mod) => mod);
|
|
53
70
|
return cachedSubagentApi;
|
|
54
71
|
}
|
|
55
|
-
|
|
72
|
+
function nonEmptyEnv(env, key) {
|
|
73
|
+
const value = env[key]?.trim();
|
|
74
|
+
return value ? value : undefined;
|
|
75
|
+
}
|
|
76
|
+
function parentSubagentRefFromEnv(env = process.env) {
|
|
77
|
+
const cwd = nonEmptyEnv(env, PARENT_SUBAGENT_CWD_ENV);
|
|
78
|
+
const runsDir = nonEmptyEnv(env, PARENT_SUBAGENT_RUNS_DIR_ENV);
|
|
79
|
+
const runId = nonEmptyEnv(env, PARENT_SUBAGENT_RUN_ID_ENV);
|
|
80
|
+
if (!cwd || !runsDir || !runId)
|
|
81
|
+
return undefined;
|
|
82
|
+
const attemptId = nonEmptyEnv(env, PARENT_SUBAGENT_ATTEMPT_ID_ENV);
|
|
83
|
+
return { cwd, runsDir, runId, ...(attemptId ? { attemptId } : {}) };
|
|
84
|
+
}
|
|
85
|
+
function terminalChildEventForTaskStatus(status) {
|
|
86
|
+
if (status === "completed")
|
|
87
|
+
return "completed";
|
|
88
|
+
if (status === "failed")
|
|
89
|
+
return "failed";
|
|
90
|
+
if (status === "interrupted")
|
|
91
|
+
return "cancelled";
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
async function recordParentSubagentChildEvent(options) {
|
|
95
|
+
const parent = parentSubagentRefFromEnv();
|
|
96
|
+
if (!parent)
|
|
97
|
+
return;
|
|
98
|
+
const api = await loadSubagentApi().catch(() => undefined);
|
|
99
|
+
if (!api?.recordSubagentChildEvent)
|
|
100
|
+
return;
|
|
101
|
+
await api
|
|
102
|
+
.recordSubagentChildEvent({
|
|
103
|
+
...parent,
|
|
104
|
+
event: options.event,
|
|
105
|
+
childRunId: options.childRunId,
|
|
106
|
+
workflowRunId: options.run.runId,
|
|
107
|
+
childTaskId: options.task.taskId,
|
|
108
|
+
...(options.failureKind === undefined
|
|
109
|
+
? {}
|
|
110
|
+
: { failureKind: options.failureKind }),
|
|
111
|
+
...(options.message === undefined ? {} : { message: options.message }),
|
|
112
|
+
})
|
|
113
|
+
.catch(() => undefined);
|
|
114
|
+
}
|
|
115
|
+
async function recordTerminalParentSubagentChildEvent(run, task, snapshot) {
|
|
116
|
+
const event = terminalChildEventForTaskStatus(task.status);
|
|
117
|
+
if (!event)
|
|
118
|
+
return;
|
|
119
|
+
const taskFailureKind = task.statusDetail && !GENERIC_TASK_STATUS_DETAILS.has(task.statusDetail)
|
|
120
|
+
? task.statusDetail
|
|
121
|
+
: undefined;
|
|
122
|
+
await recordParentSubagentChildEvent({
|
|
123
|
+
event,
|
|
124
|
+
childRunId: snapshot.runId,
|
|
125
|
+
run,
|
|
126
|
+
task,
|
|
127
|
+
failureKind: event === "completed"
|
|
128
|
+
? undefined
|
|
129
|
+
: (snapshot.failureKind ?? taskFailureKind ?? task.statusDetail),
|
|
130
|
+
message: task.lastMessage,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
let launchSlotReleaseDelayMsForTests;
|
|
56
134
|
let transientRetryJitterForTests;
|
|
57
135
|
const launchWaitQueue = [];
|
|
58
136
|
let activeLaunchSlots = 0;
|
|
@@ -82,16 +160,25 @@ function releaseLaunchSlot() {
|
|
|
82
160
|
}
|
|
83
161
|
activeLaunchSlots = Math.max(0, activeLaunchSlots - 1);
|
|
84
162
|
}
|
|
163
|
+
function resolveLaunchSlotReleaseDelayMs() {
|
|
164
|
+
if (launchSlotReleaseDelayMsForTests !== undefined) {
|
|
165
|
+
return launchSlotReleaseDelayMsForTests;
|
|
166
|
+
}
|
|
167
|
+
const override = Number.parseInt(process.env[LAUNCH_SLOT_RELEASE_DELAY_MS_ENV] ?? "", 10);
|
|
168
|
+
if (Number.isFinite(override))
|
|
169
|
+
return Math.max(0, Math.floor(override));
|
|
170
|
+
return DEFAULT_LAUNCH_SLOT_RELEASE_DELAY_MS;
|
|
171
|
+
}
|
|
85
172
|
function releaseLaunchSlotAfterDelay(delayMs, release) {
|
|
86
173
|
if (delayMs <= 0) {
|
|
87
174
|
release();
|
|
88
175
|
return;
|
|
89
176
|
}
|
|
90
|
-
|
|
91
|
-
timer.unref?.();
|
|
177
|
+
setTimeout(release, delayMs);
|
|
92
178
|
}
|
|
93
|
-
async function runWithLaunchSlot(action) {
|
|
179
|
+
async function runWithLaunchSlot(action, onAcquired) {
|
|
94
180
|
const release = await acquireLaunchSlot();
|
|
181
|
+
onAcquired?.();
|
|
95
182
|
let holdAfterReturn = false;
|
|
96
183
|
try {
|
|
97
184
|
const result = await action();
|
|
@@ -99,7 +186,7 @@ async function runWithLaunchSlot(action) {
|
|
|
99
186
|
return result;
|
|
100
187
|
}
|
|
101
188
|
finally {
|
|
102
|
-
releaseLaunchSlotAfterDelay(holdAfterReturn ?
|
|
189
|
+
releaseLaunchSlotAfterDelay(holdAfterReturn ? resolveLaunchSlotReleaseDelayMs() : 0, release);
|
|
103
190
|
}
|
|
104
191
|
}
|
|
105
192
|
function transientRetryJitterMs() {
|
|
@@ -112,10 +199,459 @@ function transientRetryJitterMs() {
|
|
|
112
199
|
function sleep(ms) {
|
|
113
200
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
114
201
|
}
|
|
202
|
+
const USAGE_METRIC_KEYS = [
|
|
203
|
+
"inputTokens",
|
|
204
|
+
"outputTokens",
|
|
205
|
+
"totalTokens",
|
|
206
|
+
"cachedInputTokens",
|
|
207
|
+
"cacheCreationInputTokens",
|
|
208
|
+
"cacheReadInputTokens",
|
|
209
|
+
"reasoningTokens",
|
|
210
|
+
"costUsd",
|
|
211
|
+
];
|
|
212
|
+
const USAGE_FIELD_ALIASES = {
|
|
213
|
+
inputTokens: [
|
|
214
|
+
["inputTokens"],
|
|
215
|
+
["input_tokens"],
|
|
216
|
+
["input"],
|
|
217
|
+
["promptTokens"],
|
|
218
|
+
["prompt_tokens"],
|
|
219
|
+
],
|
|
220
|
+
outputTokens: [
|
|
221
|
+
["outputTokens"],
|
|
222
|
+
["output_tokens"],
|
|
223
|
+
["output"],
|
|
224
|
+
["completionTokens"],
|
|
225
|
+
["completion_tokens"],
|
|
226
|
+
],
|
|
227
|
+
totalTokens: [["totalTokens"], ["total_tokens"], ["tokens"], ["total"]],
|
|
228
|
+
cachedInputTokens: [
|
|
229
|
+
["cachedInputTokens"],
|
|
230
|
+
["cached_input_tokens"],
|
|
231
|
+
["prompt_tokens_details", "cached_tokens"],
|
|
232
|
+
["input_tokens_details", "cached_tokens"],
|
|
233
|
+
],
|
|
234
|
+
cacheCreationInputTokens: [
|
|
235
|
+
["cacheCreationInputTokens"],
|
|
236
|
+
["cacheCreationTokens"],
|
|
237
|
+
["cacheWriteTokens"],
|
|
238
|
+
["cache_creation_input_tokens"],
|
|
239
|
+
["cache_write_input_tokens"],
|
|
240
|
+
["cacheWrite"],
|
|
241
|
+
["cache_write"],
|
|
242
|
+
],
|
|
243
|
+
cacheReadInputTokens: [
|
|
244
|
+
["cacheReadInputTokens"],
|
|
245
|
+
["cacheReadTokens"],
|
|
246
|
+
["cache_read_input_tokens"],
|
|
247
|
+
["cacheRead"],
|
|
248
|
+
["cache_read"],
|
|
249
|
+
],
|
|
250
|
+
reasoningTokens: [
|
|
251
|
+
["reasoningTokens"],
|
|
252
|
+
["reasoning_tokens"],
|
|
253
|
+
["reasoning"],
|
|
254
|
+
["completion_tokens_details", "reasoning_tokens"],
|
|
255
|
+
["output_tokens_details", "reasoning_tokens"],
|
|
256
|
+
],
|
|
257
|
+
costUsd: [
|
|
258
|
+
["costUsd"],
|
|
259
|
+
["cost_usd"],
|
|
260
|
+
["totalCostUsd"],
|
|
261
|
+
["total_cost_usd"],
|
|
262
|
+
["estimatedCostUsd"],
|
|
263
|
+
["estimated_cost_usd"],
|
|
264
|
+
["cost", "total"],
|
|
265
|
+
["cost", "totalUsd"],
|
|
266
|
+
["cost", "total_usd"],
|
|
267
|
+
],
|
|
268
|
+
};
|
|
269
|
+
const TIMING_AGGREGATE_KEYS = [
|
|
270
|
+
"launchWaitMs",
|
|
271
|
+
"launchDurationMs",
|
|
272
|
+
"executionMs",
|
|
273
|
+
"totalMs",
|
|
274
|
+
];
|
|
275
|
+
function isPlainRecord(value) {
|
|
276
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
277
|
+
}
|
|
278
|
+
function hasOwnValue(record, key) {
|
|
279
|
+
return Object.hasOwn(record, key);
|
|
280
|
+
}
|
|
281
|
+
function valueAtPath(record, path) {
|
|
282
|
+
let current = record;
|
|
283
|
+
for (const part of path) {
|
|
284
|
+
if (!isPlainRecord(current) || !hasOwnValue(current, part)) {
|
|
285
|
+
return { found: false, value: undefined };
|
|
286
|
+
}
|
|
287
|
+
current = current[part];
|
|
288
|
+
}
|
|
289
|
+
return { found: true, value: current };
|
|
290
|
+
}
|
|
291
|
+
function usageNumberOrNull(value) {
|
|
292
|
+
if (value === null)
|
|
293
|
+
return null;
|
|
294
|
+
if (typeof value === "number" && Number.isFinite(value) && value >= 0) {
|
|
295
|
+
return value;
|
|
296
|
+
}
|
|
297
|
+
return undefined;
|
|
298
|
+
}
|
|
299
|
+
function normalizedUsageValues(raw) {
|
|
300
|
+
const record = isPlainRecord(raw) ? raw : undefined;
|
|
301
|
+
const values = {};
|
|
302
|
+
if (!record)
|
|
303
|
+
return values;
|
|
304
|
+
for (const key of USAGE_METRIC_KEYS) {
|
|
305
|
+
for (const path of USAGE_FIELD_ALIASES[key]) {
|
|
306
|
+
const candidate = valueAtPath(record, path);
|
|
307
|
+
if (!candidate.found)
|
|
308
|
+
continue;
|
|
309
|
+
const value = usageNumberOrNull(candidate.value);
|
|
310
|
+
if (value === undefined)
|
|
311
|
+
continue;
|
|
312
|
+
values[key] = value;
|
|
313
|
+
break;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return values;
|
|
317
|
+
}
|
|
318
|
+
function firstStringValue(records, keys) {
|
|
319
|
+
for (const record of records) {
|
|
320
|
+
if (!record)
|
|
321
|
+
continue;
|
|
322
|
+
for (const key of keys) {
|
|
323
|
+
const value = record[key];
|
|
324
|
+
if (typeof value === "string" && value.trim())
|
|
325
|
+
return value;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return undefined;
|
|
329
|
+
}
|
|
330
|
+
function metadataRecord(value) {
|
|
331
|
+
if (!isPlainRecord(value))
|
|
332
|
+
return undefined;
|
|
333
|
+
return isPlainRecord(value.metadata) ? value.metadata : undefined;
|
|
334
|
+
}
|
|
335
|
+
function usageObservation(subagentResult, snapshot) {
|
|
336
|
+
const resultMetadata = metadataRecord(subagentResult);
|
|
337
|
+
if (resultMetadata && hasOwnValue(resultMetadata, "usage")) {
|
|
338
|
+
return {
|
|
339
|
+
source: "subagent-result-metadata",
|
|
340
|
+
raw: resultMetadata.usage,
|
|
341
|
+
present: true,
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
const snapshotMetadata = isPlainRecord(snapshot.metadata)
|
|
345
|
+
? snapshot.metadata
|
|
346
|
+
: undefined;
|
|
347
|
+
if (snapshotMetadata && hasOwnValue(snapshotMetadata, "usage")) {
|
|
348
|
+
return {
|
|
349
|
+
source: "subagent-snapshot-metadata",
|
|
350
|
+
raw: snapshotMetadata.usage,
|
|
351
|
+
present: true,
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
if (subagentResult && hasOwnValue(subagentResult, "usage")) {
|
|
355
|
+
return {
|
|
356
|
+
source: "subagent-result",
|
|
357
|
+
raw: subagentResult.usage,
|
|
358
|
+
present: true,
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
const snapshotRecord = snapshot;
|
|
362
|
+
if (hasOwnValue(snapshotRecord, "usage")) {
|
|
363
|
+
return {
|
|
364
|
+
source: "subagent-snapshot",
|
|
365
|
+
raw: snapshotRecord.usage,
|
|
366
|
+
present: true,
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
return undefined;
|
|
370
|
+
}
|
|
371
|
+
function buildTaskUsageAttempt(options) {
|
|
372
|
+
const resultMetadata = metadataRecord(options.subagentResult);
|
|
373
|
+
const snapshotMetadata = isPlainRecord(options.snapshot.metadata)
|
|
374
|
+
? options.snapshot.metadata
|
|
375
|
+
: undefined;
|
|
376
|
+
const resultRecord = options.subagentResult;
|
|
377
|
+
const snapshotRecord = options.snapshot;
|
|
378
|
+
const records = [
|
|
379
|
+
resultMetadata,
|
|
380
|
+
snapshotMetadata,
|
|
381
|
+
resultRecord,
|
|
382
|
+
snapshotRecord,
|
|
383
|
+
];
|
|
384
|
+
const observed = usageObservation(options.subagentResult, options.snapshot);
|
|
385
|
+
const raw = observed?.raw;
|
|
386
|
+
const unavailable = !observed || raw === null || raw === undefined;
|
|
387
|
+
const provider = firstStringValue(records, ["provider"]);
|
|
388
|
+
const model = firstStringValue(records, ["model"]) ?? options.task.runtime.model;
|
|
389
|
+
const thinking = firstStringValue(records, [
|
|
390
|
+
"thinking",
|
|
391
|
+
"thinkingLevel",
|
|
392
|
+
"reasoningLevel",
|
|
393
|
+
]) ??
|
|
394
|
+
options.task.runtime.thinkingResolution?.resolved ??
|
|
395
|
+
options.task.runtime.thinking;
|
|
396
|
+
return {
|
|
397
|
+
source: observed?.source ?? "subagent-usage-unavailable",
|
|
398
|
+
capturedAt: options.capturedAt,
|
|
399
|
+
backendRunId: options.snapshot.runId,
|
|
400
|
+
backendAttemptId: options.snapshot.attemptId,
|
|
401
|
+
...(provider === undefined ? {} : { provider }),
|
|
402
|
+
...(model === undefined ? {} : { model }),
|
|
403
|
+
...(thinking === undefined ? {} : { thinking }),
|
|
404
|
+
...(unavailable ? { unavailable: true } : {}),
|
|
405
|
+
...(observed?.present && raw !== undefined ? { raw } : {}),
|
|
406
|
+
...normalizedUsageValues(raw),
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
function usageAttemptKey(attempt) {
|
|
410
|
+
return `${attempt.backendRunId ?? ""}\0${attempt.backendAttemptId ?? ""}\0${attempt.source}`;
|
|
411
|
+
}
|
|
412
|
+
function upsertUsageAttempt(attempts, attempt) {
|
|
413
|
+
const key = usageAttemptKey(attempt);
|
|
414
|
+
const index = attempts.findIndex((candidate) => usageAttemptKey(candidate) === key);
|
|
415
|
+
if (index < 0)
|
|
416
|
+
return [...attempts, attempt];
|
|
417
|
+
return attempts.map((candidate, candidateIndex) => candidateIndex === index ? attempt : candidate);
|
|
418
|
+
}
|
|
419
|
+
function aggregateUsageAttempts(attempts) {
|
|
420
|
+
const values = {};
|
|
421
|
+
let incomplete = attempts.some((attempt) => attempt.unavailable === true);
|
|
422
|
+
for (const key of USAGE_METRIC_KEYS) {
|
|
423
|
+
const anyPresent = attempts.some((attempt) => hasOwnValue(attempt, key));
|
|
424
|
+
if (!anyPresent)
|
|
425
|
+
continue;
|
|
426
|
+
let total = 0;
|
|
427
|
+
let complete = true;
|
|
428
|
+
for (const attempt of attempts) {
|
|
429
|
+
if (!hasOwnValue(attempt, key)) {
|
|
430
|
+
complete = false;
|
|
431
|
+
break;
|
|
432
|
+
}
|
|
433
|
+
const value = attempt[key];
|
|
434
|
+
if (typeof value !== "number") {
|
|
435
|
+
complete = false;
|
|
436
|
+
break;
|
|
437
|
+
}
|
|
438
|
+
total += value;
|
|
439
|
+
}
|
|
440
|
+
values[key] = complete ? total : null;
|
|
441
|
+
if (!complete)
|
|
442
|
+
incomplete = true;
|
|
443
|
+
}
|
|
444
|
+
return { values, incomplete };
|
|
445
|
+
}
|
|
446
|
+
function latestUsageString(attempts, key) {
|
|
447
|
+
for (let index = attempts.length - 1; index >= 0; index -= 1) {
|
|
448
|
+
const value = attempts[index]?.[key];
|
|
449
|
+
if (typeof value === "string" && value.trim())
|
|
450
|
+
return value;
|
|
451
|
+
}
|
|
452
|
+
return undefined;
|
|
453
|
+
}
|
|
454
|
+
function recordTaskUsageObservation(options) {
|
|
455
|
+
const attempt = buildTaskUsageAttempt(options);
|
|
456
|
+
const attempts = upsertUsageAttempt(options.task.usage?.attempts ?? [], attempt);
|
|
457
|
+
const aggregate = aggregateUsageAttempts(attempts);
|
|
458
|
+
const usage = {
|
|
459
|
+
source: "pi-subagent",
|
|
460
|
+
capturedAt: options.capturedAt,
|
|
461
|
+
...(latestUsageString(attempts, "provider") === undefined
|
|
462
|
+
? {}
|
|
463
|
+
: { provider: latestUsageString(attempts, "provider") }),
|
|
464
|
+
...(latestUsageString(attempts, "model") === undefined
|
|
465
|
+
? {}
|
|
466
|
+
: { model: latestUsageString(attempts, "model") }),
|
|
467
|
+
...(latestUsageString(attempts, "thinking") === undefined
|
|
468
|
+
? {}
|
|
469
|
+
: { thinking: latestUsageString(attempts, "thinking") }),
|
|
470
|
+
...(aggregate.incomplete ? { incomplete: true } : {}),
|
|
471
|
+
...aggregate.values,
|
|
472
|
+
aggregate: {
|
|
473
|
+
attempts: attempts.length,
|
|
474
|
+
...(aggregate.incomplete ? { incomplete: true } : {}),
|
|
475
|
+
...aggregate.values,
|
|
476
|
+
},
|
|
477
|
+
attempts,
|
|
478
|
+
};
|
|
479
|
+
options.task.usage = usage;
|
|
480
|
+
}
|
|
481
|
+
function isoTimestampMs(timestamp) {
|
|
482
|
+
if (!timestamp)
|
|
483
|
+
return undefined;
|
|
484
|
+
const parsed = Date.parse(timestamp);
|
|
485
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
486
|
+
}
|
|
487
|
+
function durationBetween(startedAt, completedAt) {
|
|
488
|
+
const startedAtMs = isoTimestampMs(startedAt);
|
|
489
|
+
const completedAtMs = isoTimestampMs(completedAt);
|
|
490
|
+
if (startedAtMs === undefined || completedAtMs === undefined)
|
|
491
|
+
return undefined;
|
|
492
|
+
return Math.max(0, completedAtMs - startedAtMs);
|
|
493
|
+
}
|
|
494
|
+
function durationNumber(value) {
|
|
495
|
+
if (value === null)
|
|
496
|
+
return null;
|
|
497
|
+
if (typeof value === "number" && Number.isFinite(value) && value >= 0) {
|
|
498
|
+
return value;
|
|
499
|
+
}
|
|
500
|
+
return undefined;
|
|
501
|
+
}
|
|
502
|
+
function recordTaskLaunchTiming(task, observation) {
|
|
503
|
+
const capturedAt = observation.launchCompletedAt ?? nowIso();
|
|
504
|
+
const launchWaitMs = durationBetween(observation.launchQueuedAt, observation.launchStartedAt);
|
|
505
|
+
const launchDurationMs = durationBetween(observation.launchStartedAt, observation.launchCompletedAt);
|
|
506
|
+
task.timing = {
|
|
507
|
+
source: "pi-workflow",
|
|
508
|
+
capturedAt,
|
|
509
|
+
launchQueuedAt: observation.launchQueuedAt,
|
|
510
|
+
...(observation.launchStartedAt === undefined
|
|
511
|
+
? {}
|
|
512
|
+
: { launchStartedAt: observation.launchStartedAt }),
|
|
513
|
+
...(observation.launchCompletedAt === undefined
|
|
514
|
+
? {}
|
|
515
|
+
: { launchCompletedAt: observation.launchCompletedAt }),
|
|
516
|
+
...(launchWaitMs === undefined ? {} : { launchWaitMs }),
|
|
517
|
+
...(launchDurationMs === undefined ? {} : { launchDurationMs }),
|
|
518
|
+
launchSlotReleaseDelayMs: resolveLaunchSlotReleaseDelayMs(),
|
|
519
|
+
...(task.timing?.aggregate === undefined
|
|
520
|
+
? {}
|
|
521
|
+
: { aggregate: task.timing.aggregate }),
|
|
522
|
+
...(task.timing?.attempts === undefined
|
|
523
|
+
? {}
|
|
524
|
+
: { attempts: task.timing.attempts }),
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
function buildTaskTimingAttempt(options) {
|
|
528
|
+
const resultDuration = options.subagentResult?.durationMs;
|
|
529
|
+
let executionMs = durationNumber(resultDuration === undefined ? options.snapshot.durationMs : resultDuration);
|
|
530
|
+
if (executionMs === undefined || executionMs === null) {
|
|
531
|
+
executionMs =
|
|
532
|
+
durationBetween(options.startedAt, options.completedAt) ?? executionMs;
|
|
533
|
+
}
|
|
534
|
+
const totalMs = durationBetween(options.task.startedAt ?? options.task.timing?.launchQueuedAt, options.completedAt);
|
|
535
|
+
return {
|
|
536
|
+
source: "pi-subagent",
|
|
537
|
+
capturedAt: options.capturedAt,
|
|
538
|
+
backendRunId: options.snapshot.runId,
|
|
539
|
+
backendAttemptId: options.snapshot.attemptId,
|
|
540
|
+
...(options.task.timing?.launchQueuedAt === undefined
|
|
541
|
+
? {}
|
|
542
|
+
: { launchQueuedAt: options.task.timing.launchQueuedAt }),
|
|
543
|
+
...(options.task.timing?.launchStartedAt === undefined
|
|
544
|
+
? {}
|
|
545
|
+
: { launchStartedAt: options.task.timing.launchStartedAt }),
|
|
546
|
+
...(options.task.timing?.launchCompletedAt === undefined
|
|
547
|
+
? {}
|
|
548
|
+
: { launchCompletedAt: options.task.timing.launchCompletedAt }),
|
|
549
|
+
...(options.task.timing?.launchWaitMs === undefined
|
|
550
|
+
? {}
|
|
551
|
+
: { launchWaitMs: options.task.timing.launchWaitMs }),
|
|
552
|
+
...(options.task.timing?.launchDurationMs === undefined
|
|
553
|
+
? {}
|
|
554
|
+
: { launchDurationMs: options.task.timing.launchDurationMs }),
|
|
555
|
+
...(options.startedAt === undefined
|
|
556
|
+
? {}
|
|
557
|
+
: { executionStartedAt: options.startedAt }),
|
|
558
|
+
...(options.completedAt === undefined
|
|
559
|
+
? {}
|
|
560
|
+
: { executionCompletedAt: options.completedAt }),
|
|
561
|
+
...(executionMs === undefined ? {} : { executionMs }),
|
|
562
|
+
...(totalMs === undefined ? {} : { totalMs }),
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
function timingAttemptKey(attempt) {
|
|
566
|
+
return `${attempt.backendRunId ?? ""}\0${attempt.backendAttemptId ?? ""}`;
|
|
567
|
+
}
|
|
568
|
+
function upsertTimingAttempt(attempts, attempt) {
|
|
569
|
+
const key = timingAttemptKey(attempt);
|
|
570
|
+
const index = attempts.findIndex((candidate) => timingAttemptKey(candidate) === key);
|
|
571
|
+
if (index < 0)
|
|
572
|
+
return [...attempts, attempt];
|
|
573
|
+
return attempts.map((candidate, candidateIndex) => candidateIndex === index ? attempt : candidate);
|
|
574
|
+
}
|
|
575
|
+
function aggregateTimingAttempts(attempts) {
|
|
576
|
+
const aggregate = {
|
|
577
|
+
attempts: attempts.length,
|
|
578
|
+
};
|
|
579
|
+
let incomplete = false;
|
|
580
|
+
for (const key of TIMING_AGGREGATE_KEYS) {
|
|
581
|
+
const anyPresent = attempts.some((attempt) => hasOwnValue(attempt, key));
|
|
582
|
+
if (!anyPresent)
|
|
583
|
+
continue;
|
|
584
|
+
let total = 0;
|
|
585
|
+
let complete = true;
|
|
586
|
+
for (const attempt of attempts) {
|
|
587
|
+
if (!hasOwnValue(attempt, key)) {
|
|
588
|
+
complete = false;
|
|
589
|
+
break;
|
|
590
|
+
}
|
|
591
|
+
const value = attempt[key];
|
|
592
|
+
if (typeof value !== "number") {
|
|
593
|
+
complete = false;
|
|
594
|
+
break;
|
|
595
|
+
}
|
|
596
|
+
total += value;
|
|
597
|
+
}
|
|
598
|
+
aggregate[key] = complete ? total : null;
|
|
599
|
+
if (!complete)
|
|
600
|
+
incomplete = true;
|
|
601
|
+
}
|
|
602
|
+
if (incomplete)
|
|
603
|
+
aggregate.incomplete = true;
|
|
604
|
+
return aggregate;
|
|
605
|
+
}
|
|
606
|
+
function recordTaskTerminalTiming(options) {
|
|
607
|
+
const attempt = buildTaskTimingAttempt(options);
|
|
608
|
+
const attempts = upsertTimingAttempt(options.task.timing?.attempts ?? [], attempt);
|
|
609
|
+
options.task.timing = {
|
|
610
|
+
source: "pi-workflow",
|
|
611
|
+
capturedAt: options.capturedAt,
|
|
612
|
+
...(attempt.launchQueuedAt === undefined
|
|
613
|
+
? {}
|
|
614
|
+
: { launchQueuedAt: attempt.launchQueuedAt }),
|
|
615
|
+
...(attempt.launchStartedAt === undefined
|
|
616
|
+
? {}
|
|
617
|
+
: { launchStartedAt: attempt.launchStartedAt }),
|
|
618
|
+
...(attempt.launchCompletedAt === undefined
|
|
619
|
+
? {}
|
|
620
|
+
: { launchCompletedAt: attempt.launchCompletedAt }),
|
|
621
|
+
...(attempt.launchWaitMs === undefined
|
|
622
|
+
? {}
|
|
623
|
+
: { launchWaitMs: attempt.launchWaitMs }),
|
|
624
|
+
...(attempt.launchDurationMs === undefined
|
|
625
|
+
? {}
|
|
626
|
+
: { launchDurationMs: attempt.launchDurationMs }),
|
|
627
|
+
...(options.task.timing?.launchSlotReleaseDelayMs === undefined
|
|
628
|
+
? {}
|
|
629
|
+
: {
|
|
630
|
+
launchSlotReleaseDelayMs: options.task.timing.launchSlotReleaseDelayMs,
|
|
631
|
+
}),
|
|
632
|
+
...(attempt.executionStartedAt === undefined
|
|
633
|
+
? {}
|
|
634
|
+
: { executionStartedAt: attempt.executionStartedAt }),
|
|
635
|
+
...(attempt.executionCompletedAt === undefined
|
|
636
|
+
? {}
|
|
637
|
+
: { executionCompletedAt: attempt.executionCompletedAt }),
|
|
638
|
+
...(attempt.executionMs === undefined
|
|
639
|
+
? {}
|
|
640
|
+
: { executionMs: attempt.executionMs }),
|
|
641
|
+
...(attempt.totalMs === undefined ? {} : { totalMs: attempt.totalMs }),
|
|
642
|
+
aggregate: aggregateTimingAttempts(attempts),
|
|
643
|
+
attempts,
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
function recordTerminalTaskObservability(options) {
|
|
647
|
+
const capturedAt = nowIso();
|
|
648
|
+
recordTaskUsageObservation({ ...options, capturedAt });
|
|
649
|
+
recordTaskTerminalTiming({ ...options, capturedAt });
|
|
650
|
+
}
|
|
115
651
|
export function setSubagentLaunchControlsForTests(options) {
|
|
116
|
-
|
|
652
|
+
launchSlotReleaseDelayMsForTests =
|
|
117
653
|
options?.releaseDelayMs === undefined
|
|
118
|
-
?
|
|
654
|
+
? undefined
|
|
119
655
|
: Math.max(0, Math.floor(options.releaseDelayMs));
|
|
120
656
|
transientRetryJitterForTests =
|
|
121
657
|
options?.retryJitterMs === undefined
|
|
@@ -129,8 +665,6 @@ export function setSubagentLaunchControlsForTests(options) {
|
|
|
129
665
|
}
|
|
130
666
|
export async function cleanupSubagentRun(_cwd, run) {
|
|
131
667
|
for (const task of run.tasks) {
|
|
132
|
-
if (isTerminalTaskStatus(task.status))
|
|
133
|
-
continue;
|
|
134
668
|
const handle = getSubagentHandle(task);
|
|
135
669
|
if (!handle)
|
|
136
670
|
continue;
|
|
@@ -214,11 +748,22 @@ export async function launchSubagentTask(cwd, run, task, compiledTask) {
|
|
|
214
748
|
subagentOptions.extensions = extensions;
|
|
215
749
|
if (captureToolCallsEnabled())
|
|
216
750
|
subagentOptions.captureToolCalls = true;
|
|
751
|
+
const launchQueuedAt = nowIso();
|
|
752
|
+
let launchStartedAt;
|
|
753
|
+
recordTaskLaunchTiming(task, { launchQueuedAt });
|
|
217
754
|
if (isLaunchGateSaturated()) {
|
|
218
755
|
task.lastMessage = `waiting for pi-subagent launch slot (${resolveMaxConcurrentLaunches()} max)`;
|
|
219
756
|
await writeRunRecord(cwd, run).catch(() => undefined);
|
|
220
757
|
}
|
|
221
|
-
launched = await runWithLaunchSlot(() => api.runSubagent(subagentOptions))
|
|
758
|
+
launched = await runWithLaunchSlot(() => api.runSubagent(subagentOptions), () => {
|
|
759
|
+
launchStartedAt = nowIso();
|
|
760
|
+
recordTaskLaunchTiming(task, { launchQueuedAt, launchStartedAt });
|
|
761
|
+
});
|
|
762
|
+
recordTaskLaunchTiming(task, {
|
|
763
|
+
launchQueuedAt,
|
|
764
|
+
launchStartedAt,
|
|
765
|
+
launchCompletedAt: nowIso(),
|
|
766
|
+
});
|
|
222
767
|
}
|
|
223
768
|
catch (error) {
|
|
224
769
|
task.status = "pending";
|
|
@@ -240,6 +785,13 @@ export async function launchSubagentTask(cwd, run, task, compiledTask) {
|
|
|
240
785
|
task.statusDetail = "running";
|
|
241
786
|
task.lastMessage = "launched via pi-subagent/headless";
|
|
242
787
|
await writeRunRecord(cwd, run).catch(() => undefined);
|
|
788
|
+
await recordParentSubagentChildEvent({
|
|
789
|
+
event: "started",
|
|
790
|
+
childRunId: launched.runId,
|
|
791
|
+
run,
|
|
792
|
+
task,
|
|
793
|
+
message: task.lastMessage,
|
|
794
|
+
});
|
|
243
795
|
return { kind: "launched" };
|
|
244
796
|
}
|
|
245
797
|
export async function refreshRunFromSubagentArtifacts(cwd, run) {
|
|
@@ -266,8 +818,13 @@ export async function refreshRunFromSubagentArtifacts(cwd, run) {
|
|
|
266
818
|
}
|
|
267
819
|
}
|
|
268
820
|
if (!handle) {
|
|
821
|
+
if (isStaleLaunchClaim(task)) {
|
|
822
|
+
resetStaleLaunchClaim(task);
|
|
823
|
+
changed = true;
|
|
824
|
+
continue;
|
|
825
|
+
}
|
|
269
826
|
if (isTaskTimedOut(task)) {
|
|
270
|
-
|
|
827
|
+
markSubagentTaskTimedOut(task);
|
|
271
828
|
changed = true;
|
|
272
829
|
}
|
|
273
830
|
continue;
|
|
@@ -290,38 +847,34 @@ export async function refreshRunFromSubagentArtifacts(cwd, run) {
|
|
|
290
847
|
.catch(() => null);
|
|
291
848
|
if (snapshot === null) {
|
|
292
849
|
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);
|
|
850
|
+
await interruptTimedOutSubagent(api, handle);
|
|
851
|
+
markSubagentTaskTimedOut(task);
|
|
303
852
|
changed = true;
|
|
304
853
|
}
|
|
305
854
|
continue;
|
|
306
855
|
}
|
|
307
856
|
const activeAttempt = snapshot.attempts?.find((attempt) => attempt.attemptId === handle.attemptId) ?? snapshot.attempts?.at(-1);
|
|
308
|
-
|
|
857
|
+
const nextPid = activeAttempt?.workerPid ?? activeAttempt?.pid ?? task.pid;
|
|
858
|
+
if (task.pid !== nextPid) {
|
|
859
|
+
task.pid = nextPid;
|
|
860
|
+
changed = true;
|
|
861
|
+
}
|
|
309
862
|
if (snapshot.status === "running" || snapshot.status === "pending") {
|
|
310
|
-
task.
|
|
311
|
-
task.
|
|
863
|
+
await refreshRunningArtifactGraphPartialOutput(cwd, task, snapshot).catch(() => undefined);
|
|
864
|
+
if (task.statusDetail !== "running") {
|
|
865
|
+
task.statusDetail = "running";
|
|
866
|
+
changed = true;
|
|
867
|
+
}
|
|
868
|
+
const nextLastMessage = activeAttempt?.heartbeatAt
|
|
312
869
|
? `pi-subagent heartbeat ${activeAttempt.heartbeatAt}`
|
|
313
870
|
: "pi-subagent running";
|
|
871
|
+
if (task.lastMessage !== nextLastMessage) {
|
|
872
|
+
task.lastMessage = nextLastMessage;
|
|
873
|
+
changed = true;
|
|
874
|
+
}
|
|
314
875
|
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);
|
|
876
|
+
await interruptTimedOutSubagent(api, handle);
|
|
877
|
+
markSubagentTaskTimedOut(task);
|
|
325
878
|
changed = true;
|
|
326
879
|
}
|
|
327
880
|
continue;
|
|
@@ -333,6 +886,56 @@ export async function refreshRunFromSubagentArtifacts(cwd, run) {
|
|
|
333
886
|
await writeRunRecord(cwd, run);
|
|
334
887
|
return run;
|
|
335
888
|
}
|
|
889
|
+
async function refreshRunningArtifactGraphPartialOutput(cwd, task, snapshot) {
|
|
890
|
+
const partial = task.artifactGraph?.output.partial;
|
|
891
|
+
if (!partial || partial.paths.length === 0)
|
|
892
|
+
return;
|
|
893
|
+
const outputRef = findLog(snapshot, "output");
|
|
894
|
+
const outputFile = fromProjectPath(cwd, task.files.output);
|
|
895
|
+
const artifactRoot = task.backendFiles?.runsDir
|
|
896
|
+
? fromProjectPath(task.cwd, task.backendFiles.runsDir)
|
|
897
|
+
: undefined;
|
|
898
|
+
await copyLogOrEmpty(snapshot, outputRef, outputFile, artifactRoot);
|
|
899
|
+
await writeWorkflowPartialOutputLedgerFromFile({
|
|
900
|
+
taskDir: dirname(fromProjectPath(cwd, task.files.result)),
|
|
901
|
+
outputFile,
|
|
902
|
+
allowedPaths: partial.paths,
|
|
903
|
+
});
|
|
904
|
+
}
|
|
905
|
+
async function interruptTimedOutSubagent(api, handle) {
|
|
906
|
+
await api
|
|
907
|
+
.interruptSubagent({
|
|
908
|
+
cwd: handle.cwd,
|
|
909
|
+
runsDir: handle.runsDir,
|
|
910
|
+
runId: handle.runId,
|
|
911
|
+
attemptId: handle.attemptId,
|
|
912
|
+
reason: "workflow timeout",
|
|
913
|
+
})
|
|
914
|
+
.catch(() => undefined);
|
|
915
|
+
}
|
|
916
|
+
function markSubagentTaskTimedOut(task) {
|
|
917
|
+
markTaskTimedOut(task);
|
|
918
|
+
task.backendHandle = undefined;
|
|
919
|
+
task.backendTaskId = task.taskId;
|
|
920
|
+
task.pid = undefined;
|
|
921
|
+
}
|
|
922
|
+
function isStaleLaunchClaim(task) {
|
|
923
|
+
if (task.statusDetail !== "launching" || !task.startedAt)
|
|
924
|
+
return false;
|
|
925
|
+
const startedAtMs = Date.parse(task.startedAt);
|
|
926
|
+
return (Number.isFinite(startedAtMs) &&
|
|
927
|
+
Date.now() - startedAtMs > STALE_LAUNCH_CLAIM_GRACE_MS);
|
|
928
|
+
}
|
|
929
|
+
function resetStaleLaunchClaim(task) {
|
|
930
|
+
task.status = "pending";
|
|
931
|
+
task.statusDetail = "pending";
|
|
932
|
+
task.startedAt = undefined;
|
|
933
|
+
task.backendHandle = undefined;
|
|
934
|
+
task.backendFiles = undefined;
|
|
935
|
+
task.backendTaskId = task.taskId;
|
|
936
|
+
task.pid = undefined;
|
|
937
|
+
task.lastMessage = "stale pi-subagent launch claim reset";
|
|
938
|
+
}
|
|
336
939
|
async function materializeTerminalSubagentResult(cwd, run, task, snapshot) {
|
|
337
940
|
const outputRef = findLog(snapshot, "output");
|
|
338
941
|
const stderrRef = findLog(snapshot, "stderr");
|
|
@@ -385,8 +988,15 @@ async function materializeTerminalSubagentResult(cwd, run, task, snapshot) {
|
|
|
385
988
|
: undefined);
|
|
386
989
|
const contextLengthExceeded = Boolean(subagentResult?.metadata?.contextLengthExceeded ??
|
|
387
990
|
snapshot.metadata?.contextLengthExceeded);
|
|
991
|
+
recordTerminalTaskObservability({
|
|
992
|
+
task,
|
|
993
|
+
snapshot,
|
|
994
|
+
subagentResult,
|
|
995
|
+
startedAt,
|
|
996
|
+
completedAt,
|
|
997
|
+
});
|
|
388
998
|
if (task.artifactGraph?.enabled && statusInfo.status === "completed") {
|
|
389
|
-
|
|
999
|
+
const changed = await materializeTerminalArtifactGraphResult(cwd, run, task, {
|
|
390
1000
|
outputFile,
|
|
391
1001
|
stderrFile,
|
|
392
1002
|
resultFile,
|
|
@@ -395,6 +1005,8 @@ async function materializeTerminalSubagentResult(cwd, run, task, snapshot) {
|
|
|
395
1005
|
exitCode,
|
|
396
1006
|
subagentResult,
|
|
397
1007
|
});
|
|
1008
|
+
await recordTerminalParentSubagentChildEvent(run, task, snapshot);
|
|
1009
|
+
return changed;
|
|
398
1010
|
}
|
|
399
1011
|
if (shouldAttemptArtifactGraphSalvage({
|
|
400
1012
|
task,
|
|
@@ -406,7 +1018,7 @@ async function materializeTerminalSubagentResult(cwd, run, task, snapshot) {
|
|
|
406
1018
|
subagentResult,
|
|
407
1019
|
snapshot,
|
|
408
1020
|
})) {
|
|
409
|
-
|
|
1021
|
+
const changed = await materializeTerminalArtifactGraphResult(cwd, run, task, {
|
|
410
1022
|
outputFile,
|
|
411
1023
|
stderrFile,
|
|
412
1024
|
resultFile,
|
|
@@ -420,6 +1032,8 @@ async function materializeTerminalSubagentResult(cwd, run, task, snapshot) {
|
|
|
420
1032
|
subagentFailureKind: snapshot.failureKind,
|
|
421
1033
|
},
|
|
422
1034
|
});
|
|
1035
|
+
await recordTerminalParentSubagentChildEvent(run, task, snapshot);
|
|
1036
|
+
return changed;
|
|
423
1037
|
}
|
|
424
1038
|
const workflowResult = {
|
|
425
1039
|
status: statusInfo.status,
|
|
@@ -447,10 +1061,12 @@ async function materializeTerminalSubagentResult(cwd, run, task, snapshot) {
|
|
|
447
1061
|
};
|
|
448
1062
|
if (shouldRetryTransientModelFailure(statusInfo, workflowResult, outputBytes)) {
|
|
449
1063
|
await writeJson(transientFailureAttemptPath(resultFile, (task.launchRetry?.attempts ?? 0) + 1), workflowResult);
|
|
450
|
-
|
|
1064
|
+
const changed = retryOrFailTransientSubagentFailure(task, {
|
|
451
1065
|
reason: statusInfo.failureKind ?? "model",
|
|
452
1066
|
message: errorMessage ?? "pi-subagent run failed before producing output",
|
|
453
1067
|
});
|
|
1068
|
+
await recordTerminalParentSubagentChildEvent(run, task, snapshot);
|
|
1069
|
+
return changed;
|
|
454
1070
|
}
|
|
455
1071
|
await writeJson(resultFile, workflowResult);
|
|
456
1072
|
const completedAfterTimeout = resultCompletedAfterTimeout(task, completedAt);
|
|
@@ -464,6 +1080,7 @@ async function materializeTerminalSubagentResult(cwd, run, task, snapshot) {
|
|
|
464
1080
|
delete task.backendHandle;
|
|
465
1081
|
delete task.backendFiles;
|
|
466
1082
|
}
|
|
1083
|
+
await recordTerminalParentSubagentChildEvent(run, task, snapshot);
|
|
467
1084
|
return changed;
|
|
468
1085
|
}
|
|
469
1086
|
function artifactGraphRetrySession(run, task, subagentResult, attempt) {
|
|
@@ -490,6 +1107,13 @@ function artifactGraphRetrySession(run, task, subagentResult, attempt) {
|
|
|
490
1107
|
async function materializeTerminalArtifactGraphResult(cwd, run, task, options) {
|
|
491
1108
|
const rawOutput = await readFile(options.outputFile, "utf8").catch(() => "");
|
|
492
1109
|
const artifactOptions = task.artifactGraph?.output;
|
|
1110
|
+
if (artifactOptions?.partial && artifactOptions.partial.paths.length > 0) {
|
|
1111
|
+
await writeWorkflowPartialOutputLedgerFromFile({
|
|
1112
|
+
taskDir: dirname(options.resultFile),
|
|
1113
|
+
outputFile: options.outputFile,
|
|
1114
|
+
allowedPaths: artifactOptions.partial.paths,
|
|
1115
|
+
}).catch(() => undefined);
|
|
1116
|
+
}
|
|
493
1117
|
let controlJsonSchema;
|
|
494
1118
|
try {
|
|
495
1119
|
controlJsonSchema = await readTaskControlJsonSchema(task);
|
|
@@ -991,6 +1615,7 @@ async function workflowTaskExtensions(cwd, run, task, compiledTask) {
|
|
|
991
1615
|
runId: run.runId,
|
|
992
1616
|
taskId: task.taskId,
|
|
993
1617
|
cacheDir: resolve(cwd, ".pi", "workflows", run.runId, "source-cache", "fetch-content"),
|
|
1618
|
+
maxInlineChars: fetchContentInlineCharsEnvValue(),
|
|
994
1619
|
},
|
|
995
1620
|
});
|
|
996
1621
|
extensions = uniqueStrings([
|
|
@@ -1056,6 +1681,17 @@ function workflowWebSourceProviderExtensions(tools, toolProviders) {
|
|
|
1056
1681
|
function fetchContentCacheEnvValue() {
|
|
1057
1682
|
return (process.env[FETCH_CONTENT_CACHE_ENV] ?? process.env[LEGACY_FETCH_CACHE_ENV]);
|
|
1058
1683
|
}
|
|
1684
|
+
function fetchContentInlineCharsEnvValue() {
|
|
1685
|
+
const raw = process.env[FETCH_CONTENT_INLINE_CHARS_ENV];
|
|
1686
|
+
if (raw === undefined || raw.trim() === "")
|
|
1687
|
+
return DEFAULT_WORKFLOW_FETCH_CONTENT_INLINE_CHARS;
|
|
1688
|
+
if (isExplicitlyDisabled(raw))
|
|
1689
|
+
return undefined;
|
|
1690
|
+
const parsed = Number(raw);
|
|
1691
|
+
if (!Number.isFinite(parsed))
|
|
1692
|
+
return DEFAULT_WORKFLOW_FETCH_CONTENT_INLINE_CHARS;
|
|
1693
|
+
return Math.max(1, Math.floor(parsed));
|
|
1694
|
+
}
|
|
1059
1695
|
function isExplicitlyDisabled(value) {
|
|
1060
1696
|
return typeof value === "string" && /^(0|false|no|off)$/i.test(value.trim());
|
|
1061
1697
|
}
|
|
@@ -1145,6 +1781,7 @@ async function recoverSubagentHandle(run, task) {
|
|
|
1145
1781
|
const runsDir = subagentRunsDir(run, task);
|
|
1146
1782
|
const absoluteRunsDir = resolve(task.cwd, runsDir);
|
|
1147
1783
|
const expectedCorrelationId = `${run.runId}:${task.taskId}`;
|
|
1784
|
+
const claimStartedAtMs = timestampMs(task.startedAt);
|
|
1148
1785
|
const entries = await readdir(absoluteRunsDir, { withFileTypes: true }).catch(() => []);
|
|
1149
1786
|
const candidates = [];
|
|
1150
1787
|
for (const entry of entries) {
|
|
@@ -1153,6 +1790,8 @@ async function recoverSubagentHandle(run, task) {
|
|
|
1153
1790
|
const record = await readJsonLoose(join(absoluteRunsDir, entry.name, "run.json"));
|
|
1154
1791
|
if (!record || record.correlationId !== expectedCorrelationId)
|
|
1155
1792
|
continue;
|
|
1793
|
+
if (isPreClaimSubagentRecord(record, claimStartedAtMs))
|
|
1794
|
+
continue;
|
|
1156
1795
|
const attemptId = record.activeAttemptId ??
|
|
1157
1796
|
record.latestAttemptId ??
|
|
1158
1797
|
record.attempts?.at(-1)?.attemptId;
|
|
@@ -1170,6 +1809,14 @@ async function recoverSubagentHandle(run, task) {
|
|
|
1170
1809
|
candidates.sort((left, right) => right.updatedAtMs - left.updatedAtMs);
|
|
1171
1810
|
return candidates[0]?.handle;
|
|
1172
1811
|
}
|
|
1812
|
+
function isPreClaimSubagentRecord(record, claimStartedAtMs) {
|
|
1813
|
+
if (claimStartedAtMs === undefined)
|
|
1814
|
+
return false;
|
|
1815
|
+
const recordStartedAtMs = timestampMs(record.startedAt) ??
|
|
1816
|
+
timestampMs(record.attempts?.[0]?.startedAt) ??
|
|
1817
|
+
timestampMs(record.updatedAt);
|
|
1818
|
+
return (recordStartedAtMs !== undefined && recordStartedAtMs < claimStartedAtMs);
|
|
1819
|
+
}
|
|
1173
1820
|
function timestampMs(value) {
|
|
1174
1821
|
if (value === undefined)
|
|
1175
1822
|
return undefined;
|
|
@@ -1213,13 +1860,36 @@ function subagentRunsDir(run, task) {
|
|
|
1213
1860
|
function subagentSessionId(run, task) {
|
|
1214
1861
|
if (!task.artifactGraph?.enabled)
|
|
1215
1862
|
return undefined;
|
|
1216
|
-
|
|
1863
|
+
const baseSessionId = baseSubagentSessionId(run, task);
|
|
1864
|
+
if (task.outputRetry?.sessionId)
|
|
1865
|
+
return task.outputRetry.sessionId;
|
|
1866
|
+
const launchAttempt = task.launchRetry?.attempts ?? 0;
|
|
1867
|
+
if (launchAttempt > 0)
|
|
1868
|
+
return boundedSubagentSessionId(`${baseSessionId}.launch-retry-${launchAttempt}`);
|
|
1869
|
+
const resumeAttempt = task.resumeEvents?.length ?? 0;
|
|
1870
|
+
if (resumeAttempt > 0)
|
|
1871
|
+
return boundedSubagentSessionId(`${baseSessionId}.resume-${resumeAttempt}`);
|
|
1872
|
+
return baseSessionId;
|
|
1217
1873
|
}
|
|
1218
1874
|
function baseSubagentSessionId(run, task) {
|
|
1219
|
-
return `pi-workflow.${run.runId}.${task.taskId}
|
|
1875
|
+
return boundedSubagentSessionId(`pi-workflow.${run.runId}.${task.taskId}`);
|
|
1220
1876
|
}
|
|
1221
1877
|
function retrySubagentSessionId(run, task, attempt) {
|
|
1222
|
-
return `${baseSubagentSessionId(run, task)}.retry-${attempt}
|
|
1878
|
+
return boundedSubagentSessionId(`${baseSubagentSessionId(run, task)}.retry-${attempt}`);
|
|
1879
|
+
}
|
|
1880
|
+
function boundedSubagentSessionId(value) {
|
|
1881
|
+
const sanitized = value.replace(/[^A-Za-z0-9._-]/g, "-");
|
|
1882
|
+
if (sanitized.length <= MAX_SUBAGENT_SESSION_ID_LENGTH)
|
|
1883
|
+
return sanitized;
|
|
1884
|
+
const digest = createHash("sha256")
|
|
1885
|
+
.update(sanitized)
|
|
1886
|
+
.digest("hex")
|
|
1887
|
+
.slice(0, 16);
|
|
1888
|
+
const suffix = sanitized.split(".").at(-1) || "session";
|
|
1889
|
+
const prefix = `piwf.${digest}`;
|
|
1890
|
+
const maxSuffixLength = MAX_SUBAGENT_SESSION_ID_LENGTH - prefix.length - 1;
|
|
1891
|
+
const boundedSuffix = suffix.slice(-Math.max(1, maxSuffixLength));
|
|
1892
|
+
return `${prefix}.${boundedSuffix}`;
|
|
1223
1893
|
}
|
|
1224
1894
|
function buildSystemPrompt(task) {
|
|
1225
1895
|
const workflowMaxDigestChars = task.artifactGraph?.output.maxDigestChars;
|