@agwab/pi-workflow 0.2.1 → 0.3.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 (70) hide show
  1. package/dist/compiler.js +6 -8
  2. package/dist/dynamic-decision.d.ts +0 -1
  3. package/dist/dynamic-decision.js +0 -7
  4. package/dist/dynamic-profiles.d.ts +0 -1
  5. package/dist/dynamic-profiles.js +0 -3
  6. package/dist/engine-run-graph.d.ts +1 -0
  7. package/dist/engine-run-graph.js +142 -2
  8. package/dist/engine.d.ts +5 -0
  9. package/dist/engine.js +112 -27
  10. package/dist/extension.d.ts +2 -1
  11. package/dist/extension.js +27 -6
  12. package/dist/index.d.ts +3 -3
  13. package/dist/index.js +2 -1
  14. package/dist/store.js +55 -11
  15. package/dist/subagent-backend.js +155 -29
  16. package/dist/types.d.ts +6 -0
  17. package/dist/workflow-runtime.js +10 -1
  18. package/dist/workflow-view.js +3 -1
  19. package/dist/workflow-web-source-extension.js +167 -48
  20. package/dist/workflow-web-source.d.ts +2 -1
  21. package/dist/workflow-web-source.js +84 -19
  22. package/node_modules/@agwab/pi-subagent/README.md +3 -3
  23. package/node_modules/@agwab/pi-subagent/api.mjs +1 -0
  24. package/node_modules/@agwab/pi-subagent/docs/usage.md +63 -12
  25. package/node_modules/@agwab/pi-subagent/package.json +2 -2
  26. package/node_modules/@agwab/pi-subagent/src/api.ts +54 -1
  27. package/node_modules/@agwab/pi-subagent/src/artifacts/registry.ts +9 -4
  28. package/node_modules/@agwab/pi-subagent/src/artifacts/result.ts +8 -0
  29. package/node_modules/@agwab/pi-subagent/src/core/constants.ts +9 -0
  30. package/node_modules/@agwab/pi-subagent/src/core/validation.ts +21 -0
  31. package/node_modules/@agwab/pi-subagent/src/index.ts +995 -573
  32. package/node_modules/@agwab/pi-subagent/src/orchestrate/async.ts +279 -156
  33. package/node_modules/@agwab/pi-subagent/src/orchestrate/interrupt.ts +165 -89
  34. package/node_modules/@agwab/pi-subagent/src/orchestrate/reconcile.ts +111 -65
  35. package/node_modules/@agwab/pi-subagent/src/orchestrate/run-ref.ts +219 -0
  36. package/node_modules/@agwab/pi-subagent/src/orchestrate/run.ts +88 -8
  37. package/node_modules/@agwab/pi-subagent/src/orchestrate/status.ts +614 -298
  38. package/node_modules/@agwab/pi-subagent/src/panel.ts +1352 -560
  39. package/node_modules/@agwab/pi-subagent/src/runners/headless-model.ts +53 -5
  40. package/node_modules/@agwab/pi-subagent/src/runners/tmux.ts +13 -6
  41. package/package.json +2 -2
  42. package/src/compiler.ts +14 -9
  43. package/src/dynamic-decision.ts +0 -11
  44. package/src/dynamic-profiles.ts +0 -4
  45. package/src/engine-run-graph.ts +185 -2
  46. package/src/engine.ts +145 -24
  47. package/src/extension.ts +33 -4
  48. package/src/index.ts +3 -1
  49. package/src/store.ts +74 -11
  50. package/src/subagent-backend.ts +201 -28
  51. package/src/types.ts +6 -0
  52. package/src/workflow-runtime.ts +18 -2
  53. package/src/workflow-view.ts +2 -1
  54. package/src/workflow-web-source-extension.ts +621 -228
  55. package/src/workflow-web-source.ts +118 -28
  56. package/workflows/deep-research/helpers/claim-evidence-gate.mjs +56 -16
  57. package/workflows/deep-research/helpers/final-audit-packet.mjs +1 -4
  58. package/workflows/deep-research/helpers/normalize-input-packet.mjs +1 -1
  59. package/workflows/deep-research/helpers/render-executive.mjs +8 -21
  60. package/workflows/deep-research/helpers/sanitize-verification-candidates.mjs +89 -15
  61. package/workflows/deep-research/schemas/deep-research-executive-render-control.schema.json +0 -1
  62. package/workflows/deep-research/schemas/deep-research-verify-claims-control.schema.json +4 -1
  63. package/workflows/impact-review/spec.json +3 -3
  64. package/workflows/spec-review/helpers/spec-review-pipeline.mjs +1 -8
  65. package/dist/dynamic-loader.d.ts +0 -25
  66. package/dist/dynamic-loader.js +0 -13
  67. package/src/dynamic-loader.ts +0 -49
  68. package/workflows/impact-review/schemas/docs-release-impact-control.schema.json +0 -42
  69. package/workflows/impact-review/schemas/security-performance-impact-control.schema.json +0 -42
  70. package/workflows/impact-review/schemas/state-data-impact-control.schema.json +0 -42
