@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.
Files changed (119) 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 +52 -19
  6. package/dist/dynamic-generated-task-runtime.js +3 -1
  7. package/dist/dynamic-profiles.d.ts +1 -1
  8. package/dist/engine-run-graph.d.ts +3 -0
  9. package/dist/engine-run-graph.js +194 -4
  10. package/dist/engine.d.ts +5 -0
  11. package/dist/engine.js +389 -41
  12. package/dist/extension.d.ts +2 -1
  13. package/dist/extension.js +30 -8
  14. package/dist/index.d.ts +11 -3
  15. package/dist/index.js +6 -1
  16. package/dist/prompt-json.d.ts +7 -0
  17. package/dist/prompt-json.js +13 -0
  18. package/dist/roles.d.ts +1 -1
  19. package/dist/roles.js +5 -8
  20. package/dist/store.d.ts +20 -1
  21. package/dist/store.js +139 -35
  22. package/dist/strings.d.ts +11 -0
  23. package/dist/strings.js +24 -0
  24. package/dist/subagent-backend.js +710 -40
  25. package/dist/types.d.ts +107 -1
  26. package/dist/verification-ontology.d.ts +31 -0
  27. package/dist/verification-ontology.js +66 -0
  28. package/dist/workflow-artifact-tool.js +5 -6
  29. package/dist/workflow-artifacts.d.ts +7 -0
  30. package/dist/workflow-artifacts.js +55 -4
  31. package/dist/workflow-fetch-cache-extension.d.ts +1 -0
  32. package/dist/workflow-fetch-cache-extension.js +57 -9
  33. package/dist/workflow-metrics.d.ts +113 -0
  34. package/dist/workflow-metrics.js +272 -0
  35. package/dist/workflow-output-artifacts.js +5 -3
  36. package/dist/workflow-partial-output.d.ts +45 -0
  37. package/dist/workflow-partial-output.js +205 -0
  38. package/dist/workflow-progress-health.js +42 -10
  39. package/dist/workflow-runtime.js +10 -1
  40. package/dist/workflow-view.js +3 -1
  41. package/dist/workflow-web-source-extension.js +194 -52
  42. package/dist/workflow-web-source.d.ts +2 -1
  43. package/dist/workflow-web-source.js +109 -30
  44. package/docs/usage.md +76 -29
  45. package/node_modules/@agwab/pi-subagent/README.md +3 -3
  46. package/node_modules/@agwab/pi-subagent/api.mjs +1 -0
  47. package/node_modules/@agwab/pi-subagent/docs/usage.md +63 -12
  48. package/node_modules/@agwab/pi-subagent/package.json +2 -2
  49. package/node_modules/@agwab/pi-subagent/src/api.ts +54 -1
  50. package/node_modules/@agwab/pi-subagent/src/artifacts/registry.ts +9 -4
  51. package/node_modules/@agwab/pi-subagent/src/artifacts/result.ts +8 -0
  52. package/node_modules/@agwab/pi-subagent/src/core/constants.ts +9 -0
  53. package/node_modules/@agwab/pi-subagent/src/core/validation.ts +21 -0
  54. package/node_modules/@agwab/pi-subagent/src/index.ts +1046 -576
  55. package/node_modules/@agwab/pi-subagent/src/orchestrate/async.ts +279 -156
  56. package/node_modules/@agwab/pi-subagent/src/orchestrate/interrupt.ts +165 -89
  57. package/node_modules/@agwab/pi-subagent/src/orchestrate/reconcile.ts +111 -65
  58. package/node_modules/@agwab/pi-subagent/src/orchestrate/run-ref.ts +219 -0
  59. package/node_modules/@agwab/pi-subagent/src/orchestrate/run.ts +88 -8
  60. package/node_modules/@agwab/pi-subagent/src/orchestrate/status.ts +614 -298
  61. package/node_modules/@agwab/pi-subagent/src/panel.ts +1356 -560
  62. package/node_modules/@agwab/pi-subagent/src/runners/headless-model.ts +53 -5
  63. package/node_modules/@agwab/pi-subagent/src/runners/tmux.ts +13 -6
  64. package/package.json +2 -2
  65. package/skills/workflow-guide/SKILL.md +1 -0
  66. package/src/artifact-graph-runtime.ts +19 -13
  67. package/src/artifact-graph-schema.ts +143 -3
  68. package/src/cli.mjs +52 -0
  69. package/src/compiler.ts +63 -18
  70. package/src/dynamic-generated-task-runtime.ts +3 -1
  71. package/src/dynamic-profiles.ts +1 -1
  72. package/src/engine-run-graph.ts +246 -4
  73. package/src/engine.ts +545 -38
  74. package/src/extension.ts +36 -6
  75. package/src/index.ts +52 -1
  76. package/src/prompt-json.ts +13 -0
  77. package/src/roles.ts +6 -9
  78. package/src/store.ts +194 -42
  79. package/src/strings.ts +38 -0
  80. package/src/subagent-backend.ts +921 -62
  81. package/src/types.ts +116 -2
  82. package/src/verification-ontology.ts +88 -0
  83. package/src/workflow-artifact-tool.ts +5 -7
  84. package/src/workflow-artifacts.ts +83 -3
  85. package/src/workflow-fetch-cache-extension.ts +78 -13
  86. package/src/workflow-metrics.ts +478 -0
  87. package/src/workflow-output-artifacts.ts +5 -3
  88. package/src/workflow-partial-output.ts +299 -0
  89. package/src/workflow-progress-health.ts +47 -15
  90. package/src/workflow-runtime.ts +18 -2
  91. package/src/workflow-view.ts +2 -1
  92. package/src/workflow-web-source-extension.ts +654 -232
  93. package/src/workflow-web-source.ts +153 -39
  94. package/workflows/README.md +7 -25
  95. package/workflows/deep-research/batched-verification.spec.json +253 -0
  96. package/workflows/deep-research/helpers/batch-verification-candidates.mjs +136 -0
  97. package/workflows/deep-research/helpers/claim-evidence-gate.mjs +229 -36
  98. package/workflows/deep-research/helpers/final-audit-packet.mjs +1 -4
  99. package/workflows/deep-research/helpers/normalize-input-packet.mjs +81 -2
  100. package/workflows/deep-research/helpers/render-executive.mjs +40 -26
  101. package/workflows/deep-research/helpers/sanitize-verification-candidates.mjs +89 -15
  102. package/workflows/deep-research/helpers/shadow-select-verification.mjs +229 -0
  103. package/workflows/deep-research/helpers/verification-ontology.mjs +77 -0
  104. package/workflows/deep-research/schemas/deep-research-executive-render-control.schema.json +3 -3
  105. package/workflows/deep-research/schemas/deep-research-research-questions-control.schema.json +38 -0
  106. package/workflows/deep-research/schemas/deep-research-sanitize-claims-control.schema.json +63 -0
  107. package/workflows/deep-research/schemas/deep-research-verify-claims-batch-control.schema.json +47 -0
  108. package/workflows/deep-research/schemas/deep-research-verify-claims-control.schema.json +13 -3
  109. package/workflows/deep-research/spec.json +32 -12
  110. package/workflows/impact-review/spec.json +3 -3
  111. package/workflows/spec-review/helpers/spec-review-pipeline.mjs +1 -8
  112. package/dist/dynamic-loader.d.ts +0 -25
  113. package/dist/dynamic-loader.js +0 -13
  114. package/skills/workflow-guide/scaffolds/dag-required-reads/spec.json.validate.stderr +0 -0
  115. package/skills/workflow-guide/scaffolds/dag-required-reads/spec.json.validate.stdout +0 -13
  116. package/src/dynamic-loader.ts +0 -49
  117. package/workflows/impact-review/schemas/docs-release-impact-control.schema.json +0 -42
  118. package/workflows/impact-review/schemas/security-performance-impact-control.schema.json +0 -42
  119. package/workflows/impact-review/schemas/state-data-impact-control.schema.json +0 -42
