@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/src/subagent-backend.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import { existsSync } from "node:fs";
|
|
2
3
|
import {
|
|
3
4
|
copyFile,
|
|
@@ -24,6 +25,11 @@ import type {
|
|
|
24
25
|
CompiledTask,
|
|
25
26
|
CompiledToolProvider,
|
|
26
27
|
WorkflowRunRecord,
|
|
28
|
+
WorkflowTaskTimingAttemptRecord,
|
|
29
|
+
WorkflowTaskTimingRecord,
|
|
30
|
+
WorkflowTaskUsageAttemptRecord,
|
|
31
|
+
WorkflowTaskUsageRecord,
|
|
32
|
+
WorkflowTaskUsageValues,
|
|
27
33
|
WorkflowTaskRunRecord,
|
|
28
34
|
} from "./types.js";
|
|
29
35
|
import type { JsonSchema } from "./json-schema.js";
|
|
@@ -49,15 +55,26 @@ import {
|
|
|
49
55
|
parseWorkflowOutputForBundle,
|
|
50
56
|
writeWorkflowTaskArtifactBundle,
|
|
51
57
|
} from "./workflow-output-artifacts.js";
|
|
58
|
+
import { writeWorkflowPartialOutputLedgerFromFile } from "./workflow-partial-output.js";
|
|
52
59
|
|
|
53
60
|
const DEFAULT_SUBAGENT_RUNS_ROOT = ".pi/workflow-subagents";
|
|
61
|
+
const MAX_SUBAGENT_SESSION_ID_LENGTH = 64;
|
|
54
62
|
const EXTRA_SUBAGENT_EXTENSIONS_ENV = "PI_WORKFLOW_SUBAGENT_EXTRA_EXTENSIONS";
|
|
55
63
|
const FETCH_CONTENT_CACHE_ENV = "PI_WORKFLOW_FETCH_CONTENT_CACHE";
|
|
56
64
|
const LEGACY_FETCH_CACHE_ENV = "PI_WORKFLOW_FETCH_CACHE";
|
|
65
|
+
const FETCH_CONTENT_INLINE_CHARS_ENV = "PI_WORKFLOW_FETCH_CONTENT_INLINE_CHARS";
|
|
66
|
+
const DEFAULT_WORKFLOW_FETCH_CONTENT_INLINE_CHARS = 12_000;
|
|
57
67
|
const DEFAULT_TRANSIENT_MODEL_FAILURE_RETRIES = 5;
|
|
58
68
|
const DEFAULT_ARTIFACT_OUTPUT_RETRIES = 2;
|
|
59
69
|
const MAX_CONCURRENT_LAUNCHES_ENV = "PI_WORKFLOW_MAX_CONCURRENT_LAUNCHES";
|
|
70
|
+
const LAUNCH_SLOT_RELEASE_DELAY_MS_ENV =
|
|
71
|
+
"PI_WORKFLOW_LAUNCH_SLOT_RELEASE_DELAY_MS";
|
|
72
|
+
const PARENT_SUBAGENT_CWD_ENV = "PI_WORKFLOW_PARENT_SUBAGENT_CWD";
|
|
73
|
+
const PARENT_SUBAGENT_RUNS_DIR_ENV = "PI_WORKFLOW_PARENT_SUBAGENT_RUNS_DIR";
|
|
74
|
+
const PARENT_SUBAGENT_RUN_ID_ENV = "PI_WORKFLOW_PARENT_SUBAGENT_RUN_ID";
|
|
75
|
+
const PARENT_SUBAGENT_ATTEMPT_ID_ENV = "PI_WORKFLOW_PARENT_SUBAGENT_ATTEMPT_ID";
|
|
60
76
|
const DEFAULT_LAUNCH_SLOT_RELEASE_DELAY_MS = 3_000;
|
|
77
|
+
const STALE_LAUNCH_CLAIM_GRACE_MS = 30_000;
|
|
61
78
|
const MIN_TRANSIENT_RETRY_JITTER_MS = 1_000;
|
|
62
79
|
const MAX_TRANSIENT_RETRY_JITTER_MS = 5_000;
|
|
63
80
|
const MODULE_PATH = fileURLToPath(import.meta.url);
|
|
@@ -138,6 +155,7 @@ interface SubagentRunStatusSnapshot {
|
|
|
138
155
|
failureKind: string | null;
|
|
139
156
|
startedAt: string;
|
|
140
157
|
completedAt: string | null;
|
|
158
|
+
durationMs?: number | null;
|
|
141
159
|
logs: SubagentRunLogRef[];
|
|
142
160
|
metadata?: { contextLengthExceeded?: boolean; [key: string]: unknown };
|
|
143
161
|
completion?: unknown;
|
|
@@ -161,8 +179,29 @@ interface SubagentApi {
|
|
|
161
179
|
): Promise<SubagentRunStatusSnapshot | null>;
|
|
162
180
|
interruptSubagent(options: Record<string, unknown>): Promise<unknown>;
|
|
163
181
|
reconcileSubagentRun(options: Record<string, unknown>): Promise<unknown>;
|
|
182
|
+
recordSubagentChildEvent?(options: Record<string, unknown>): Promise<unknown>;
|
|
164
183
|
}
|
|
165
184
|
|
|
185
|
+
type ParentSubagentChildEvent =
|
|
186
|
+
| "started"
|
|
187
|
+
| "completed"
|
|
188
|
+
| "failed"
|
|
189
|
+
| "cancelled";
|
|
190
|
+
|
|
191
|
+
interface ParentSubagentRef {
|
|
192
|
+
cwd: string;
|
|
193
|
+
runsDir: string;
|
|
194
|
+
runId: string;
|
|
195
|
+
attemptId?: string;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const GENERIC_TASK_STATUS_DETAILS = new Set([
|
|
199
|
+
"completed",
|
|
200
|
+
"failed",
|
|
201
|
+
"interrupted",
|
|
202
|
+
"running",
|
|
203
|
+
]);
|
|
204
|
+
|
|
166
205
|
const subagentApiSpecifier = "@agwab/pi-subagent/api";
|
|
167
206
|
let cachedSubagentApi: Promise<SubagentApi> | undefined;
|
|
168
207
|
let injectedSubagentApi: SubagentApi | undefined;
|
|
@@ -180,7 +219,86 @@ async function loadSubagentApi(): Promise<SubagentApi> {
|
|
|
180
219
|
return cachedSubagentApi;
|
|
181
220
|
}
|
|
182
221
|
|
|
183
|
-
|
|
222
|
+
function nonEmptyEnv(
|
|
223
|
+
env: Record<string, string | undefined>,
|
|
224
|
+
key: string,
|
|
225
|
+
): string | undefined {
|
|
226
|
+
const value = env[key]?.trim();
|
|
227
|
+
return value ? value : undefined;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function parentSubagentRefFromEnv(
|
|
231
|
+
env: Record<string, string | undefined> = process.env,
|
|
232
|
+
): ParentSubagentRef | undefined {
|
|
233
|
+
const cwd = nonEmptyEnv(env, PARENT_SUBAGENT_CWD_ENV);
|
|
234
|
+
const runsDir = nonEmptyEnv(env, PARENT_SUBAGENT_RUNS_DIR_ENV);
|
|
235
|
+
const runId = nonEmptyEnv(env, PARENT_SUBAGENT_RUN_ID_ENV);
|
|
236
|
+
if (!cwd || !runsDir || !runId) return undefined;
|
|
237
|
+
const attemptId = nonEmptyEnv(env, PARENT_SUBAGENT_ATTEMPT_ID_ENV);
|
|
238
|
+
return { cwd, runsDir, runId, ...(attemptId ? { attemptId } : {}) };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function terminalChildEventForTaskStatus(
|
|
242
|
+
status: WorkflowTaskRunRecord["status"],
|
|
243
|
+
): ParentSubagentChildEvent | undefined {
|
|
244
|
+
if (status === "completed") return "completed";
|
|
245
|
+
if (status === "failed") return "failed";
|
|
246
|
+
if (status === "interrupted") return "cancelled";
|
|
247
|
+
return undefined;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
async function recordParentSubagentChildEvent(options: {
|
|
251
|
+
event: ParentSubagentChildEvent;
|
|
252
|
+
childRunId: string;
|
|
253
|
+
run: WorkflowRunRecord;
|
|
254
|
+
task: WorkflowTaskRunRecord;
|
|
255
|
+
failureKind?: string | null;
|
|
256
|
+
message?: string;
|
|
257
|
+
}): Promise<void> {
|
|
258
|
+
const parent = parentSubagentRefFromEnv();
|
|
259
|
+
if (!parent) return;
|
|
260
|
+
const api = await loadSubagentApi().catch(() => undefined);
|
|
261
|
+
if (!api?.recordSubagentChildEvent) return;
|
|
262
|
+
await api
|
|
263
|
+
.recordSubagentChildEvent({
|
|
264
|
+
...parent,
|
|
265
|
+
event: options.event,
|
|
266
|
+
childRunId: options.childRunId,
|
|
267
|
+
workflowRunId: options.run.runId,
|
|
268
|
+
childTaskId: options.task.taskId,
|
|
269
|
+
...(options.failureKind === undefined
|
|
270
|
+
? {}
|
|
271
|
+
: { failureKind: options.failureKind }),
|
|
272
|
+
...(options.message === undefined ? {} : { message: options.message }),
|
|
273
|
+
})
|
|
274
|
+
.catch(() => undefined);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
async function recordTerminalParentSubagentChildEvent(
|
|
278
|
+
run: WorkflowRunRecord,
|
|
279
|
+
task: WorkflowTaskRunRecord,
|
|
280
|
+
snapshot: SubagentRunStatusSnapshot,
|
|
281
|
+
): Promise<void> {
|
|
282
|
+
const event = terminalChildEventForTaskStatus(task.status);
|
|
283
|
+
if (!event) return;
|
|
284
|
+
const taskFailureKind =
|
|
285
|
+
task.statusDetail && !GENERIC_TASK_STATUS_DETAILS.has(task.statusDetail)
|
|
286
|
+
? task.statusDetail
|
|
287
|
+
: undefined;
|
|
288
|
+
await recordParentSubagentChildEvent({
|
|
289
|
+
event,
|
|
290
|
+
childRunId: snapshot.runId,
|
|
291
|
+
run,
|
|
292
|
+
task,
|
|
293
|
+
failureKind:
|
|
294
|
+
event === "completed"
|
|
295
|
+
? undefined
|
|
296
|
+
: (snapshot.failureKind ?? taskFailureKind ?? task.statusDetail),
|
|
297
|
+
message: task.lastMessage,
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
let launchSlotReleaseDelayMsForTests: number | undefined;
|
|
184
302
|
let transientRetryJitterForTests: (() => number) | undefined;
|
|
185
303
|
const launchWaitQueue: Array<() => void> = [];
|
|
186
304
|
let activeLaunchSlots = 0;
|
|
@@ -217,6 +335,18 @@ function releaseLaunchSlot(): void {
|
|
|
217
335
|
activeLaunchSlots = Math.max(0, activeLaunchSlots - 1);
|
|
218
336
|
}
|
|
219
337
|
|
|
338
|
+
function resolveLaunchSlotReleaseDelayMs(): number {
|
|
339
|
+
if (launchSlotReleaseDelayMsForTests !== undefined) {
|
|
340
|
+
return launchSlotReleaseDelayMsForTests;
|
|
341
|
+
}
|
|
342
|
+
const override = Number.parseInt(
|
|
343
|
+
process.env[LAUNCH_SLOT_RELEASE_DELAY_MS_ENV] ?? "",
|
|
344
|
+
10,
|
|
345
|
+
);
|
|
346
|
+
if (Number.isFinite(override)) return Math.max(0, Math.floor(override));
|
|
347
|
+
return DEFAULT_LAUNCH_SLOT_RELEASE_DELAY_MS;
|
|
348
|
+
}
|
|
349
|
+
|
|
220
350
|
function releaseLaunchSlotAfterDelay(
|
|
221
351
|
delayMs: number,
|
|
222
352
|
release: () => void,
|
|
@@ -225,12 +355,15 @@ function releaseLaunchSlotAfterDelay(
|
|
|
225
355
|
release();
|
|
226
356
|
return;
|
|
227
357
|
}
|
|
228
|
-
|
|
229
|
-
timer.unref?.();
|
|
358
|
+
setTimeout(release, delayMs);
|
|
230
359
|
}
|
|
231
360
|
|
|
232
|
-
async function runWithLaunchSlot<T>(
|
|
361
|
+
async function runWithLaunchSlot<T>(
|
|
362
|
+
action: () => Promise<T>,
|
|
363
|
+
onAcquired?: () => void,
|
|
364
|
+
): Promise<T> {
|
|
233
365
|
const release = await acquireLaunchSlot();
|
|
366
|
+
onAcquired?.();
|
|
234
367
|
let holdAfterReturn = false;
|
|
235
368
|
try {
|
|
236
369
|
const result = await action();
|
|
@@ -238,7 +371,7 @@ async function runWithLaunchSlot<T>(action: () => Promise<T>): Promise<T> {
|
|
|
238
371
|
return result;
|
|
239
372
|
} finally {
|
|
240
373
|
releaseLaunchSlotAfterDelay(
|
|
241
|
-
holdAfterReturn ?
|
|
374
|
+
holdAfterReturn ? resolveLaunchSlotReleaseDelayMs() : 0,
|
|
242
375
|
release,
|
|
243
376
|
);
|
|
244
377
|
}
|
|
@@ -259,13 +392,571 @@ function sleep(ms: number): Promise<void> {
|
|
|
259
392
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
260
393
|
}
|
|
261
394
|
|
|
395
|
+
type UsageMetricKey = keyof WorkflowTaskUsageValues;
|
|
396
|
+
const USAGE_METRIC_KEYS: UsageMetricKey[] = [
|
|
397
|
+
"inputTokens",
|
|
398
|
+
"outputTokens",
|
|
399
|
+
"totalTokens",
|
|
400
|
+
"cachedInputTokens",
|
|
401
|
+
"cacheCreationInputTokens",
|
|
402
|
+
"cacheReadInputTokens",
|
|
403
|
+
"reasoningTokens",
|
|
404
|
+
"costUsd",
|
|
405
|
+
];
|
|
406
|
+
const USAGE_FIELD_ALIASES: Record<
|
|
407
|
+
UsageMetricKey,
|
|
408
|
+
readonly (readonly string[])[]
|
|
409
|
+
> = {
|
|
410
|
+
inputTokens: [
|
|
411
|
+
["inputTokens"],
|
|
412
|
+
["input_tokens"],
|
|
413
|
+
["input"],
|
|
414
|
+
["promptTokens"],
|
|
415
|
+
["prompt_tokens"],
|
|
416
|
+
],
|
|
417
|
+
outputTokens: [
|
|
418
|
+
["outputTokens"],
|
|
419
|
+
["output_tokens"],
|
|
420
|
+
["output"],
|
|
421
|
+
["completionTokens"],
|
|
422
|
+
["completion_tokens"],
|
|
423
|
+
],
|
|
424
|
+
totalTokens: [["totalTokens"], ["total_tokens"], ["tokens"], ["total"]],
|
|
425
|
+
cachedInputTokens: [
|
|
426
|
+
["cachedInputTokens"],
|
|
427
|
+
["cached_input_tokens"],
|
|
428
|
+
["prompt_tokens_details", "cached_tokens"],
|
|
429
|
+
["input_tokens_details", "cached_tokens"],
|
|
430
|
+
],
|
|
431
|
+
cacheCreationInputTokens: [
|
|
432
|
+
["cacheCreationInputTokens"],
|
|
433
|
+
["cacheCreationTokens"],
|
|
434
|
+
["cacheWriteTokens"],
|
|
435
|
+
["cache_creation_input_tokens"],
|
|
436
|
+
["cache_write_input_tokens"],
|
|
437
|
+
["cacheWrite"],
|
|
438
|
+
["cache_write"],
|
|
439
|
+
],
|
|
440
|
+
cacheReadInputTokens: [
|
|
441
|
+
["cacheReadInputTokens"],
|
|
442
|
+
["cacheReadTokens"],
|
|
443
|
+
["cache_read_input_tokens"],
|
|
444
|
+
["cacheRead"],
|
|
445
|
+
["cache_read"],
|
|
446
|
+
],
|
|
447
|
+
reasoningTokens: [
|
|
448
|
+
["reasoningTokens"],
|
|
449
|
+
["reasoning_tokens"],
|
|
450
|
+
["reasoning"],
|
|
451
|
+
["completion_tokens_details", "reasoning_tokens"],
|
|
452
|
+
["output_tokens_details", "reasoning_tokens"],
|
|
453
|
+
],
|
|
454
|
+
costUsd: [
|
|
455
|
+
["costUsd"],
|
|
456
|
+
["cost_usd"],
|
|
457
|
+
["totalCostUsd"],
|
|
458
|
+
["total_cost_usd"],
|
|
459
|
+
["estimatedCostUsd"],
|
|
460
|
+
["estimated_cost_usd"],
|
|
461
|
+
["cost", "total"],
|
|
462
|
+
["cost", "totalUsd"],
|
|
463
|
+
["cost", "total_usd"],
|
|
464
|
+
],
|
|
465
|
+
};
|
|
466
|
+
|
|
467
|
+
type TimingAggregateKey =
|
|
468
|
+
| "launchWaitMs"
|
|
469
|
+
| "launchDurationMs"
|
|
470
|
+
| "executionMs"
|
|
471
|
+
| "totalMs";
|
|
472
|
+
const TIMING_AGGREGATE_KEYS: TimingAggregateKey[] = [
|
|
473
|
+
"launchWaitMs",
|
|
474
|
+
"launchDurationMs",
|
|
475
|
+
"executionMs",
|
|
476
|
+
"totalMs",
|
|
477
|
+
];
|
|
478
|
+
|
|
479
|
+
function isPlainRecord(value: unknown): value is Record<string, unknown> {
|
|
480
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function hasOwnValue(record: object, key: string): boolean {
|
|
484
|
+
return Object.hasOwn(record, key);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function valueAtPath(
|
|
488
|
+
record: Record<string, unknown>,
|
|
489
|
+
path: readonly string[],
|
|
490
|
+
): { found: boolean; value: unknown } {
|
|
491
|
+
let current: unknown = record;
|
|
492
|
+
for (const part of path) {
|
|
493
|
+
if (!isPlainRecord(current) || !hasOwnValue(current, part)) {
|
|
494
|
+
return { found: false, value: undefined };
|
|
495
|
+
}
|
|
496
|
+
current = current[part];
|
|
497
|
+
}
|
|
498
|
+
return { found: true, value: current };
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function usageNumberOrNull(value: unknown): number | null | undefined {
|
|
502
|
+
if (value === null) return null;
|
|
503
|
+
if (typeof value === "number" && Number.isFinite(value) && value >= 0) {
|
|
504
|
+
return value;
|
|
505
|
+
}
|
|
506
|
+
return undefined;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function normalizedUsageValues(raw: unknown): WorkflowTaskUsageValues {
|
|
510
|
+
const record = isPlainRecord(raw) ? raw : undefined;
|
|
511
|
+
const values: WorkflowTaskUsageValues = {};
|
|
512
|
+
if (!record) return values;
|
|
513
|
+
for (const key of USAGE_METRIC_KEYS) {
|
|
514
|
+
for (const path of USAGE_FIELD_ALIASES[key]) {
|
|
515
|
+
const candidate = valueAtPath(record, path);
|
|
516
|
+
if (!candidate.found) continue;
|
|
517
|
+
const value = usageNumberOrNull(candidate.value);
|
|
518
|
+
if (value === undefined) continue;
|
|
519
|
+
values[key] = value;
|
|
520
|
+
break;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
return values;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function firstStringValue(
|
|
527
|
+
records: Array<Record<string, unknown> | undefined>,
|
|
528
|
+
keys: string[],
|
|
529
|
+
): string | undefined {
|
|
530
|
+
for (const record of records) {
|
|
531
|
+
if (!record) continue;
|
|
532
|
+
for (const key of keys) {
|
|
533
|
+
const value = record[key];
|
|
534
|
+
if (typeof value === "string" && value.trim()) return value;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
return undefined;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function metadataRecord(value: unknown): Record<string, unknown> | undefined {
|
|
541
|
+
if (!isPlainRecord(value)) return undefined;
|
|
542
|
+
return isPlainRecord(value.metadata) ? value.metadata : undefined;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function usageObservation(
|
|
546
|
+
subagentResult: Record<string, unknown> | undefined,
|
|
547
|
+
snapshot: SubagentRunStatusSnapshot,
|
|
548
|
+
): { source: string; raw: unknown; present: true } | undefined {
|
|
549
|
+
const resultMetadata = metadataRecord(subagentResult);
|
|
550
|
+
if (resultMetadata && hasOwnValue(resultMetadata, "usage")) {
|
|
551
|
+
return {
|
|
552
|
+
source: "subagent-result-metadata",
|
|
553
|
+
raw: resultMetadata.usage,
|
|
554
|
+
present: true,
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
const snapshotMetadata = isPlainRecord(snapshot.metadata)
|
|
558
|
+
? snapshot.metadata
|
|
559
|
+
: undefined;
|
|
560
|
+
if (snapshotMetadata && hasOwnValue(snapshotMetadata, "usage")) {
|
|
561
|
+
return {
|
|
562
|
+
source: "subagent-snapshot-metadata",
|
|
563
|
+
raw: snapshotMetadata.usage,
|
|
564
|
+
present: true,
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
if (subagentResult && hasOwnValue(subagentResult, "usage")) {
|
|
568
|
+
return {
|
|
569
|
+
source: "subagent-result",
|
|
570
|
+
raw: subagentResult.usage,
|
|
571
|
+
present: true,
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
const snapshotRecord = snapshot as unknown as Record<string, unknown>;
|
|
575
|
+
if (hasOwnValue(snapshotRecord, "usage")) {
|
|
576
|
+
return {
|
|
577
|
+
source: "subagent-snapshot",
|
|
578
|
+
raw: snapshotRecord.usage,
|
|
579
|
+
present: true,
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
return undefined;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
function buildTaskUsageAttempt(options: {
|
|
586
|
+
task: WorkflowTaskRunRecord;
|
|
587
|
+
snapshot: SubagentRunStatusSnapshot;
|
|
588
|
+
subagentResult?: Record<string, unknown>;
|
|
589
|
+
capturedAt: string;
|
|
590
|
+
}): WorkflowTaskUsageAttemptRecord {
|
|
591
|
+
const resultMetadata = metadataRecord(options.subagentResult);
|
|
592
|
+
const snapshotMetadata = isPlainRecord(options.snapshot.metadata)
|
|
593
|
+
? options.snapshot.metadata
|
|
594
|
+
: undefined;
|
|
595
|
+
const resultRecord = options.subagentResult;
|
|
596
|
+
const snapshotRecord = options.snapshot as unknown as Record<string, unknown>;
|
|
597
|
+
const records = [
|
|
598
|
+
resultMetadata,
|
|
599
|
+
snapshotMetadata,
|
|
600
|
+
resultRecord,
|
|
601
|
+
snapshotRecord,
|
|
602
|
+
];
|
|
603
|
+
const observed = usageObservation(options.subagentResult, options.snapshot);
|
|
604
|
+
const raw = observed?.raw;
|
|
605
|
+
const unavailable = !observed || raw === null || raw === undefined;
|
|
606
|
+
const provider = firstStringValue(records, ["provider"]);
|
|
607
|
+
const model =
|
|
608
|
+
firstStringValue(records, ["model"]) ?? options.task.runtime.model;
|
|
609
|
+
const thinking =
|
|
610
|
+
firstStringValue(records, [
|
|
611
|
+
"thinking",
|
|
612
|
+
"thinkingLevel",
|
|
613
|
+
"reasoningLevel",
|
|
614
|
+
]) ??
|
|
615
|
+
options.task.runtime.thinkingResolution?.resolved ??
|
|
616
|
+
options.task.runtime.thinking;
|
|
617
|
+
return {
|
|
618
|
+
source: observed?.source ?? "subagent-usage-unavailable",
|
|
619
|
+
capturedAt: options.capturedAt,
|
|
620
|
+
backendRunId: options.snapshot.runId,
|
|
621
|
+
backendAttemptId: options.snapshot.attemptId,
|
|
622
|
+
...(provider === undefined ? {} : { provider }),
|
|
623
|
+
...(model === undefined ? {} : { model }),
|
|
624
|
+
...(thinking === undefined ? {} : { thinking }),
|
|
625
|
+
...(unavailable ? { unavailable: true as const } : {}),
|
|
626
|
+
...(observed?.present && raw !== undefined ? { raw } : {}),
|
|
627
|
+
...normalizedUsageValues(raw),
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
function usageAttemptKey(attempt: WorkflowTaskUsageAttemptRecord): string {
|
|
632
|
+
return `${attempt.backendRunId ?? ""}\0${attempt.backendAttemptId ?? ""}\0${attempt.source}`;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
function upsertUsageAttempt(
|
|
636
|
+
attempts: WorkflowTaskUsageAttemptRecord[],
|
|
637
|
+
attempt: WorkflowTaskUsageAttemptRecord,
|
|
638
|
+
): WorkflowTaskUsageAttemptRecord[] {
|
|
639
|
+
const key = usageAttemptKey(attempt);
|
|
640
|
+
const index = attempts.findIndex(
|
|
641
|
+
(candidate) => usageAttemptKey(candidate) === key,
|
|
642
|
+
);
|
|
643
|
+
if (index < 0) return [...attempts, attempt];
|
|
644
|
+
return attempts.map((candidate, candidateIndex) =>
|
|
645
|
+
candidateIndex === index ? attempt : candidate,
|
|
646
|
+
);
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
function aggregateUsageAttempts(attempts: WorkflowTaskUsageAttemptRecord[]): {
|
|
650
|
+
values: WorkflowTaskUsageValues;
|
|
651
|
+
incomplete: boolean;
|
|
652
|
+
} {
|
|
653
|
+
const values: WorkflowTaskUsageValues = {};
|
|
654
|
+
let incomplete = attempts.some((attempt) => attempt.unavailable === true);
|
|
655
|
+
for (const key of USAGE_METRIC_KEYS) {
|
|
656
|
+
const anyPresent = attempts.some((attempt) => hasOwnValue(attempt, key));
|
|
657
|
+
if (!anyPresent) continue;
|
|
658
|
+
let total = 0;
|
|
659
|
+
let complete = true;
|
|
660
|
+
for (const attempt of attempts) {
|
|
661
|
+
if (!hasOwnValue(attempt, key)) {
|
|
662
|
+
complete = false;
|
|
663
|
+
break;
|
|
664
|
+
}
|
|
665
|
+
const value = attempt[key];
|
|
666
|
+
if (typeof value !== "number") {
|
|
667
|
+
complete = false;
|
|
668
|
+
break;
|
|
669
|
+
}
|
|
670
|
+
total += value;
|
|
671
|
+
}
|
|
672
|
+
values[key] = complete ? total : null;
|
|
673
|
+
if (!complete) incomplete = true;
|
|
674
|
+
}
|
|
675
|
+
return { values, incomplete };
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
function latestUsageString(
|
|
679
|
+
attempts: WorkflowTaskUsageAttemptRecord[],
|
|
680
|
+
key: "provider" | "model" | "thinking",
|
|
681
|
+
): string | undefined {
|
|
682
|
+
for (let index = attempts.length - 1; index >= 0; index -= 1) {
|
|
683
|
+
const value = attempts[index]?.[key];
|
|
684
|
+
if (typeof value === "string" && value.trim()) return value;
|
|
685
|
+
}
|
|
686
|
+
return undefined;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
function recordTaskUsageObservation(options: {
|
|
690
|
+
task: WorkflowTaskRunRecord;
|
|
691
|
+
snapshot: SubagentRunStatusSnapshot;
|
|
692
|
+
subagentResult?: Record<string, unknown>;
|
|
693
|
+
capturedAt: string;
|
|
694
|
+
}): void {
|
|
695
|
+
const attempt = buildTaskUsageAttempt(options);
|
|
696
|
+
const attempts = upsertUsageAttempt(
|
|
697
|
+
options.task.usage?.attempts ?? [],
|
|
698
|
+
attempt,
|
|
699
|
+
);
|
|
700
|
+
const aggregate = aggregateUsageAttempts(attempts);
|
|
701
|
+
const usage: WorkflowTaskUsageRecord = {
|
|
702
|
+
source: "pi-subagent",
|
|
703
|
+
capturedAt: options.capturedAt,
|
|
704
|
+
...(latestUsageString(attempts, "provider") === undefined
|
|
705
|
+
? {}
|
|
706
|
+
: { provider: latestUsageString(attempts, "provider") }),
|
|
707
|
+
...(latestUsageString(attempts, "model") === undefined
|
|
708
|
+
? {}
|
|
709
|
+
: { model: latestUsageString(attempts, "model") }),
|
|
710
|
+
...(latestUsageString(attempts, "thinking") === undefined
|
|
711
|
+
? {}
|
|
712
|
+
: { thinking: latestUsageString(attempts, "thinking") }),
|
|
713
|
+
...(aggregate.incomplete ? { incomplete: true } : {}),
|
|
714
|
+
...aggregate.values,
|
|
715
|
+
aggregate: {
|
|
716
|
+
attempts: attempts.length,
|
|
717
|
+
...(aggregate.incomplete ? { incomplete: true } : {}),
|
|
718
|
+
...aggregate.values,
|
|
719
|
+
},
|
|
720
|
+
attempts,
|
|
721
|
+
};
|
|
722
|
+
options.task.usage = usage;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
function isoTimestampMs(timestamp: string | undefined): number | undefined {
|
|
726
|
+
if (!timestamp) return undefined;
|
|
727
|
+
const parsed = Date.parse(timestamp);
|
|
728
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
function durationBetween(
|
|
732
|
+
startedAt: string | undefined,
|
|
733
|
+
completedAt: string | undefined,
|
|
734
|
+
): number | undefined {
|
|
735
|
+
const startedAtMs = isoTimestampMs(startedAt);
|
|
736
|
+
const completedAtMs = isoTimestampMs(completedAt);
|
|
737
|
+
if (startedAtMs === undefined || completedAtMs === undefined)
|
|
738
|
+
return undefined;
|
|
739
|
+
return Math.max(0, completedAtMs - startedAtMs);
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
function durationNumber(value: unknown): number | null | undefined {
|
|
743
|
+
if (value === null) return null;
|
|
744
|
+
if (typeof value === "number" && Number.isFinite(value) && value >= 0) {
|
|
745
|
+
return value;
|
|
746
|
+
}
|
|
747
|
+
return undefined;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
function recordTaskLaunchTiming(
|
|
751
|
+
task: WorkflowTaskRunRecord,
|
|
752
|
+
observation: {
|
|
753
|
+
launchQueuedAt: string;
|
|
754
|
+
launchStartedAt?: string;
|
|
755
|
+
launchCompletedAt?: string;
|
|
756
|
+
},
|
|
757
|
+
): void {
|
|
758
|
+
const capturedAt = observation.launchCompletedAt ?? nowIso();
|
|
759
|
+
const launchWaitMs = durationBetween(
|
|
760
|
+
observation.launchQueuedAt,
|
|
761
|
+
observation.launchStartedAt,
|
|
762
|
+
);
|
|
763
|
+
const launchDurationMs = durationBetween(
|
|
764
|
+
observation.launchStartedAt,
|
|
765
|
+
observation.launchCompletedAt,
|
|
766
|
+
);
|
|
767
|
+
task.timing = {
|
|
768
|
+
source: "pi-workflow",
|
|
769
|
+
capturedAt,
|
|
770
|
+
launchQueuedAt: observation.launchQueuedAt,
|
|
771
|
+
...(observation.launchStartedAt === undefined
|
|
772
|
+
? {}
|
|
773
|
+
: { launchStartedAt: observation.launchStartedAt }),
|
|
774
|
+
...(observation.launchCompletedAt === undefined
|
|
775
|
+
? {}
|
|
776
|
+
: { launchCompletedAt: observation.launchCompletedAt }),
|
|
777
|
+
...(launchWaitMs === undefined ? {} : { launchWaitMs }),
|
|
778
|
+
...(launchDurationMs === undefined ? {} : { launchDurationMs }),
|
|
779
|
+
launchSlotReleaseDelayMs: resolveLaunchSlotReleaseDelayMs(),
|
|
780
|
+
...(task.timing?.aggregate === undefined
|
|
781
|
+
? {}
|
|
782
|
+
: { aggregate: task.timing.aggregate }),
|
|
783
|
+
...(task.timing?.attempts === undefined
|
|
784
|
+
? {}
|
|
785
|
+
: { attempts: task.timing.attempts }),
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
function buildTaskTimingAttempt(options: {
|
|
790
|
+
task: WorkflowTaskRunRecord;
|
|
791
|
+
snapshot: SubagentRunStatusSnapshot;
|
|
792
|
+
subagentResult?: Record<string, unknown>;
|
|
793
|
+
startedAt?: string;
|
|
794
|
+
completedAt?: string;
|
|
795
|
+
capturedAt: string;
|
|
796
|
+
}): WorkflowTaskTimingAttemptRecord {
|
|
797
|
+
const resultDuration = options.subagentResult?.durationMs;
|
|
798
|
+
let executionMs = durationNumber(
|
|
799
|
+
resultDuration === undefined ? options.snapshot.durationMs : resultDuration,
|
|
800
|
+
);
|
|
801
|
+
if (executionMs === undefined || executionMs === null) {
|
|
802
|
+
executionMs =
|
|
803
|
+
durationBetween(options.startedAt, options.completedAt) ?? executionMs;
|
|
804
|
+
}
|
|
805
|
+
const totalMs = durationBetween(
|
|
806
|
+
options.task.startedAt ?? options.task.timing?.launchQueuedAt,
|
|
807
|
+
options.completedAt,
|
|
808
|
+
);
|
|
809
|
+
return {
|
|
810
|
+
source: "pi-subagent",
|
|
811
|
+
capturedAt: options.capturedAt,
|
|
812
|
+
backendRunId: options.snapshot.runId,
|
|
813
|
+
backendAttemptId: options.snapshot.attemptId,
|
|
814
|
+
...(options.task.timing?.launchQueuedAt === undefined
|
|
815
|
+
? {}
|
|
816
|
+
: { launchQueuedAt: options.task.timing.launchQueuedAt }),
|
|
817
|
+
...(options.task.timing?.launchStartedAt === undefined
|
|
818
|
+
? {}
|
|
819
|
+
: { launchStartedAt: options.task.timing.launchStartedAt }),
|
|
820
|
+
...(options.task.timing?.launchCompletedAt === undefined
|
|
821
|
+
? {}
|
|
822
|
+
: { launchCompletedAt: options.task.timing.launchCompletedAt }),
|
|
823
|
+
...(options.task.timing?.launchWaitMs === undefined
|
|
824
|
+
? {}
|
|
825
|
+
: { launchWaitMs: options.task.timing.launchWaitMs }),
|
|
826
|
+
...(options.task.timing?.launchDurationMs === undefined
|
|
827
|
+
? {}
|
|
828
|
+
: { launchDurationMs: options.task.timing.launchDurationMs }),
|
|
829
|
+
...(options.startedAt === undefined
|
|
830
|
+
? {}
|
|
831
|
+
: { executionStartedAt: options.startedAt }),
|
|
832
|
+
...(options.completedAt === undefined
|
|
833
|
+
? {}
|
|
834
|
+
: { executionCompletedAt: options.completedAt }),
|
|
835
|
+
...(executionMs === undefined ? {} : { executionMs }),
|
|
836
|
+
...(totalMs === undefined ? {} : { totalMs }),
|
|
837
|
+
};
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
function timingAttemptKey(attempt: WorkflowTaskTimingAttemptRecord): string {
|
|
841
|
+
return `${attempt.backendRunId ?? ""}\0${attempt.backendAttemptId ?? ""}`;
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
function upsertTimingAttempt(
|
|
845
|
+
attempts: WorkflowTaskTimingAttemptRecord[],
|
|
846
|
+
attempt: WorkflowTaskTimingAttemptRecord,
|
|
847
|
+
): WorkflowTaskTimingAttemptRecord[] {
|
|
848
|
+
const key = timingAttemptKey(attempt);
|
|
849
|
+
const index = attempts.findIndex(
|
|
850
|
+
(candidate) => timingAttemptKey(candidate) === key,
|
|
851
|
+
);
|
|
852
|
+
if (index < 0) return [...attempts, attempt];
|
|
853
|
+
return attempts.map((candidate, candidateIndex) =>
|
|
854
|
+
candidateIndex === index ? attempt : candidate,
|
|
855
|
+
);
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
function aggregateTimingAttempts(
|
|
859
|
+
attempts: WorkflowTaskTimingAttemptRecord[],
|
|
860
|
+
): NonNullable<WorkflowTaskTimingRecord["aggregate"]> {
|
|
861
|
+
const aggregate: NonNullable<WorkflowTaskTimingRecord["aggregate"]> = {
|
|
862
|
+
attempts: attempts.length,
|
|
863
|
+
};
|
|
864
|
+
let incomplete = false;
|
|
865
|
+
for (const key of TIMING_AGGREGATE_KEYS) {
|
|
866
|
+
const anyPresent = attempts.some((attempt) => hasOwnValue(attempt, key));
|
|
867
|
+
if (!anyPresent) continue;
|
|
868
|
+
let total = 0;
|
|
869
|
+
let complete = true;
|
|
870
|
+
for (const attempt of attempts) {
|
|
871
|
+
if (!hasOwnValue(attempt, key)) {
|
|
872
|
+
complete = false;
|
|
873
|
+
break;
|
|
874
|
+
}
|
|
875
|
+
const value = attempt[key];
|
|
876
|
+
if (typeof value !== "number") {
|
|
877
|
+
complete = false;
|
|
878
|
+
break;
|
|
879
|
+
}
|
|
880
|
+
total += value;
|
|
881
|
+
}
|
|
882
|
+
aggregate[key] = complete ? total : null;
|
|
883
|
+
if (!complete) incomplete = true;
|
|
884
|
+
}
|
|
885
|
+
if (incomplete) aggregate.incomplete = true;
|
|
886
|
+
return aggregate;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
function recordTaskTerminalTiming(options: {
|
|
890
|
+
task: WorkflowTaskRunRecord;
|
|
891
|
+
snapshot: SubagentRunStatusSnapshot;
|
|
892
|
+
subagentResult?: Record<string, unknown>;
|
|
893
|
+
startedAt?: string;
|
|
894
|
+
completedAt?: string;
|
|
895
|
+
capturedAt: string;
|
|
896
|
+
}): void {
|
|
897
|
+
const attempt = buildTaskTimingAttempt(options);
|
|
898
|
+
const attempts = upsertTimingAttempt(
|
|
899
|
+
options.task.timing?.attempts ?? [],
|
|
900
|
+
attempt,
|
|
901
|
+
);
|
|
902
|
+
options.task.timing = {
|
|
903
|
+
source: "pi-workflow",
|
|
904
|
+
capturedAt: options.capturedAt,
|
|
905
|
+
...(attempt.launchQueuedAt === undefined
|
|
906
|
+
? {}
|
|
907
|
+
: { launchQueuedAt: attempt.launchQueuedAt }),
|
|
908
|
+
...(attempt.launchStartedAt === undefined
|
|
909
|
+
? {}
|
|
910
|
+
: { launchStartedAt: attempt.launchStartedAt }),
|
|
911
|
+
...(attempt.launchCompletedAt === undefined
|
|
912
|
+
? {}
|
|
913
|
+
: { launchCompletedAt: attempt.launchCompletedAt }),
|
|
914
|
+
...(attempt.launchWaitMs === undefined
|
|
915
|
+
? {}
|
|
916
|
+
: { launchWaitMs: attempt.launchWaitMs }),
|
|
917
|
+
...(attempt.launchDurationMs === undefined
|
|
918
|
+
? {}
|
|
919
|
+
: { launchDurationMs: attempt.launchDurationMs }),
|
|
920
|
+
...(options.task.timing?.launchSlotReleaseDelayMs === undefined
|
|
921
|
+
? {}
|
|
922
|
+
: {
|
|
923
|
+
launchSlotReleaseDelayMs:
|
|
924
|
+
options.task.timing.launchSlotReleaseDelayMs,
|
|
925
|
+
}),
|
|
926
|
+
...(attempt.executionStartedAt === undefined
|
|
927
|
+
? {}
|
|
928
|
+
: { executionStartedAt: attempt.executionStartedAt }),
|
|
929
|
+
...(attempt.executionCompletedAt === undefined
|
|
930
|
+
? {}
|
|
931
|
+
: { executionCompletedAt: attempt.executionCompletedAt }),
|
|
932
|
+
...(attempt.executionMs === undefined
|
|
933
|
+
? {}
|
|
934
|
+
: { executionMs: attempt.executionMs }),
|
|
935
|
+
...(attempt.totalMs === undefined ? {} : { totalMs: attempt.totalMs }),
|
|
936
|
+
aggregate: aggregateTimingAttempts(attempts),
|
|
937
|
+
attempts,
|
|
938
|
+
};
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
function recordTerminalTaskObservability(options: {
|
|
942
|
+
task: WorkflowTaskRunRecord;
|
|
943
|
+
snapshot: SubagentRunStatusSnapshot;
|
|
944
|
+
subagentResult?: Record<string, unknown>;
|
|
945
|
+
startedAt?: string;
|
|
946
|
+
completedAt?: string;
|
|
947
|
+
}): void {
|
|
948
|
+
const capturedAt = nowIso();
|
|
949
|
+
recordTaskUsageObservation({ ...options, capturedAt });
|
|
950
|
+
recordTaskTerminalTiming({ ...options, capturedAt });
|
|
951
|
+
}
|
|
952
|
+
|
|
262
953
|
export function setSubagentLaunchControlsForTests(options?: {
|
|
263
954
|
releaseDelayMs?: number;
|
|
264
955
|
retryJitterMs?: number | (() => number);
|
|
265
956
|
}): void {
|
|
266
|
-
|
|
957
|
+
launchSlotReleaseDelayMsForTests =
|
|
267
958
|
options?.releaseDelayMs === undefined
|
|
268
|
-
?
|
|
959
|
+
? undefined
|
|
269
960
|
: Math.max(0, Math.floor(options.releaseDelayMs));
|
|
270
961
|
transientRetryJitterForTests =
|
|
271
962
|
options?.retryJitterMs === undefined
|
|
@@ -282,7 +973,6 @@ export async function cleanupSubagentRun(
|
|
|
282
973
|
run: WorkflowRunRecord,
|
|
283
974
|
): Promise<void> {
|
|
284
975
|
for (const task of run.tasks) {
|
|
285
|
-
if (isTerminalTaskStatus(task.status)) continue;
|
|
286
976
|
const handle = getSubagentHandle(task);
|
|
287
977
|
if (!handle) continue;
|
|
288
978
|
const api = await loadSubagentApi();
|
|
@@ -377,11 +1067,25 @@ export async function launchSubagentTask(
|
|
|
377
1067
|
};
|
|
378
1068
|
subagentOptions.extensions = extensions;
|
|
379
1069
|
if (captureToolCallsEnabled()) subagentOptions.captureToolCalls = true;
|
|
1070
|
+
const launchQueuedAt = nowIso();
|
|
1071
|
+
let launchStartedAt: string | undefined;
|
|
1072
|
+
recordTaskLaunchTiming(task, { launchQueuedAt });
|
|
380
1073
|
if (isLaunchGateSaturated()) {
|
|
381
1074
|
task.lastMessage = `waiting for pi-subagent launch slot (${resolveMaxConcurrentLaunches()} max)`;
|
|
382
1075
|
await writeRunRecord(cwd, run).catch(() => undefined);
|
|
383
1076
|
}
|
|
384
|
-
launched = await runWithLaunchSlot(
|
|
1077
|
+
launched = await runWithLaunchSlot(
|
|
1078
|
+
() => api.runSubagent(subagentOptions),
|
|
1079
|
+
() => {
|
|
1080
|
+
launchStartedAt = nowIso();
|
|
1081
|
+
recordTaskLaunchTiming(task, { launchQueuedAt, launchStartedAt });
|
|
1082
|
+
},
|
|
1083
|
+
);
|
|
1084
|
+
recordTaskLaunchTiming(task, {
|
|
1085
|
+
launchQueuedAt,
|
|
1086
|
+
launchStartedAt,
|
|
1087
|
+
launchCompletedAt: nowIso(),
|
|
1088
|
+
});
|
|
385
1089
|
} catch (error) {
|
|
386
1090
|
task.status = "pending";
|
|
387
1091
|
task.statusDetail = "pending";
|
|
@@ -409,6 +1113,13 @@ export async function launchSubagentTask(
|
|
|
409
1113
|
task.statusDetail = "running";
|
|
410
1114
|
task.lastMessage = "launched via pi-subagent/headless";
|
|
411
1115
|
await writeRunRecord(cwd, run).catch(() => undefined);
|
|
1116
|
+
await recordParentSubagentChildEvent({
|
|
1117
|
+
event: "started",
|
|
1118
|
+
childRunId: launched.runId,
|
|
1119
|
+
run,
|
|
1120
|
+
task,
|
|
1121
|
+
message: task.lastMessage,
|
|
1122
|
+
});
|
|
412
1123
|
return { kind: "launched" };
|
|
413
1124
|
}
|
|
414
1125
|
|
|
@@ -440,8 +1151,13 @@ export async function refreshRunFromSubagentArtifacts(
|
|
|
440
1151
|
}
|
|
441
1152
|
}
|
|
442
1153
|
if (!handle) {
|
|
1154
|
+
if (isStaleLaunchClaim(task)) {
|
|
1155
|
+
resetStaleLaunchClaim(task);
|
|
1156
|
+
changed = true;
|
|
1157
|
+
continue;
|
|
1158
|
+
}
|
|
443
1159
|
if (isTaskTimedOut(task)) {
|
|
444
|
-
|
|
1160
|
+
markSubagentTaskTimedOut(task);
|
|
445
1161
|
changed = true;
|
|
446
1162
|
}
|
|
447
1163
|
continue;
|
|
@@ -466,16 +1182,8 @@ export async function refreshRunFromSubagentArtifacts(
|
|
|
466
1182
|
|
|
467
1183
|
if (snapshot === null) {
|
|
468
1184
|
if (isTaskTimedOut(task)) {
|
|
469
|
-
await api
|
|
470
|
-
|
|
471
|
-
cwd: handle.cwd,
|
|
472
|
-
runsDir: handle.runsDir,
|
|
473
|
-
runId: handle.runId,
|
|
474
|
-
attemptId: handle.attemptId,
|
|
475
|
-
reason: "workflow timeout",
|
|
476
|
-
})
|
|
477
|
-
.catch(() => undefined);
|
|
478
|
-
markTaskTimedOut(task);
|
|
1185
|
+
await interruptTimedOutSubagent(api, handle);
|
|
1186
|
+
markSubagentTaskTimedOut(task);
|
|
479
1187
|
changed = true;
|
|
480
1188
|
}
|
|
481
1189
|
continue;
|
|
@@ -485,23 +1193,29 @@ export async function refreshRunFromSubagentArtifacts(
|
|
|
485
1193
|
snapshot.attempts?.find(
|
|
486
1194
|
(attempt) => attempt.attemptId === handle.attemptId,
|
|
487
1195
|
) ?? snapshot.attempts?.at(-1);
|
|
488
|
-
|
|
1196
|
+
const nextPid = activeAttempt?.workerPid ?? activeAttempt?.pid ?? task.pid;
|
|
1197
|
+
if (task.pid !== nextPid) {
|
|
1198
|
+
task.pid = nextPid;
|
|
1199
|
+
changed = true;
|
|
1200
|
+
}
|
|
489
1201
|
if (snapshot.status === "running" || snapshot.status === "pending") {
|
|
490
|
-
task.
|
|
491
|
-
|
|
1202
|
+
await refreshRunningArtifactGraphPartialOutput(cwd, task, snapshot).catch(
|
|
1203
|
+
() => undefined,
|
|
1204
|
+
);
|
|
1205
|
+
if (task.statusDetail !== "running") {
|
|
1206
|
+
task.statusDetail = "running";
|
|
1207
|
+
changed = true;
|
|
1208
|
+
}
|
|
1209
|
+
const nextLastMessage = activeAttempt?.heartbeatAt
|
|
492
1210
|
? `pi-subagent heartbeat ${activeAttempt.heartbeatAt}`
|
|
493
1211
|
: "pi-subagent running";
|
|
1212
|
+
if (task.lastMessage !== nextLastMessage) {
|
|
1213
|
+
task.lastMessage = nextLastMessage;
|
|
1214
|
+
changed = true;
|
|
1215
|
+
}
|
|
494
1216
|
if (isTaskTimedOut(task)) {
|
|
495
|
-
await api
|
|
496
|
-
|
|
497
|
-
cwd: handle.cwd,
|
|
498
|
-
runsDir: handle.runsDir,
|
|
499
|
-
runId: handle.runId,
|
|
500
|
-
attemptId: handle.attemptId,
|
|
501
|
-
reason: "workflow timeout",
|
|
502
|
-
})
|
|
503
|
-
.catch(() => undefined);
|
|
504
|
-
markTaskTimedOut(task);
|
|
1217
|
+
await interruptTimedOutSubagent(api, handle);
|
|
1218
|
+
markSubagentTaskTimedOut(task);
|
|
505
1219
|
changed = true;
|
|
506
1220
|
}
|
|
507
1221
|
continue;
|
|
@@ -515,6 +1229,68 @@ export async function refreshRunFromSubagentArtifacts(
|
|
|
515
1229
|
return run;
|
|
516
1230
|
}
|
|
517
1231
|
|
|
1232
|
+
async function refreshRunningArtifactGraphPartialOutput(
|
|
1233
|
+
cwd: string,
|
|
1234
|
+
task: WorkflowTaskRunRecord,
|
|
1235
|
+
snapshot: SubagentRunStatusSnapshot,
|
|
1236
|
+
): Promise<void> {
|
|
1237
|
+
const partial = task.artifactGraph?.output.partial;
|
|
1238
|
+
if (!partial || partial.paths.length === 0) return;
|
|
1239
|
+
const outputRef = findLog(snapshot, "output");
|
|
1240
|
+
const outputFile = fromProjectPath(cwd, task.files.output);
|
|
1241
|
+
const artifactRoot = task.backendFiles?.runsDir
|
|
1242
|
+
? fromProjectPath(task.cwd, task.backendFiles.runsDir)
|
|
1243
|
+
: undefined;
|
|
1244
|
+
await copyLogOrEmpty(snapshot, outputRef, outputFile, artifactRoot);
|
|
1245
|
+
await writeWorkflowPartialOutputLedgerFromFile({
|
|
1246
|
+
taskDir: dirname(fromProjectPath(cwd, task.files.result)),
|
|
1247
|
+
outputFile,
|
|
1248
|
+
allowedPaths: partial.paths,
|
|
1249
|
+
});
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
async function interruptTimedOutSubagent(
|
|
1253
|
+
api: Awaited<ReturnType<typeof loadSubagentApi>>,
|
|
1254
|
+
handle: NonNullable<WorkflowTaskRunRecord["backendHandle"]>,
|
|
1255
|
+
): Promise<void> {
|
|
1256
|
+
await api
|
|
1257
|
+
.interruptSubagent({
|
|
1258
|
+
cwd: handle.cwd,
|
|
1259
|
+
runsDir: handle.runsDir,
|
|
1260
|
+
runId: handle.runId,
|
|
1261
|
+
attemptId: handle.attemptId,
|
|
1262
|
+
reason: "workflow timeout",
|
|
1263
|
+
})
|
|
1264
|
+
.catch(() => undefined);
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
function markSubagentTaskTimedOut(task: WorkflowTaskRunRecord): void {
|
|
1268
|
+
markTaskTimedOut(task);
|
|
1269
|
+
task.backendHandle = undefined;
|
|
1270
|
+
task.backendTaskId = task.taskId;
|
|
1271
|
+
task.pid = undefined;
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
function isStaleLaunchClaim(task: WorkflowTaskRunRecord): boolean {
|
|
1275
|
+
if (task.statusDetail !== "launching" || !task.startedAt) return false;
|
|
1276
|
+
const startedAtMs = Date.parse(task.startedAt);
|
|
1277
|
+
return (
|
|
1278
|
+
Number.isFinite(startedAtMs) &&
|
|
1279
|
+
Date.now() - startedAtMs > STALE_LAUNCH_CLAIM_GRACE_MS
|
|
1280
|
+
);
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
function resetStaleLaunchClaim(task: WorkflowTaskRunRecord): void {
|
|
1284
|
+
task.status = "pending";
|
|
1285
|
+
task.statusDetail = "pending";
|
|
1286
|
+
task.startedAt = undefined;
|
|
1287
|
+
task.backendHandle = undefined;
|
|
1288
|
+
task.backendFiles = undefined;
|
|
1289
|
+
task.backendTaskId = task.taskId;
|
|
1290
|
+
task.pid = undefined;
|
|
1291
|
+
task.lastMessage = "stale pi-subagent launch claim reset";
|
|
1292
|
+
}
|
|
1293
|
+
|
|
518
1294
|
async function materializeTerminalSubagentResult(
|
|
519
1295
|
cwd: string,
|
|
520
1296
|
run: WorkflowRunRecord,
|
|
@@ -592,16 +1368,30 @@ async function materializeTerminalSubagentResult(
|
|
|
592
1368
|
(subagentResult?.metadata as any)?.contextLengthExceeded ??
|
|
593
1369
|
snapshot.metadata?.contextLengthExceeded,
|
|
594
1370
|
);
|
|
1371
|
+
recordTerminalTaskObservability({
|
|
1372
|
+
task,
|
|
1373
|
+
snapshot,
|
|
1374
|
+
subagentResult,
|
|
1375
|
+
startedAt,
|
|
1376
|
+
completedAt,
|
|
1377
|
+
});
|
|
595
1378
|
if (task.artifactGraph?.enabled && statusInfo.status === "completed") {
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
1379
|
+
const changed = await materializeTerminalArtifactGraphResult(
|
|
1380
|
+
cwd,
|
|
1381
|
+
run,
|
|
1382
|
+
task,
|
|
1383
|
+
{
|
|
1384
|
+
outputFile,
|
|
1385
|
+
stderrFile,
|
|
1386
|
+
resultFile,
|
|
1387
|
+
completedAt,
|
|
1388
|
+
startedAt,
|
|
1389
|
+
exitCode,
|
|
1390
|
+
subagentResult,
|
|
1391
|
+
},
|
|
1392
|
+
);
|
|
1393
|
+
await recordTerminalParentSubagentChildEvent(run, task, snapshot);
|
|
1394
|
+
return changed;
|
|
605
1395
|
}
|
|
606
1396
|
if (
|
|
607
1397
|
shouldAttemptArtifactGraphSalvage({
|
|
@@ -615,20 +1405,28 @@ async function materializeTerminalSubagentResult(
|
|
|
615
1405
|
snapshot,
|
|
616
1406
|
})
|
|
617
1407
|
) {
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
1408
|
+
const changed = await materializeTerminalArtifactGraphResult(
|
|
1409
|
+
cwd,
|
|
1410
|
+
run,
|
|
1411
|
+
task,
|
|
1412
|
+
{
|
|
1413
|
+
outputFile,
|
|
1414
|
+
stderrFile,
|
|
1415
|
+
resultFile,
|
|
1416
|
+
completedAt,
|
|
1417
|
+
startedAt,
|
|
1418
|
+
exitCode,
|
|
1419
|
+
subagentResult,
|
|
1420
|
+
salvage: {
|
|
1421
|
+
failureKind:
|
|
1422
|
+
statusInfo.failureKind ?? snapshot.failureKind ?? "model",
|
|
1423
|
+
subagentStatus: snapshot.status,
|
|
1424
|
+
subagentFailureKind: snapshot.failureKind,
|
|
1425
|
+
},
|
|
630
1426
|
},
|
|
631
|
-
|
|
1427
|
+
);
|
|
1428
|
+
await recordTerminalParentSubagentChildEvent(run, task, snapshot);
|
|
1429
|
+
return changed;
|
|
632
1430
|
}
|
|
633
1431
|
const workflowResult = {
|
|
634
1432
|
status: statusInfo.status,
|
|
@@ -664,10 +1462,12 @@ async function materializeTerminalSubagentResult(
|
|
|
664
1462
|
),
|
|
665
1463
|
workflowResult,
|
|
666
1464
|
);
|
|
667
|
-
|
|
1465
|
+
const changed = retryOrFailTransientSubagentFailure(task, {
|
|
668
1466
|
reason: statusInfo.failureKind ?? "model",
|
|
669
1467
|
message: errorMessage ?? "pi-subagent run failed before producing output",
|
|
670
1468
|
});
|
|
1469
|
+
await recordTerminalParentSubagentChildEvent(run, task, snapshot);
|
|
1470
|
+
return changed;
|
|
671
1471
|
}
|
|
672
1472
|
await writeJson(resultFile, workflowResult);
|
|
673
1473
|
|
|
@@ -682,6 +1482,7 @@ async function materializeTerminalSubagentResult(
|
|
|
682
1482
|
delete task.backendHandle;
|
|
683
1483
|
delete task.backendFiles;
|
|
684
1484
|
}
|
|
1485
|
+
await recordTerminalParentSubagentChildEvent(run, task, snapshot);
|
|
685
1486
|
return changed;
|
|
686
1487
|
}
|
|
687
1488
|
|
|
@@ -737,6 +1538,13 @@ async function materializeTerminalArtifactGraphResult(
|
|
|
737
1538
|
): Promise<boolean> {
|
|
738
1539
|
const rawOutput = await readFile(options.outputFile, "utf8").catch(() => "");
|
|
739
1540
|
const artifactOptions = task.artifactGraph?.output;
|
|
1541
|
+
if (artifactOptions?.partial && artifactOptions.partial.paths.length > 0) {
|
|
1542
|
+
await writeWorkflowPartialOutputLedgerFromFile({
|
|
1543
|
+
taskDir: dirname(options.resultFile),
|
|
1544
|
+
outputFile: options.outputFile,
|
|
1545
|
+
allowedPaths: artifactOptions.partial.paths,
|
|
1546
|
+
}).catch(() => undefined);
|
|
1547
|
+
}
|
|
740
1548
|
let controlJsonSchema: JsonSchema | undefined;
|
|
741
1549
|
try {
|
|
742
1550
|
controlJsonSchema = await readTaskControlJsonSchema(task);
|
|
@@ -1432,6 +2240,7 @@ async function workflowTaskExtensions(
|
|
|
1432
2240
|
"source-cache",
|
|
1433
2241
|
"fetch-content",
|
|
1434
2242
|
),
|
|
2243
|
+
maxInlineChars: fetchContentInlineCharsEnvValue(),
|
|
1435
2244
|
},
|
|
1436
2245
|
});
|
|
1437
2246
|
extensions = uniqueStrings([
|
|
@@ -1536,6 +2345,17 @@ function fetchContentCacheEnvValue(): string | undefined {
|
|
|
1536
2345
|
);
|
|
1537
2346
|
}
|
|
1538
2347
|
|
|
2348
|
+
function fetchContentInlineCharsEnvValue(): number | undefined {
|
|
2349
|
+
const raw = process.env[FETCH_CONTENT_INLINE_CHARS_ENV];
|
|
2350
|
+
if (raw === undefined || raw.trim() === "")
|
|
2351
|
+
return DEFAULT_WORKFLOW_FETCH_CONTENT_INLINE_CHARS;
|
|
2352
|
+
if (isExplicitlyDisabled(raw)) return undefined;
|
|
2353
|
+
const parsed = Number(raw);
|
|
2354
|
+
if (!Number.isFinite(parsed))
|
|
2355
|
+
return DEFAULT_WORKFLOW_FETCH_CONTENT_INLINE_CHARS;
|
|
2356
|
+
return Math.max(1, Math.floor(parsed));
|
|
2357
|
+
}
|
|
2358
|
+
|
|
1539
2359
|
function isExplicitlyDisabled(value: string | undefined): boolean {
|
|
1540
2360
|
return typeof value === "string" && /^(0|false|no|off)$/i.test(value.trim());
|
|
1541
2361
|
}
|
|
@@ -1674,6 +2494,7 @@ async function recoverSubagentHandle(
|
|
|
1674
2494
|
const runsDir = subagentRunsDir(run, task);
|
|
1675
2495
|
const absoluteRunsDir = resolve(task.cwd, runsDir);
|
|
1676
2496
|
const expectedCorrelationId = `${run.runId}:${task.taskId}`;
|
|
2497
|
+
const claimStartedAtMs = timestampMs(task.startedAt);
|
|
1677
2498
|
const entries = await readdir(absoluteRunsDir, { withFileTypes: true }).catch(
|
|
1678
2499
|
() => [],
|
|
1679
2500
|
);
|
|
@@ -1688,6 +2509,7 @@ async function recoverSubagentHandle(
|
|
|
1688
2509
|
join(absoluteRunsDir, entry.name, "run.json"),
|
|
1689
2510
|
);
|
|
1690
2511
|
if (!record || record.correlationId !== expectedCorrelationId) continue;
|
|
2512
|
+
if (isPreClaimSubagentRecord(record, claimStartedAtMs)) continue;
|
|
1691
2513
|
const attemptId =
|
|
1692
2514
|
record.activeAttemptId ??
|
|
1693
2515
|
record.latestAttemptId ??
|
|
@@ -1714,6 +2536,20 @@ async function recoverSubagentHandle(
|
|
|
1714
2536
|
return candidates[0]?.handle;
|
|
1715
2537
|
}
|
|
1716
2538
|
|
|
2539
|
+
function isPreClaimSubagentRecord(
|
|
2540
|
+
record: SubagentRunRecordLike,
|
|
2541
|
+
claimStartedAtMs: number | undefined,
|
|
2542
|
+
): boolean {
|
|
2543
|
+
if (claimStartedAtMs === undefined) return false;
|
|
2544
|
+
const recordStartedAtMs =
|
|
2545
|
+
timestampMs(record.startedAt) ??
|
|
2546
|
+
timestampMs(record.attempts?.[0]?.startedAt) ??
|
|
2547
|
+
timestampMs(record.updatedAt);
|
|
2548
|
+
return (
|
|
2549
|
+
recordStartedAtMs !== undefined && recordStartedAtMs < claimStartedAtMs
|
|
2550
|
+
);
|
|
2551
|
+
}
|
|
2552
|
+
|
|
1717
2553
|
function timestampMs(value: string | undefined): number | undefined {
|
|
1718
2554
|
if (value === undefined) return undefined;
|
|
1719
2555
|
const time = Date.parse(value);
|
|
@@ -1774,17 +2610,24 @@ function subagentSessionId(
|
|
|
1774
2610
|
task: WorkflowTaskRunRecord,
|
|
1775
2611
|
): string | undefined {
|
|
1776
2612
|
if (!task.artifactGraph?.enabled) return undefined;
|
|
1777
|
-
|
|
2613
|
+
const baseSessionId = baseSubagentSessionId(run, task);
|
|
2614
|
+
if (task.outputRetry?.sessionId) return task.outputRetry.sessionId;
|
|
2615
|
+
const launchAttempt = task.launchRetry?.attempts ?? 0;
|
|
2616
|
+
if (launchAttempt > 0)
|
|
2617
|
+
return boundedSubagentSessionId(
|
|
2618
|
+
`${baseSessionId}.launch-retry-${launchAttempt}`,
|
|
2619
|
+
);
|
|
2620
|
+
const resumeAttempt = task.resumeEvents?.length ?? 0;
|
|
2621
|
+
if (resumeAttempt > 0)
|
|
2622
|
+
return boundedSubagentSessionId(`${baseSessionId}.resume-${resumeAttempt}`);
|
|
2623
|
+
return baseSessionId;
|
|
1778
2624
|
}
|
|
1779
2625
|
|
|
1780
2626
|
function baseSubagentSessionId(
|
|
1781
2627
|
run: WorkflowRunRecord,
|
|
1782
2628
|
task: WorkflowTaskRunRecord,
|
|
1783
2629
|
): string {
|
|
1784
|
-
return `pi-workflow.${run.runId}.${task.taskId}
|
|
1785
|
-
/[^A-Za-z0-9._-]/g,
|
|
1786
|
-
"-",
|
|
1787
|
-
);
|
|
2630
|
+
return boundedSubagentSessionId(`pi-workflow.${run.runId}.${task.taskId}`);
|
|
1788
2631
|
}
|
|
1789
2632
|
|
|
1790
2633
|
function retrySubagentSessionId(
|
|
@@ -1792,7 +2635,23 @@ function retrySubagentSessionId(
|
|
|
1792
2635
|
task: WorkflowTaskRunRecord,
|
|
1793
2636
|
attempt: number,
|
|
1794
2637
|
): string {
|
|
1795
|
-
return
|
|
2638
|
+
return boundedSubagentSessionId(
|
|
2639
|
+
`${baseSubagentSessionId(run, task)}.retry-${attempt}`,
|
|
2640
|
+
);
|
|
2641
|
+
}
|
|
2642
|
+
|
|
2643
|
+
function boundedSubagentSessionId(value: string): string {
|
|
2644
|
+
const sanitized = value.replace(/[^A-Za-z0-9._-]/g, "-");
|
|
2645
|
+
if (sanitized.length <= MAX_SUBAGENT_SESSION_ID_LENGTH) return sanitized;
|
|
2646
|
+
const digest = createHash("sha256")
|
|
2647
|
+
.update(sanitized)
|
|
2648
|
+
.digest("hex")
|
|
2649
|
+
.slice(0, 16);
|
|
2650
|
+
const suffix = sanitized.split(".").at(-1) || "session";
|
|
2651
|
+
const prefix = `piwf.${digest}`;
|
|
2652
|
+
const maxSuffixLength = MAX_SUBAGENT_SESSION_ID_LENGTH - prefix.length - 1;
|
|
2653
|
+
const boundedSuffix = suffix.slice(-Math.max(1, maxSuffixLength));
|
|
2654
|
+
return `${prefix}.${boundedSuffix}`;
|
|
1796
2655
|
}
|
|
1797
2656
|
|
|
1798
2657
|
function buildSystemPrompt(task: CompiledTask): string {
|