@agwab/pi-workflow 0.1.0 → 0.1.2

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 (58) hide show
  1. package/README.md +14 -3
  2. package/agents/researcher.md +17 -7
  3. package/dist/artifact-graph-runtime.js +1 -0
  4. package/dist/compiler.js +2 -2
  5. package/dist/dynamic-generated-task-runtime.js +4 -3
  6. package/dist/dynamic-runtime-bundle.js +3 -2
  7. package/dist/extension.js +40 -1
  8. package/dist/subagent-backend.js +82 -27
  9. package/dist/tool-metadata.d.ts +1 -0
  10. package/dist/tool-metadata.js +13 -1
  11. package/dist/workflow-artifact-extension.js +3 -2
  12. package/dist/workflow-artifact-tool.js +84 -4
  13. package/dist/workflow-web-source-extension.d.ts +43 -0
  14. package/dist/workflow-web-source-extension.js +1194 -0
  15. package/dist/workflow-web-source.d.ts +171 -0
  16. package/dist/workflow-web-source.js +897 -0
  17. package/docs/usage.md +32 -45
  18. package/node_modules/@agwab/pi-subagent/package.json +1 -1
  19. package/node_modules/@agwab/pi-subagent/src/api.ts +245 -132
  20. package/node_modules/@agwab/pi-subagent/src/artifacts/result.ts +243 -163
  21. package/node_modules/@agwab/pi-subagent/src/core/constants.ts +117 -90
  22. package/node_modules/@agwab/pi-subagent/src/core/validation.ts +728 -475
  23. package/node_modules/@agwab/pi-subagent/src/orchestrate/run.ts +305 -209
  24. package/node_modules/@agwab/pi-subagent/src/runners/headless-model.ts +750 -439
  25. package/node_modules/@agwab/pi-subagent/src/runners/tmux.ts +422 -268
  26. package/package.json +3 -4
  27. package/skills/workflow-guide/scaffolds/object-tool-fallback/schemas/fetch-control.schema.json +1 -1
  28. package/skills/workflow-guide/scaffolds/object-tool-fallback/spec.json +4 -3
  29. package/src/artifact-graph-runtime.ts +1 -0
  30. package/src/compiler.ts +2 -1
  31. package/src/dynamic-generated-task-runtime.ts +4 -2
  32. package/src/dynamic-runtime-bundle.ts +3 -2
  33. package/src/extension.ts +46 -1
  34. package/src/subagent-backend.ts +121 -37
  35. package/src/tool-metadata.ts +22 -1
  36. package/src/workflow-artifact-extension.ts +3 -2
  37. package/src/workflow-artifact-tool.ts +96 -4
  38. package/src/workflow-web-source-extension.ts +1411 -0
  39. package/src/workflow-web-source.ts +1171 -0
  40. package/workflows/README.md +1 -1
  41. package/workflows/deep-research/helpers/claim-evidence-gate.mjs +474 -40
  42. package/workflows/deep-research/helpers/final-audit-packet.mjs +219 -0
  43. package/workflows/deep-research/helpers/normalize-input-packet.mjs +436 -0
  44. package/workflows/deep-research/helpers/render-executive.mjs +571 -198
  45. package/workflows/deep-research/schemas/deep-research-executive-render-control.schema.json +35 -8
  46. package/workflows/deep-research/schemas/deep-research-normalize-claims-control.schema.json +45 -4
  47. package/workflows/deep-research/schemas/deep-research-verify-claims-control.schema.json +0 -2
  48. package/workflows/deep-research/spec.json +36 -21
  49. package/workflows/deep-review/helpers/render-review-report.mjs +502 -0
  50. package/workflows/deep-review/schemas/deep-review-render-control.schema.json +50 -0
  51. package/workflows/deep-review/spec.json +22 -1
  52. package/docs/release.md +0 -89
  53. package/node_modules/@pondwader/socks5-server/.DS_Store +0 -0
  54. package/node_modules/commander/.DS_Store +0 -0
  55. package/node_modules/jiti/.DS_Store +0 -0
  56. package/node_modules/node-forge/.DS_Store +0 -0
  57. package/node_modules/shell-quote/.DS_Store +0 -0
  58. package/node_modules/zod/.DS_Store +0 -0
package/README.md CHANGED
@@ -12,7 +12,9 @@
12
12
 
13
13
  `pi-workflow` lets Pi run named, repeatable multi-step workflows: research, code review, spec conformance checks, impact review, and project-specific team routines.
14
14
 