@@ -72,14 +72,26 @@ export function registerWorkflowWebSourceExtension(pi, config, providerExtension
72
72
  name: "workflow_web_fetch_source",
73
73
  description: "Fetch one or more URLs into the workflow web-source cache and return compact source cards with sourceRefs.",
74
74
  parameters: Type.Object({
75
- url: Type.Optional(Type.String({ description: "Single URL to fetch into the workflow web-source cache." })),
76
- urls: Type.Optional(Type.Array(Type.String(), { description: "Multiple URLs to fetch in one tool call. Prefer this over repeated fetch calls when caching several promising sources." })),
75
+ url: Type.Optional(Type.String({
76
+ description: "Single URL to fetch into the workflow web-source cache.",
77
+ })),
78
+ urls: Type.Optional(Type.Array(Type.String(), {
79
+ description: "Multiple URLs to fetch in one tool call. Prefer this over repeated fetch calls when caching several promising sources.",
80
+ })),
77
81
  sources: Type.Optional(Type.Array(Type.Object({
78
- url: Type.String({ description: "URL to fetch into the workflow web-source cache." }),
82
+ url: Type.String({
83
+ description: "URL to fetch into the workflow web-source cache.",
84
+ }),
79
85
  title: Type.Optional(Type.String({ description: "Optional source title override." })),
80
- }), { description: "Multiple URL/title objects to fetch in one tool call." })),
81
- title: Type.Optional(Type.String({ description: "Optional source title override for single-url fetches." })),
82
- titles: Type.Optional(Type.Array(Type.String(), { description: "Optional title overrides paired by index with urls." })),
86
+ }), {
87
+ description: "Multiple URL/title objects to fetch in one tool call.",
88
+ })),
89
+ title: Type.Optional(Type.String({
90
+ description: "Optional source title override for single-url fetches.",
91
+ })),
92
+ titles: Type.Optional(Type.Array(Type.String(), {
93
+ description: "Optional title overrides paired by index with urls.",
94
+ })),
83
95
  }),
84
96
  execute: async (toolCallId, params, signal, onUpdate, ctx) => {
85
97
  const batchRequested = fetchSourceBatchRequested(params);
@@ -101,8 +113,12 @@ export function registerWorkflowWebSourceExtension(pi, config, providerExtension
101
113
  url: sanitizeUrlForModel(request.url),
102
114
  status: typeof payload.status === "string" ? payload.status : "unknown",
103
115
  ...(typeof payload.code === "string" ? { code: payload.code } : {}),
104
- ...(typeof payload.message === "string" ? { message: payload.message } : {}),
105
- ...(typeof card?.sourceRef === "string" ? { sourceRef: card.sourceRef } : {}),
116
+ ...(typeof payload.message === "string"
117
+ ? { message: payload.message }
118
+ : {}),
119
+ ...(typeof card?.sourceRef === "string"
120
+ ? { sourceRef: card.sourceRef }
121
+ : {}),
106
122
  ...(card ? { cardIndex: cards.length - 1 } : {}),
107
123
  });
108
124
  }
@@ -159,10 +175,15 @@ export function registerWorkflowWebSourceExtension(pi, config, providerExtension
159
175
  url: existing.redactedUrl,
160
176
  visibleChars: budget.used,
161
177
  });
162
- return toolResultFromJson({ status: "ok", tool: "workflow_web_fetch_source", card });
178
+ return toolResultFromJson({
179
+ status: "ok",
180
+ tool: "workflow_web_fetch_source",
181
+ card,
182
+ });
163
183
  }
164
184
  const fetchKey = sourceUrlCacheKey(fetchUrl);
