@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 { copyFile, mkdir, readFile, readdir, rm, writeFile, } from "node:fs/promises";
3
4
  import { delimiter, dirname, extname, isAbsolute, join, relative, resolve, sep, } from "node:path";
@@ -10,13 +11,18 @@ import { writeWorkflowFetchCacheExtensionWrapper } from "./workflow-fetch-cache-
10
11
  import { writeWorkflowWebSourceExtensionWrapper } from "./workflow-web-source-extension.js";
11
12
  import { isWorkflowWebSourceTool } from "./workflow-web-source.js";
12
13
  import { buildWorkflowOutputRetryInstructions, parseWorkflowOutputForBundle, writeWorkflowTaskArtifactBundle, } from "./workflow-output-artifacts.js";
14
+ import { writeWorkflowPartialOutputLedgerFromFile } from "./workflow-partial-output.js";
13
15
  const DEFAULT_SUBAGENT_RUNS_ROOT = ".pi/workflow-subagents";
16
+ const MAX_SUBAGENT_SESSION_ID_LENGTH = 64;
14
17
  const EXTRA_SUBAGENT_EXTENSIONS_ENV = "PI_WORKFLOW_SUBAGENT_EXTRA_EXTENSIONS";
15
18
  const FETCH_CONTENT_CACHE_ENV = "PI_WORKFLOW_FETCH_CONTENT_CACHE";
16
19
  const LEGACY_FETCH_CACHE_ENV = "PI_WORKFLOW_FETCH_CACHE";
20
+ const FETCH_CONTENT_INLINE_CHARS_ENV = "PI_WORKFLOW_FETCH_CONTENT_INLINE_CHARS";
21
+ const DEFAULT_WORKFLOW_FETCH_CONTENT_INLINE_CHARS = 12_000;
17
22
  const DEFAULT_TRANSIENT_MODEL_FAILURE_RETRIES = 5;
18
23
  const DEFAULT_ARTIFACT_OUTPUT_RETRIES = 2;
19
24
  const MAX_CONCURRENT_LAUNCHES_ENV = "PI_WORKFLOW_MAX_CONCURRENT_LAUNCHES";
25
+ const LAUNCH_SLOT_RELEASE_DELAY_MS_ENV = "PI_WORKFLOW_LAUNCH_SLOT_RELEASE_DELAY_MS";
20
26
  const PARENT_SUBAGENT_CWD_ENV = "PI_WORKFLOW_PARENT_SUBAGENT_CWD";
21
27
  const PARENT_SUBAGENT_RUNS_DIR_ENV = "PI_WORKFLOW_PARENT_SUBAGENT_RUNS_DIR";
22
28
  const PARENT_SUBAGENT_RUN_ID_ENV = "PI_WORKFLOW_PARENT_SUBAGENT_RUN_ID";
@@ -124,7 +130,7 @@ async function recordTerminalParentSubagentChildEvent(run, task, snapshot) {
124
130
  message: task.lastMessage,
125
131
  });
126
132
  }
127
- let launchSlotReleaseDelayMs = DEFAULT_LAUNCH_SLOT_RELEASE_DELAY_MS;
133
+ let launchSlotReleaseDelayMsForTests;
128
134
  let transientRetryJitterForTests;
129
135
  const launchWaitQueue = [];
130
136
  let activeLaunchSlots = 0;
@@ -154,6 +160,15 @@ function releaseLaunchSlot() {
154
160
  }
155
161
  activeLaunchSlots = Math.max(0, activeLaunchSlots - 1);
156
162
  }
