@agwab/pi-workflow 0.3.0 → 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 +46 -11
- package/dist/dynamic-decision.d.ts +1 -0
- package/dist/dynamic-decision.js +7 -0
- package/dist/dynamic-generated-task-runtime.js +3 -1
- package/dist/dynamic-profiles.d.ts +1 -0
- package/dist/dynamic-profiles.js +3 -0
- package/dist/engine-run-graph.d.ts +2 -0
- package/dist/engine-run-graph.js +55 -5
- package/dist/engine.js +278 -15
- package/dist/extension.js +3 -2
- package/dist/index.d.ts +8 -0
- package/dist/index.js +4 -0
- 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 +89 -29
- package/dist/strings.d.ts +11 -0
- package/dist/strings.js +24 -0
- package/dist/subagent-backend.js +557 -13
- package/dist/types.d.ts +101 -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-web-source-extension.js +27 -4
- package/dist/workflow-web-source.js +26 -12
- package/docs/usage.md +76 -29
- package/node_modules/@agwab/pi-subagent/package.json +1 -1
- package/node_modules/@agwab/pi-subagent/src/index.ts +53 -5
- package/node_modules/@agwab/pi-subagent/src/panel.ts +7 -3
- 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 +49 -9
- package/src/dynamic-decision.ts +11 -0
- package/src/dynamic-generated-task-runtime.ts +3 -1
- package/src/dynamic-profiles.ts +4 -0
- package/src/engine-run-graph.ts +63 -4
- package/src/engine.ts +400 -14
- package/src/extension.ts +3 -2
- package/src/index.ts +49 -0
- package/src/prompt-json.ts +13 -0
- package/src/roles.ts +6 -9
- package/src/store.ts +123 -34
- package/src/strings.ts +38 -0
- package/src/subagent-backend.ts +727 -41
- package/src/types.ts +110 -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-web-source-extension.ts +33 -4
- package/src/workflow-web-source.ts +36 -12
- 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 +173 -20
- package/workflows/deep-research/helpers/normalize-input-packet.mjs +80 -1
- package/workflows/deep-research/helpers/render-executive.mjs +32 -5
- 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 -2
- 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 +10 -3
- package/workflows/deep-research/spec.json +32 -12
- 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/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,14 +55,20 @@ 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";
|
|
60
72
|
const PARENT_SUBAGENT_CWD_ENV = "PI_WORKFLOW_PARENT_SUBAGENT_CWD";
|
|
61
73
|
const PARENT_SUBAGENT_RUNS_DIR_ENV = "PI_WORKFLOW_PARENT_SUBAGENT_RUNS_DIR";
|
|
62
74
|
const PARENT_SUBAGENT_RUN_ID_ENV = "PI_WORKFLOW_PARENT_SUBAGENT_RUN_ID";
|
|
@@ -143,6 +155,7 @@ interface SubagentRunStatusSnapshot {
|
|
|
143
155
|
failureKind: string | null;
|
|
144
156
|
startedAt: string;
|
|
145
157
|
completedAt: string | null;
|
|
158
|
+
durationMs?: number | null;
|
|
146
159
|
logs: SubagentRunLogRef[];
|
|
147
160
|
metadata?: { contextLengthExceeded?: boolean; [key: string]: unknown };
|
|
148
161
|
completion?: unknown;
|
|
@@ -166,9 +179,7 @@ interface SubagentApi {
|
|
|
166
179
|
): Promise<SubagentRunStatusSnapshot | null>;
|
|
167
180
|
interruptSubagent(options: Record<string, unknown>): Promise<unknown>;
|
|
168
181
|
reconcileSubagentRun(options: Record<string, unknown>): Promise<unknown>;
|
|
169
|
-
recordSubagentChildEvent?(
|
|
170
|
-
options: Record<string, unknown>,
|
|
171
|
-
): Promise<unknown>;
|
|
182
|
+
recordSubagentChildEvent?(options: Record<string, unknown>): Promise<unknown>;
|
|
172
183
|
}
|
|
173
184
|
|
|
174
185
|
type ParentSubagentChildEvent =
|
|
@@ -287,7 +298,7 @@ async function recordTerminalParentSubagentChildEvent(
|
|
|
287
298
|
});
|
|
288
299
|
}
|
|
289
300
|
|
|
290
|
-
let
|
|
301
|
+
let launchSlotReleaseDelayMsForTests: number | undefined;
|
|
291
302
|
let transientRetryJitterForTests: (() => number) | undefined;
|
|
292
303
|
const launchWaitQueue: Array<() => void> = [];
|
|
293
304
|
let activeLaunchSlots = 0;
|
|
@@ -324,6 +335,18 @@ function releaseLaunchSlot(): void {
|
|
|
324
335
|
activeLaunchSlots = Math.max(0, activeLaunchSlots - 1);
|
|
325
336
|
}
|
|
326
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
|
+
|
|
327
350
|
function releaseLaunchSlotAfterDelay(
|
|
328
351
|
delayMs: number,
|
|
329
352
|
release: () => void,
|
|
@@ -335,8 +358,12 @@ function releaseLaunchSlotAfterDelay(
|
|
|
335
358
|
setTimeout(release, delayMs);
|
|
336
359
|
}
|
|
337
360
|
|
|
338
|
-
async function runWithLaunchSlot<T>(
|
|
361
|
+
async function runWithLaunchSlot<T>(
|
|
362
|
+
action: () => Promise<T>,
|
|
363
|
+
onAcquired?: () => void,
|
|
364
|
+
): Promise<T> {
|
|
339
365
|
const release = await acquireLaunchSlot();
|
|
366
|
+
onAcquired?.();
|
|
340
367
|
let holdAfterReturn = false;
|
|
341
368
|
try {
|
|
342
369
|
const result = await action();
|
|
@@ -344,7 +371,7 @@ async function runWithLaunchSlot<T>(action: () => Promise<T>): Promise<T> {
|
|
|
344
371
|
return result;
|
|
345
372
|
} finally {
|
|
346
373
|
releaseLaunchSlotAfterDelay(
|
|
347
|
-
holdAfterReturn ?
|
|
374
|
+
holdAfterReturn ? resolveLaunchSlotReleaseDelayMs() : 0,
|
|
348
375
|
release,
|
|
349
376
|
);
|
|
350
377
|
}
|
|
@@ -365,13 +392,571 @@ function sleep(ms: number): Promise<void> {
|
|
|
365
392
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
366
393
|
}
|
|
367
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
|
+
|
|
368
953
|
export function setSubagentLaunchControlsForTests(options?: {
|
|
369
954
|
releaseDelayMs?: number;
|
|
370
955
|
retryJitterMs?: number | (() => number);
|
|
371
956
|
}): void {
|
|
372
|
-
|
|
957
|
+
launchSlotReleaseDelayMsForTests =
|
|
373
958
|
options?.releaseDelayMs === undefined
|
|
374
|
-
?
|
|
959
|
+
? undefined
|
|
375
960
|
: Math.max(0, Math.floor(options.releaseDelayMs));
|
|
376
961
|
transientRetryJitterForTests =
|
|
377
962
|
options?.retryJitterMs === undefined
|
|
@@ -482,11 +1067,25 @@ export async function launchSubagentTask(
|
|
|
482
1067
|
};
|
|
483
1068
|
subagentOptions.extensions = extensions;
|
|
484
1069
|
if (captureToolCallsEnabled()) subagentOptions.captureToolCalls = true;
|
|
1070
|
+
const launchQueuedAt = nowIso();
|
|
1071
|
+
let launchStartedAt: string | undefined;
|
|
1072
|
+
recordTaskLaunchTiming(task, { launchQueuedAt });
|
|
485
1073
|
if (isLaunchGateSaturated()) {
|
|
486
1074
|
task.lastMessage = `waiting for pi-subagent launch slot (${resolveMaxConcurrentLaunches()} max)`;
|
|
487
1075
|
await writeRunRecord(cwd, run).catch(() => undefined);
|
|
488
1076
|
}
|
|
489
|
-
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
|
+
});
|
|
490
1089
|
} catch (error) {
|
|
491
1090
|
task.status = "pending";
|
|
492
1091
|
task.statusDetail = "pending";
|
|
@@ -594,12 +1193,26 @@ export async function refreshRunFromSubagentArtifacts(
|
|
|
594
1193
|
snapshot.attempts?.find(
|
|
595
1194
|
(attempt) => attempt.attemptId === handle.attemptId,
|
|
596
1195
|
) ?? snapshot.attempts?.at(-1);
|
|
597
|
-
|
|
1196
|
+
const nextPid = activeAttempt?.workerPid ?? activeAttempt?.pid ?? task.pid;
|
|
1197
|
+
if (task.pid !== nextPid) {
|
|
1198
|
+
task.pid = nextPid;
|
|
1199
|
+
changed = true;
|
|
1200
|
+
}
|
|
598
1201
|
if (snapshot.status === "running" || snapshot.status === "pending") {
|
|
599
|
-
task.
|
|
600
|
-
|
|
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
|
|
601
1210
|
? `pi-subagent heartbeat ${activeAttempt.heartbeatAt}`
|
|
602
1211
|
: "pi-subagent running";
|
|
1212
|
+
if (task.lastMessage !== nextLastMessage) {
|
|
1213
|
+
task.lastMessage = nextLastMessage;
|
|
1214
|
+
changed = true;
|
|
1215
|
+
}
|
|
603
1216
|
if (isTaskTimedOut(task)) {
|
|
604
1217
|
await interruptTimedOutSubagent(api, handle);
|
|
605
1218
|
markSubagentTaskTimedOut(task);
|
|
@@ -616,6 +1229,26 @@ export async function refreshRunFromSubagentArtifacts(
|
|
|
616
1229
|
return run;
|
|
617
1230
|
}
|
|
618
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
|
+
|
|
619
1252
|
async function interruptTimedOutSubagent(
|
|
620
1253
|
api: Awaited<ReturnType<typeof loadSubagentApi>>,
|
|
621
1254
|
handle: NonNullable<WorkflowTaskRunRecord["backendHandle"]>,
|
|
@@ -735,16 +1368,28 @@ async function materializeTerminalSubagentResult(
|
|
|
735
1368
|
(subagentResult?.metadata as any)?.contextLengthExceeded ??
|
|
736
1369
|
snapshot.metadata?.contextLengthExceeded,
|
|
737
1370
|
);
|
|
1371
|
+
recordTerminalTaskObservability({
|
|
1372
|
+
task,
|
|
1373
|
+
snapshot,
|
|
1374
|
+
subagentResult,
|
|
1375
|
+
startedAt,
|
|
1376
|
+
completedAt,
|
|
1377
|
+
});
|
|
738
1378
|
if (task.artifactGraph?.enabled && statusInfo.status === "completed") {
|
|
739
|
-
const changed = await materializeTerminalArtifactGraphResult(
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
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
|
+
);
|
|
748
1393
|
await recordTerminalParentSubagentChildEvent(run, task, snapshot);
|
|
749
1394
|
return changed;
|
|
750
1395
|
}
|
|
@@ -760,20 +1405,26 @@ async function materializeTerminalSubagentResult(
|
|
|
760
1405
|
snapshot,
|
|
761
1406
|
})
|
|
762
1407
|
) {
|
|
763
|
-
const changed = await materializeTerminalArtifactGraphResult(
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
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
|
+
},
|
|
775
1426
|
},
|
|
776
|
-
|
|
1427
|
+
);
|
|
777
1428
|
await recordTerminalParentSubagentChildEvent(run, task, snapshot);
|
|
778
1429
|
return changed;
|
|
779
1430
|
}
|
|
@@ -887,6 +1538,13 @@ async function materializeTerminalArtifactGraphResult(
|
|
|
887
1538
|
): Promise<boolean> {
|
|
888
1539
|
const rawOutput = await readFile(options.outputFile, "utf8").catch(() => "");
|
|
889
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
|
+
}
|
|
890
1548
|
let controlJsonSchema: JsonSchema | undefined;
|
|
891
1549
|
try {
|
|
892
1550
|
controlJsonSchema = await readTaskControlJsonSchema(task);
|
|
@@ -1582,6 +2240,7 @@ async function workflowTaskExtensions(
|
|
|
1582
2240
|
"source-cache",
|
|
1583
2241
|
"fetch-content",
|
|
1584
2242
|
),
|
|
2243
|
+
maxInlineChars: fetchContentInlineCharsEnvValue(),
|
|
1585
2244
|
},
|
|
1586
2245
|
});
|
|
1587
2246
|
extensions = uniqueStrings([
|
|
@@ -1686,6 +2345,17 @@ function fetchContentCacheEnvValue(): string | undefined {
|
|
|
1686
2345
|
);
|
|
1687
2346
|
}
|
|
1688
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
|
+
|
|
1689
2359
|
function isExplicitlyDisabled(value: string | undefined): boolean {
|
|
1690
2360
|
return typeof value === "string" && /^(0|false|no|off)$/i.test(value.trim());
|
|
1691
2361
|
}
|
|
@@ -1944,9 +2614,12 @@ function subagentSessionId(
|
|
|
1944
2614
|
if (task.outputRetry?.sessionId) return task.outputRetry.sessionId;
|
|
1945
2615
|
const launchAttempt = task.launchRetry?.attempts ?? 0;
|
|
1946
2616
|
if (launchAttempt > 0)
|
|
1947
|
-
return
|
|
2617
|
+
return boundedSubagentSessionId(
|
|
2618
|
+
`${baseSessionId}.launch-retry-${launchAttempt}`,
|
|
2619
|
+
);
|
|
1948
2620
|
const resumeAttempt = task.resumeEvents?.length ?? 0;
|
|
1949
|
-
if (resumeAttempt > 0)
|
|
2621
|
+
if (resumeAttempt > 0)
|
|
2622
|
+
return boundedSubagentSessionId(`${baseSessionId}.resume-${resumeAttempt}`);
|
|
1950
2623
|
return baseSessionId;
|
|
1951
2624
|
}
|
|
1952
2625
|
|
|
@@ -1954,10 +2627,7 @@ function baseSubagentSessionId(
|
|
|
1954
2627
|
run: WorkflowRunRecord,
|
|
1955
2628
|
task: WorkflowTaskRunRecord,
|
|
1956
2629
|
): string {
|
|
1957
|
-
return `pi-workflow.${run.runId}.${task.taskId}
|
|
1958
|
-
/[^A-Za-z0-9._-]/g,
|
|
1959
|
-
"-",
|
|
1960
|
-
);
|
|
2630
|
+
return boundedSubagentSessionId(`pi-workflow.${run.runId}.${task.taskId}`);
|
|
1961
2631
|
}
|
|
1962
2632
|
|
|
1963
2633
|
function retrySubagentSessionId(
|
|
@@ -1965,7 +2635,23 @@ function retrySubagentSessionId(
|
|
|
1965
2635
|
task: WorkflowTaskRunRecord,
|
|
1966
2636
|
attempt: number,
|
|
1967
2637
|
): string {
|
|
1968
|
-
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}`;
|
|
1969
2655
|
}
|
|
1970
2656
|
|
|
1971
2657
|
function buildSystemPrompt(task: CompiledTask): string {
|