165
- const cachedFailure = fetchFailures.get(fetchKey) ?? await readDurableFetchFailure(config, fetchKey);
185
+ const cachedFailure = fetchFailures.get(fetchKey) ??
186
+ (await readDurableFetchFailure(config, fetchKey));
166
187
  if (cachedFailure) {
167
188
  fetchFailures.set(fetchKey, cachedFailure);
168
189
  await recordWorkflowWebSourceEvent(config, "fetch_negative_cache_hit", {
@@ -178,25 +199,43 @@ export function registerWorkflowWebSourceExtension(pi, config, providerExtension
178
199
  if (!source)
179
200
  return result;
180
201
  sourceCache.set(source.sourceRef, source);
181
- const card = buildWorkflowWebSourceCard({ source, policy, budget, duplicate: true });
202
+ const card = buildWorkflowWebSourceCard({
203
+ source,
204
+ policy,
205
+ budget,
206
+ duplicate: true,
207
+ });
182
208
  await recordWorkflowWebSourceEvent(config, "fetch_duplicate", {
183
209
  sourceRef: source.sourceRef,
184
210
  url: source.redactedUrl,
185
211
  visibleChars: budget.used,
186
212
  });
187
- return toolResultFromJson({ status: "ok", tool: "workflow_web_fetch_source", card });
213
+ return toolResultFromJson({
214
+ status: "ok",
215
+ tool: "workflow_web_fetch_source",
216
+ card,
217
+ });
188
218
  }
189
219
  const fetchPromise = withWorkflowWebFetchLock(config, fetchKey, signal, async () => {
190
220
  const lockedExisting = await findWorkflowWebSourceByUrl(config, fetchUrl);
191
221
  if (lockedExisting) {
192
222
  sourceCache.set(lockedExisting.sourceRef, lockedExisting);
193
- const card = buildWorkflowWebSourceCard({ source: lockedExisting, policy, budget, duplicate: true });
223
+ const card = buildWorkflowWebSourceCard({
224
+ source: lockedExisting,
225
+ policy,
226
+ budget,
227
+ duplicate: true,
228
+ });
194
229
  await recordWorkflowWebSourceEvent(config, "fetch_duplicate", {
195
230
  sourceRef: lockedExisting.sourceRef,
196
231
  url: lockedExisting.redactedUrl,
197
232
  visibleChars: budget.used,
198
233
  });
199
- return toolResultFromJson({ status: "ok", tool: "workflow_web_fetch_source", card });
234
+ return toolResultFromJson({
235
+ status: "ok",
236
+ tool: "workflow_web_fetch_source",
237
+ card,
238
+ });
200
239
  }
201
240
  const lockedFailure = await readDurableFetchFailure(config, fetchKey);
202
241
  if (lockedFailure) {
@@ -221,7 +260,10 @@ export function registerWorkflowWebSourceExtension(pi, config, providerExtension
221
260
  return await cachedFetchFailureResult(config, fetchFailures, fetchKey, {
222
261
  code: "blocked_url",
223
262
  message: "URL was blocked by workflow web-source security policy before content fetch.",
224
- extra: { reason: safeFetch.reason, url: sanitizeUrlForModel(safeFetch.url) },
263
+ extra: {
264
+ reason: safeFetch.reason,
265
+ url: sanitizeUrlForModel(safeFetch.url),
266
+ },
225
267
  reason: safeFetch.reason,
226
268
  });
227
269
  }
@@ -311,10 +353,16 @@ export function registerWorkflowWebSourceExtension(pi, config, providerExtension
311
353
  textChars: source.textChars,
312
354
  visibleChars: budget.used,
313
355
  });
314
- return toolResultFromJson({ status: "ok", tool: "workflow_web_fetch_source", card });
356
+ return toolResultFromJson({
357
+ status: "ok",
358
+ tool: "workflow_web_fetch_source",
359
+ card,
360
+ });
315
361
  }).catch(async (error) => {
316
362
  const message = error instanceof Error ? error.message : "workflow_web_fetch_failed";
317
- const code = message === "fetch_lock_timeout" ? "fetch_lock_timeout" : "workflow_web_fetch_failed";
363
+ const code = message === "fetch_lock_timeout"
364
+ ? "fetch_lock_timeout"
365
+ : "workflow_web_fetch_failed";
318
366
  await recordWorkflowWebSourceEvent(config, "fetch_failed", {
319
367
  url: sanitizeUrlForModel(fetchUrl),
320
368
  code,
@@ -335,23 +383,47 @@ export function registerWorkflowWebSourceExtension(pi, config, providerExtension
335
383
  name: "workflow_web_source_read",
336
384
  description: "Read one or more narrow exact/fuzzy/term-matched snippets from a cached workflow web source by sourceRef.",
337
385
  parameters: Type.Object({
338
- sourceRef: Type.String({ description: "Opaque sourceRef returned by workflow_web_fetch_source." }),
339
- query: Type.Optional(Type.String({ description: "Exact or fuzzy text to locate in the cached source." })),
340
- queries: Type.Optional(Type.Array(Type.String(), { description: "Multiple exact/fuzzy texts to locate in one cached source. Prefer this over repeated calls when reading several snippets from the same sourceRef." })),
341
- exact: Type.Optional(Type.String({ description: "Exact text to locate in the cached source." })),
342
- exactTexts: Type.Optional(Type.Array(Type.String(), { description: "Multiple exact texts to locate in one cached source." })),
343
- claim: Type.Optional(Type.String({ description: "Claim to locate when the exact quote is not known. Use with terms for deterministic quote harvesting." })),
344
- terms: Type.Optional(Type.Array(Type.String(), { description: "Important terms that should co-occur in the returned source window." })),
386
+ sourceRef: Type.String({
387
+ description: "Opaque sourceRef returned by workflow_web_fetch_source.",
388
+ }),
389
+ query: Type.Optional(Type.String({
390
+ description: "Exact or fuzzy text to locate in the cached source.",
391
+ })),
392
+ queries: Type.Optional(Type.Array(Type.String(), {
393
+ description: "Multiple exact/fuzzy texts to locate in one cached source. Prefer this over repeated calls when reading several snippets from the same sourceRef.",
394
+ })),
395
+ exact: Type.Optional(Type.String({
396
+ description: "Exact text to locate in the cached source.",
397
+ })),
398
+ exactTexts: Type.Optional(Type.Array(Type.String(), {
399
+ description: "Multiple exact texts to locate in one cached source.",
400
+ })),
401
+ claim: Type.Optional(Type.String({
402
+ description: "Claim to locate when the exact quote is not known. Use with terms for deterministic quote harvesting.",
403
+ })),
404
+ terms: Type.Optional(Type.Array(Type.String(), {
405
+ description: "Important terms that should co-occur in the returned source window.",
406
+ })),
345
407
  reads: Type.Optional(Type.Array(Type.Object({
346
408
  query: Type.Optional(Type.String({ description: "Exact or fuzzy text to locate." })),
347
409
  exact: Type.Optional(Type.String({ description: "Exact text to locate." })),
348
410
  exactText: Type.Optional(Type.String({ description: "Exact text to locate." })),
349
411
  text: Type.Optional(Type.String({ description: "Text to locate." })),
350
- claim: Type.Optional(Type.String({ description: "Claim to locate when exact quote is unknown." })),
351
- terms: Type.Optional(Type.Array(Type.String(), { description: "Important terms for deterministic quote harvesting." })),
352
- maxChars: Type.Optional(Type.Number({ description: "Maximum visible snippet characters for this read." })),
353
- }), { description: "Mixed batch reads for one sourceRef; each item can use query or claim+terms." })),
354
- maxChars: Type.Optional(Type.Number({ description: "Maximum visible snippet characters per query." })),
412
+ claim: Type.Optional(Type.String({
413
+ description: "Claim to locate when exact quote is unknown.",
414
+ })),
415
+ terms: Type.Optional(Type.Array(Type.String(), {
416
+ description: "Important terms for deterministic quote harvesting.",
417
+ })),
418
+ maxChars: Type.Optional(Type.Number({
419
+ description: "Maximum visible snippet characters for this read.",
420
+ })),
421
+ }), {
422
+ description: "Mixed batch reads for one sourceRef; each item can use query or claim+terms.",
423
+ })),
424
+ maxChars: Type.Optional(Type.Number({
425
+ description: "Maximum visible snippet characters per query.",
426
+ })),
355
427
  }),
356
428
  execute: async (_toolCallId, params) => {
357
429
  const sourceRef = stringParam(params, "sourceRef") ?? stringParam(params, "source_ref");
@@ -361,7 +433,9 @@ export function registerWorkflowWebSourceExtension(pi, config, providerExtension
361
433
  }
362
434
  const source = await readCachedWorkflowWebSource(sourceRef);
363
435
  if (!source) {
364
- await recordWorkflowWebSourceEvent(config, "source_read_missing", { sourceRef });
436
+ await recordWorkflowWebSourceEvent(config, "source_read_missing", {
437
+ sourceRef,
438
+ });
365
439
  return errorToolResult("source_not_found", "No cached workflow web source exists for sourceRef.", {
366
440
  sourceRef,
367
441
  });
@@ -391,6 +465,7 @@ export function registerWorkflowWebSourceExtension(pi, config, providerExtension
391
465
  missingTerms: read.missingTerms,
392
466
  coverageRatio: read.coverageRatio,
393
467
  candidateOnly: read.candidateOnly,
468
+ truncated: read.truncated,
394
469
  quote: status === "budget_exhausted" ? undefined : read.quote,
395
470
  startOffset: read.startOffset,
396
471
  endOffset: read.endOffset,
@@ -420,25 +495,33 @@ export function registerWorkflowWebSourceExtension(pi, config, providerExtension
420
495
  missingTerms: result.missingTerms,
421
496
  coverageRatio: result.coverageRatio,
422
497
  candidateOnly: result.candidateOnly,
498
+ truncated: result.truncated,
423
499
  quote: result.status === "budget_exhausted" ? undefined : result.quote,
424
500
  startOffset: result.startOffset,
425
501
  endOffset: result.endOffset,
426
- budget: budgetSnapshot(result.status === "budget_exhausted"),
502
+ budget: budgetSnapshot(result.status === "budget_exhausted" ||
503
+ result.status === "truncated"),
427
504
  next: result.status === "budget_exhausted"
428
505
  ? "Visible web-source budget is exhausted for this task; cite the sourceRef as an evidence gap or use a smaller query in a fresh task."
429
- : undefined,
506
+ : result.status === "truncated"
507
+ ? "The matched web-source snippet was truncated by the visible budget or maxChars; use a smaller exact query or a fresh task if the full quote is required."
508
+ : undefined,
430
509
  });
431
510
  }
511
+ const hasBudgetExhaustedRead = results.some((result) => result.status === "budget_exhausted");
512
+ const hasTruncatedRead = results.some((result) => result.status === "truncated");
432
513
  return toolResultFromJson({
433
514
  status: responseStatus,
434
515
  tool: "workflow_web_source_read",
435
516
  sourceRef,
436
517
  url: source.redactedUrl,
437
518
  results,
438
- budget: budgetSnapshot(results.some((result) => result.status === "budget_exhausted")),
439
- next: responseStatus === "budget_exhausted"
519
+ budget: budgetSnapshot(hasBudgetExhaustedRead || hasTruncatedRead),
520
+ next: hasBudgetExhaustedRead
440
521
  ? "Visible web-source budget is exhausted for this task; cite missing quotes as evidence gaps or use smaller query batches in a fresh task."
441
- : undefined,
522
+ : hasTruncatedRead
523
+ ? "One or more matched web-source snippets were truncated by the visible budget or maxChars; use smaller exact queries or a fresh task if full quotes are required."
524
+ : undefined,
442
525
  });
443
526
  },
444
527
  });
@@ -596,7 +679,9 @@ function normalizeFetchFailure(value) {
596
679
  message: value.message,
597
680
  extra,
598
681
  ...(typeof value.reason === "string" ? { reason: value.reason } : {}),
599
- ...(typeof value.createdAt === "string" ? { createdAt: value.createdAt } : {}),
682
+ ...(typeof value.createdAt === "string"
683
+ ? { createdAt: value.createdAt }
684
+ : {}),
600
685
  };
601
686
  }
602
687
  function fetchLockPath(config, key) {
@@ -621,7 +706,9 @@ function shouldCacheFetchFailure(reason) {
621
706
  reason === "unsupported_content_type");
622
707
  }
623
708
  function shouldCacheFetchFailureInMemory(reason) {
624
- return reason === "empty_source" || reason === "dns_resolution_failed" || reason.includes("ENOTFOUND");
709
+ return (reason === "empty_source" ||
710
+ reason === "dns_resolution_failed" ||
711
+ reason.includes("ENOTFOUND"));
625
712
  }
626
713
  const WORKFLOW_WEB_FETCH_TIMEOUT_MS = 30_000;
627
714
  const WORKFLOW_WEB_FETCH_MAX_CHARS = 1_000_000;
@@ -636,12 +723,20 @@ async function safeFetchWorkflowWebText(url, security, signal) {
636
723
  return response;
637
724
  if (response.status >= 300 && response.status < 400) {
638
725
  if (!response.location)
639
- return { ok: false, reason: "redirect_without_location", url: checked.normalizedUrl };
726
+ return {
727
+ ok: false,
728
+ reason: "redirect_without_location",
729
+ url: checked.normalizedUrl,
730
+ };
640
731
  current = new URL(response.location, checked.normalizedUrl).href;
641
732
  continue;
642
733
  }
643
734
  if (response.status < 200 || response.status >= 300) {
644
- return { ok: false, reason: `http_${response.status}`, url: checked.normalizedUrl };
735
+ return {
736
+ ok: false,
737
+ reason: `http_${response.status}`,
738
+ url: checked.normalizedUrl,
739
+ };
645
740
  }
646
741
  const extracted = extractWorkflowWebResponseText(response.text, response.contentType);
647
742
  return {
@@ -675,13 +770,17 @@ function safeFetchOnce(url, security, signal) {
675
770
  lookupPublicAddress(hostname, security)
676
771
  .then((address) => {
677
772
  if (isLookupAllOptions(options)) {
678
- callback(null, [{ address: address.address, family: address.family }]);
773
+ callback(null, [
774
+ { address: address.address, family: address.family },
775
+ ]);
679
776
  return;
680
777
  }
681
778
  callback(null, address.address, address.family);
682
779
  })
683
780
  .catch((error) => {
684
- const reason = error instanceof Error ? error.message : "dns_resolution_failed";
781
+ const reason = error instanceof Error
782
+ ? error.message
783
+ : "dns_resolution_failed";
685
784
  callback(new Error(reason), "", 4);
686
785
  });
687
786
  },
@@ -693,7 +792,10 @@ function safeFetchOnce(url, security, signal) {
693
792
  ? res.headers["content-type"][0]
694
793
  : res.headers["content-type"];
695
794
  const status = res.statusCode ?? 0;
696
- if (status >= 200 && status < 300 && contentType && !isWorkflowWebTextContentType(contentType)) {
795
+ if (status >= 200 &&
796
+ status < 300 &&
797
+ contentType &&
798
+ !isWorkflowWebTextContentType(contentType)) {
697
799
  res.resume();
698
800
  settle({ ok: false, reason: "unsupported_content_type", url });
699
801
  return;
@@ -772,7 +874,10 @@ async function validateResolvedHost(url, security) {
772
874
  return { ok: false, reason: "invalid_url", url };
773
875
  }
774
876
  try {
775
- const addresses = await lookup(parsed.hostname, { all: true, verbatim: true });
877
+ const addresses = await lookup(parsed.hostname, {
878
+ all: true,
879
+ verbatim: true,
880
+ });
776
881
  for (const address of addresses) {
777
882
  const reason = privateIpReason(address.address);
778
883
  if (reason)
@@ -800,7 +905,8 @@ function privateIpReason(address) {
800
905
  }
801
906
  if (isIP(lower) === 4) {
802
907
  const parts = lower.split(".").map((part) => Number(part));
803
- if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255))
908
+ if (parts.length !== 4 ||
909
+ parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255))
804
910
  return "private_host_blocked";
805
911
  const [a, b, c, d] = parts;
806
912
  if (a === 0 || a === 10 || a === 127 || a >= 224)
@@ -961,7 +1067,9 @@ function fetchSourceRequestsFromParams(params) {
961
1067
  const titles = Array.isArray(params.titles) ? params.titles : [];
962
1068
  if (Array.isArray(params.sources)) {
963
1069
  for (const source of params.sources) {
964
- if (!isRecord(source) || typeof source.url !== "string" || !source.url.trim())
1070
+ if (!isRecord(source) ||
1071
+ typeof source.url !== "string" ||
1072
+ !source.url.trim())
965
1073
  continue;
966
1074
  requests.push({
967
1075
  url: source.url.trim(),
@@ -978,7 +1086,9 @@ function fetchSourceRequestsFromParams(params) {
978
1086
  const title = titles[index];
979
1087
  requests.push({
980
1088
  url: url.trim(),
981
- ...(typeof title === "string" && title.trim() ? { title: title.trim() } : {}),
1089
+ ...(typeof title === "string" && title.trim()
1090
+ ? { title: title.trim() }
1091
+ : {}),
982
1092
  });
983
1093
  }
984
1094
  }
@@ -1086,12 +1196,18 @@ function dedupeSourceReadRequests(requests) {
1086
1196
  return deduped;
1087
1197
  }
1088
1198
  function sourceReadBatchRequested(params) {
1089
- return ((isRecord(params) && Array.isArray(params.reads) && params.reads.length > 0) ||
1199
+ return ((isRecord(params) &&
1200
+ Array.isArray(params.reads) &&
1201
+ params.reads.length > 0) ||
1090
1202
  stringArrayParam(params, "queries").length > 0 ||
1091
1203
  stringArrayParam(params, "exactTexts").length > 0 ||
1092
1204
  stringArrayParam(params, "texts").length > 0);
1093
1205
  }
1094
1206
  function sourceReadResponseStatus(read) {
1207
+ if (read.status === "truncated" && !read.quote)
1208
+ return "budget_exhausted";
1209
+ if (read.status === "truncated")
1210
+ return "truncated";
1095
1211
  if (read.status === "matched" && !read.quote)
1096
1212
  return "budget_exhausted";
1097
1213
  if (read.status === "matched" && read.candidateOnly)
@@ -1105,6 +1221,8 @@ function aggregateSourceReadStatus(statuses) {
1105
1221
  return "ok";
1106
1222
  if (statuses.every((status) => status === "candidate"))
1107
1223
  return "candidate";
1224
+ if (statuses.every((status) => status === "truncated"))
1225
+ return "truncated";
1108
1226
  if (statuses.every((status) => status === "not_found"))
1109
1227
  return "not_found";
1110
1228
  if (statuses.every((status) => status === "budget_exhausted"))
@@ -1140,7 +1258,8 @@ function isWorkflowWebTextContentType(contentType) {
1140
1258
  return /^(text\/|application\/(json|xml|xhtml\+xml|ld\+json)|[^;]+\+json\b|[^;]+\+xml\b)/i.test(contentType.trim());
1141
1259
  }
1142
1260
  function extractWorkflowWebResponseText(text, contentType) {
1143
- const looksHtml = /html/i.test(contentType ?? "") || /<html[\s>]|<body[\s>]|<title[\s>]/i.test(text);
1261
+ const looksHtml = /html/i.test(contentType ?? "") ||
1262
+ /<html[\s>]|<body[\s>]|<title[\s>]/i.test(text);
1144
1263
  if (!looksHtml) {
1145
1264
  return { text, title: titleFromPlainText(text) };
1146
1265
  }
@@ -67,7 +67,7 @@ export interface WorkflowWebSourceReadRequest {
67
67
  maxChars?: number;
68
68
  }
69
69
  export interface WorkflowWebSourceReadResult {
70
- status: "matched" | "not_found";
70
+ status: "matched" | "truncated" | "not_found";
71
71
  matchType?: "exact" | "normalized" | "terms";
72
72
  quote?: string;
73
73
  startOffset?: number;
@@ -77,6 +77,7 @@ export interface WorkflowWebSourceReadResult {
77
77
  missingTerms?: string[];
78
78
  coverageRatio?: number;
79
79
  candidateOnly?: boolean;
80
+ truncated?: boolean;
80
81
  }
81
82
  export interface WorkflowWebSourceCard {
82
83
  sourceRef: string;
@@ -526,19 +526,32 @@ function snippetForTerms(options) {
526
526
  return right.score - left.score;
527
527
  return right.matchedTerms.length - left.matchedTerms.length;
528
528
  })[0];
529
- const raw = redactInlineSecrets(options.text.slice(best.start, best.end));
530
- const consumed = consumeWorkflowWebVisibleBudget(options.budget, raw, options.maxChars);
529
+ const consumed = consumeAnchoredSnippet({
530
+ text: options.text,
531
+ anchorStart: best.anchorStart,
532
+ anchorEnd: best.anchorEnd,
533
+ maxChars: options.maxChars,
534
+ budget: options.budget,
535
+ });
536
+ const returnedWindowNorm = normalizeForSearch(options.text.slice(consumed.sourceStart, consumed.sourceEnd)).normalized;
537
+ const matchedTerms = needles
538
+ .filter((term) => returnedWindowNorm.includes(term.normalized))
539
+ .map((term) => term.raw);
540
+ const missingTerms = needles
541
+ .filter((term) => !returnedWindowNorm.includes(term.normalized))
542
+ .map((term) => term.raw);
531
543
  return {
532
- status: "matched",
544
+ status: consumed.status,
533
545
  matchType: "terms",
534
- quote: consumed.text,
535
- startOffset: best.start,
536
- endOffset: best.end,
537
- visibleChars: consumed.text.length,
538
- matchedTerms: best.matchedTerms,
539
- missingTerms: best.missingTerms,
540
- coverageRatio: best.matchedTerms.length / Math.max(1, needles.length),
546
+ quote: consumed.quote || undefined,
547
+ startOffset: consumed.sourceStart,
548
+ endOffset: consumed.sourceEnd,
549
+ visibleChars: consumed.visibleChars,
550
+ matchedTerms,
551
+ missingTerms,
552
+ coverageRatio: matchedTerms.length / Math.max(1, needles.length),
541
553
  candidateOnly: true,
554
+ truncated: consumed.truncated || undefined,
542
555
  };
543
556
  }
544
557
  function scoreTermWindow(text, matchStart, matchEnd, maxChars, terms) {
@@ -559,6 +572,8 @@ function scoreTermWindow(text, matchStart, matchEnd, maxChars, terms) {
559
572
  return {
560
573
  start,
561
574
  end,
575
+ anchorStart: matchStart,
576
+ anchorEnd: matchEnd,
562
577
  matchedTerms,
563
578
  missingTerms,
564
579
  score: matchedTerms.length * 1_000 + occurrenceScore,
@@ -631,20 +646,70 @@ const SOURCE_READ_STOPWORDS = new Set([
631
646
  "without",
632
647
  ]);
633
648
  function snippetForMatch(options) {
634
- const matchLength = Math.max(0, options.end - options.start);
635
- const slack = Math.max(0, options.maxChars - matchLength);
636
- const before = Math.floor(slack / 2);
637
- const snippetStart = Math.max(0, options.start - before);
638
- const snippetEnd = Math.min(options.text.length, snippetStart + options.maxChars);
639
- const raw = redactInlineSecrets(options.text.slice(snippetStart, snippetEnd));
640
- const consumed = consumeWorkflowWebVisibleBudget(options.budget, raw, options.maxChars);
649
+ const consumed = consumeAnchoredSnippet({
650
+ text: options.text,
651
+ anchorStart: options.start,
652
+ anchorEnd: options.end,
653
+ maxChars: options.maxChars,
654
+ budget: options.budget,
655
+ });
641
656
  return {
642
- status: "matched",
657
+ status: consumed.status,
643
658
  matchType: options.matchType,
644
- quote: consumed.text,
659
+ quote: consumed.quote || undefined,
645
660
  startOffset: options.start,
646
661
  endOffset: options.end,
662
+ visibleChars: consumed.visibleChars,
663
+ truncated: consumed.truncated || undefined,
664
+ };
665
+ }
666
+ function consumeAnchoredSnippet(options) {
667
+ const maxChars = Math.max(0, Math.floor(options.maxChars));
668
+ const remainingBefore = Math.max(0, options.budget.limit - options.budget.used);
669
+ const visibleLimit = Math.max(0, Math.min(maxChars, remainingBefore));
670
+ const anchorStart = Math.max(0, Math.min(options.text.length, Math.floor(options.anchorStart)));
671
+ const anchorEnd = Math.max(anchorStart, Math.min(options.text.length, Math.floor(options.anchorEnd)));
672
+ const anchorLength = Math.max(0, anchorEnd - anchorStart);
673
+ if (visibleLimit <= 0) {
674
+ return {
675
+ status: "truncated",
676
+ quote: "",
677
+ visibleChars: 0,
678
+ sourceStart: anchorStart,
679
+ sourceEnd: anchorStart,
680
+ truncated: true,
681
+ };
682
+ }
683
+ let sourceStart;
684
+ let sourceEnd;
685
+ let status = "matched";
686
+ if (anchorLength > visibleLimit) {
687
+ sourceStart = anchorStart;
688
+ sourceEnd = Math.min(options.text.length, sourceStart + visibleLimit);
689
+ status = "truncated";
690
+ }
691
+ else {
692
+ const slack = Math.max(0, visibleLimit - anchorLength);
693
+ sourceStart = Math.max(0, anchorStart - Math.floor(slack / 2));
694
+ sourceEnd = Math.min(options.text.length, sourceStart + visibleLimit);
695
+ if (sourceEnd < anchorEnd) {
696
+ sourceEnd = anchorEnd;
697
+ sourceStart = Math.max(0, sourceEnd - visibleLimit);
698
+ }
699
+ else if (sourceEnd === options.text.length) {
700
+ sourceStart = Math.max(0, sourceEnd - visibleLimit);
701
+ }
702
+ }
703
+ const raw = redactInlineSecrets(options.text.slice(sourceStart, sourceEnd));
704
+ const consumed = consumeWorkflowWebVisibleBudget(options.budget, raw, visibleLimit);
705
+ const truncated = status === "truncated" || consumed.truncated;
706
+ return {
707
+ status,
708
+ quote: consumed.text,
647
709
  visibleChars: consumed.text.length,
710
+ sourceStart,
711
+ sourceEnd,
712
+ truncated,
648
713
  };
649
714
  }
650
715
  function normalizeForSearch(text) {
@@ -38,7 +38,6 @@ Run this check in a sandboxed worker and report the artifact paths.
38
38
  Start a background audit and let me inspect it in /subagent panel.
39
39
  ```
40
40
 
41
-
42
41
  ## What it does
43
42
 
44
43
  Tool: `subagent`
@@ -121,9 +120,11 @@ Existing run:
121
120
  { "action": "status", "runId": "run_..." }
122
121
  ```
123
122
 
123
+ Recent runs can be addressed by `runId` even when they were launched from another cwd; legacy records still resolve from the explicit or current cwd.
124
+
124
125
  ### Panel
125
126
 
126
- Inspect runs, attempts, artifacts, and log tails in a live TUI.
127
+ Inspect runs, attempts, artifacts, and log tails in a live TUI. The panel defaults to the current Pi session, can switch to current cwd or all indexed runs, and includes status filters plus a scrollable detail pane. It shows active and recent terminal runs by default, with in-panel `m` to show more, and counts stale/malformed run pointers without exposing raw session ids.
127
128
 
128
129
  Open the run monitor:
129
130
 
@@ -147,4 +148,3 @@ const status = await getSubagentStatus({ runId: run.runId });
147
148
  ## Detailed docs
148
149
 
149
150
  - [`docs/usage.md`](./docs/usage.md) — full argument reference, code API, `action` behavior, backend selection, sandbox/worktree behavior, artifacts, and validation notes.
150
-
@@ -9,4 +9,5 @@ export const getSubagentLogs = api.getSubagentLogs;
9
9
  export const waitForSubagent = api.waitForSubagent;
10
10
  export const interruptSubagent = api.interruptSubagent;
11
11
  export const reconcileSubagentRun = api.reconcileSubagentRun;
12
+ export const recordSubagentChildEvent = api.recordSubagentChildEvent;
12
13
  export const SubagentValidationError = api.SubagentValidationError;