163
+ function resolveLaunchSlotReleaseDelayMs() {
164
+ if (launchSlotReleaseDelayMsForTests !== undefined) {
165
+ return launchSlotReleaseDelayMsForTests;
166
+ }
167
+ const override = Number.parseInt(process.env[LAUNCH_SLOT_RELEASE_DELAY_MS_ENV] ?? "", 10);
168
+ if (Number.isFinite(override))
169
+ return Math.max(0, Math.floor(override));
170
+ return DEFAULT_LAUNCH_SLOT_RELEASE_DELAY_MS;
171
+ }
157
172
  function releaseLaunchSlotAfterDelay(delayMs, release) {
158
173
  if (delayMs <= 0) {
159
174
  release();
@@ -161,8 +176,9 @@ function releaseLaunchSlotAfterDelay(delayMs, release) {
161
176
  }
162
177
  setTimeout(release, delayMs);
163
178
  }
164
- async function runWithLaunchSlot(action) {
179
+ async function runWithLaunchSlot(action, onAcquired) {
165
180
  const release = await acquireLaunchSlot();
181
+ onAcquired?.();
166
182
  let holdAfterReturn = false;
167
183
  try {
168
184
  const result = await action();
@@ -170,7 +186,7 @@ async function runWithLaunchSlot(action) {
170
186
  return result;
171
187
  }
172
188
  finally {
173
- releaseLaunchSlotAfterDelay(holdAfterReturn ? launchSlotReleaseDelayMs : 0, release);
189
+ releaseLaunchSlotAfterDelay(holdAfterReturn ? resolveLaunchSlotReleaseDelayMs() : 0, release);
174
190
  }
175
191
  }
176
192
  function transientRetryJitterMs() {
@@ -183,10 +199,459 @@ function transientRetryJitterMs() {
183
199
  function sleep(ms) {
184
200
  return new Promise((resolve) => setTimeout(resolve, ms));
185
201
  }
202
+ const USAGE_METRIC_KEYS = [
203
+ "inputTokens",
204
+ "outputTokens",
205
+ "totalTokens",
206
+ "cachedInputTokens",
207
+ "cacheCreationInputTokens",
208
+ "cacheReadInputTokens",
209
+ "reasoningTokens",
210
+ "costUsd",
211
+ ];
212
+ const USAGE_FIELD_ALIASES = {
213
+ inputTokens: [
214
+ ["inputTokens"],
215
+ ["input_tokens"],
216
+ ["input"],
217
+ ["promptTokens"],
218
+ ["prompt_tokens"],
219
+ ],
220
+ outputTokens: [
221
+ ["outputTokens"],
222
+ ["output_tokens"],
223
+ ["output"],
224
+ ["completionTokens"],
225
+ ["completion_tokens"],
226
+ ],
227
+ totalTokens: [["totalTokens"], ["total_tokens"], ["tokens"], ["total"]],
228
+ cachedInputTokens: [
229
+ ["cachedInputTokens"],
230
+ ["cached_input_tokens"],
231
+ ["prompt_tokens_details", "cached_tokens"],
232
+ ["input_tokens_details", "cached_tokens"],
233
+ ],
234
+ cacheCreationInputTokens: [
235
+ ["cacheCreationInputTokens"],
236
+ ["cacheCreationTokens"],
237
+ ["cacheWriteTokens"],
238
+ ["cache_creation_input_tokens"],
239
+ ["cache_write_input_tokens"],
240
+ ["cacheWrite"],
241
+ ["cache_write"],
242
+ ],
243
+ cacheReadInputTokens: [
244
+ ["cacheReadInputTokens"],
245
+ ["cacheReadTokens"],
246
+ ["cache_read_input_tokens"],
247
+ ["cacheRead"],
248
+ ["cache_read"],
249
+ ],
250
+ reasoningTokens: [
251
+ ["reasoningTokens"],
252
+ ["reasoning_tokens"],
253
+ ["reasoning"],
254
+ ["completion_tokens_details", "reasoning_tokens"],
255
+ ["output_tokens_details", "reasoning_tokens"],
256
+ ],
257
+ costUsd: [
258
+ ["costUsd"],
259
+ ["cost_usd"],
260
+ ["totalCostUsd"],
261
+ ["total_cost_usd"],
262
+ ["estimatedCostUsd"],
263
+ ["estimated_cost_usd"],
264
+ ["cost", "total"],
265
+ ["cost", "totalUsd"],
266
+ ["cost", "total_usd"],
267
+ ],
268
+ };
269
+ const TIMING_AGGREGATE_KEYS = [
270
+ "launchWaitMs",
271
+ "launchDurationMs",
272
+ "executionMs",
273
+ "totalMs",
274
+ ];
275
+ function isPlainRecord(value) {
276
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
277
+ }
278
+ function hasOwnValue(record, key) {
279
+ return Object.hasOwn(record, key);
280
+ }
281
+ function valueAtPath(record, path) {
282
+ let current = record;
283
+ for (const part of path) {
284
+ if (!isPlainRecord(current) || !hasOwnValue(current, part)) {
285
+ return { found: false, value: undefined };
286
+ }
287
+ current = current[part];
288
+ }
289
+ return { found: true, value: current };
290
+ }
291
+ function usageNumberOrNull(value) {
292
+ if (value === null)
293
+ return null;
294
+ if (typeof value === "number" && Number.isFinite(value) && value >= 0) {
295
+ return value;
296
+ }
297
+ return undefined;
298
+ }
299
+ function normalizedUsageValues(raw) {
300
+ const record = isPlainRecord(raw) ? raw : undefined;
301
+ const values = {};
302
+ if (!record)
303
+ return values;
304
+ for (const key of USAGE_METRIC_KEYS) {
305
+ for (const path of USAGE_FIELD_ALIASES[key]) {
306
+ const candidate = valueAtPath(record, path);
307
+ if (!candidate.found)
308
+ continue;
309
+ const value = usageNumberOrNull(candidate.value);
310
+ if (value === undefined)
311
+ continue;
312
+ values[key] = value;
313
+ break;
314
+ }
315
+ }
316
+ return values;
317
+ }
318
+ function firstStringValue(records, keys) {
319
+ for (const record of records) {
320
+ if (!record)
321
+ continue;
322
+ for (const key of keys) {
323
+ const value = record[key];
324
+ if (typeof value === "string" && value.trim())
325
+ return value;
326
+ }
327
+ }
328
+ return undefined;
329
+ }
330
+ function metadataRecord(value) {
331
+ if (!isPlainRecord(value))
332
+ return undefined;
333
+ return isPlainRecord(value.metadata) ? value.metadata : undefined;
334
+ }
335
+ function usageObservation(subagentResult, snapshot) {
336
+ const resultMetadata = metadataRecord(subagentResult);
337
+ if (resultMetadata && hasOwnValue(resultMetadata, "usage")) {
338
+ return {
339
+ source: "subagent-result-metadata",
340
+ raw: resultMetadata.usage,
341
+ present: true,
342
+ };
343
+ }
344
+ const snapshotMetadata = isPlainRecord(snapshot.metadata)
345
+ ? snapshot.metadata
346
+ : undefined;
347
+ if (snapshotMetadata && hasOwnValue(snapshotMetadata, "usage")) {
348
+ return {
349
+ source: "subagent-snapshot-metadata",
350
+ raw: snapshotMetadata.usage,
351
+ present: true,
352
+ };
353
+ }
354
+ if (subagentResult && hasOwnValue(subagentResult, "usage")) {
355
+ return {
356
+ source: "subagent-result",
357
+ raw: subagentResult.usage,
358
+ present: true,
359
+ };
360
+ }
361
+ const snapshotRecord = snapshot;
362
+ if (hasOwnValue(snapshotRecord, "usage")) {
363
+ return {
364
+ source: "subagent-snapshot",
365
+ raw: snapshotRecord.usage,
366
+ present: true,
367
+ };
368
+ }
369
+ return undefined;
370
+ }
371
+ function buildTaskUsageAttempt(options) {
372
+ const resultMetadata = metadataRecord(options.subagentResult);
373
+ const snapshotMetadata = isPlainRecord(options.snapshot.metadata)
374
+ ? options.snapshot.metadata
375
+ : undefined;
376
+ const resultRecord = options.subagentResult;
377
+ const snapshotRecord = options.snapshot;
378
+ const records = [
379
+ resultMetadata,
380
+ snapshotMetadata,
381
+ resultRecord,
382
+ snapshotRecord,
383
+ ];
384
+ const observed = usageObservation(options.subagentResult, options.snapshot);
385
+ const raw = observed?.raw;
386
+ const unavailable = !observed || raw === null || raw === undefined;
387
+ const provider = firstStringValue(records, ["provider"]);
388
+ const model = firstStringValue(records, ["model"]) ?? options.task.runtime.model;
389
+ const thinking = firstStringValue(records, [
390
+ "thinking",
391
+ "thinkingLevel",
392
+ "reasoningLevel",
393
+ ]) ??
394
+ options.task.runtime.thinkingResolution?.resolved ??
395
+ options.task.runtime.thinking;
396
+ return {
397
+ source: observed?.source ?? "subagent-usage-unavailable",
398
+ capturedAt: options.capturedAt,
399
+ backendRunId: options.snapshot.runId,
400
+ backendAttemptId: options.snapshot.attemptId,
401
+ ...(provider === undefined ? {} : { provider }),
402
+ ...(model === undefined ? {} : { model }),
403
+ ...(thinking === undefined ? {} : { thinking }),
404
+ ...(unavailable ? { unavailable: true } : {}),
405
+ ...(observed?.present && raw !== undefined ? { raw } : {}),
406
+ ...normalizedUsageValues(raw),
407
+ };
408
+ }
409
+ function usageAttemptKey(attempt) {
410
+ return `${attempt.backendRunId ?? ""}\0${attempt.backendAttemptId ?? ""}\0${attempt.source}`;
411
+ }
412
+ function upsertUsageAttempt(attempts, attempt) {
413
+ const key = usageAttemptKey(attempt);
414
+ const index = attempts.findIndex((candidate) => usageAttemptKey(candidate) === key);
415
+ if (index < 0)
416
+ return [...attempts, attempt];
417
+ return attempts.map((candidate, candidateIndex) => candidateIndex === index ? attempt : candidate);
418
+ }
419
+ function aggregateUsageAttempts(attempts) {
420
+ const values = {};
421
+ let incomplete = attempts.some((attempt) => attempt.unavailable === true);
422
+ for (const key of USAGE_METRIC_KEYS) {
423
+ const anyPresent = attempts.some((attempt) => hasOwnValue(attempt, key));
424
+ if (!anyPresent)
425
+ continue;
426
+ let total = 0;
427
+ let complete = true;
428
+ for (const attempt of attempts) {
429
+ if (!hasOwnValue(attempt, key)) {
430
+ complete = false;
431
+ break;
432
+ }
433
+ const value = attempt[key];
434
+ if (typeof value !== "number") {
435
+ complete = false;
436
+ break;
437
+ }
438
+ total += value;
439
+ }
440
+ values[key] = complete ? total : null;
441
+ if (!complete)
442
+ incomplete = true;
443
+ }
444
+ return { values, incomplete };
445
+ }
446
+ function latestUsageString(attempts, key) {
447
+ for (let index = attempts.length - 1; index >= 0; index -= 1) {
448
+ const value = attempts[index]?.[key];
449
+ if (typeof value === "string" && value.trim())
450
+ return value;
451
+ }
452
+ return undefined;
453
+ }
454
+ function recordTaskUsageObservation(options) {
455
+ const attempt = buildTaskUsageAttempt(options);
456
+ const attempts = upsertUsageAttempt(options.task.usage?.attempts ?? [], attempt);
457
+ const aggregate = aggregateUsageAttempts(attempts);
458
+ const usage = {
459
+ source: "pi-subagent",
460
+ capturedAt: options.capturedAt,
461
+ ...(latestUsageString(attempts, "provider") === undefined
462
+ ? {}
463
+ : { provider: latestUsageString(attempts, "provider") }),
464
+ ...(latestUsageString(attempts, "model") === undefined
465
+ ? {}
466
+ : { model: latestUsageString(attempts, "model") }),
467
+ ...(latestUsageString(attempts, "thinking") === undefined
468
+ ? {}
469
+ : { thinking: latestUsageString(attempts, "thinking") }),
470
+ ...(aggregate.incomplete ? { incomplete: true } : {}),
471
+ ...aggregate.values,
472
+ aggregate: {
473
+ attempts: attempts.length,
474
+ ...(aggregate.incomplete ? { incomplete: true } : {}),
475
+ ...aggregate.values,
476
+ },
477
+ attempts,
478
+ };
479
+ options.task.usage = usage;
480
+ }
481
+ function isoTimestampMs(timestamp) {
482
+ if (!timestamp)
483
+ return undefined;
484
+ const parsed = Date.parse(timestamp);
485
+ return Number.isFinite(parsed) ? parsed : undefined;
486
+ }
487
+ function durationBetween(startedAt, completedAt) {
488
+ const startedAtMs = isoTimestampMs(startedAt);
489
+ const completedAtMs = isoTimestampMs(completedAt);
490
+ if (startedAtMs === undefined || completedAtMs === undefined)
491
+ return undefined;
492
+ return Math.max(0, completedAtMs - startedAtMs);
493
+ }
494
+ function durationNumber(value) {
495
+ if (value === null)
496
+ return null;
497
+ if (typeof value === "number" && Number.isFinite(value) && value >= 0) {
498
+ return value;
499
+ }
500
+ return undefined;
501
+ }
502
+ function recordTaskLaunchTiming(task, observation) {
503
+ const capturedAt = observation.launchCompletedAt ?? nowIso();
504
+ const launchWaitMs = durationBetween(observation.launchQueuedAt, observation.launchStartedAt);
505
+ const launchDurationMs = durationBetween(observation.launchStartedAt, observation.launchCompletedAt);
506
+ task.timing = {
507
+ source: "pi-workflow",
508
+ capturedAt,
509
+ launchQueuedAt: observation.launchQueuedAt,
510
+ ...(observation.launchStartedAt === undefined
511
+ ? {}
512
+ : { launchStartedAt: observation.launchStartedAt }),
513
+ ...(observation.launchCompletedAt === undefined
514
+ ? {}
515
+ : { launchCompletedAt: observation.launchCompletedAt }),
516
+ ...(launchWaitMs === undefined ? {} : { launchWaitMs }),
517
+ ...(launchDurationMs === undefined ? {} : { launchDurationMs }),
518
+ launchSlotReleaseDelayMs: resolveLaunchSlotReleaseDelayMs(),
519
+ ...(task.timing?.aggregate === undefined
520
+ ? {}
521
+ : { aggregate: task.timing.aggregate }),
522
+ ...(task.timing?.attempts === undefined
523
+ ? {}
524
+ : { attempts: task.timing.attempts }),
525
+ };
526
+ }
527
+ function buildTaskTimingAttempt(options) {
528
+ const resultDuration = options.subagentResult?.durationMs;
529
+ let executionMs = durationNumber(resultDuration === undefined ? options.snapshot.durationMs : resultDuration);
530
+ if (executionMs === undefined || executionMs === null) {
531
+ executionMs =
532
+ durationBetween(options.startedAt, options.completedAt) ?? executionMs;
533
+ }
534
+ const totalMs = durationBetween(options.task.startedAt ?? options.task.timing?.launchQueuedAt, options.completedAt);
535
+ return {
536
+ source: "pi-subagent",
537
+ capturedAt: options.capturedAt,
538
+ backendRunId: options.snapshot.runId,
539
+ backendAttemptId: options.snapshot.attemptId,
540
+ ...(options.task.timing?.launchQueuedAt === undefined
541
+ ? {}
542
+ : { launchQueuedAt: options.task.timing.launchQueuedAt }),
543
+ ...(options.task.timing?.launchStartedAt === undefined
544
+ ? {}
545
+ : { launchStartedAt: options.task.timing.launchStartedAt }),
546
+ ...(options.task.timing?.launchCompletedAt === undefined
547
+ ? {}
548
+ : { launchCompletedAt: options.task.timing.launchCompletedAt }),
549
+ ...(options.task.timing?.launchWaitMs === undefined
550
+ ? {}
551
+ : { launchWaitMs: options.task.timing.launchWaitMs }),
552
+ ...(options.task.timing?.launchDurationMs === undefined
553
+ ? {}
554
+ : { launchDurationMs: options.task.timing.launchDurationMs }),
555
+ ...(options.startedAt === undefined
556
+ ? {}
557
+ : { executionStartedAt: options.startedAt }),
558
+ ...(options.completedAt === undefined
559
+ ? {}
560
+ : { executionCompletedAt: options.completedAt }),
561
+ ...(executionMs === undefined ? {} : { executionMs }),
562
+ ...(totalMs === undefined ? {} : { totalMs }),
563
+ };
564
+ }
565
+ function timingAttemptKey(attempt) {
566
+ return `${attempt.backendRunId ?? ""}\0${attempt.backendAttemptId ?? ""}`;
567
+ }
568
+ function upsertTimingAttempt(attempts, attempt) {
569
+ const key = timingAttemptKey(attempt);
570
+ const index = attempts.findIndex((candidate) => timingAttemptKey(candidate) === key);
571
+ if (index < 0)
572
+ return [...attempts, attempt];
573
+ return attempts.map((candidate, candidateIndex) => candidateIndex === index ? attempt : candidate);
574
+ }
575
+ function aggregateTimingAttempts(attempts) {
576
+ const aggregate = {
577
+ attempts: attempts.length,
578
+ };
579
+ let incomplete = false;
580
+ for (const key of TIMING_AGGREGATE_KEYS) {
581
+ const anyPresent = attempts.some((attempt) => hasOwnValue(attempt, key));
582
+ if (!anyPresent)
583
+ continue;
584
+ let total = 0;
585
+ let complete = true;
586
+ for (const attempt of attempts) {
587
+ if (!hasOwnValue(attempt, key)) {
588
+ complete = false;
589
+ break;
590
+ }
591
+ const value = attempt[key];
592
+ if (typeof value !== "number") {
593
+ complete = false;
594
+ break;
595
+ }
596
+ total += value;
597
+ }
598
+ aggregate[key] = complete ? total : null;
599
+ if (!complete)
600
+ incomplete = true;
601
+ }
602
+ if (incomplete)
603
+ aggregate.incomplete = true;
604
+ return aggregate;
605
+ }
606
+ function recordTaskTerminalTiming(options) {
607
+ const attempt = buildTaskTimingAttempt(options);
608
+ const attempts = upsertTimingAttempt(options.task.timing?.attempts ?? [], attempt);
609
+ options.task.timing = {
610
+ source: "pi-workflow",
611
+ capturedAt: options.capturedAt,
612
+ ...(attempt.launchQueuedAt === undefined
613
+ ? {}
614
+ : { launchQueuedAt: attempt.launchQueuedAt }),
615
+ ...(attempt.launchStartedAt === undefined
616
+ ? {}
617
+ : { launchStartedAt: attempt.launchStartedAt }),
618
+ ...(attempt.launchCompletedAt === undefined
619
+ ? {}
620
+ : { launchCompletedAt: attempt.launchCompletedAt }),
621
+ ...(attempt.launchWaitMs === undefined
622
+ ? {}
623
+ : { launchWaitMs: attempt.launchWaitMs }),
624
+ ...(attempt.launchDurationMs === undefined
625
+ ? {}
626
+ : { launchDurationMs: attempt.launchDurationMs }),
627
+ ...(options.task.timing?.launchSlotReleaseDelayMs === undefined
628
+ ? {}
629
+ : {
630
+ launchSlotReleaseDelayMs: options.task.timing.launchSlotReleaseDelayMs,
631
+ }),
632
+ ...(attempt.executionStartedAt === undefined
633
+ ? {}
634
+ : { executionStartedAt: attempt.executionStartedAt }),
635
+ ...(attempt.executionCompletedAt === undefined
636
+ ? {}
637
+ : { executionCompletedAt: attempt.executionCompletedAt }),
638
+ ...(attempt.executionMs === undefined
639
+ ? {}
640
+ : { executionMs: attempt.executionMs }),
641
+ ...(attempt.totalMs === undefined ? {} : { totalMs: attempt.totalMs }),
642
+ aggregate: aggregateTimingAttempts(attempts),
643
+ attempts,
644
+ };
645
+ }
646
+ function recordTerminalTaskObservability(options) {
647
+ const capturedAt = nowIso();
648
+ recordTaskUsageObservation({ ...options, capturedAt });
649
+ recordTaskTerminalTiming({ ...options, capturedAt });
650
+ }
186
651
  export function setSubagentLaunchControlsForTests(options) {
187
- launchSlotReleaseDelayMs =
652
+ launchSlotReleaseDelayMsForTests =
188
653
  options?.releaseDelayMs === undefined
189
- ? DEFAULT_LAUNCH_SLOT_RELEASE_DELAY_MS
654
+ ? undefined
190
655
  : Math.max(0, Math.floor(options.releaseDelayMs));
191
656
  transientRetryJitterForTests =
192
657
  options?.retryJitterMs === undefined
@@ -283,11 +748,22 @@ export async function launchSubagentTask(cwd, run, task, compiledTask) {
283
748
  subagentOptions.extensions = extensions;
284
749
  if (captureToolCallsEnabled())
285
750
  subagentOptions.captureToolCalls = true;
751
+ const launchQueuedAt = nowIso();
752
+ let launchStartedAt;
753
+ recordTaskLaunchTiming(task, { launchQueuedAt });
286
754
  if (isLaunchGateSaturated()) {
287
755
  task.lastMessage = `waiting for pi-subagent launch slot (${resolveMaxConcurrentLaunches()} max)`;
288
756
  await writeRunRecord(cwd, run).catch(() => undefined);
289
757
  }
290
- launched = await runWithLaunchSlot(() => api.runSubagent(subagentOptions));
758
+ launched = await runWithLaunchSlot(() => api.runSubagent(subagentOptions), () => {
759
+ launchStartedAt = nowIso();
760
+ recordTaskLaunchTiming(task, { launchQueuedAt, launchStartedAt });
761
+ });
762
+ recordTaskLaunchTiming(task, {
763
+ launchQueuedAt,
764
+ launchStartedAt,
765
+ launchCompletedAt: nowIso(),
766
+ });
291
767
  }
292
768
  catch (error) {
293
769
  task.status = "pending";
@@ -378,12 +854,24 @@ export async function refreshRunFromSubagentArtifacts(cwd, run) {
378
854
  continue;
379
855
  }
380
856
  const activeAttempt = snapshot.attempts?.find((attempt) => attempt.attemptId === handle.attemptId) ?? snapshot.attempts?.at(-1);
381
- task.pid = activeAttempt?.workerPid ?? activeAttempt?.pid ?? task.pid;
857
+ const nextPid = activeAttempt?.workerPid ?? activeAttempt?.pid ?? task.pid;
858
+ if (task.pid !== nextPid) {
859
+ task.pid = nextPid;
860
+ changed = true;
861
+ }
382
862
  if (snapshot.status === "running" || snapshot.status === "pending") {
383
- task.statusDetail = "running";
384
- task.lastMessage = activeAttempt?.heartbeatAt
863
+ await refreshRunningArtifactGraphPartialOutput(cwd, task, snapshot).catch(() => undefined);
864
+ if (task.statusDetail !== "running") {
865
+ task.statusDetail = "running";
866
+ changed = true;
867
+ }
868
+ const nextLastMessage = activeAttempt?.heartbeatAt
385
869
  ? `pi-subagent heartbeat ${activeAttempt.heartbeatAt}`
386
870
  : "pi-subagent running";
871
+ if (task.lastMessage !== nextLastMessage) {
872
+ task.lastMessage = nextLastMessage;
873
+ changed = true;
874
+ }
387
875
  if (isTaskTimedOut(task)) {
388
876
  await interruptTimedOutSubagent(api, handle);
389
877
  markSubagentTaskTimedOut(task);
@@ -398,6 +886,22 @@ export async function refreshRunFromSubagentArtifacts(cwd, run) {
398
886
  await writeRunRecord(cwd, run);
399
887
  return run;
400
888
  }
889
+ async function refreshRunningArtifactGraphPartialOutput(cwd, task, snapshot) {
890
+ const partial = task.artifactGraph?.output.partial;
891
+ if (!partial || partial.paths.length === 0)
892
+ return;
893
+ const outputRef = findLog(snapshot, "output");
894
+ const outputFile = fromProjectPath(cwd, task.files.output);
895
+ const artifactRoot = task.backendFiles?.runsDir
896
+ ? fromProjectPath(task.cwd, task.backendFiles.runsDir)
897
+ : undefined;
898
+ await copyLogOrEmpty(snapshot, outputRef, outputFile, artifactRoot);
899
+ await writeWorkflowPartialOutputLedgerFromFile({
900
+ taskDir: dirname(fromProjectPath(cwd, task.files.result)),
901
+ outputFile,
902
+ allowedPaths: partial.paths,
903
+ });
904
+ }
401
905
  async function interruptTimedOutSubagent(api, handle) {
402
906
  await api
403
907
  .interruptSubagent({
@@ -484,6 +988,13 @@ async function materializeTerminalSubagentResult(cwd, run, task, snapshot) {
484
988
  : undefined);
485
989
  const contextLengthExceeded = Boolean(subagentResult?.metadata?.contextLengthExceeded ??
486
990
  snapshot.metadata?.contextLengthExceeded);
991
+ recordTerminalTaskObservability({
992
+ task,
993
+ snapshot,
994
+ subagentResult,
995
+ startedAt,
996
+ completedAt,
997
+ });
487
998
  if (task.artifactGraph?.enabled && statusInfo.status === "completed") {
488
999
  const changed = await materializeTerminalArtifactGraphResult(cwd, run, task, {
489
1000
  outputFile,
@@ -596,6 +1107,13 @@ function artifactGraphRetrySession(run, task, subagentResult, attempt) {
596
1107
  async function materializeTerminalArtifactGraphResult(cwd, run, task, options) {
597
1108
  const rawOutput = await readFile(options.outputFile, "utf8").catch(() => "");
598
1109
  const artifactOptions = task.artifactGraph?.output;
1110
+ if (artifactOptions?.partial && artifactOptions.partial.paths.length > 0) {
1111
+ await writeWorkflowPartialOutputLedgerFromFile({
1112
+ taskDir: dirname(options.resultFile),
1113
+ outputFile: options.outputFile,
1114
+ allowedPaths: artifactOptions.partial.paths,
1115
+ }).catch(() => undefined);
1116
+ }
599
1117
  let controlJsonSchema;
600
1118
  try {
601
1119
  controlJsonSchema = await readTaskControlJsonSchema(task);
@@ -1097,6 +1615,7 @@ async function workflowTaskExtensions(cwd, run, task, compiledTask) {
1097
1615
  runId: run.runId,
1098
1616
  taskId: task.taskId,
1099
1617
  cacheDir: resolve(cwd, ".pi", "workflows", run.runId, "source-cache", "fetch-content"),
1618
+ maxInlineChars: fetchContentInlineCharsEnvValue(),
1100
1619
  },
1101
1620
  });
1102
1621
  extensions = uniqueStrings([
@@ -1162,6 +1681,17 @@ function workflowWebSourceProviderExtensions(tools, toolProviders) {
1162
1681
  function fetchContentCacheEnvValue() {
1163
1682
  return (process.env[FETCH_CONTENT_CACHE_ENV] ?? process.env[LEGACY_FETCH_CACHE_ENV]);
1164
1683
  }
1684
+ function fetchContentInlineCharsEnvValue() {
1685
+ const raw = process.env[FETCH_CONTENT_INLINE_CHARS_ENV];
1686
+ if (raw === undefined || raw.trim() === "")
1687
+ return DEFAULT_WORKFLOW_FETCH_CONTENT_INLINE_CHARS;
1688
+ if (isExplicitlyDisabled(raw))
1689
+ return undefined;
1690
+ const parsed = Number(raw);
1691
+ if (!Number.isFinite(parsed))
1692
+ return DEFAULT_WORKFLOW_FETCH_CONTENT_INLINE_CHARS;
1693
+ return Math.max(1, Math.floor(parsed));
1694
+ }
1165
1695
  function isExplicitlyDisabled(value) {
1166
1696
  return typeof value === "string" && /^(0|false|no|off)$/i.test(value.trim());
1167
1697
  }
@@ -1335,17 +1865,31 @@ function subagentSessionId(run, task) {
1335
1865
  return task.outputRetry.sessionId;
1336
1866
  const launchAttempt = task.launchRetry?.attempts ?? 0;
1337
1867
  if (launchAttempt > 0)
1338
- return `${baseSessionId}:launch-retry-${launchAttempt}`;
1868
+ return boundedSubagentSessionId(`${baseSessionId}.launch-retry-${launchAttempt}`);
1339
1869
  const resumeAttempt = task.resumeEvents?.length ?? 0;
1340
1870
  if (resumeAttempt > 0)
1341
- return `${baseSessionId}:resume-${resumeAttempt}`;
1871
+ return boundedSubagentSessionId(`${baseSessionId}.resume-${resumeAttempt}`);
1342
1872
  return baseSessionId;
1343
1873
  }
1344
1874
  function baseSubagentSessionId(run, task) {
1345
- return `pi-workflow.${run.runId}.${task.taskId}`.replace(/[^A-Za-z0-9._-]/g, "-");
1875
+ return boundedSubagentSessionId(`pi-workflow.${run.runId}.${task.taskId}`);
1346
1876
  }
1347
1877
  function retrySubagentSessionId(run, task, attempt) {
1348
- return `${baseSubagentSessionId(run, task)}.retry-${attempt}`;
1878
+ return boundedSubagentSessionId(`${baseSubagentSessionId(run, task)}.retry-${attempt}`);
1879
+ }
1880
+ function boundedSubagentSessionId(value) {
1881
+ const sanitized = value.replace(/[^A-Za-z0-9._-]/g, "-");
1882
+ if (sanitized.length <= MAX_SUBAGENT_SESSION_ID_LENGTH)
1883
+ return sanitized;
1884
+ const digest = createHash("sha256")
1885
+ .update(sanitized)
1886
+ .digest("hex")
1887
+ .slice(0, 16);
1888
+ const suffix = sanitized.split(".").at(-1) || "session";
1889
+ const prefix = `piwf.${digest}`;
1890
+ const maxSuffixLength = MAX_SUBAGENT_SESSION_ID_LENGTH - prefix.length - 1;
1891
+ const boundedSuffix = suffix.slice(-Math.max(1, maxSuffixLength));
1892
+ return `${prefix}.${boundedSuffix}`;
1349
1893
  }
1350
1894
  function buildSystemPrompt(task) {
1351
1895
  const workflowMaxDigestChars = task.artifactGraph?.output.maxDigestChars;