@agwab/pi-workflow 0.1.1 → 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.
- package/README.md +14 -3
- package/agents/researcher.md +17 -7
- package/dist/artifact-graph-runtime.js +1 -0
- package/dist/compiler.js +2 -2
- package/dist/dynamic-generated-task-runtime.js +4 -3
- package/dist/dynamic-runtime-bundle.js +3 -2
- package/dist/extension.js +40 -1
- package/dist/subagent-backend.js +82 -27
- package/dist/tool-metadata.d.ts +1 -0
- package/dist/tool-metadata.js +13 -1
- package/dist/workflow-artifact-extension.js +3 -2
- package/dist/workflow-artifact-tool.js +84 -4
- package/dist/workflow-web-source-extension.d.ts +43 -0
- package/dist/workflow-web-source-extension.js +1194 -0
- package/dist/workflow-web-source.d.ts +171 -0
- package/dist/workflow-web-source.js +897 -0
- package/docs/usage.md +32 -18
- package/node_modules/@agwab/pi-subagent/package.json +1 -1
- package/node_modules/@agwab/pi-subagent/src/api.ts +245 -132
- package/node_modules/@agwab/pi-subagent/src/artifacts/result.ts +243 -163
- package/node_modules/@agwab/pi-subagent/src/core/constants.ts +117 -90
- package/node_modules/@agwab/pi-subagent/src/core/validation.ts +728 -475
- package/node_modules/@agwab/pi-subagent/src/orchestrate/run.ts +305 -209
- package/node_modules/@agwab/pi-subagent/src/runners/headless-model.ts +750 -439
- package/node_modules/@agwab/pi-subagent/src/runners/tmux.ts +422 -268
- package/package.json +2 -2
- package/skills/workflow-guide/scaffolds/object-tool-fallback/schemas/fetch-control.schema.json +1 -1
- package/skills/workflow-guide/scaffolds/object-tool-fallback/spec.json +4 -3
- package/src/artifact-graph-runtime.ts +1 -0
- package/src/compiler.ts +2 -1
- package/src/dynamic-generated-task-runtime.ts +4 -2
- package/src/dynamic-runtime-bundle.ts +3 -2
- package/src/extension.ts +46 -1
- package/src/subagent-backend.ts +121 -37
- package/src/tool-metadata.ts +22 -1
- package/src/workflow-artifact-extension.ts +3 -2
- package/src/workflow-artifact-tool.ts +96 -4
- package/src/workflow-web-source-extension.ts +1411 -0
- package/src/workflow-web-source.ts +1171 -0
- package/workflows/README.md +1 -1
- package/workflows/deep-research/helpers/claim-evidence-gate.mjs +474 -40
- package/workflows/deep-research/helpers/final-audit-packet.mjs +219 -0
- package/workflows/deep-research/helpers/normalize-input-packet.mjs +436 -0
- package/workflows/deep-research/helpers/render-executive.mjs +571 -198
- package/workflows/deep-research/schemas/deep-research-executive-render-control.schema.json +35 -8
- package/workflows/deep-research/schemas/deep-research-normalize-claims-control.schema.json +45 -4
- package/workflows/deep-research/schemas/deep-research-verify-claims-control.schema.json +0 -2
- package/workflows/deep-research/spec.json +36 -21
- package/workflows/deep-review/helpers/render-review-report.mjs +502 -0
- package/workflows/deep-review/schemas/deep-review-render-control.schema.json +50 -0
- package/workflows/deep-review/spec.json +22 -1
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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
package/agents/researcher.md
CHANGED
|
@@ -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
|
-
-
|
|
32
|
-
articles, issues, and community discussions.
|
|
33
|
-
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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 (!
|
|
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
|
|
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
|
|
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
|
-
"
|
|
11
|
-
"
|
|
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 {
|
package/dist/subagent-backend.js
CHANGED
|
@@ -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
|
|
851
|
-
|
|
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
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
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
|
|
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
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
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}'.`,
|
package/dist/tool-metadata.d.ts
CHANGED
|
@@ -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;
|
package/dist/tool-metadata.js
CHANGED
|
@@ -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 && !
|
|
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
|
-
|
|
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,
|
|
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:
|
|
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 {};
|