@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.
Files changed (90) hide show
  1. package/README.md +3 -1
  2. package/dist/artifact-graph-runtime.d.ts +1 -1
  3. package/dist/artifact-graph-runtime.js +10 -5
  4. package/dist/artifact-graph-schema.js +127 -5
  5. package/dist/compiler.js +46 -11
  6. package/dist/dynamic-decision.d.ts +1 -0
  7. package/dist/dynamic-decision.js +7 -0
  8. package/dist/dynamic-generated-task-runtime.js +3 -1
  9. package/dist/dynamic-profiles.d.ts +1 -0
  10. package/dist/dynamic-profiles.js +3 -0
  11. package/dist/engine-run-graph.d.ts +2 -0
  12. package/dist/engine-run-graph.js +55 -5
  13. package/dist/engine.js +278 -15
  14. package/dist/extension.js +3 -2
  15. package/dist/index.d.ts +8 -0
  16. package/dist/index.js +4 -0
  17. package/dist/prompt-json.d.ts +7 -0
  18. package/dist/prompt-json.js +13 -0
  19. package/dist/roles.d.ts +1 -1
  20. package/dist/roles.js +5 -8
  21. package/dist/store.d.ts +20 -1
  22. package/dist/store.js +89 -29
  23. package/dist/strings.d.ts +11 -0
  24. package/dist/strings.js +24 -0
  25. package/dist/subagent-backend.js +557 -13
  26. package/dist/types.d.ts +101 -1
  27. package/dist/verification-ontology.d.ts +31 -0
  28. package/dist/verification-ontology.js +66 -0
  29. package/dist/workflow-artifact-tool.js +5 -6
  30. package/dist/workflow-artifacts.d.ts +7 -0
  31. package/dist/workflow-artifacts.js +55 -4
  32. package/dist/workflow-fetch-cache-extension.d.ts +1 -0
  33. package/dist/workflow-fetch-cache-extension.js +57 -9
  34. package/dist/workflow-metrics.d.ts +113 -0
  35. package/dist/workflow-metrics.js +272 -0
  36. package/dist/workflow-output-artifacts.js +5 -3
  37. package/dist/workflow-partial-output.d.ts +45 -0
  38. package/dist/workflow-partial-output.js +205 -0
  39. package/dist/workflow-progress-health.js +42 -10
  40. package/dist/workflow-web-source-extension.js +27 -4
  41. package/dist/workflow-web-source.js +26 -12
  42. package/docs/usage.md +76 -29
  43. package/node_modules/@agwab/pi-subagent/package.json +1 -1
  44. package/node_modules/@agwab/pi-subagent/src/index.ts +53 -5
  45. package/node_modules/@agwab/pi-subagent/src/panel.ts +7 -3
  46. package/package.json +2 -2
  47. package/skills/workflow-guide/SKILL.md +1 -0
  48. package/src/artifact-graph-runtime.ts +19 -13
  49. package/src/artifact-graph-schema.ts +143 -3
  50. package/src/cli.mjs +52 -0
  51. package/src/compiler.ts +49 -9
  52. package/src/dynamic-decision.ts +11 -0
  53. package/src/dynamic-generated-task-runtime.ts +3 -1
  54. package/src/dynamic-profiles.ts +4 -0
  55. package/src/engine-run-graph.ts +63 -4
  56. package/src/engine.ts +400 -14
  57. package/src/extension.ts +3 -2
  58. package/src/index.ts +49 -0
  59. package/src/prompt-json.ts +13 -0
  60. package/src/roles.ts +6 -9
  61. package/src/store.ts +123 -34
  62. package/src/strings.ts +38 -0
  63. package/src/subagent-backend.ts +727 -41
  64. package/src/types.ts +110 -2
  65. package/src/verification-ontology.ts +88 -0
  66. package/src/workflow-artifact-tool.ts +5 -7
  67. package/src/workflow-artifacts.ts +83 -3
  68. package/src/workflow-fetch-cache-extension.ts +78 -13
  69. package/src/workflow-metrics.ts +478 -0
  70. package/src/workflow-output-artifacts.ts +5 -3
  71. package/src/workflow-partial-output.ts +299 -0
  72. package/src/workflow-progress-health.ts +47 -15
  73. package/src/workflow-web-source-extension.ts +33 -4
  74. package/src/workflow-web-source.ts +36 -12
  75. package/workflows/README.md +7 -25
  76. package/workflows/deep-research/batched-verification.spec.json +253 -0
  77. package/workflows/deep-research/helpers/batch-verification-candidates.mjs +136 -0
  78. package/workflows/deep-research/helpers/claim-evidence-gate.mjs +173 -20
  79. package/workflows/deep-research/helpers/normalize-input-packet.mjs +80 -1
  80. package/workflows/deep-research/helpers/render-executive.mjs +32 -5
  81. package/workflows/deep-research/helpers/shadow-select-verification.mjs +229 -0
  82. package/workflows/deep-research/helpers/verification-ontology.mjs +77 -0
  83. package/workflows/deep-research/schemas/deep-research-executive-render-control.schema.json +3 -2
  84. package/workflows/deep-research/schemas/deep-research-research-questions-control.schema.json +38 -0
  85. package/workflows/deep-research/schemas/deep-research-sanitize-claims-control.schema.json +63 -0
  86. package/workflows/deep-research/schemas/deep-research-verify-claims-batch-control.schema.json +47 -0
  87. package/workflows/deep-research/schemas/deep-research-verify-claims-control.schema.json +10 -3
  88. package/workflows/deep-research/spec.json +32 -12
  89. package/skills/workflow-guide/scaffolds/dag-required-reads/spec.json.validate.stderr +0 -0
  90. package/skills/workflow-guide/scaffolds/dag-required-reads/spec.json.validate.stdout +0 -13
@@ -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 launchSlotReleaseDelayMs = DEFAULT_LAUNCH_SLOT_RELEASE_DELAY_MS;
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>(action: () => Promise<T>): Promise<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 ? launchSlotReleaseDelayMs : 0,
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
- launchSlotReleaseDelayMs =
957
+ launchSlotReleaseDelayMsForTests =
373
958
  options?.releaseDelayMs === undefined
374
- ? DEFAULT_LAUNCH_SLOT_RELEASE_DELAY_MS
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(() => api.runSubagent(subagentOptions));
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
- task.pid = activeAttempt?.workerPid ?? activeAttempt?.pid ?? task.pid;
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.statusDetail = "running";
600
- task.lastMessage = activeAttempt?.heartbeatAt
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(cwd, run, task, {
740
- outputFile,
741
- stderrFile,
742
- resultFile,
743
- completedAt,
744
- startedAt,
745
- exitCode,
746
- subagentResult,
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(cwd, run, task, {
764
- outputFile,
765
- stderrFile,
766
- resultFile,
767
- completedAt,
768
- startedAt,
769
- exitCode,
770
- subagentResult,
771
- salvage: {
772
- failureKind: statusInfo.failureKind ?? snapshot.failureKind ?? "model",
773
- subagentStatus: snapshot.status,
774
- subagentFailureKind: snapshot.failureKind,
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 `${baseSessionId}:launch-retry-${launchAttempt}`;
2617
+ return boundedSubagentSessionId(
2618
+ `${baseSessionId}.launch-retry-${launchAttempt}`,
2619
+ );
1948
2620
  const resumeAttempt = task.resumeEvents?.length ?? 0;
1949
- if (resumeAttempt > 0) return `${baseSessionId}:resume-${resumeAttempt}`;
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}`.replace(
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 `${baseSubagentSessionId(run, task)}.retry-${attempt}`;
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 {