15
- You choose a workflow and describe the task in natural language. `pi-workflow` coordinates the steps, passes results between them, and records the run so it can be inspected or resumed.
15
+ Built on [`@agwab/pi-subagent`](https://github.com/AgwaB/pi-subagent), it coordinates Pi subagent workers across workflow steps, passes results between them, and records the run so it can be inspected or resumed.
16
+
17
+ You choose a workflow and describe the task in natural language.
16
18
 
17
19
  ## Installation
18
20
 
@@ -169,7 +171,7 @@ Parallel execution is a graph shape, not a stage type: model parallel branches a
169
171
 
170
172
  Dynamic workflows are the advanced form of adaptive orchestration. A JSON `type: "dynamic"` stage points at trusted bundle-local `.mjs` controller code; that controller can add official workflow tasks, call helpers, or run nested workflows while preserving replayable run state. See [`docs/usage.md`](./docs/usage.md) for approval, detach, helper retry, nested workflow, and cache details.
171
173
 
172
- Workflow `fetch_content` calls use a run-scoped file cache by default under `.pi/workflows/<run-id>/source-cache/fetch-content/`. Set `PI_WORKFLOW_FETCH_CONTENT_CACHE=0` to opt out.
174
+ New workflows should prefer `workflow_web_search`, `workflow_web_fetch_source`, and `workflow_web_source_read`: search returns compact candidates, fetch returns a source card with an opaque `sourceRef`, and source-read retrieves narrow evidence snippets. Preserve `sourceRef` through workflow outputs; use `urls: [...]` or `sources: [...]` to batch several source fetches, use `queries: [...]` or `reads: [...]` to batch several snippets from one sourceRef, and use `claim` + distinctive `terms` to get candidate quote windows with match metadata when the exact quote is unknown. Normalized web sources are cached under `.pi/workflows/<run-id>/web-source-cache/` without exposing cache paths to agents; same-URL fetches and deterministic terminal failures are coordinated across parallel workers. Legacy `fetch_content` calls still use `.pi/workflows/<run-id>/source-cache/fetch-content/`; set `PI_WORKFLOW_FETCH_CONTENT_CACHE=0` to opt out of that legacy cache.
173
175
 
174
176
  ## Predefined workflows
175
177
 
@@ -214,5 +216,14 @@ Open a task detail view with its artifact output.
214
216
 
215
217
  ## More
216
218
 
217
- - [`docs/usage.md`](./docs/usage.md) — command reference, workflow resolution, run artifacts, authoring rules, and release checks.
219
+ - [`docs/usage.md`](./docs/usage.md) — command reference, workflow resolution, run artifacts, and authoring rules.
218
220
  - [`workflows/README.md`](./workflows/README.md) — bundled workflow notes.
221
+
222
+ ## Runtime dependencies
223
+
224
+ `pi-workflow` bundles the runtime pieces it needs:
225
+
226
+ - [`@agwab/pi-subagent`](https://github.com/AgwaB/pi-subagent) launches and tracks the Pi subagent workers used by workflow tasks.
227
+ - [`pi-web-access`](https://github.com/nicobailon/pi-web-access) provides web tools such as `web_search`, `fetch_content`, `get_search_content`, and `code_search` when a workflow or agent requests them.
228
+
229
+ The web provider is just a tool provider. You can replace or narrow it with your own extension if it exposes compatible tool names, arguments, and results. Be careful when changing providers: if the tool result shape, reference/evidence formatting, or field names differ, workflow specs, prompts, and control/output schemas that depend on those fields may need to change too. The stage graph and normal workflow run record format do not need to change for a compatible web-tool implementation swap.
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: researcher
3
3
  description: Read-only source-backed research agent.
4
- tools: read, grep, find, ls, web_search, fetch_content
4
+ tools: read, grep, find, ls, workflow_web_search, workflow_web_fetch_source, workflow_web_source_read, web_search, fetch_content
5
5
  readOnly: true
6
6
  ---
7
7
 
@@ -28,12 +28,22 @@ evidence beyond the repository's immediate code.
28
28
 
29
29
  - Use `read`, `grep`, `find`, and `ls` for local files, vendored docs,
30
30
  package metadata, and downloaded/reference material already on disk.
31
- - Use `web_search` to discover candidate sources across papers, docs,
32
- articles, issues, and community discussions.
33
- - Use `fetch_content` to extract ordinary URLs.
34
- - Full cached search-content hydration is intentionally unavailable in
35
- autonomous workflows; if source extraction is insufficient, report the
36
- evidence gap instead of broad raw document retrieval.
31
+ - Prefer `workflow_web_search` to discover candidate sources across papers,
32
+ docs, articles, issues, and community discussions.
33
+ - Prefer `workflow_web_fetch_source` to cache URLs and return compact source
34
+ cards, then use `workflow_web_source_read` for exact evidence snippets. Preserve
35
+ `sourceRef` values in structured outputs. When several source cards are needed,
36
+ batch fetches with `urls: [...]` or `sources: [...]`; when several snippets are
37
+ needed from one `sourceRef`, batch them with `queries: [...]` or `reads: [...]`
38
+ instead of making repeated source-read calls. If the exact quote text is not
39
+ known, pass `claim` plus 2-6 distinctive `terms` so the tool can harvest a
40
+ candidate source window before trying another source. Treat term/claim matches
41
+ as candidate evidence; preserve `matchType`, `matchedTerms`, `missingTerms`,
42
+ `coverageRatio`, and `candidateOnly` when citing them.
43
+ - Do not read workflow web-source cache files directly; use source refs and
44
+ `workflow_web_source_read` instead.
45
+ - Legacy `web_search` and `fetch_content` may be available during migration;
46
+ use them only when normalized workflow web tools are unavailable.
37
47
  - If network access, credentials, provider quota, or the web extension is
38
48
  unavailable, report that limitation instead of guessing.
39
49
 
@@ -665,6 +665,7 @@ export function formatArtifactGraphSourceContext(sources, requiredReads) {
665
665
  return [
666
666
  "# Workflow Artifact Inputs",
667
667
  "Use workflow_artifact to list/read upstream workflow artifacts. Inline controlProjection fields are authoritative for the projected data they contain; use artifact reads for declared requiredReads, missing fields, or debug detail.",
668
+ "Projected reads must include a JSON path when using maxItems or maxChars, for example {\"action\":\"read\",\"source\":\"plan\",\"artifact\":\"control\",\"path\":\"$.factSlots\",\"maxItems\":8,\"maxChars\":2000}. For a whole artifact read, omit maxItems/maxChars.",
668
669
  requiredReads.length > 0
669
670
  ? [
670
671
  "Required reads before final output:",
package/dist/compiler.js CHANGED
@@ -2,7 +2,7 @@ import { readFile } from "node:fs/promises";
2
2
  import { dirname, resolve } from "node:path";
3
3
  import { loadAgentByName } from "./agents.js";
4
4
  import { DYNAMIC_OUTPUT_PROFILES } from "./dynamic-profiles.js";
5
- import { classifyToolCapability, effectiveToolClassification, providersForSelectedTools, resolveToolSelection, TOOL_NAME_PATTERN, toolNameForSpec, } from "./tool-metadata.js";
5
+ import { classifyToolCapability, effectiveToolClassification, providersForSelectedTools, resolveToolSelection, TOOL_NAME_PATTERN, toolAllowedByAuthorityCeiling, toolNameForSpec, } from "./tool-metadata.js";
6
6
  import { WorkflowValidationError, WORKFLOW_RUN_TYPE, } from "./types.js";
7
7
  const DELEGATION_TOOLS = new Set([
8
8
  "skill_test_subagent",
@@ -207,7 +207,7 @@ function validateToolSubset(requestedTools, agent, issues, path) {
207
207
  }
208
208
  const allowed = new Set(agent.tools);
209
209
  for (const tool of requestedTools) {
210
- if (!allowed.has(tool)) {
210
+ if (!toolAllowedByAuthorityCeiling(tool, allowed)) {
211
211
  issues.push({
212
212
  path,
213
213
  message: `tool "${tool}" expands agent ${agent.displayName}; allowed tools: ${agent.tools.join(", ")}`,
@@ -5,7 +5,7 @@ import { isDynamicOutputProfile, } from "./dynamic-profiles.js";
5
5
  import { readOrRebuildDynamicState } from "./dynamic-state.js";
6
6
  import { sanitizeTaskId } from "./engine-run-graph.js";
7
7
  import { fromProjectPath, isTerminalTaskStatus, readJson } from "./store.js";
8
- import { classifyToolCapability, effectiveToolClassification, providersForSelectedTools, } from "./tool-metadata.js";
8
+ import { classifyToolCapability, effectiveToolClassification, providersForSelectedTools, toolAllowedByAuthorityCeiling, } from "./tool-metadata.js";
9
9
  const DYNAMIC_OUTPUT_MAX_DIGEST_CHARS = 1000;
10
10
  const DYNAMIC_DELEGATION_TOOLS = new Set([
11
11
  "skill_test_subagent",
@@ -57,7 +57,8 @@ export async function buildDynamicGeneratedCompiledTask(input) {
57
57
  throw new Error(`dynamic agent ${requestedAgent} does not declare a tools authority ceiling`);
58
58
  }
59
59
  if (tools && agentDefinition.tools) {
60
- const missing = tools.filter((tool) => !agentDefinition.tools?.includes(tool));
60
+ const allowed = new Set(agentDefinition.tools);
61
+ const missing = tools.filter((tool) => !toolAllowedByAuthorityCeiling(tool, allowed));
61
62
  if (missing.length > 0) {
62
63
  throw new Error(`dynamic agent requested tools not declared by ${requestedAgent}: ${missing.join(", ")}`);
63
64
  }
@@ -571,7 +572,7 @@ function appendDynamicOutputInstructions(prompt, outputProfile, maxDigestChars =
571
572
  `The control.digest string must be at most ${maxDigestChars} characters; prefer one short sentence.`,
572
573
  "Use schema `dynamic-task-result-v1` unless the dynamic controller asks for a more specific control schema.",
573
574
  refsMinItems !== undefined && refsMinItems > 0
574
- ? `The <refs> JSON array must include at least ${refsMinItems} item${refsMinItems === 1 ? "" : "s"}. Include URLs or local file paths used by the analysis. Verify external URLs with fetch_content before including them; do not include stale, guessed, or unreachable URLs.`
575
+ ? `The <refs> JSON array must include at least ${refsMinItems} item${refsMinItems === 1 ? "" : "s"}. Include URLs or local file paths used by the analysis. Verify external URLs with available workflow web fetch/source-read tools before including them; do not include stale, guessed, or unreachable URLs.`
575
576
  : undefined,
576
577
  dynamicOutputProfileInstructions(outputProfile),
577
578
  ]
@@ -7,8 +7,9 @@ const DIRECT_DYNAMIC_RUNTIME_TOOLS = [
7
7
  "grep",
8
8
  "find",
9
9
  "ls",
10
- "web_search",
11
- "fetch_content",
10
+ "workflow_web_search",
11
+ "workflow_web_fetch_source",
12
+ "workflow_web_source_read",
12
13
  ];
13
14
  export async function ensureDirectDynamicRuntimeBundle(cwd) {
14
15
  const bundleDir = join(cwd, ".pi", "workflow-runtime", DIRECT_DYNAMIC_RUNTIME_VERSION);
package/dist/extension.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { closeSync, openSync } from "node:fs";
3
- import { readFile } from "node:fs/promises";
3
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
4
4
  import { join, relative } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import { discoverAgents } from "./agents.js";
@@ -15,6 +15,7 @@ import { listWorkflows, resolveWorkflowRef } from "./workflow-specs.js";
15
15
  import { WorkflowValidationError, } from "./types.js";
16
16
  const UNFINISHED_RUN_NOTICE_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
17
17
  const UNFINISHED_RUN_NOTICE_MAX_RUNS = 5;
18
+ const UNFINISHED_RUN_NOTICE_DEDUPE_MS = 6 * 60 * 60 * 1000;
18
19
  const RUN_FEEDBACK_POLL_MS = 2_000;
19
20
  const runFeedbackTimers = new Map();
20
21
  export const WORKFLOW_LIST_TOOL = "workflow_list";
@@ -470,6 +471,9 @@ export async function notifyUnfinishedRuns(cwd, notify, nowMs = Date.now()) {
470
471
  }
471
472
  if (unfinished.length === 0)
472
473
  return;
474
+ const noticeKey = unfinishedNoticeKey(unfinished);
475
+ if (await shouldSuppressUnfinishedNotice(cwd, noticeKey, nowMs))
476
+ return;
473
477
  const lines = unfinished
474
478
  .slice(0, UNFINISHED_RUN_NOTICE_MAX_RUNS)
475
479
  .map((run) => {
@@ -488,6 +492,41 @@ export async function notifyUnfinishedRuns(cwd, notify, nowMs = Date.now()) {
488
492
  ...lines,
489
493
  ].join("\n"), "warning");
490
494
  }
495
+ function unfinishedNoticeKey(runs) {
496
+ return runs
497
+ .map((run) => `${run.runId}:${run.status}:${run.updatedAt ?? ""}`)
498
+ .sort()
499
+ .join("|");
500
+ }
501
+ async function shouldSuppressUnfinishedNotice(cwd, noticeKey, nowMs) {
502
+ if (!noticeKey)
503
+ return true;
504
+ const dir = join(cwd, ".pi", "workflows");
505
+ const file = join(dir, "unfinished-notices.json");
506
+ let state = {};
507
+ try {
508
+ state = JSON.parse(await readFile(file, "utf8"));
509
+ }
510
+ catch {
511
+ state = {};
512
+ }
513
+ const notices = state.notices ?? {};
514
+ const previousMs = Date.parse(notices[noticeKey]?.lastNotifiedAt ?? "");
515
+ if (Number.isFinite(previousMs) &&
516
+ nowMs - previousMs < UNFINISHED_RUN_NOTICE_DEDUPE_MS) {
517
+ return true;
518
+ }
519
+ const cutoff = nowMs - UNFINISHED_RUN_NOTICE_MAX_AGE_MS;
520
+ for (const [key, item] of Object.entries(notices)) {
521
+ const itemMs = Date.parse(item.lastNotifiedAt ?? "");
522
+ if (!Number.isFinite(itemMs) || itemMs < cutoff)
523
+ delete notices[key];
524
+ }
525
+ notices[noticeKey] = { lastNotifiedAt: new Date(nowMs).toISOString() };
526
+ await mkdir(dir, { recursive: true });
527
+ await writeFile(file, `${JSON.stringify({ notices }, null, 2)}\n`, "utf8");
528
+ return false;
529
+ }
491
530
  async function handleWorkflowCommand(args, ctx, api) {
492
531
  const tokens = splitArgs(args);
493
532
  try {
@@ -6,6 +6,8 @@ import { fromProjectPath, isTerminalTaskStatus, nowIso, toProjectPath, writeRunR
6
6
  import { applyTaskResultArtifact, isTaskTimedOut, markTaskTimedOut, } from "./result.js";
7
7
  import { readWorkflowArtifactReadLedger } from "./workflow-artifact-tool.js";
8
8
  import { writeWorkflowFetchCacheExtensionWrapper } from "./workflow-fetch-cache-extension.js";
9
+ import { writeWorkflowWebSourceExtensionWrapper } from "./workflow-web-source-extension.js";
10
+ import { isWorkflowWebSourceTool } from "./workflow-web-source.js";
9
11
  import { buildWorkflowOutputRetryInstructions, parseWorkflowOutputForBundle, writeWorkflowTaskArtifactBundle, } from "./workflow-output-artifacts.js";
10
12
  const DEFAULT_SUBAGENT_RUNS_ROOT = ".pi/workflow-subagents";
11
13
  const EXTRA_SUBAGENT_EXTENSIONS_ENV = "PI_WORKFLOW_SUBAGENT_EXTRA_EXTENSIONS";
@@ -18,6 +20,7 @@ const MODULE_DIR = dirname(MODULE_PATH);
18
20
  const BUNDLED_PI_WEB_ACCESS_EXTENSION = bundledNodeModulePath("pi-web-access", "index.ts");
19
21
  const BUNDLED_PI_WEB_ACCESS_STORAGE = bundledNodeModulePath("pi-web-access", "storage.ts");
20
22
  const WORKFLOW_FETCH_CACHE_EXTENSION_IMPORT = resolve(MODULE_DIR, `workflow-fetch-cache-extension${extname(MODULE_PATH)}`);
23
+ const WORKFLOW_WEB_SOURCE_EXTENSION_IMPORT = resolve(MODULE_DIR, `workflow-web-source-extension${extname(MODULE_PATH)}`);
21
24
  const TOOL_PROVIDER_EXTENSIONS = {
22
25
  web_search: [BUNDLED_PI_WEB_ACCESS_EXTENSION],
23
26
  code_search: [BUNDLED_PI_WEB_ACCESS_EXTENSION],
@@ -847,36 +850,85 @@ function captureToolCallsEnabled() {
847
850
  return typeof value === "string" && /^(1|true|yes|on)$/i.test(value.trim());
848
851
  }
849
852
  async function workflowTaskExtensions(cwd, run, task, compiledTask) {
850
- const baseExtensions = uniqueStrings([
851
- ...providerExtensionsForTools(compiledTask.runtime.tools, compiledTask.runtime.toolProviders),
853
+ const tools = compiledTask.runtime.tools;
854
+ let extensions = uniqueStrings([
855
+ ...providerExtensionsForTools(tools, compiledTask.runtime.toolProviders),
852
856
  ...extraSubagentExtensionsFromEnv(),
853
857
  ]);
854
- if (!shouldUseFetchContentCache(compiledTask.runtime.tools)) {
855
- return baseExtensions;
856
- }
857
858
  const taskDir = dirname(fromProjectPath(cwd, task.files.result));
858
- const wrapperPath = join(taskDir, "workflow-fetch-cache-extension.ts");
859
- await writeWorkflowFetchCacheExtensionWrapper({
860
- wrapperPath,
861
- importPath: WORKFLOW_FETCH_CACHE_EXTENSION_IMPORT,
862
- webAccessExtensionPath: BUNDLED_PI_WEB_ACCESS_EXTENSION,
863
- webAccessStoragePath: BUNDLED_PI_WEB_ACCESS_STORAGE,
864
- config: {
865
- runId: run.runId,
866
- taskId: task.taskId,
867
- cacheDir: resolve(cwd, ".pi", "workflows", run.runId, "source-cache", "fetch-content"),
868
- },
869
- });
870
- return uniqueStrings([
871
- ...baseExtensions.filter((extension) => resolve(extension) !== BUNDLED_PI_WEB_ACCESS_EXTENSION),
872
- wrapperPath,
873
- ]);
859
+ if (shouldUseFetchContentCache(tools)) {
860
+ const wrapperPath = join(taskDir, "workflow-fetch-cache-extension.ts");
861
+ await writeWorkflowFetchCacheExtensionWrapper({
862
+ wrapperPath,
863
+ importPath: WORKFLOW_FETCH_CACHE_EXTENSION_IMPORT,
864
+ webAccessExtensionPath: BUNDLED_PI_WEB_ACCESS_EXTENSION,
865
+ webAccessStoragePath: BUNDLED_PI_WEB_ACCESS_STORAGE,
866
+ config: {
867
+ runId: run.runId,
868
+ taskId: task.taskId,
869
+ cacheDir: resolve(cwd, ".pi", "workflows", run.runId, "source-cache", "fetch-content"),
870
+ },
871
+ });
872
+ extensions = uniqueStrings([
873
+ ...extensions.filter((extension) => resolve(extension) !== BUNDLED_PI_WEB_ACCESS_EXTENSION),
874
+ wrapperPath,
875
+ ]);
876
+ }
877
+ if (shouldUseWorkflowWebSource(tools)) {
878
+ const providerExtensionPath = workflowWebSourceProviderExtension(tools, compiledTask.runtime.toolProviders);
879
+ const wrapperPath = join(taskDir, "workflow-web-source-extension.ts");
880
+ await writeWorkflowWebSourceExtensionWrapper({
881
+ wrapperPath,
882
+ importPath: WORKFLOW_WEB_SOURCE_EXTENSION_IMPORT,
883
+ providerExtensionPath,
884
+ config: {
885
+ schema: "workflow-web-source-launch-config-v1",
886
+ runId: run.runId,
887
+ taskId: task.taskId,
888
+ cwd,
889
+ cacheDir: resolve(cwd, ".pi", "workflows", run.runId, "web-source-cache"),
890
+ provider: {
891
+ kind: providerExtensionPath === BUNDLED_PI_WEB_ACCESS_EXTENSION
892
+ ? "pi-web-access"
893
+ : "extension",
894
+ extensionPath: providerExtensionPath,
895
+ },
896
+ securityPolicy: {
897
+ allowPrivateHosts: false,
898
+ cacheRawProviderPayloads: false,
899
+ },
900
+ },
901
+ });
902
+ const capturedProviderExtensions = new Set(workflowWebSourceProviderExtensions(tools, compiledTask.runtime.toolProviders));
903
+ extensions = uniqueStrings([
904
+ ...extensions.filter((extension) => !capturedProviderExtensions.has(extension)),
905
+ wrapperPath,
906
+ ]);
907
+ }
908
+ return extensions;
874
909
  }
875
910
  function shouldUseFetchContentCache(tools) {
876
911
  if (!(tools ?? []).includes("fetch_content"))
877
912
  return false;
878
913
  return !isExplicitlyDisabled(fetchContentCacheEnvValue());
879
914
  }
915
+ function shouldUseWorkflowWebSource(tools) {
916
+ return (tools ?? []).some((tool) => isWorkflowWebSourceTool(tool));
917
+ }
918
+ function workflowWebSourceProviderExtension(tools, toolProviders) {
919
+ return (workflowWebSourceProviderExtensions(tools, toolProviders)[0] ??
920
+ BUNDLED_PI_WEB_ACCESS_EXTENSION);
921
+ }
922
+ function workflowWebSourceProviderExtensions(tools, toolProviders) {
923
+ const providers = new Set();
924
+ for (const tool of tools ?? []) {
925
+ if (!isWorkflowWebSourceTool(tool))
926
+ continue;
927
+ for (const provider of toolProviders?.[tool]?.extensions ?? [])
928
+ providers.add(provider);
929
+ }
930
+ return [...providers];
931
+ }
880
932
  function fetchContentCacheEnvValue() {
881
933
  return (process.env[FETCH_CONTENT_CACHE_ENV] ?? process.env[LEGACY_FETCH_CACHE_ENV]);
882
934
  }
@@ -1068,7 +1120,7 @@ function buildSystemPrompt(task) {
1068
1120
  : []),
1069
1121
  ...(workflowRefsUrlValidation
1070
1122
  ? [
1071
- "External URLs in <refs> are validated before completion. Use fetch_content to verify each URL you cite; replace stale or unreachable URLs with working canonical URLs or omit them.",
1123
+ "External URLs in <refs> are validated before completion. Use available workflow web tools to fetch/cache the URL and read exact evidence before citing it; replace stale or unreachable URLs with working canonical URLs or omit them.",
1072
1124
  ]
1073
1125
  : []),
1074
1126
  ]
@@ -1082,11 +1134,14 @@ function buildSystemPrompt(task) {
1082
1134
  ? `Only these tools are enabled for this workflow task: ${enabledTools.join(", ")}.`
1083
1135
  : "No tools are enabled for this workflow task.",
1084
1136
  "If the agent definition below mentions tools that are not in this enabled list, ignore those mentions; unavailable tools cannot be called in this workflow run.",
1085
- !enabledTools.includes("get_search_content") &&
1086
- (enabledTools.includes("web_search") ||
1087
- enabledTools.includes("fetch_content"))
1088
- ? "Full cached search-content hydration is unavailable here. Use web_search/fetch_content results and report evidence gaps instead of broad raw document retrieval."
1089
- : undefined,
1137
+ enabledTools.includes("workflow_web_fetch_source") ||
1138
+ enabledTools.includes("workflow_web_source_read")
1139
+ ? "Workflow web-source tools return compact source cards. Preserve sourceRef values in structured outputs. Use workflow_web_source_read for exact evidence snippets; when several snippets are needed from the same sourceRef, batch them with queries:[...] or reads:[...] instead of making repeated calls. If the exact quote is unknown, pass claim plus 2-6 distinctive terms to harvest a candidate source window and preserve its match metadata. Do not read workflow cache files directly."
1140
+ : !enabledTools.includes("get_search_content") &&
1141
+ (enabledTools.includes("web_search") ||
1142
+ enabledTools.includes("fetch_content"))
1143
+ ? "Full cached search-content hydration is unavailable here. Use web_search/fetch_content results and report evidence gaps instead of broad raw document retrieval."
1144
+ : undefined,
1090
1145
  ].filter((line) => typeof line === "string");
1091
1146
  return [
1092
1147
  `You are Pi workflow subagent '${task.agent}'.`,
@@ -33,3 +33,4 @@ export declare function effectiveToolClassification(tool: string, toolProviders:
33
33
  export declare function classifyToolCapability(tools: string[] | undefined, toolProviders: Record<string, CompiledToolProvider> | undefined, readOnlyDeclared: boolean, options?: ClassifyToolCapabilityOptions): TaskCapability;
34
34
  export declare function buildAvailableToolView(tools?: readonly string[], toolProviders?: Record<string, CompiledToolProvider>): AvailableToolViewItem[];
35
35
  export declare function validateToolAuthority(tools: readonly string[] | undefined, options?: ToolAuthorityValidationOptions): string[];
36
+ export declare function toolAllowedByAuthorityCeiling(tool: string, allowed: ReadonlySet<string>): boolean;
@@ -16,6 +16,9 @@ const BUILTIN_TOOL_METADATA = {
16
16
  code_search: { classification: "read-only" },
17
17
  fetch_content: { classification: "read-only" },
18
18
  get_search_content: { classification: "read-only" },
19
+ workflow_web_search: { classification: "read-only" },
20
+ workflow_web_fetch_source: { classification: "read-only" },
21
+ workflow_web_source_read: { classification: "read-only" },
19
22
  scrapling_fetch: { classification: "read-only" },
20
23
  edit: { classification: "write-capable" },
21
24
  write: { classification: "write-capable" },
@@ -26,6 +29,11 @@ const NON_DOWNGRADABLE_TOOL_FLOORS = {
26
29
  write: "write-capable",
27
30
  bash: "mutation-capable",
28
31
  };
32
+ const TOOL_AUTHORITY_COMPAT_ALIASES = {
33
+ workflow_web_search: ["web_search"],
34
+ workflow_web_fetch_source: ["fetch_content"],
35
+ workflow_web_source_read: ["fetch_content", "get_search_content"],
36
+ };
29
37
  export function toolNameForSpec(tool) {
30
38
  if (typeof tool === "string")
31
39
  return tool;
@@ -168,7 +176,7 @@ export function validateToolAuthority(tools, options = {}) {
168
176
  ? new Set(options.allowedTools)
169
177
  : undefined;
170
178
  for (const tool of tools) {
171
- if (allowed && !allowed.has(tool)) {
179
+ if (allowed && !toolAllowedByAuthorityCeiling(tool, allowed)) {
172
180
  errors.push(`tool "${tool}" is outside the allowed tool ceiling`);
173
181
  continue;
174
182
  }
@@ -179,6 +187,10 @@ export function validateToolAuthority(tools, options = {}) {
179
187
  }
180
188
  return errors;
181
189
  }
190
+ export function toolAllowedByAuthorityCeiling(tool, allowed) {
191
+ return (allowed.has(tool) ||
192
+ (TOOL_AUTHORITY_COMPAT_ALIASES[tool] ?? []).some((alias) => allowed.has(alias)));
193
+ }
182
194
  function maxClassification(...values) {
183
195
  let best;
184
196
  for (const value of values) {
@@ -32,7 +32,7 @@ const workflowArtifactParameters = {
32
32
  },
33
33
  path: {
34
34
  type: "string",
35
- description: "Optional simple JSON path for projected reads, for example $.claims or $.claimIndex.items. Supported only for JSON artifacts.",
35
+ description: "Optional simple JSON path for projected reads, for example $.claims or $.claimIndex.items. Required when maxItems or maxChars is provided. Supported only for JSON artifacts.",
36
36
  },
37
37
  maxItems: {
38
38
  type: "integer",
@@ -42,7 +42,7 @@ const workflowArtifactParameters = {
42
42
  maxChars: {
43
43
  type: "integer",
44
44
  minimum: 0,
45
- description: "Optional character limit for the projected JSON value after maxItems is applied.",
45
+ description: "Optional character limit for the projected JSON value after maxItems is applied. Requires path; omit maxChars for whole-artifact reads.",
46
46
  },
47
47
  },
48
48
  required: ["action"],
@@ -56,6 +56,7 @@ export function registerWorkflowArtifactTool(pi, config) {
56
56
  promptGuidelines: [
57
57
  "Use workflow_artifact to inspect upstream workflow artifacts when the workflow prompt lists available sources or required reads.",
58
58
  "Call workflow_artifact with action=list to see visible source names before reading an artifact if unsure.",
59
+ "When using maxItems or maxChars, include a JSON path such as $.claims; for whole-artifact reads, omit maxItems/maxChars.",
59
60
  "Do not use repository read for workflow artifacts; workflow_artifact records required-read evidence.",
60
61
  ],
61
62
  parameters: workflowArtifactParameters,
@@ -23,7 +23,24 @@ const WORKFLOW_ARTIFACT_KIND_SET = new Set(WORKFLOW_ARTIFACT_KINDS);
23
23
  const DEFAULT_MAX_BYTES = 50 * 1024;
24
24
  const DEFAULT_MAX_LINES = 2000;
25
25
  const SOURCE_NAME_PATTERN = /^[A-Za-z0-9_.:-]+$/;
26
- const SIMPLE_JSON_PATH_PATTERN = /^(\$|\$(\.[A-Za-z0-9_-]+)+)$/;
26
+ const SIMPLE_JSON_PATH_PATTERN = /^(\$|\$(\.[A-Za-z0-9_-]+(\[(\*|\d+|\d*:\d*)\])?)+)$/;
27
+ const JSON_PATH_SEGMENT_ALIASES = {
28
+ axes: "researchAxes",
29
+ claimVerdicts: "claimVerdictLedger",
30
+ factSlot: "factSlots",
31
+ gaps: "remainingGaps",
32
+ primarySources: "sourcePolicy",
33
+ priorities: "verificationPriorities",
34
+ questions: "researchQuestions",
35
+ requiredSources: "sourcePolicy",
36
+ scope: "researchScope",
37
+ slots: "factSlots",
38
+ sourceQualityRules: "sourcePolicy",
39
+ sourceRequirements: "sourcePolicy",
40
+ verification: "verificationPriorities",
41
+ verificationPriority: "verificationPriorities",
42
+ verdicts: "claimVerdictLedger",
43
+ };
27
44
  export async function loadWorkflowSourceManifest(manifestPath, options = {}) {
28
45
  const absoluteManifestPath = resolve(manifestPath);
29
46
  const runDir = resolve(options.runDir ?? inferRunDirFromManifestPath(absoluteManifestPath));
@@ -192,17 +209,28 @@ export async function readWorkflowArtifact(manifest, sourceName, artifact, optio
192
209
  }
193
210
  async function readProjectedWorkflowArtifact(options) {
194
211
  const parsed = JSON.parse(await readFile(options.artifactPath, "utf8"));
195
- const resolved = readSimpleJsonPath(parsed, options.path);
212
+ let effectivePath = options.path;
213
+ let resolved;
214
+ for (const candidatePath of projectionPathCandidates(options.path, options.source, options.artifact)) {
215
+ resolved = readSimpleJsonPath(parsed, candidatePath);
216
+ if (resolved !== undefined) {
217
+ effectivePath = candidatePath;
218
+ break;
219
+ }
220
+ }
196
221
  if (resolved === undefined) {
197
222
  throw new Error(`workflow_artifact path did not resolve: ${options.path}`);
198
223
  }
199
- const sliced = applyProjectionItemLimit(resolved, options);
224
+ const sliced = applyProjectionItemLimit(resolved, {
225
+ ...options,
226
+ path: effectivePath,
227
+ });
200
228
  const serialized = JSON.stringify(sliced.value, null, 2);
201
229
  const preview = options.maxChars !== undefined && serialized.length > options.maxChars
202
230
  ? serialized.slice(0, options.maxChars)
203
231
  : serialized;
204
232
  const projection = {
205
- path: options.path,
233
+ path: effectivePath,
206
234
  valueType: jsonValueType(resolved),
207
235
  ...(options.maxItems === undefined ? {} : { maxItems: options.maxItems }),
208
236
  ...(options.maxChars === undefined ? {} : { maxChars: options.maxChars }),
@@ -234,6 +262,58 @@ async function readProjectedWorkflowArtifact(options) {
234
262
  projection,
235
263
  };
236
264
  }
265
+ function projectionPathCandidates(path, source, artifact) {
266
+ const candidates = [];
267
+ const seen = new Set();
268
+ const queue = [path];
269
+ for (let index = 0; index < queue.length && index < 32; index += 1) {
270
+ const candidate = queue[index];
271
+ if (seen.has(candidate))
272
+ continue;
273
+ seen.add(candidate);
274
+ candidates.push(candidate);
275
+ for (const next of [
276
+ stripArraySelector(candidate),
277
+ stripSourcePathPrefix(candidate, source),
278
+ stripArtifactPathPrefix(candidate, artifact),
279
+ applyJsonPathSegmentAliases(candidate),
280
+ ]) {
281
+ if (next !== candidate && !seen.has(next))
282
+ queue.push(next);
283
+ }
284
+ }
285
+ return candidates;
286
+ }
287
+ function stripArraySelector(path) {
288
+ return path.replace(/\[(\*|\d+|\d*:\d*)\]/gu, "");
289
+ }
290
+ function stripSourcePathPrefix(path, source) {
291
+ const sourcePrefix = `$.${source}.`;
292
+ if (!path.startsWith(sourcePrefix))
293
+ return path;
294
+ return `$.${path.slice(sourcePrefix.length)}`;
295
+ }
296
+ function stripArtifactPathPrefix(path, artifact) {
297
+ const artifactPath = `$.${artifact}`;
298
+ if (path === artifactPath)
299
+ return "$";
300
+ const artifactPrefix = `${artifactPath}.`;
301
+ if (!path.startsWith(artifactPrefix))
302
+ return path;
303
+ return `$.${path.slice(artifactPrefix.length)}`;
304
+ }
305
+ function applyJsonPathSegmentAliases(path) {
306
+ if (path === "$")
307
+ return path;
308
+ const segments = path
309
+ .slice(2)
310
+ .split(".")
311
+ .map((segment) => segment.replace(/\[(\*|\d+|\d*:\d*)\]$/u, ""));
312
+ const aliased = segments.map((segment) => JSON_PATH_SEGMENT_ALIASES[segment] ?? segment);
313
+ if (aliased.every((segment, index) => segment === segments[index]))
314
+ return path;
315
+ return `$.${aliased.join(".")}`;
316
+ }
237
317
  function applyProjectionItemLimit(value, options) {
238
318
  if (options.maxItems === undefined)
239
319
  return { value };
@@ -0,0 +1,43 @@
1
+ import { type WorkflowWebSecurityPolicy, type WorkflowWebSourceCacheConfig, type WorkflowWebSourcePolicy } from "./workflow-web-source.js";
2
+ export declare const WORKFLOW_WEB_SOURCE_LAUNCH_CONFIG_SCHEMA: "workflow-web-source-launch-config-v1";
3
+ export interface WorkflowWebProviderLaunchConfig {
4
+ kind: "pi-web-access" | "extension" | "none";
5
+ extensionPath?: string;
6
+ }
7
+ export interface WorkflowWebSourceLaunchConfig extends WorkflowWebSourceCacheConfig {
8
+ schema: typeof WORKFLOW_WEB_SOURCE_LAUNCH_CONFIG_SCHEMA;
9
+ workflowName?: string;
10
+ stageId?: string;
11
+ taskKey?: string;
12
+ cwd: string;
13
+ provider: WorkflowWebProviderLaunchConfig;
14
+ webSourcePolicy?: Partial<WorkflowWebSourcePolicy>;
15
+ securityPolicy?: Partial<WorkflowWebSecurityPolicy>;
16
+ exposeLegacyTools?: boolean;
17
+ }
18
+ export interface WorkflowWebSourceExtensionWrapperOptions {
19
+ wrapperPath: string;
20
+ importPath: string;
21
+ providerExtensionPath?: string;
22
+ config: WorkflowWebSourceLaunchConfig;
23
+ }
24
+ type ToolResult = {
25
+ content?: Array<Record<string, unknown>>;
26
+ details?: Record<string, unknown>;
27
+ [key: string]: unknown;
28
+ };
29
+ type ToolSpec = {
30
+ name?: string;
31
+ execute?: (toolCallId: string, params: unknown, signal?: AbortSignal, onUpdate?: unknown, ctx?: unknown) => Promise<ToolResult>;
32
+ [key: string]: unknown;
33
+ };
34
+ type PiLike = Record<string | symbol, unknown> & {
35
+ registerTool(tool: ToolSpec): void;
36
+ appendEntry?(type: string, data: unknown): void;
37
+ };
38
+ type ProviderExtension = (pi: PiLike) => void;
39
+ export declare function registerWorkflowWebSourceExtension(pi: PiLike, config: WorkflowWebSourceLaunchConfig, providerExtension?: ProviderExtension): void;
40
+ export declare function buildWorkflowWebSourceExtensionWrapper(options: Omit<WorkflowWebSourceExtensionWrapperOptions, "wrapperPath">): string;
41
+ export declare function writeWorkflowWebSourceExtensionWrapper(options: WorkflowWebSourceExtensionWrapperOptions): Promise<string>;
42
+ export declare function workflowWebSourceModuleImportPath(modulePath: string): string;
43
+ export {};