@agwab/pi-workflow 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/dist/artifact-graph-runtime.d.ts +1 -1
- package/dist/artifact-graph-runtime.js +10 -5
- package/dist/artifact-graph-schema.js +127 -5
- package/dist/compiler.js +46 -11
- package/dist/dynamic-decision.d.ts +1 -0
- package/dist/dynamic-decision.js +7 -0
- package/dist/dynamic-generated-task-runtime.js +3 -1
- package/dist/dynamic-profiles.d.ts +1 -0
- package/dist/dynamic-profiles.js +3 -0
- package/dist/engine-run-graph.d.ts +2 -0
- package/dist/engine-run-graph.js +55 -5
- package/dist/engine.js +278 -15
- package/dist/extension.js +3 -2
- package/dist/index.d.ts +8 -0
- package/dist/index.js +4 -0
- package/dist/prompt-json.d.ts +7 -0
- package/dist/prompt-json.js +13 -0
- package/dist/roles.d.ts +1 -1
- package/dist/roles.js +5 -8
- package/dist/store.d.ts +20 -1
- package/dist/store.js +89 -29
- package/dist/strings.d.ts +11 -0
- package/dist/strings.js +24 -0
- package/dist/subagent-backend.js +557 -13
- package/dist/types.d.ts +101 -1
- package/dist/verification-ontology.d.ts +31 -0
- package/dist/verification-ontology.js +66 -0
- package/dist/workflow-artifact-tool.js +5 -6
- package/dist/workflow-artifacts.d.ts +7 -0
- package/dist/workflow-artifacts.js +55 -4
- package/dist/workflow-fetch-cache-extension.d.ts +1 -0
- package/dist/workflow-fetch-cache-extension.js +57 -9
- package/dist/workflow-metrics.d.ts +113 -0
- package/dist/workflow-metrics.js +272 -0
- package/dist/workflow-output-artifacts.js +5 -3
- package/dist/workflow-partial-output.d.ts +45 -0
- package/dist/workflow-partial-output.js +205 -0
- package/dist/workflow-progress-health.js +42 -10
- package/dist/workflow-web-source-extension.js +27 -4
- package/dist/workflow-web-source.js +26 -12
- package/docs/usage.md +76 -29
- package/node_modules/@agwab/pi-subagent/package.json +1 -1
- package/node_modules/@agwab/pi-subagent/src/index.ts +53 -5
- package/node_modules/@agwab/pi-subagent/src/panel.ts +7 -3
- package/package.json +2 -2
- package/skills/workflow-guide/SKILL.md +1 -0
- package/src/artifact-graph-runtime.ts +19 -13
- package/src/artifact-graph-schema.ts +143 -3
- package/src/cli.mjs +52 -0
- package/src/compiler.ts +49 -9
- package/src/dynamic-decision.ts +11 -0
- package/src/dynamic-generated-task-runtime.ts +3 -1
- package/src/dynamic-profiles.ts +4 -0
- package/src/engine-run-graph.ts +63 -4
- package/src/engine.ts +400 -14
- package/src/extension.ts +3 -2
- package/src/index.ts +49 -0
- package/src/prompt-json.ts +13 -0
- package/src/roles.ts +6 -9
- package/src/store.ts +123 -34
- package/src/strings.ts +38 -0
- package/src/subagent-backend.ts +727 -41
- package/src/types.ts +110 -2
- package/src/verification-ontology.ts +88 -0
- package/src/workflow-artifact-tool.ts +5 -7
- package/src/workflow-artifacts.ts +83 -3
- package/src/workflow-fetch-cache-extension.ts +78 -13
- package/src/workflow-metrics.ts +478 -0
- package/src/workflow-output-artifacts.ts +5 -3
- package/src/workflow-partial-output.ts +299 -0
- package/src/workflow-progress-health.ts +47 -15
- package/src/workflow-web-source-extension.ts +33 -4
- package/src/workflow-web-source.ts +36 -12
- package/workflows/README.md +7 -25
- package/workflows/deep-research/batched-verification.spec.json +253 -0
- package/workflows/deep-research/helpers/batch-verification-candidates.mjs +136 -0
- package/workflows/deep-research/helpers/claim-evidence-gate.mjs +173 -20
- package/workflows/deep-research/helpers/normalize-input-packet.mjs +80 -1
- package/workflows/deep-research/helpers/render-executive.mjs +32 -5
- package/workflows/deep-research/helpers/shadow-select-verification.mjs +229 -0
- package/workflows/deep-research/helpers/verification-ontology.mjs +77 -0
- package/workflows/deep-research/schemas/deep-research-executive-render-control.schema.json +3 -2
- package/workflows/deep-research/schemas/deep-research-research-questions-control.schema.json +38 -0
- package/workflows/deep-research/schemas/deep-research-sanitize-claims-control.schema.json +63 -0
- package/workflows/deep-research/schemas/deep-research-verify-claims-batch-control.schema.json +47 -0
- package/workflows/deep-research/schemas/deep-research-verify-claims-control.schema.json +10 -3
- package/workflows/deep-research/spec.json +32 -12
- package/skills/workflow-guide/scaffolds/dag-required-reads/spec.json.validate.stderr +0 -0
- package/skills/workflow-guide/scaffolds/dag-required-reads/spec.json.validate.stdout +0 -13
|
@@ -608,8 +608,8 @@ async function cachedFetchFailureResult(config, cache, key, failure) {
|
|
|
608
608
|
}
|
|
609
609
|
return errorToolResult(failure.code, failure.message, failure.extra);
|
|
610
610
|
}
|
|
611
|
-
const FETCH_LOCK_STALE_MS = 60_000;
|
|
612
|
-
const FETCH_LOCK_WAIT_MS =
|
|
611
|
+
const FETCH_LOCK_STALE_MS = 4 * 60_000;
|
|
612
|
+
const FETCH_LOCK_WAIT_MS = 5 * 60_000;
|
|
613
613
|
async function withWorkflowWebFetchLock(config, key, signal, fn) {
|
|
614
614
|
const release = await acquireWorkflowWebFetchLock(config, key, signal);
|
|
615
615
|
try {
|
|
@@ -627,10 +627,11 @@ async function acquireWorkflowWebFetchLock(config, key, signal) {
|
|
|
627
627
|
if (signal?.aborted)
|
|
628
628
|
throw new Error("aborted");
|
|
629
629
|
try {
|
|
630
|
+
const ownerId = `${process.pid}:${Date.now()}:${Math.random().toString(36).slice(2)}`;
|
|
630
631
|
await mkdir(lockDir);
|
|
631
|
-
await writeFile(resolve(lockDir, "owner.json"), `${JSON.stringify({ pid: process.pid, createdAt: new Date().toISOString(), key }, null, 2)}\n`, "utf8");
|
|
632
|
+
await writeFile(resolve(lockDir, "owner.json"), `${JSON.stringify({ ownerId, pid: process.pid, createdAt: new Date().toISOString(), key }, null, 2)}\n`, "utf8");
|
|
632
633
|
return async () => {
|
|
633
|
-
await
|
|
634
|
+
await releaseWorkflowWebFetchLock(lockDir, ownerId);
|
|
634
635
|
};
|
|
635
636
|
}
|
|
636
637
|
catch (error) {
|
|
@@ -644,6 +645,17 @@ async function acquireWorkflowWebFetchLock(config, key, signal) {
|
|
|
644
645
|
}
|
|
645
646
|
}
|
|
646
647
|
}
|
|
648
|
+
async function releaseWorkflowWebFetchLock(lockDir, ownerId) {
|
|
649
|
+
try {
|
|
650
|
+
const current = await readFetchLockOwner(lockDir);
|
|
651
|
+
if (current?.ownerId !== ownerId)
|
|
652
|
+
return;
|
|
653
|
+
await rm(lockDir, { recursive: true, force: true });
|
|
654
|
+
}
|
|
655
|
+
catch {
|
|
656
|
+
// Missing or unreadable lock will be retried by the caller.
|
|
657
|
+
}
|
|
658
|
+
}
|
|
647
659
|
async function removeStaleFetchLock(lockDir) {
|
|
648
660
|
try {
|
|
649
661
|
const current = await stat(lockDir);
|
|
@@ -655,6 +667,17 @@ async function removeStaleFetchLock(lockDir) {
|
|
|
655
667
|
// Missing or unreadable lock will be retried by the caller.
|
|
656
668
|
}
|
|
657
669
|
}
|
|
670
|
+
async function readFetchLockOwner(lockDir) {
|
|
671
|
+
try {
|
|
672
|
+
const parsed = JSON.parse(await readFile(resolve(lockDir, "owner.json"), "utf8"));
|
|
673
|
+
return isRecord(parsed) && typeof parsed.ownerId === "string"
|
|
674
|
+
? { ownerId: parsed.ownerId }
|
|
675
|
+
: undefined;
|
|
676
|
+
}
|
|
677
|
+
catch {
|
|
678
|
+
return undefined;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
658
681
|
async function readDurableFetchFailure(config, key) {
|
|
659
682
|
try {
|
|
660
683
|
const parsed = JSON.parse(await readFile(fetchFailurePath(config, key), "utf8"));
|
|
@@ -2,6 +2,7 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { appendFile, mkdir, readFile, readdir, rename, writeFile, } from "node:fs/promises";
|
|
3
3
|
import { isIP } from "node:net";
|
|
4
4
|
import { dirname, resolve } from "node:path";
|
|
5
|
+
import { compactStrings } from "./strings.js";
|
|
5
6
|
export const WORKFLOW_WEB_SOURCE_CACHE_SCHEMA = "workflow-web-source-cache-v1";
|
|
6
7
|
export const WORKFLOW_WEB_SOURCE_INDEX_SCHEMA = "workflow-web-source-index-v1";
|
|
7
8
|
export const WORKFLOW_WEB_SOURCE_INDEX_EVENT_SCHEMA = "workflow-web-source-index-event-v1";
|
|
@@ -22,7 +23,7 @@ export const DEFAULT_WORKFLOW_WEB_SECURITY_POLICY = {
|
|
|
22
23
|
allowPrivateHosts: false,
|
|
23
24
|
cacheRawProviderPayloads: false,
|
|
24
25
|
};
|
|
25
|
-
const SENSITIVE_QUERY_PARAM_PATTERN = /(^|[-_])(access[-_]?token|auth|code|credential|key|password|secret|session|signature|sig|token)([-_]|$)/i;
|
|
26
|
+
const SENSITIVE_QUERY_PARAM_PATTERN = /(^|[-_])(access[-_]?token|auth|code|credential|key|password|secret|session|session[-_]?id|sessionid|signature|sig|sid|jwt|token)([-_]|$)/i;
|
|
26
27
|
const PRIVATE_HOST_PATTERNS = [
|
|
27
28
|
/^localhost$/i,
|
|
28
29
|
/^127\./,
|
|
@@ -165,7 +166,7 @@ export function createWorkflowWebSource(options) {
|
|
|
165
166
|
redactedUrl,
|
|
166
167
|
urlKey: sourceUrlCacheKey(options.url),
|
|
167
168
|
domain,
|
|
168
|
-
...(options.title ? { title: options.title } : {}),
|
|
169
|
+
...(options.title ? { title: redactInlineSecrets(options.title) } : {}),
|
|
169
170
|
...(options.provider ? { provider: options.provider } : {}),
|
|
170
171
|
contentHash,
|
|
171
172
|
text: options.text,
|
|
@@ -355,15 +356,12 @@ export function extractTextFromToolResult(result) {
|
|
|
355
356
|
const content = result.content;
|
|
356
357
|
if (!Array.isArray(content))
|
|
357
358
|
return "";
|
|
358
|
-
return content
|
|
359
|
-
.map((entry) => {
|
|
359
|
+
return compactStrings(content.map((entry) => {
|
|
360
360
|
if (!isRecord(entry))
|
|
361
361
|
return "";
|
|
362
362
|
const text = entry.text;
|
|
363
363
|
return typeof text === "string" ? text : "";
|
|
364
|
-
})
|
|
365
|
-
.filter(Boolean)
|
|
366
|
-
.join("\n\n");
|
|
364
|
+
}), { trim: false, unique: false }).join("\n\n");
|
|
367
365
|
}
|
|
368
366
|
export function extractTitleFromToolResult(result) {
|
|
369
367
|
if (!isRecord(result))
|
|
@@ -446,7 +444,7 @@ function sourceToIndexEntry(source) {
|
|
|
446
444
|
redactedUrl: source.redactedUrl,
|
|
447
445
|
...(source.urlKey ? { urlKey: source.urlKey } : {}),
|
|
448
446
|
domain: source.domain,
|
|
449
|
-
...(source.title ? { title: source.title } : {}),
|
|
447
|
+
...(source.title ? { title: redactInlineSecrets(source.title) } : {}),
|
|
450
448
|
contentHash: source.contentHash,
|
|
451
449
|
textChars: source.textChars,
|
|
452
450
|
...(source.provider ? { provider: source.provider } : {}),
|
|
@@ -702,9 +700,15 @@ function consumeAnchoredSnippet(options) {
|
|
|
702
700
|
}
|
|
703
701
|
const raw = redactInlineSecrets(options.text.slice(sourceStart, sourceEnd));
|
|
704
702
|
const consumed = consumeWorkflowWebVisibleBudget(options.budget, raw, visibleLimit);
|
|
703
|
+
// Redaction can expand secrets. Promote only when the redacted anchor
|
|
704
|
+
// itself no longer fits; clipping trailing context can remain a match.
|
|
705
|
+
const redactedThroughAnchorLength = consumed.truncated
|
|
706
|
+
? redactInlineSecrets(options.text.slice(sourceStart, Math.min(sourceEnd, anchorEnd))).length
|
|
707
|
+
: 0;
|
|
708
|
+
const anchorTruncated = status === "truncated" || redactedThroughAnchorLength > visibleLimit;
|
|
705
709
|
const truncated = status === "truncated" || consumed.truncated;
|
|
706
710
|
return {
|
|
707
|
-
status,
|
|
711
|
+
status: anchorTruncated ? "truncated" : status,
|
|
708
712
|
quote: consumed.text,
|
|
709
713
|
visibleChars: consumed.text.length,
|
|
710
714
|
sourceStart,
|
|
@@ -737,7 +741,15 @@ function normalizeForSearch(text) {
|
|
|
737
741
|
map.push(index);
|
|
738
742
|
}
|
|
739
743
|
}
|
|
740
|
-
|
|
744
|
+
while (normalized.startsWith(" ")) {
|
|
745
|
+
normalized = normalized.slice(1);
|
|
746
|
+
map.shift();
|
|
747
|
+
}
|
|
748
|
+
while (normalized.endsWith(" ")) {
|
|
749
|
+
normalized = normalized.slice(0, -1);
|
|
750
|
+
map.pop();
|
|
751
|
+
}
|
|
752
|
+
return { normalized, map };
|
|
741
753
|
}
|
|
742
754
|
function nearbySnippet(text, needle, maxChars) {
|
|
743
755
|
const index = text.indexOf(needle);
|
|
@@ -834,7 +846,9 @@ function sourceIndexEntryFromUnknown(value) {
|
|
|
834
846
|
redactedUrl: value.redactedUrl,
|
|
835
847
|
...(typeof value.urlKey === "string" ? { urlKey: value.urlKey } : {}),
|
|
836
848
|
domain: value.domain,
|
|
837
|
-
...(typeof value.title === "string"
|
|
849
|
+
...(typeof value.title === "string"
|
|
850
|
+
? { title: redactInlineSecrets(value.title) }
|
|
851
|
+
: {}),
|
|
838
852
|
contentHash: value.contentHash,
|
|
839
853
|
textChars: Number(value.textChars),
|
|
840
854
|
...(typeof value.provider === "string" ? { provider: value.provider } : {}),
|
|
@@ -972,7 +986,7 @@ function redactInlineSecrets(value) {
|
|
|
972
986
|
function redactInlineSecretsNoUrls(value) {
|
|
973
987
|
return value
|
|
974
988
|
.replace(/(authorization|cookie|set-cookie):\s*[^\n\r]+/gi, "$1: REDACTED")
|
|
975
|
-
.replace(/(token|secret|password|api[-_]?key)=([^\s&]+)/gi, "$1=REDACTED")
|
|
989
|
+
.replace(/(token|secret|password|api[-_]?key|jwt|sid|sessionid|session[-_]?id)=([^\s&]+)/gi, "$1=REDACTED")
|
|
976
990
|
.replace(/\/Users\/[^\s:'")]+/g, "/Users/REDACTED");
|
|
977
991
|
}
|
|
978
992
|
function isRecord(value) {
|
package/docs/usage.md
CHANGED
|
@@ -114,9 +114,11 @@ For reusable workflow authoring, `workflow-guide` includes validated scaffold bu
|
|
|
114
114
|
| `/workflow show <run-id-or-workflow-name>` | If the ref starts with `workflow_`, show run details; otherwise show the raw workflow spec. |
|
|
115
115
|
| `/workflow logs <run-id> [task-id] [lines]` | Print captured logs for a workflow task. Defaults to `task-1`. |
|
|
116
116
|
| `/workflow wait <run-id> [timeout-ms]` | Poll until the run finishes or the optional timeout elapses. |
|
|
117
|
+
| `/workflow stop <run-id>` | Interrupt a non-terminal run, best-effort interrupt active subagents, mark unfinished tasks interrupted, and stop the local supervisor watch. Use `/workflow resume <run-id>` if you want to restart unfinished work later. |
|
|
117
118
|
| `/workflow resume <run-id>` | Resume a failed, interrupted, or resumable blocked run (including dynamic approval blocked in headless mode): completed tasks are preserved; failed/interrupted/skipped or resumable blocked tasks reset to pending and reschedule. Loop workflows are not supported yet. |
|
|
119
|
+
| `/workflow stop <run-id>` | Stop a non-terminal run: best-effort interrupt of active subagent workers, then mark unfinished tasks `interrupted`. Completed task artifacts are preserved, and the stopped run can be restarted later with `/workflow resume` (resumed tasks start fresh sessions). |
|
|
118
120
|
|
|
119
|
-
Not implemented: `/workflow continue` and `/workflow delegate`. Use `status`, `show`, `logs`, `wait`, `resume`, and `pi-workflow inspect` for text/CLI inspection. The standalone CLI also offers `pi-workflow supervise <run-id>|--all` to drive scheduling from outside a Pi session (unfinished failed/interrupted or resumable blocked runs within the last 7 days are announced at session start with resume hints).
|
|
121
|
+
Not implemented: `/workflow continue` and `/workflow delegate`. Use `status`, `show`, `logs`, `wait`, `stop`, `resume`, and `pi-workflow inspect` for text/CLI inspection. The standalone CLI also offers `pi-workflow supervise <run-id>|--all` to drive scheduling from outside a Pi session (unfinished failed/interrupted or resumable blocked runs within the last 7 days are announced at session start with resume hints).
|
|
120
122
|
|
|
121
123
|
### Workflow board controls
|
|
122
124
|
|
|
@@ -198,13 +200,30 @@ For lower-latency runs, pass `--thinking low` explicitly:
|
|
|
198
200
|
|
|
199
201
|
This is an opt-in fast mode. Package defaults remain conservative until a separate holdout evaluation provides enough evidence to change them. Current evidence is limited but encouraging for explicit fast runs: the 2026-07-02 `deep-research` combined gate on P1/P2/P3-style prompts resolved non-support tasks to `low`, completed selected valid runs in about 15-17 minutes, passed the strict gate 9/9, and had zero source-ref join failures across those 9 runs. Treat this as a speed option, not proof that every workflow should default to `low`.
|
|
200
202
|
|
|
203
|
+
### Opt-in batched verification for deep-research
|
|
204
|
+
|
|
205
|
+
`deep-research` still verifies one claim per verifier task by default. For controlled runs where verifier batching is acceptable, use the explicit path-ref variant:
|
|
206
|
+
|
|
207
|
+
```text
|
|
208
|
+
/workflow validate ./workflows/deep-research/batched-verification.spec.json
|
|
209
|
+
/workflow run ./workflows/deep-research/batched-verification.spec.json "Research this repository and verify the key claims."
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
This path-ref variant keeps the same planner/research/normalization/audit/final stages, but feeds `verify-claims` from `verification-batches` and requires each verifier task to return one `results[]` row per claim id. It is not registered as an official bundled workflow name and does not change package defaults. Treat speed/cost results as task-specific: claim a win only when the run's audit reports zero missing/duplicate/invalid verifier rows, zero sourceRef join failures, and no verified-floor regression.
|
|
213
|
+
|
|
214
|
+
### Verification outcome ontology
|
|
215
|
+
|
|
216
|
+
The package exports a small verification outcome vocabulary for workflows that verify source-backed claims: `verified`, `partially_supported`, `unsupported`, `conflicting`, and `verification_blocked`. Bundled workflow helpers must use bundle-local shims that stay in parity with the package export, because helper imports are bundled from the workflow spec directory. `verification_blocked` means the verifier could not evaluate the claim because required evidence, source access, tool execution, or policy constraints blocked verification. It is not a weaker form of `verified`, never counts toward verified floors, and should remain visible in audit summaries so operators can decide whether to rerun, change source access, or treat the claim as unresolved.
|
|
217
|
+
|
|
218
|
+
Adopt this vocabulary only for evidence-verification outcomes. Do not force it onto workflow-control, finding-disposition, or ship-readiness verdicts such as `KEEP`, `DROP`, `READY`, or `NEEDS_WORK`. Deep-diff-review revival is not part of this ontology change.
|
|
219
|
+
|
|
201
220
|
### Run-scoped web-source cache
|
|
202
221
|
|
|
203
222
|
Prefer normalized workflow web tools in new workflows:
|
|
204
223
|
|
|
205
224
|
- `workflow_web_search` returns compact candidate cards.
|
|
206
225
|
- `workflow_web_fetch_source` caches one or more URLs and returns compact source cards with `sourceRef` values; pass `urls: [...]` or `sources: [{ url, title }]` to batch several fetches in one tool call.
|
|
207
|
-
- `workflow_web_source_read` reads narrow exact/fuzzy/term-matched evidence snippets by `sourceRef`; pass `queries: [...]` or `reads: [...]` to batch several snippets from the same source in one tool call, or `claim` + distinctive `terms` when the exact quote is unknown. Term/claim reads return candidate metadata (`matchedTerms`, `missingTerms`, `coverageRatio`) rather than a proof verdict.
|
|
226
|
+
- `workflow_web_source_read` reads narrow exact/fuzzy/term-matched evidence snippets by `sourceRef`; pass `queries: [...]` or `reads: [...]` to batch several snippets from the same source in one tool call, or `claim` + distinctive `terms` when the exact quote is unknown. Term/claim reads return candidate metadata (`matchedTerms`, `missingTerms`, `coverageRatio`) rather than a proof verdict. Snippet windows are anchored to the match: a result may report `status: "truncated"` when the per-task visible budget or `maxChars` clips the window (the returned quote still starts at the match), and `status: "budget_exhausted"` when no visible budget remains; both include a `next` hint suggesting smaller queries or a fresh task.
|
|
208
227
|
|
|
209
228
|
The normalized cache is stored under the workflow run directory:
|
|
210
229
|
|
|
@@ -214,7 +233,11 @@ The normalized cache is stored under the workflow run directory:
|
|
|
214
233
|
|
|
215
234
|
Do not instruct agents to read that directory directly; source cards intentionally expose only opaque refs and short previews. The cache also writes an append-only index ledger plus same-URL fetch locks/negative-cache files so duplicate lookup and deterministic terminal failures can recover across parallel worker processes. Custom extension `fetch_content` providers are treated as trusted fetchers and are disabled under the default private-host policy; use the default safe fetch path or opt into trusted private-host behavior only for controlled providers. Legacy workflow tasks that still use `fetch_content` keep the older run-scoped file cache under `.pi/workflows/<run-id>/source-cache/fetch-content/`. Set `PI_WORKFLOW_FETCH_CONTENT_CACHE=0` to disable that legacy fetch cache for a run.
|
|
216
235
|
|
|
217
|
-
|
|
236
|
+
To reduce worker context pressure for legacy `fetch_content` tasks, the bundled
|
|
237
|
+
workflow fetch wrapper caps inline response text while preserving full stored
|
|
238
|
+
source content. Override with `PI_WORKFLOW_FETCH_CONTENT_INLINE_CHARS=<n>` or
|
|
239
|
+
disable the inline cap with `PI_WORKFLOW_FETCH_CONTENT_INLINE_CHARS=0` when you
|
|
240
|
+
intentionally need the provider's full inline response.
|
|
218
241
|
|
|
219
242
|
## Bundled workflows
|
|
220
243
|
|
|
@@ -284,7 +307,15 @@ Dynamic workflows keep JSON as the source of truth while allowing trusted bundle
|
|
|
284
307
|
}
|
|
285
308
|
```
|
|
286
309
|
|
|
287
|
-
Controller/helper/nested workflow refs must be bundle-local `./...` paths. Nested workflow specs are intentionally self-contained at their own directory level: refs inside a nested spec may point to files in that nested spec's subtree, but not to parent-level shared files via
|
|
310
|
+
Controller/helper/nested workflow refs must be bundle-local `./...` paths. Nested workflow specs are intentionally self-contained at their own directory level: refs inside a nested spec may point to files in that nested spec's subtree, but not to parent-level shared files via `../` — put shared helpers/schemas under each nested workflow subtree or expose them through the parent controller/helper layer. Controller/helper code is trusted Node.js code for orchestration and timeout isolation, not a security sandbox.
|
|
311
|
+
|
|
312
|
+
Controller context rules:
|
|
313
|
+
|
|
314
|
+
- Generated agents are real workflow tasks: `ctx.agent({ id, agent, prompt, tools })` inserts a deterministic `stageId.id` task into `compiled.json` and `run.json`, persists a request hash in `dynamic/events.jsonl`, and replays fail-closed if the same id later changes request shape.
|
|
315
|
+
- On resume, controllers must re-issue previously recorded `ctx.agent`, `ctx.helper`, and `ctx.workflow` operations in the same order before issuing new operations; omitted or out-of-order replay fails closed with an explicit replay-invariant error.
|
|
316
|
+
- Use `ctx.parallel([() => ctx.agent(...), ...])` for dynamic fan-out; the runtime records queued sibling generation ops before the controller suspends, and non-suspension operation failures make the controller fail closed. Generated dependency cycles are rejected.
|
|
317
|
+
- `ctx.helper(name, input)` can call only helpers declared in `dynamic.helpers`; pure/retry-safe helpers may set `idempotent: true` so a crash after `helper.started` but before `helper.completed` can retry the helper instead of permanently failing closed.
|
|
318
|
+
- `ctx.workflow(name, input)` can call only nested specs declared in `dynamic.workflows`.
|
|
288
319
|
|
|
289
320
|
Dynamic outputs should be compact typed artifacts. The controller returns normal workflow sections through `{ control, analysis, refs }`; generated child agents must return the same `<control>`, `<analysis>`, `<refs>` protocol as other artifact-graph tasks. When a controller result includes `outputTasks`/`outputTaskIds` (the built-in decision loop sets this from accepted `synthesize` actions), downstream `from: "<dynamic-stage>"` reducers also receive those exported task artifacts as stable sources such as `<dynamic-stage>.output`. Runtime state is stored under `.pi/workflows/<run-id>/dynamic/`:
|
|
290
321
|
|
|
@@ -301,13 +332,7 @@ Budgets bound controller behavior (`maxAgents`, `maxConcurrency`, `maxRuntimeMs`
|
|
|
301
332
|
|
|
302
333
|
### DAG authoring
|
|
303
334
|
|
|
304
|
-
Top-level `artifactGraph.stages` is DAG-capable by default. A nested `type: "dag"` is a workflow/control container, not a leaf subagent task: it must contain child `stages` and should not have its own prompt. The runtime lowers public graph relationships onto the internal dependency scheduler while preserving artifact/data boundaries.
|
|
305
|
-
|
|
306
|
-
Keep these layers distinct:
|
|
307
|
-
|
|
308
|
-
- **Workflow layer**: graph/control/data-dependency semantics such as `id`, `from`, `after`, `sourcePolicy`, `sourceProjection`, scheduling, and artifacts.
|
|
309
|
-
- **Subagent layer**: model-backed execution patterns such as `single`, `foreach`, `reduce`, and loop child stages.
|
|
310
|
-
- **Support layer**: deterministic local helper execution through `support: { uses, options }`.
|
|
335
|
+
Top-level `artifactGraph.stages` is DAG-capable by default. A nested `type: "dag"` is a workflow/control container, not a leaf subagent task: it must contain child `stages` and should not have its own prompt. The runtime lowers public graph relationships onto the internal dependency scheduler while preserving artifact/data boundaries. Keep the authoring layers described under "Stage model" distinct when composing DAGs.
|
|
311
336
|
|
|
312
337
|
DAG rules:
|
|
313
338
|
|
|
@@ -405,6 +430,28 @@ Use workflow-local JSON Schema files when the control plane needs stronger valid
|
|
|
405
430
|
|
|
406
431
|
The built-in validator supports the subset used by bundled workflows: `type`, `required`, `properties`, `items`, `enum`, `const`, length/item/number bounds, `additionalProperties`, and simple `allOf`/`anyOf`/`oneOf`. Unsupported keywords such as `$ref`, `$defs`, `definitions`, and `pattern` are rejected when the workflow is loaded.
|
|
407
432
|
|
|
433
|
+
### Opt-in partial output for streaming foreach
|
|
434
|
+
|
|
435
|
+
A producer stage can declare stable array paths that may be published before terminal completion:
|
|
436
|
+
|
|
437
|
+
```json
|
|
438
|
+
"output": {
|
|
439
|
+
"partial": { "paths": ["$.items"] }
|
|
440
|
+
}
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
A downstream `foreach` may then opt in on the matching `from` edge:
|
|
444
|
+
|
|
445
|
+
```json
|
|
446
|
+
"from": {
|
|
447
|
+
"source": "plan",
|
|
448
|
+
"path": "$.items",
|
|
449
|
+
"streaming": { "enabled": true, "minChunk": 2 }
|
|
450
|
+
}
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
The runtime accepts only partial items for declared paths. Published partial items must be final/stable JSON objects with a non-empty string `id`; the producer may emit them as `<partial-control>{"schema":"workflow-partial-output-v1","path":"$.items","items":[...]}</partial-control>` before the final workflow output. If the final `control.json` later changes a published item with the same id, the streaming foreach placeholder blocks fail-closed. Downstream reducers still wait for the foreach placeholder plus all generated item tasks, so partial output overlaps item work without relaxing final fan-in gates.
|
|
454
|
+
|
|
408
455
|
## Support helpers
|
|
409
456
|
|
|
410
457
|
A support node runs local helper code inline instead of launching a subagent. It is declared by adding a `support` object; it does not use a separate `type` value:
|
|
@@ -542,6 +589,15 @@ Authoring checklist:
|
|
|
542
589
|
7. Add JSON output contracts for model-produced data that later stages depend on.
|
|
543
590
|
8. Run `/workflow validate <workflow-or-file>` before using the workflow.
|
|
544
591
|
|
|
592
|
+
### Roles
|
|
593
|
+
|
|
594
|
+
A workflow can declare reusable role context under top-level `roles`. Compiled role text is injected into subagent task prompts as a `# Role Context` block, and `/workflow roles <workflow>` shows the compiled result per role. Role fields:
|
|
595
|
+
|
|
596
|
+
- `fromAgent`: extract sections from a discoverable Pi agent's markdown body. By default only safe knowledge sections are included (`Core Principles`, `Domain Expertise`, `Safety Review`, `Rules`, `Research Manifest`); orchestration and output-format sections are always excluded.
|
|
597
|
+
- `includeSections` / `excludeSections`: override which agent sections are extracted.
|
|
598
|
+
- `prompt`: literal role text, appended after any extracted agent sections.
|
|
599
|
+
- `maxChars`: compiled role budget (default 12000). Longer content is truncated and flagged in `/workflow roles` output.
|
|
600
|
+
|
|
545
601
|
### Tool allowlists
|
|
546
602
|
|
|
547
603
|
Workflow `tools` are still the child-worker allowlist. Entries can be strings:
|
|
@@ -578,26 +634,17 @@ Scope order is agent frontmatter fallback < `defaults.tools` < stage `tools`: th
|
|
|
578
634
|
- Write-capable workflows should use managed worktrees in git repositories.
|
|
579
635
|
- In non-git workspaces with `worktreePolicy: "off"`, writes mutate the live directory.
|
|
580
636
|
- No backend fallback exists. The compiled backend/strategy is fixed for the run.
|
|
637
|
+
- Subagent process launches are gated per Pi process to avoid boot storms: at most `max(2, floor(cpu cores / 2))` concurrent launches, overridable with the `PI_WORKFLOW_MAX_CONCURRENT_LAUNCHES` environment variable. Queued tasks report a waiting message in their status. Deterministic boot failures (extension load or configuration errors) fail fast instead of consuming transient-failure retries.
|
|
581
638
|
- External content, source files, and web pages used by workflow workers are untrusted data, not instructions.
|
|
582
639
|
|
|
583
640
|
## Web tools
|
|
584
641
|
|
|
585
642
|
New workflows should use `workflow_web_search`, `workflow_web_fetch_source`, and
|
|
586
|
-
`workflow_web_source_read
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
candidate evidence and include matched/missing term metadata; they are not a
|
|
595
|
-
verdict by themselves. The bundled `pi-web-access` adapter remains
|
|
596
|
-
available as the default compatibility provider for this release scope.
|
|
597
|
-
|
|
598
|
-
Legacy workflows that use `web_search`, `fetch_content`, `get_search_content`, or
|
|
599
|
-
`code_search` still use the bundled `pi-web-access` dependency packaged with
|
|
600
|
-
pi-workflow. Object-form custom tool `extensions` are merged with built-in
|
|
601
|
-
mappings and deduplicated for the subagent launch. Web calls can still fail when
|
|
602
|
-
network access, provider credentials, browser state, or quota are unavailable;
|
|
603
|
-
research workflows should report those limits instead of guessing.
|
|
643
|
+
`workflow_web_source_read` — tool semantics, batching forms, and the run-scoped
|
|
644
|
+
cache are documented under "Run-scoped web-source cache" above. The bundled
|
|
645
|
+
`pi-web-access` adapter remains the default compatibility provider for this
|
|
646
|
+
release scope.
|
|
647
|
+
|
|
648
|
+
- Legacy workflows that use `web_search`, `fetch_content`, `get_search_content`, or `code_search` still use the bundled `pi-web-access` dependency packaged with pi-workflow.
|
|
649
|
+
- Object-form custom tool `extensions` are merged with built-in mappings and deduplicated for the subagent launch.
|
|
650
|
+
- Web calls can still fail when network access, provider credentials, browser state, or quota are unavailable; research workflows should report those limits instead of guessing.
|
|
@@ -210,6 +210,52 @@ function getExecuteParams(first: unknown, second: unknown): unknown {
|
|
|
210
210
|
return second === undefined ? first : second;
|
|
211
211
|
}
|
|
212
212
|
|
|
213
|
+
function isAbortSignalLike(value: unknown): value is AbortSignal {
|
|
214
|
+
return isRecord(value) && typeof value.aborted === "boolean";
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function normalizeExecuteArgs(args: unknown[]): {
|
|
218
|
+
params: unknown;
|
|
219
|
+
signal?: AbortSignal;
|
|
220
|
+
onUpdate?: ToolUpdateCallback;
|
|
221
|
+
ctx?: unknown;
|
|
222
|
+
} {
|
|
223
|
+
const [first, second, third, fourth, fifth] = args;
|
|
224
|
+
const params = getExecuteParams(first, second);
|
|
225
|
+
|
|
226
|
+
// Pi has shipped both execute(toolCallId, params, signal, onUpdate, ctx)
|
|
227
|
+
// and execute(toolCallId, params, onUpdate, ctx, signal) call orders. Support
|
|
228
|
+
// both so context-scoped metadata (cwd/session) and cancellation survive either
|
|
229
|
+
// host version.
|
|
230
|
+
if (typeof third === "function") {
|
|
231
|
+
return {
|
|
232
|
+
params,
|
|
233
|
+
onUpdate: third as ToolUpdateCallback,
|
|
234
|
+
ctx: fourth,
|
|
235
|
+
...(isAbortSignalLike(fifth) ? { signal: fifth } : {}),
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (isAbortSignalLike(fifth) && !isAbortSignalLike(third)) {
|
|
240
|
+
return {
|
|
241
|
+
params,
|
|
242
|
+
signal: fifth,
|
|
243
|
+
...(typeof fourth === "function"
|
|
244
|
+
? { onUpdate: fourth as ToolUpdateCallback }
|
|
245
|
+
: { ctx: fourth }),
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return {
|
|
250
|
+
params,
|
|
251
|
+
...(isAbortSignalLike(third) ? { signal: third } : {}),
|
|
252
|
+
...(typeof fourth === "function"
|
|
253
|
+
? { onUpdate: fourth as ToolUpdateCallback }
|
|
254
|
+
: {}),
|
|
255
|
+
ctx: fifth,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
213
259
|
function getCwd(ctx: unknown): string {
|
|
214
260
|
if (isRecord(ctx) && typeof ctx.cwd === "string" && ctx.cwd.length > 0)
|
|
215
261
|
return ctx.cwd;
|
|
@@ -595,9 +641,10 @@ async function writeUnsupportedResult(
|
|
|
595
641
|
});
|
|
596
642
|
}
|
|
597
643
|
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
644
|
+
type ToolUpdateCallback = (update: {
|
|
645
|
+
content: ToolTextContent[];
|
|
646
|
+
details: unknown;
|
|
647
|
+
}) => void;
|
|
601
648
|
|
|
602
649
|
interface NotificationContext {
|
|
603
650
|
ui?: {
|
|
@@ -957,8 +1004,9 @@ export default function registerSubagentEngine(pi: ExtensionAPI) {
|
|
|
957
1004
|
: summary;
|
|
958
1005
|
return new SingleLineComponent(`${title} ${theme.fg("muted", rest)}`);
|
|
959
1006
|
},
|
|
960
|
-
async execute(
|
|
961
|
-
const params
|
|
1007
|
+
async execute(...executeArgs: unknown[]) {
|
|
1008
|
+
const { params, signal, onUpdate, ctx } =
|
|
1009
|
+
normalizeExecuteArgs(executeArgs);
|
|
962
1010
|
const cwd = getCwd(ctx);
|
|
963
1011
|
|
|
964
1012
|
try {
|
|
@@ -512,12 +512,14 @@ async function readTask(
|
|
|
512
512
|
mtimeMs: number,
|
|
513
513
|
loadTails: boolean,
|
|
514
514
|
currentSessionId?: string,
|
|
515
|
+
options: { staleOverride?: boolean } = {},
|
|
515
516
|
): Promise<TaskRow | null> {
|
|
516
517
|
const parsed = await readJson(resultPath);
|
|
517
518
|
if (!isResultEnvelope(parsed)) return null;
|
|
518
519
|
const log = await readLogTail(cwd, parsed, loadTails, currentSessionId);
|
|
519
520
|
const stale =
|
|
520
|
-
isActive(parsed.status) &&
|
|
521
|
+
isActive(parsed.status) &&
|
|
522
|
+
(options.staleOverride ?? Date.now() - mtimeMs > STALE_RUN_AFTER_MS);
|
|
521
523
|
return {
|
|
522
524
|
attemptId: parsed.attemptId ?? parsed.taskId ?? "unknown",
|
|
523
525
|
status: stale ? "failed" : parsed.status,
|
|
@@ -571,6 +573,7 @@ async function readTaskFromRegistry(
|
|
|
571
573
|
loadTails: boolean,
|
|
572
574
|
currentSessionId?: string,
|
|
573
575
|
): Promise<TaskRow> {
|
|
576
|
+
const registryStale = registryTaskStale(task);
|
|
574
577
|
if (
|
|
575
578
|
task.artifactCwd !== undefined &&
|
|
576
579
|
task.resultPath !== undefined &&
|
|
@@ -590,13 +593,14 @@ async function readTaskFromRegistry(
|
|
|
590
593
|
statInfo.mtimeMs,
|
|
591
594
|
loadTails,
|
|
592
595
|
currentSessionId,
|
|
596
|
+
isActive(task.status) ? { staleOverride: registryStale } : undefined,
|
|
593
597
|
);
|
|
594
598
|
if (parsed !== null) return parsed;
|
|
595
599
|
}
|
|
596
600
|
}
|
|
597
601
|
}
|
|
598
602
|
const log = await readTailFromRegistryPath(task, loadTails, currentSessionId);
|
|
599
|
-
const stale =
|
|
603
|
+
const stale = registryStale;
|
|
600
604
|
return {
|
|
601
605
|
attemptId: task.attemptId ?? task.taskId ?? "unknown",
|
|
602
606
|
status: stale ? "failed" : task.status,
|
|
@@ -1087,7 +1091,7 @@ export class SubagentPanel implements Component {
|
|
|
1087
1091
|
}
|
|
1088
1092
|
|
|
1089
1093
|
render(width: number): string[] {
|
|
1090
|
-
const safeWidth = Math.max(
|
|
1094
|
+
const safeWidth = Math.max(1, width);
|
|
1091
1095
|
const maxLines = panelLineBudget();
|
|
1092
1096
|
const lines: string[] = [];
|
|
1093
1097
|
const active = this.snapshot.runs.filter((run) =>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agwab/pi-workflow",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Workflow orchestration for Pi subagents.",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
@@ -76,7 +76,7 @@
|
|
|
76
76
|
"node": ">=22.19.0"
|
|
77
77
|
},
|
|
78
78
|
"dependencies": {
|
|
79
|
-
"@agwab/pi-subagent": "^0.4.
|
|
79
|
+
"@agwab/pi-subagent": "^0.4.2",
|
|
80
80
|
"pi-web-access": "^0.10.7",
|
|
81
81
|
"typebox": "^1.1.39"
|
|
82
82
|
},
|
|
@@ -29,6 +29,7 @@ Resolve paths relative to this skill directory. Treat those docs as the source o
|
|
|
29
29
|
- For deterministic local post-processing, declare a `support` object with `support.uses` pointing to a bundle-local `./*.mjs` helper; support is trusted local code, not sandboxed subagent work and does not use a separate `type` value.
|
|
30
30
|
- For bounded iteration, use `loop` with fixed child stages, `maxRounds`, and deterministic `until`.
|
|
31
31
|
- Agent-declared tools are the authority ceiling; workflow `tools` can only narrow them.
|
|
32
|
+
- To reuse agent knowledge across stages, declare top-level `roles` (`fromAgent` extracts safe agent sections; `prompt` appends literal text). Compiled role text is injected as a `# Role Context` block; check the result with `/workflow roles <workflow>`. See "Roles" in `docs/usage.md`.
|
|
32
33
|
- Keep review/research workflows read-only unless the workflow explicitly documents managed-worktree mutation.
|
|
33
34
|
- Write-capable workflows need explicit worktree policy, validation/check stages, and protected-path awareness.
|
|
34
35
|
- In non-git workspaces with `worktreePolicy: "off"`, writes mutate the live directory.
|
|
@@ -2,6 +2,8 @@ import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
|
2
2
|
import { dirname, extname, join } from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
|
|
5
|
+
import { stringifyPromptJson } from "./prompt-json.js";
|
|
6
|
+
import { compactStrings } from "./strings.js";
|
|
5
7
|
import { loadWorkflowHelper } from "./workflow-helpers.js";
|
|
6
8
|
import {
|
|
7
9
|
WORKFLOW_ARTIFACT_TOOL_NAME,
|
|
@@ -13,7 +15,7 @@ import {
|
|
|
13
15
|
type WorkflowSourceManifestSource,
|
|
14
16
|
} from "./workflow-artifact-tool.js";
|
|
15
17
|
import { writeWorkflowTaskArtifactBundle } from "./workflow-output-artifacts.js";
|
|
16
|
-
import {
|
|
18
|
+
import type { JsonSchema } from "./json-schema.js";
|
|
17
19
|
import {
|
|
18
20
|
buildRunSourceContext,
|
|
19
21
|
readOutputText,
|
|
@@ -29,11 +31,11 @@ import {
|
|
|
29
31
|
writeJsonAtomic,
|
|
30
32
|
writeRunRecord,
|
|
31
33
|
} from "./store.js";
|
|
32
|
-
import {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
import type {
|
|
35
|
+
CompiledTask,
|
|
36
|
+
CompiledWorkflow,
|
|
37
|
+
WorkflowRunRecord,
|
|
38
|
+
WorkflowTaskRunRecord,
|
|
37
39
|
} from "./types.js";
|
|
38
40
|
|
|
39
41
|
export async function executeSupportTask(
|
|
@@ -372,7 +374,9 @@ export function normalizeDynamicControllerOutput(value: unknown): {
|
|
|
372
374
|
refs: [],
|
|
373
375
|
};
|
|
374
376
|
}
|
|
375
|
-
export function normalizeSupportControl(
|
|
377
|
+
export function normalizeSupportControl(
|
|
378
|
+
value: unknown,
|
|
379
|
+
): Record<string, unknown> {
|
|
376
380
|
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
377
381
|
const record = value as Record<string, unknown>;
|
|
378
382
|
return {
|
|
@@ -474,7 +478,7 @@ export async function prepareDagTask(
|
|
|
474
478
|
compiledTask.compiledPrompt,
|
|
475
479
|
"# Source Stage Context",
|
|
476
480
|
"Use this deterministic source context packet. Prefer structuredOutput over outputPreview. Do not assume dependencies beyond this explicit packet.",
|
|
477
|
-
|
|
481
|
+
stringifyPromptJson({ ...context, missingDependencies: missing }),
|
|
478
482
|
].join("\n\n"),
|
|
479
483
|
};
|
|
480
484
|
}
|
|
@@ -486,6 +490,10 @@ async function prepareArtifactGraphTask(
|
|
|
486
490
|
task: WorkflowTaskRunRecord,
|
|
487
491
|
contextDependsOn: readonly string[],
|
|
488
492
|
): Promise<CompiledTask> {
|
|
493
|
+
if (compiledTask.artifactGraph?.artifactAccess === "none") {
|
|
494
|
+
return { ...compiledTask, cwd: task.cwd };
|
|
495
|
+
}
|
|
496
|
+
|
|
489
497
|
const taskDir = dirname(fromProjectPath(cwd, task.files.result));
|
|
490
498
|
const manifestPath = join(taskDir, "source-manifest.json");
|
|
491
499
|
const ledgerPath = join(taskDir, "read-ledger.jsonl");
|
|
@@ -880,7 +888,7 @@ export function formatArtifactGraphSourceContext(
|
|
|
880
888
|
return [
|
|
881
889
|
"# Workflow Artifact Inputs",
|
|
882
890
|
"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.",
|
|
883
|
-
|
|
891
|
+
'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.',
|
|
884
892
|
requiredReads.length > 0
|
|
885
893
|
? [
|
|
886
894
|
"Required reads before final output:",
|
|
@@ -888,7 +896,7 @@ export function formatArtifactGraphSourceContext(
|
|
|
888
896
|
].join("\n")
|
|
889
897
|
: "No hard requiredReads are declared for this stage.",
|
|
890
898
|
"Available sources:",
|
|
891
|
-
|
|
899
|
+
stringifyPromptJson(
|
|
892
900
|
sources.map((source) => ({
|
|
893
901
|
source: source.source,
|
|
894
902
|
taskId: source.taskId,
|
|
@@ -904,13 +912,11 @@ export function formatArtifactGraphSourceContext(
|
|
|
904
912
|
projectionTruncated: source.projectionTruncated,
|
|
905
913
|
availableArtifacts: Object.keys(source.artifacts),
|
|
906
914
|
})),
|
|
907
|
-
null,
|
|
908
|
-
2,
|
|
909
915
|
),
|
|
910
916
|
].join("\n\n");
|
|
911
917
|
}
|
|
912
918
|
function uniqueStrings(values: readonly string[]): string[] {
|
|
913
|
-
return
|
|
919
|
+
return compactStrings(values, { trim: false, dropWhitespaceOnly: true });
|
|
914
920
|
}
|
|
915
921
|
|
|
916
922
|
export async function readArtifactGraphControl(
|