package/src/types.ts CHANGED
@@ -203,7 +203,14 @@ export interface ArtifactGraphStageSpec {
203
203
  maxRuntimeMs?: number;
204
204
  maxConcurrency?: number;
205
205
  maxItems?: number;
206
- from?: string | string[] | { source: string; path: string };
206
+ from?:
207
+ | string
208
+ | string[]
209
+ | {
210
+ source: string;
211
+ path: string;
212
+ streaming?: { enabled: true; minChunk?: number };
213
+ };
207
214
  after?: string | string[];
208
215
  sourcePolicy?: "success" | "partial" | "require-success";
209
216
  sourceProjection?: {
@@ -213,12 +220,14 @@ export interface ArtifactGraphStageSpec {
213
220
  inputPolicy?: {
214
221
  requiredReads?: string[];
215
222
  enforcement?: "fail";
223
+ artifactAccess?: "enabled" | "none";
216
224
  };
217
225
  output?: {
218
226
  controlSchema?: string;
219
227
  analysis?: { required?: boolean };
220
228
  refs?: { required?: boolean; minItems?: number };
221
229
  maxDigestChars?: number;
230
+ partial?: { paths: string[] };
222
231
  };
223
232
  each?: Record<string, unknown>;
224
233
  stages?: ArtifactGraphStageSpec[];
@@ -490,8 +499,10 @@ export interface CompiledArtifactGraphTask {
490
499
  controlSchema?: string;
491
500
  controlSchemaPath?: string;
492
501
  maxDigestChars?: number;
502
+ partial?: { paths: string[] };
493
503
  };
494
504
  requiredReads: string[];
505
+ artifactAccess: "enabled" | "none";
495
506
  sourceProjection?: {
496
507
  include?: string[];
497
508
  maxChars?: number;
@@ -542,6 +553,13 @@ export interface CompiledTask {
542
553
  branchId?: string;
543
554
  outputProfile?: string;
544
555
  };
556
+ foreachGenerated?: {
557
+ placeholderSpecId: string;
558
+ itemHash?: string;
559
+ itemSourceSpecId?: string;
560
+ itemSourceKind?: "control" | "partial";
561
+ itemRef?: string;
562
+ };
545
563
  loopChild?: CompiledLoopChildTaskRef;
546
564
  loopPlaceholder?: {
547
565
  loopId: string;
@@ -568,6 +586,89 @@ export type WorkflowRunStatus =
568
586
  | "failed"
569
587
  | "interrupted";
570
588
 
589
+ export interface WorkflowTaskUsageValues {
590
+ inputTokens?: number | null;
591
+ outputTokens?: number | null;
592
+ totalTokens?: number | null;
593
+ cachedInputTokens?: number | null;
594
+ cacheCreationInputTokens?: number | null;
595
+ cacheReadInputTokens?: number | null;
596
+ reasoningTokens?: number | null;
597
+ costUsd?: number | null;
598
+ }
599
+
600
+ export interface WorkflowTaskUsageAttemptRecord
601
+ extends WorkflowTaskUsageValues {
602
+ source: string;
603
+ capturedAt: string;
604
+ provider?: string;
605
+ model?: string;
606
+ thinking?: ThinkingLevel | string;
607
+ backendRunId?: string;
608
+ backendAttemptId?: string;
609
+ unavailable?: true;
610
+ raw?: unknown;
611
+ }
612
+
613
+ export interface WorkflowTaskUsageAggregateRecord
614
+ extends WorkflowTaskUsageValues {
615
+ attempts: number;
616
+ incomplete?: boolean;
617
+ }
618
+
619
+ export interface WorkflowTaskUsageRecord extends WorkflowTaskUsageValues {
620
+ source: "pi-subagent";
621
+ capturedAt: string;
622
+ provider?: string;
623
+ model?: string;
624
+ thinking?: ThinkingLevel | string;
625
+ incomplete?: boolean;
626
+ aggregate?: WorkflowTaskUsageAggregateRecord;
627
+ attempts?: WorkflowTaskUsageAttemptRecord[];
628
+ }
629
+
630
+ export interface WorkflowTaskTimingAttemptRecord {
631
+ source: string;
632
+ capturedAt: string;
633
+ backendRunId?: string;
634
+ backendAttemptId?: string;
635
+ launchQueuedAt?: string;
636
+ launchStartedAt?: string;
637
+ launchCompletedAt?: string;
638
+ launchWaitMs?: number;
639
+ launchDurationMs?: number;
640
+ executionStartedAt?: string;
641
+ executionCompletedAt?: string;
642
+ executionMs?: number | null;
643
+ totalMs?: number;
644
+ }
645
+
646
+ export interface WorkflowTaskTimingAggregateRecord {
647
+ attempts: number;
648
+ launchWaitMs?: number | null;
649
+ launchDurationMs?: number | null;
650
+ executionMs?: number | null;
651
+ totalMs?: number | null;
652
+ incomplete?: boolean;
653
+ }
654
+
655
+ export interface WorkflowTaskTimingRecord {
656
+ source: "pi-workflow";
657
+ capturedAt: string;
658
+ launchQueuedAt?: string;
659
+ launchStartedAt?: string;
660
+ launchCompletedAt?: string;
661
+ launchWaitMs?: number;
662
+ launchDurationMs?: number;
663
+ launchSlotReleaseDelayMs?: number;
664
+ executionStartedAt?: string;
665
+ executionCompletedAt?: string;
666
+ executionMs?: number | null;
667
+ totalMs?: number;
668
+ aggregate?: WorkflowTaskTimingAggregateRecord;
669
+ attempts?: WorkflowTaskTimingAttemptRecord[];
670
+ }
671
+
571
672
  export interface WorkflowTaskRunRecord {
572
673
  taskId: string;
573
674
  specId: string;
@@ -606,6 +707,8 @@ export interface WorkflowTaskRunRecord {
606
707
  startedAt?: string;
607
708
  completedAt?: string;
608
709
  elapsedMs?: number;
710
+ usage?: WorkflowTaskUsageRecord;
711
+ timing?: WorkflowTaskTimingRecord;
609
712
  exitCode?: number;
610
713
  files: {
611
714
  systemPrompt: string;
@@ -634,6 +737,13 @@ export interface WorkflowTaskRunRecord {
634
737
  branchId?: string;
635
738
  outputProfile?: string;
636
739
  };
740
+ foreachGenerated?: {
741
+ placeholderSpecId: string;
742
+ itemHash?: string;
743
+ itemSourceSpecId?: string;
744
+ itemSourceKind?: "control" | "partial";
745
+ itemRef?: string;
746
+ };
637
747
  launchRetry?: {
638
748
  attempts: number;
639
749
  maxAttempts?: number;
@@ -724,7 +834,11 @@ export interface WorkflowIndexRecord {
724
834
  rootRunId?: string;
725
835
  round?: number;
726
836
  fanout?: unknown[];
727
- tasks: Array<{
837
+ /**
838
+ * Deprecated compatibility projection. New index writes omit task rows;
839
+ * consumers that need task-level details should load runJson/run.json.
840
+ */
841
+ tasks?: Array<{
728
842
  taskId: string;
729
843
  displayName: string;
730
844
  agent: string;
@@ -0,0 +1,88 @@
1
+ export const VERIFICATION_STATUS = Object.freeze({
2
+ VERIFIED: "verified",
3
+ PARTIALLY_SUPPORTED: "partially_supported",
4
+ UNSUPPORTED: "unsupported",
5
+ CONFLICTING: "conflicting",
6
+ VERIFICATION_BLOCKED: "verification_blocked",
7
+ UNVERIFIED: "unverified",
8
+ } as const);
9
+
10
+ export type VerificationStatus =
11
+ (typeof VERIFICATION_STATUS)[keyof typeof VERIFICATION_STATUS];
12
+
13
+ export type TerminalVerificationStatus = Exclude<
14
+ VerificationStatus,
15
+ (typeof VERIFICATION_STATUS)["UNVERIFIED"]
16
+ >;
17
+
18
+ export const VERIFICATION_STATUS_VALUES = Object.freeze([
19
+ VERIFICATION_STATUS.VERIFIED,
20
+ VERIFICATION_STATUS.PARTIALLY_SUPPORTED,
21
+ VERIFICATION_STATUS.UNSUPPORTED,
22
+ VERIFICATION_STATUS.CONFLICTING,
23
+ VERIFICATION_STATUS.VERIFICATION_BLOCKED,
24
+ ] as const satisfies readonly TerminalVerificationStatus[]);
25
+
26
+ export const VERIFICATION_STATUS_BUCKETS = Object.freeze({
27
+ [VERIFICATION_STATUS.VERIFIED]: "verified",
28
+ [VERIFICATION_STATUS.PARTIALLY_SUPPORTED]: "partiallySupported",
29
+ [VERIFICATION_STATUS.UNSUPPORTED]: "unsupported",
30
+ [VERIFICATION_STATUS.CONFLICTING]: "conflicting",
31
+ [VERIFICATION_STATUS.VERIFICATION_BLOCKED]: "verificationBlocked",
32
+ } as const satisfies Record<TerminalVerificationStatus, string>);
33
+
34
+ export const VERIFICATION_STATUS_LABELS = Object.freeze({
35
+ [VERIFICATION_STATUS.VERIFIED]: "verified",
36
+ [VERIFICATION_STATUS.PARTIALLY_SUPPORTED]: "partially supported",
37
+ [VERIFICATION_STATUS.UNSUPPORTED]: "unsupported",
38
+ [VERIFICATION_STATUS.CONFLICTING]: "conflicting",
39
+ [VERIFICATION_STATUS.VERIFICATION_BLOCKED]: "verification blocked",
40
+ [VERIFICATION_STATUS.UNVERIFIED]: "unverified",
41
+ } as const satisfies Record<VerificationStatus, string>);
42
+
43
+ export function canonicalVerificationStatus(
44
+ status: unknown,
45
+ ): VerificationStatus {
46
+ const text = String(status ?? "").trim();
47
+ if (!text) return VERIFICATION_STATUS.UNVERIFIED;
48
+ if (text === "partiallySupported") {
49
+ return VERIFICATION_STATUS.PARTIALLY_SUPPORTED;
50
+ }
51
+ if (text === "verificationBlocked" || text === "blocked") {
52
+ return VERIFICATION_STATUS.VERIFICATION_BLOCKED;
53
+ }
54
+ return Object.values(VERIFICATION_STATUS).includes(text as VerificationStatus)
55
+ ? (text as VerificationStatus)
56
+ : VERIFICATION_STATUS.UNVERIFIED;
57
+ }
58
+
59
+ export function verificationStatusBucket(status: unknown): string {
60
+ const canonical = canonicalVerificationStatus(status);
61
+ return canonical in VERIFICATION_STATUS_BUCKETS
62
+ ? VERIFICATION_STATUS_BUCKETS[canonical as TerminalVerificationStatus]
63
+ : "other";
64
+ }
65
+
66
+ export function isVerifiedStatus(status: unknown): boolean {
67
+ return canonicalVerificationStatus(status) === VERIFICATION_STATUS.VERIFIED;
68
+ }
69
+
70
+ export function isVerificationBlockedStatus(status: unknown): boolean {
71
+ return (
72
+ canonicalVerificationStatus(status) ===
73
+ VERIFICATION_STATUS.VERIFICATION_BLOCKED
74
+ );
75
+ }
76
+
77
+ const NON_VERIFIED_TERMINAL_STATUSES = new Set<VerificationStatus>([
78
+ VERIFICATION_STATUS.PARTIALLY_SUPPORTED,
79
+ VERIFICATION_STATUS.UNSUPPORTED,
80
+ VERIFICATION_STATUS.CONFLICTING,
81
+ VERIFICATION_STATUS.VERIFICATION_BLOCKED,
82
+ ]);
83
+
84
+ export function isNonVerifiedTerminalStatus(status: unknown): boolean {
85
+ return NON_VERIFIED_TERMINAL_STATUSES.has(
86
+ canonicalVerificationStatus(status),
87
+ );
88
+ }
@@ -147,8 +147,7 @@ const WORKFLOW_ARTIFACT_KIND_SET = new Set<string>(WORKFLOW_ARTIFACT_KINDS);
147
147
  const DEFAULT_MAX_BYTES = 50 * 1024;
148
148
  const DEFAULT_MAX_LINES = 2000;
149
149
  const SOURCE_NAME_PATTERN = /^[A-Za-z0-9_.:-]+$/;
150
- const SIMPLE_JSON_PATH_PATTERN =
151
- /^(\$|\$(\.[A-Za-z0-9_-]+(\[(\*|\d+|\d*:\d*)\])?)+)$/;
150
+ const SIMPLE_JSON_PATH_PATTERN = /^(\$|\$(\.[A-Za-z0-9_-]+)+)$/;
152
151
  const JSON_PATH_SEGMENT_ALIASES: Record<string, string> = {
153
152
  axes: "researchAxes",
154
153
  claimVerdicts: "claimVerdictLedger",
@@ -466,15 +465,14 @@ async function readProjectedWorkflowArtifact(options: {
466
465
  path: effectivePath,
467
466
  });
468
467
  const serialized = JSON.stringify(sliced.value, null, 2);
468
+ const maxChars = options.maxChars ?? DEFAULT_MAX_BYTES;
469
469
  const preview =
470
- options.maxChars !== undefined && serialized.length > options.maxChars
471
- ? serialized.slice(0, options.maxChars)
472
- : serialized;
470
+ serialized.length > maxChars ? serialized.slice(0, maxChars) : serialized;
473
471
  const projection: WorkflowArtifactProjectionMetadata = {
474
472
  path: effectivePath,
475
473
  valueType: jsonValueType(resolved),
476
474
  ...(options.maxItems === undefined ? {} : { maxItems: options.maxItems }),
477
- ...(options.maxChars === undefined ? {} : { maxChars: options.maxChars }),
475
+ maxChars,
478
476
  ...(sliced.totalItems === undefined
479
477
  ? {}
480
478
  : { totalItems: sliced.totalItems }),
@@ -810,7 +808,7 @@ function normalizeProjectionPath(value: unknown): string | undefined {
810
808
  if (path === undefined) return undefined;
811
809
  if (!SIMPLE_JSON_PATH_PATTERN.test(path)) {
812
810
  throw new Error(
813
- "path must be $ or a simple dot JSON path like $.claims.items",
811
+ "path must be $ or a simple dot JSON path like $.claims.items; array selectors are not supported",
814
812
  );
815
813
  }
816
814
  return path;
@@ -9,6 +9,7 @@ interface WorkflowTelemetryAccumulator {
9
9
  launchRetries: number;
10
10
  resumeEvents: number;
11
11
  resumedTasks: number;
12
+ contextLimitFailures: number;
12
13
  retryReasons: WorkflowTelemetrySummary["retryReasons"];
13
14
  resumeStatusCounts: StatusCounts;
14
15
  outputRepairCounts: OutputRepairCounts;
@@ -18,6 +19,13 @@ export interface WorkflowTelemetrySummary {
18
19
  taskCount: number;
19
20
  wallClockMs: number | null;
20
21
  statusCounts: StatusCounts;
22
+ completion: {
23
+ health: "clean" | "repaired" | "incomplete";
24
+ clean: boolean;
25
+ repaired: boolean;
26
+ repairEvents: number;
27
+ contextLimitFailures: number;
28
+ };
21
29
  retryCounts: { output: number; launch: number };
22
30
  retryReasons: {
23
31
  output: Record<string, number>;
@@ -77,10 +85,23 @@ export function summarizeWorkflowTelemetry(
77
85
  stage.outputBytes += taskOutputBytes;
78
86
  }
79
87
 
88
+ const repairEvents =
89
+ accumulator.outputRetries +
90
+ accumulator.launchRetries +
91
+ accumulator.resumeEvents;
92
+ const health = completionHealth(tasks, repairEvents, accumulator);
93
+
80
94
  return {
81
95
  taskCount: tasks.length,
82
96
  wallClockMs: durationBetween(run.createdAt, run.updatedAt),
83
97
  statusCounts,
98
+ completion: {
99
+ health,
100
+ clean: health === "clean",
101
+ repaired: health === "repaired",
102
+ repairEvents,
103
+ contextLimitFailures: accumulator.contextLimitFailures,
104
+ },
84
105
  retryCounts: {
85
106
  output: accumulator.outputRetries,
86
107
  launch: accumulator.launchRetries,
@@ -103,6 +124,7 @@ function createWorkflowTelemetryAccumulator(): WorkflowTelemetryAccumulator {
103
124
  launchRetries: 0,
104
125
  resumeEvents: 0,
105
126
  resumedTasks: 0,
127
+ contextLimitFailures: 0,
106
128
  retryReasons: { output: {}, launch: {} },
107
129
  resumeStatusCounts: {},
108
130
  outputRepairCounts: { sameSession: 0, newSession: 0, unknown: 0 },
@@ -113,6 +135,7 @@ function accumulateTaskReliability(
113
135
  task: Partial<WorkflowTaskRunRecord>,
114
136
  accumulator: WorkflowTelemetryAccumulator,
115
137
  ): void {
138
+ if (taskHasContextLimitFailure(task)) accumulator.contextLimitFailures += 1;
116
139
  const currentOutputAttempts = positiveCount(task.outputRetry?.attempts);
117
140
  accumulator.outputRetries += currentOutputAttempts;
118
141
  if (currentOutputAttempts > 0) {
@@ -137,17 +160,40 @@ function accumulateTaskReliability(
137
160
  for (const event of resumeEvents) accumulateResumeEvent(event, accumulator);
138
161
  }
139
162
 
163
+ function completionHealth(
164
+ tasks: Array<Partial<WorkflowTaskRunRecord>>,
165
+ repairEvents: number,
166
+ accumulator: WorkflowTelemetryAccumulator,
167
+ ): WorkflowTelemetrySummary["completion"]["health"] {
168
+ const allCompleted =
169
+ tasks.length > 0 && tasks.every((task) => task.status === "completed");
170
+ if (!allCompleted) return "incomplete";
171
+ return repairEvents === 0 && accumulator.contextLimitFailures === 0
172
+ ? "clean"
173
+ : "repaired";
174
+ }
175
+
140
176
  function accumulateResumeEvent(
141
177
  event: NonNullable<WorkflowTaskRunRecord["resumeEvents"]>[number],
142
178
  accumulator: WorkflowTelemetryAccumulator,
143
179
  ): void {
144
180
  accumulator.resumeStatusCounts[event.fromStatus] =
145
181
  (accumulator.resumeStatusCounts[event.fromStatus] ?? 0) + 1;
182
+ if (resumeEventHasContextLimitFailure(event))
183
+ accumulator.contextLimitFailures += 1;
146
184
  const previousOutputAttempts = positiveCount(event.outputRetryAttempts);
147
185
  accumulator.outputRetries += previousOutputAttempts;
148
- if (previousOutputAttempts === 0) return;
149
- countReason(accumulator.retryReasons.output, event.outputRetryReason);
150
- countRepairMode(accumulator.outputRepairCounts, event.outputRetryRepairMode);
186
+ if (previousOutputAttempts > 0) {
187
+ countReason(accumulator.retryReasons.output, event.outputRetryReason);
188
+ countRepairMode(
189
+ accumulator.outputRepairCounts,
190
+ event.outputRetryRepairMode,
191
+ );
192
+ }
193
+ const previousLaunchAttempts = positiveCount(event.launchRetryAttempts);
194
+ accumulator.launchRetries += previousLaunchAttempts;
195
+ if (previousLaunchAttempts > 0)
196
+ countReason(accumulator.retryReasons.launch, event.launchRetryReason);
151
197
  }
152
198
 
153
199
  function positiveCount(value: number | undefined): number {
@@ -172,6 +218,40 @@ function countRepairMode(
172
218
  else counts.unknown += 1;
173
219
  }
174
220
 
221
+ function taskHasContextLimitFailure(
222
+ task: Partial<WorkflowTaskRunRecord>,
223
+ ): boolean {
224
+ return [
225
+ task.statusDetail,
226
+ task.lastMessage,
227
+ task.outputRetry?.reason,
228
+ task.outputRetry?.message,
229
+ task.launchRetry?.reason,
230
+ task.launchRetry?.message,
231
+ ].some(isContextLimitText);
232
+ }
233
+
234
+ function resumeEventHasContextLimitFailure(
235
+ event: NonNullable<WorkflowTaskRunRecord["resumeEvents"]>[number],
236
+ ): boolean {
237
+ return [
238
+ event.fromStatusDetail,
239
+ event.lastMessage,
240
+ event.outputRetryReason,
241
+ event.launchRetryReason,
242
+ ].some(isContextLimitText);
243
+ }
244
+
245
+ function isContextLimitText(value: string | undefined): boolean {
246
+ const text = value?.toLowerCase() ?? "";
247
+ return (
248
+ text.includes("context_or_request_too_large") ||
249
+ /context (window|length)|maximum context|request too large|token limit/.test(
250
+ text,
251
+ )
252
+ );
253
+ }
254
+
175
255
  export interface SourceContextPacket {
176
256
  tasks: SourceContextTask[];
177
257
  byStage: Record<
@@ -18,6 +18,7 @@ export interface WorkflowFetchCacheConfig {
18
18
  runId: string;
19
19
  taskId: string;
20
20
  cacheDir: string;
21
+ maxInlineChars?: number;
21
22
  }
22
23
 
23
24
  export interface WorkflowFetchCacheExtensionWrapperOptions {
@@ -83,7 +84,7 @@ export function registerWorkflowFetchCacheExtension(
83
84
  webAccessExtension: WebAccessExtension,
84
85
  storage: WebAccessStorage,
85
86
  ): void {
86
- let capturedFetchData: Record<string, unknown> | undefined;
87
+ const capturedFetchDataByResponseId = new Map<string, Record<string, unknown>>();
87
88
  const adapter = new Proxy(pi, {
88
89
  get(target, property, receiver) {
89
90
  if (property === "registerTool") {
@@ -97,32 +98,43 @@ export function registerWorkflowFetchCacheExtension(
97
98
  execute: async (toolCallId, params, signal, onUpdate) => {
98
99
  const cacheKey = cacheKeyForParams(params);
99
100
  if (!cacheKey) {
100
- return await tool.execute!(
101
- toolCallId,
102
- params,
103
- signal,
104
- onUpdate,
101
+ return capFetchContentInlineResult(
102
+ await tool.execute!(
103
+ toolCallId,
104
+ params,
105
+ signal,
106
+ onUpdate,
107
+ ),
108
+ config.maxInlineChars,
105
109
  );
106
110
  }
107
111
  const hit = await readCacheRecord(config, cacheKey.key);
108
112
  if (hit) {
109
113
  await recordCacheEvent(config, "hit", cacheKey);
110
- return materializeCacheHit(pi, storage, hit);
114
+ return capFetchContentInlineResult(
115
+ materializeCacheHit(pi, storage, hit),
116
+ config.maxInlineChars,
117
+ );
111
118
  }
112
119
  await recordCacheEvent(config, "miss", cacheKey);
113
- capturedFetchData = undefined;
114
120
  const result = await tool.execute!(
115
121
  toolCallId,
116
122
  params,
117
123
  signal,
118
124
  onUpdate,
119
125
  );
120
- const storedData = capturedFetchData;
121
- capturedFetchData = undefined;
126
+ const responseId = stringValue(result.details?.responseId);
127
+ const storedData = responseId
128
+ ? capturedFetchDataByResponseId.get(responseId)
129
+ : undefined;
130
+ if (responseId) capturedFetchDataByResponseId.delete(responseId);
122
131
  const writeReason = cacheWriteSkipReason(result, storedData);
123
132
  if (writeReason) {
124
133
  await recordCacheEvent(config, "skip", cacheKey, writeReason);
125
- return result;
134
+ return capFetchContentInlineResult(
135
+ result,
136
+ config.maxInlineChars,
137
+ );
126
138
  }
127
139
  await writeCacheRecord(config, {
128
140
  schema: WORKFLOW_FETCH_CONTENT_CACHE_SCHEMA,
@@ -134,7 +146,10 @@ export function registerWorkflowFetchCacheExtension(
134
146
  storedData: storedData!,
135
147
  });
136
148
  await recordCacheEvent(config, "write", cacheKey);
137
- return withCacheDetails(result, { hit: false });
149
+ return capFetchContentInlineResult(
150
+ withCacheDetails(result, { hit: false }),
151
+ config.maxInlineChars,
152
+ );
138
153
  },
139
154
  });
140
155
  };
@@ -142,7 +157,10 @@ export function registerWorkflowFetchCacheExtension(
142
157
  if (property === "appendEntry") {
143
158
  return (type: string, data: unknown) => {
144
159
  if (type === "web-search-results" && isFetchStoredData(data)) {
145
- capturedFetchData = cloneJsonObject(data);
160
+ const cloned = cloneJsonObject(data);
161
+ const responseId = stringValue(cloned?.id);
162
+ if (responseId && cloned)
163
+ capturedFetchDataByResponseId.set(responseId, cloned);
146
164
  }
147
165
  return pi.appendEntry?.(type, data);
148
166
  };
@@ -300,6 +318,49 @@ function withCacheDetails(
300
318
  };
301
319
  }
302
320
 
321
+ function capFetchContentInlineResult(
322
+ result: ToolResult,
323
+ maxInlineChars: number | undefined,
324
+ ): ToolResult {
325
+ const maxChars = normalizeInlineCharCap(maxInlineChars);
326
+ if (maxChars === undefined || !Array.isArray(result.content)) return result;
327
+
328
+ let truncated = false;
329
+ const content = result.content.map((entry) => {
330
+ if (entry.type !== "text" || typeof entry.text !== "string")
331
+ return entry;
332
+ if (entry.text.length <= maxChars) return entry;
333
+ truncated = true;
334
+ return {
335
+ ...entry,
336
+ text:
337
+ entry.text.slice(0, maxChars) +
338
+ `\n\n[Workflow inline fetch content capped at ${maxChars} chars; full source content remains in workflow source cache.]`,
339
+ };
340
+ });
341
+ if (!truncated) return result;
342
+
343
+ return {
344
+ ...result,
345
+ content,
346
+ details: {
347
+ ...(result.details ?? {}),
348
+ truncated: true,
349
+ workflowInlineContentCap: {
350
+ type: "fetch_content",
351
+ maxChars,
352
+ truncated: true,
353
+ },
354
+ },
355
+ };
356
+ }
357
+
358
+ function normalizeInlineCharCap(value: number | undefined): number | undefined {
359
+ if (value === undefined || !Number.isFinite(value)) return undefined;
360
+ const cap = Math.floor(value);
361
+ return cap > 0 ? cap : undefined;
362
+ }
363
+
303
364
  function cacheWriteSkipReason(
304
365
  result: ToolResult,
305
366
  storedData: Record<string, unknown> | undefined,
@@ -366,6 +427,10 @@ function cloneJsonObject(value: unknown): Record<string, unknown> | undefined {
366
427
  return JSON.parse(JSON.stringify(value)) as Record<string, unknown>;
367
428
  }
368
429
 
430
+ function stringValue(value: unknown): string | undefined {
431
+ return typeof value === "string" && value ? value : undefined;
432
+ }
433
+
369
434
  function isFetchStoredData(value: unknown): value is Record<string, unknown> {
370
435
  return isRecord(value) && value.type === "fetch" && Array.isArray(value.urls);
371
436
  }