@agwab/pi-workflow 0.2.1 → 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 +52 -19
- package/dist/dynamic-generated-task-runtime.js +3 -1
- package/dist/dynamic-profiles.d.ts +1 -1
- package/dist/engine-run-graph.d.ts +3 -0
- package/dist/engine-run-graph.js +194 -4
- package/dist/engine.d.ts +5 -0
- package/dist/engine.js +389 -41
- package/dist/extension.d.ts +2 -1
- package/dist/extension.js +30 -8
- package/dist/index.d.ts +11 -3
- package/dist/index.js +6 -1
- 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 +139 -35
- package/dist/strings.d.ts +11 -0
- package/dist/strings.js +24 -0
- package/dist/subagent-backend.js +710 -40
- package/dist/types.d.ts +107 -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-runtime.js +10 -1
- package/dist/workflow-view.js +3 -1
- package/dist/workflow-web-source-extension.js +194 -52
- package/dist/workflow-web-source.d.ts +2 -1
- package/dist/workflow-web-source.js +109 -30
- package/docs/usage.md +76 -29
- package/node_modules/@agwab/pi-subagent/README.md +3 -3
- package/node_modules/@agwab/pi-subagent/api.mjs +1 -0
- package/node_modules/@agwab/pi-subagent/docs/usage.md +63 -12
- package/node_modules/@agwab/pi-subagent/package.json +2 -2
- package/node_modules/@agwab/pi-subagent/src/api.ts +54 -1
- package/node_modules/@agwab/pi-subagent/src/artifacts/registry.ts +9 -4
- package/node_modules/@agwab/pi-subagent/src/artifacts/result.ts +8 -0
- package/node_modules/@agwab/pi-subagent/src/core/constants.ts +9 -0
- package/node_modules/@agwab/pi-subagent/src/core/validation.ts +21 -0
- package/node_modules/@agwab/pi-subagent/src/index.ts +1046 -576
- package/node_modules/@agwab/pi-subagent/src/orchestrate/async.ts +279 -156
- package/node_modules/@agwab/pi-subagent/src/orchestrate/interrupt.ts +165 -89
- package/node_modules/@agwab/pi-subagent/src/orchestrate/reconcile.ts +111 -65
- package/node_modules/@agwab/pi-subagent/src/orchestrate/run-ref.ts +219 -0
- package/node_modules/@agwab/pi-subagent/src/orchestrate/run.ts +88 -8
- package/node_modules/@agwab/pi-subagent/src/orchestrate/status.ts +614 -298
- package/node_modules/@agwab/pi-subagent/src/panel.ts +1356 -560
- package/node_modules/@agwab/pi-subagent/src/runners/headless-model.ts +53 -5
- package/node_modules/@agwab/pi-subagent/src/runners/tmux.ts +13 -6
- 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 +63 -18
- package/src/dynamic-generated-task-runtime.ts +3 -1
- package/src/dynamic-profiles.ts +1 -1
- package/src/engine-run-graph.ts +246 -4
- package/src/engine.ts +545 -38
- package/src/extension.ts +36 -6
- package/src/index.ts +52 -1
- package/src/prompt-json.ts +13 -0
- package/src/roles.ts +6 -9
- package/src/store.ts +194 -42
- package/src/strings.ts +38 -0
- package/src/subagent-backend.ts +921 -62
- package/src/types.ts +116 -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-runtime.ts +18 -2
- package/src/workflow-view.ts +2 -1
- package/src/workflow-web-source-extension.ts +654 -232
- package/src/workflow-web-source.ts +153 -39
- 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 +229 -36
- package/workflows/deep-research/helpers/final-audit-packet.mjs +1 -4
- package/workflows/deep-research/helpers/normalize-input-packet.mjs +81 -2
- package/workflows/deep-research/helpers/render-executive.mjs +40 -26
- package/workflows/deep-research/helpers/sanitize-verification-candidates.mjs +89 -15
- 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 -3
- 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 +13 -3
- package/workflows/deep-research/spec.json +32 -12
- package/workflows/impact-review/spec.json +3 -3
- package/workflows/spec-review/helpers/spec-review-pipeline.mjs +1 -8
- package/dist/dynamic-loader.d.ts +0 -25
- package/dist/dynamic-loader.js +0 -13
- 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
- package/src/dynamic-loader.ts +0 -49
- package/workflows/impact-review/schemas/docs-release-impact-control.schema.json +0 -42
- package/workflows/impact-review/schemas/security-performance-impact-control.schema.json +0 -42
- package/workflows/impact-review/schemas/state-data-impact-control.schema.json +0 -42
|
@@ -79,6 +79,13 @@ export interface PiJsonParseResult {
|
|
|
79
79
|
metadata: Partial<ResultMetadata>;
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
export interface ContextLengthResolution {
|
|
83
|
+
rawContextLengthExceeded: boolean;
|
|
84
|
+
contextLengthExceeded: boolean;
|
|
85
|
+
contextOverflowRecovered: boolean;
|
|
86
|
+
recoveredStreamErrors: string[];
|
|
87
|
+
}
|
|
88
|
+
|
|
82
89
|
const CONTEXT_LENGTH_ERROR_PATTERN =
|
|
83
90
|
/\bcontext[_ -]?length[_ -]?exceeded\b|\bcontext[_ -]?window[_ -]?(?:exceeded|overflow|exhausted)\b|\b(?:maximum|max)[_ -]?context[_ -]?length\b|\btoo many tokens\b|\b(?:prompt|input|request)[^\n]{0,80}\btoo large\b|\bcontext_length_exceeded\b/i;
|
|
84
91
|
|
|
@@ -94,6 +101,32 @@ export function detectContextLengthExceeded(signals: {
|
|
|
94
101
|
return CONTEXT_LENGTH_ERROR_PATTERN.test(text);
|
|
95
102
|
}
|
|
96
103
|
|
|
104
|
+
export function resolveContextLengthState(
|
|
105
|
+
parsed: PiJsonParseResult,
|
|
106
|
+
rawContextLengthExceeded: boolean,
|
|
107
|
+
): ContextLengthResolution {
|
|
108
|
+
const contextOverflowRecovered =
|
|
109
|
+
rawContextLengthExceeded && finalAssistantSucceeded(parsed);
|
|
110
|
+
return {
|
|
111
|
+
rawContextLengthExceeded,
|
|
112
|
+
contextLengthExceeded:
|
|
113
|
+
rawContextLengthExceeded && !contextOverflowRecovered,
|
|
114
|
+
contextOverflowRecovered,
|
|
115
|
+
recoveredStreamErrors: contextOverflowRecovered
|
|
116
|
+
? parsed.errors.filter((error) =>
|
|
117
|
+
detectContextLengthExceeded({ errors: [error] }),
|
|
118
|
+
)
|
|
119
|
+
: [],
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function finalAssistantSucceeded(parsed: PiJsonParseResult): boolean {
|
|
124
|
+
return (
|
|
125
|
+
parsed.finalAssistantText.length > 0 &&
|
|
126
|
+
parsed.metadata.stopReason !== "error"
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
97
130
|
function normalizeTimeoutMs(timeoutMs: number | undefined): number | undefined {
|
|
98
131
|
if (timeoutMs === undefined) return undefined;
|
|
99
132
|
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
|
|
@@ -390,18 +423,29 @@ export function resolvePiJsonOutcome(
|
|
|
390
423
|
|
|
391
424
|
export function resultMetadataFromParse(
|
|
392
425
|
parsed: PiJsonParseResult,
|
|
393
|
-
|
|
426
|
+
contextLength: ContextLengthResolution,
|
|
394
427
|
outcome: ProcessOutcome,
|
|
395
428
|
): Partial<ResultMetadata> {
|
|
396
429
|
return {
|
|
397
430
|
...parsed.metadata,
|
|
398
|
-
contextLengthExceeded,
|
|
431
|
+
contextLengthExceeded: contextLength.contextLengthExceeded,
|
|
432
|
+
...(contextLength.contextOverflowRecovered
|
|
433
|
+
? { contextOverflowRecovered: true }
|
|
434
|
+
: {}),
|
|
399
435
|
...(parsed.errors.length === 0
|
|
400
436
|
? {}
|
|
401
437
|
: { streamErrors: parsed.errors.slice(0, MAX_METADATA_ERRORS) }),
|
|
402
438
|
...(outcome.status === "completed" && parsed.errors.length > 0
|
|
403
439
|
? { nonFatalStreamErrors: parsed.errors.slice(0, MAX_METADATA_ERRORS) }
|
|
404
440
|
: {}),
|
|
441
|
+
...(contextLength.recoveredStreamErrors.length === 0
|
|
442
|
+
? {}
|
|
443
|
+
: {
|
|
444
|
+
recoveredStreamErrors: contextLength.recoveredStreamErrors.slice(
|
|
445
|
+
0,
|
|
446
|
+
MAX_METADATA_ERRORS,
|
|
447
|
+
),
|
|
448
|
+
}),
|
|
405
449
|
...(parsed.parseErrors.length === 0
|
|
406
450
|
? {}
|
|
407
451
|
: { parseErrors: parsed.parseErrors.slice(0, MAX_METADATA_ERRORS) }),
|
|
@@ -774,14 +818,18 @@ export async function runHeadlessModel(
|
|
|
774
818
|
stderrText,
|
|
775
819
|
stderrContextLengthExceeded,
|
|
776
820
|
} = processResult;
|
|
777
|
-
const
|
|
821
|
+
const rawContextLengthExceeded =
|
|
778
822
|
stderrContextLengthExceeded ||
|
|
779
823
|
detectContextLengthExceeded({ stderrText, errors: parsed.errors });
|
|
824
|
+
const contextLength = resolveContextLengthState(
|
|
825
|
+
parsed,
|
|
826
|
+
rawContextLengthExceeded,
|
|
827
|
+
);
|
|
780
828
|
|
|
781
829
|
const outcome = resolvePiJsonOutcome(
|
|
782
830
|
processOutcome,
|
|
783
831
|
parsed,
|
|
784
|
-
contextLengthExceeded,
|
|
832
|
+
contextLength.contextLengthExceeded,
|
|
785
833
|
);
|
|
786
834
|
|
|
787
835
|
const completedAt = new Date();
|
|
@@ -811,7 +859,7 @@ export async function runHeadlessModel(
|
|
|
811
859
|
artifacts,
|
|
812
860
|
correlationId: options.correlationId,
|
|
813
861
|
metadata: {
|
|
814
|
-
...resultMetadataFromParse(parsed,
|
|
862
|
+
...resultMetadataFromParse(parsed, contextLength, outcome),
|
|
815
863
|
...sessionMetadata,
|
|
816
864
|
...(options.parentSessionId === undefined
|
|
817
865
|
? {}
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
detectContextLengthExceeded,
|
|
21
21
|
parsePiJsonFile,
|
|
22
22
|
parsePiJsonLines,
|
|
23
|
+
resolveContextLengthState,
|
|
23
24
|
resolvePiJsonOutcome,
|
|
24
25
|
resultMetadataFromParse,
|
|
25
26
|
resultSessionMetadata,
|
|
@@ -157,9 +158,7 @@ function workerScript(
|
|
|
157
158
|
return `import { spawn } from "node:child_process";\nimport { appendFileSync, closeSync, openSync, writeFileSync } from "node:fs";\nconst argv = ${JSON.stringify(argv)};\nconst cwd = ${JSON.stringify(cwd)};\nconst eventPath = ${JSON.stringify(eventPath)};\nconst stderrPath = ${JSON.stringify(stderrPath)};\nconst metaPath = ${JSON.stringify(metaPath)};\nconst messageUpdatePattern = /"type"\\s*:\\s*"message_update"/;\nconst maxStdoutLogLineChars = 64 * 1024 * 1024;\ncloseSync(openSync(eventPath, "w"));\ncloseSync(openSync(stderrPath, "w"));\nlet settled = false;\nlet stdoutBuffer = "";\nlet discardingOversizedLine = false;\nlet omittedMessageUpdates = 0;\nlet omittedMessageUpdateBytes = 0;\nlet omittedOversizedLines = 0;\nlet omittedOversizedBytes = 0;\nfunction writeStdoutLine(line) {\n if (messageUpdatePattern.test(line)) {\n omittedMessageUpdates += 1;\n omittedMessageUpdateBytes += Buffer.byteLength(line, "utf8");\n return;\n }\n appendFileSync(eventPath, line);\n process.stdout.write(line);\n}\nfunction handleStdoutChunk(chunk) {\n let text = chunk.toString("utf8");\n while (text.length > 0) {\n if (discardingOversizedLine) {\n const newline = text.indexOf("\\n");\n omittedOversizedBytes += Buffer.byteLength(newline < 0 ? text : text.slice(0, newline + 1), "utf8");\n if (newline < 0) return;\n discardingOversizedLine = false;\n text = text.slice(newline + 1);\n continue;\n }\n const newline = text.indexOf("\\n");\n const segment = newline < 0 ? text : text.slice(0, newline + 1);\n stdoutBuffer += segment;\n text = newline < 0 ? "" : text.slice(newline + 1);\n if (stdoutBuffer.length > maxStdoutLogLineChars) {\n omittedOversizedLines += 1;\n omittedOversizedBytes += Buffer.byteLength(stdoutBuffer, "utf8");\n stdoutBuffer = "";\n discardingOversizedLine = newline < 0;\n continue;\n }\n if (newline >= 0) {\n writeStdoutLine(stdoutBuffer);\n stdoutBuffer = "";\n }\n }\n}\nfunction finishStdoutFilter() {\n if (!discardingOversizedLine && stdoutBuffer.length > 0) writeStdoutLine(stdoutBuffer);\n stdoutBuffer = "";\n if (omittedMessageUpdates > 0 || omittedOversizedLines > 0) {\n appendFileSync(eventPath, JSON.stringify({ type: "pi-subagent.stdout_filter", omitted: { messageUpdateEvents: omittedMessageUpdates, messageUpdateBytes: omittedMessageUpdateBytes, oversizedLines: omittedOversizedLines, oversizedBytes: omittedOversizedBytes }, reason: "cumulative message_update snapshots are omitted from durable stdout artifacts; final assistant text is stored in output.log" }) + "\\n");\n }\n}\nfunction writeMeta(meta) {\n if (settled) return;\n settled = true;\n finishStdoutFilter();\n writeFileSync(metaPath, JSON.stringify(meta, null, 2) + "\\n");\n}\nconst env = { ...process.env };\ndelete env.TMUX;\nconst child = spawn(argv[0], argv.slice(1), { cwd, shell: false, stdio: ["ignore", "pipe", "pipe"], env });\nchild.stdout?.on("data", handleStdoutChunk);\nchild.stderr?.on("data", (chunk) => { appendFileSync(stderrPath, chunk); process.stderr.write(chunk); });\nchild.on("error", () => { writeMeta({ status: "failed", failureKind: "spawn", exitCode: null, signal: null }); });\nchild.on("close", (exitCode, signal) => {\n const failureKind = exitCode === 0 ? null : "exit";\n writeMeta({ status: failureKind === null ? "completed" : "failed", failureKind, exitCode, signal });\n});\n`;
|
|
158
159
|
}
|
|
159
160
|
|
|
160
|
-
async function runTmuxProcess(
|
|
161
|
-
options: RunTmuxProcessOptions,
|
|
162
|
-
): Promise<{
|
|
161
|
+
async function runTmuxProcess(options: RunTmuxProcessOptions): Promise<{
|
|
163
162
|
result: TmuxRunResult | null;
|
|
164
163
|
store: Awaited<ReturnType<typeof createAttemptArtifactStore>>;
|
|
165
164
|
cwd: string;
|
|
@@ -439,11 +438,19 @@ export async function runTmuxModel(
|
|
|
439
438
|
parsePiJsonLines(""),
|
|
440
439
|
);
|
|
441
440
|
await unlink(result.eventPath).catch(() => undefined);
|
|
442
|
-
const
|
|
441
|
+
const rawContextLengthExceeded = detectContextLengthExceeded({
|
|
443
442
|
stderrText,
|
|
444
443
|
errors: parsed.errors,
|
|
445
444
|
});
|
|
446
|
-
const
|
|
445
|
+
const contextLength = resolveContextLengthState(
|
|
446
|
+
parsed,
|
|
447
|
+
rawContextLengthExceeded,
|
|
448
|
+
);
|
|
449
|
+
const meta = resolvePiJsonOutcome(
|
|
450
|
+
result.meta,
|
|
451
|
+
parsed,
|
|
452
|
+
contextLength.contextLengthExceeded,
|
|
453
|
+
);
|
|
447
454
|
|
|
448
455
|
const outputRef = await store.writeTextArtifact(
|
|
449
456
|
"output",
|
|
@@ -464,7 +471,7 @@ export async function runTmuxModel(
|
|
|
464
471
|
tmux: result.tmux,
|
|
465
472
|
correlationId: options.correlationId,
|
|
466
473
|
metadata: {
|
|
467
|
-
...resultMetadataFromParse(parsed,
|
|
474
|
+
...resultMetadataFromParse(parsed, contextLength, meta),
|
|
468
475
|
...sessionMetadata,
|
|
469
476
|
...(options.parentSessionId === undefined
|
|
470
477
|
? {}
|
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.
|
|
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(
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { isAbsolute } from "node:path";
|
|
2
2
|
|
|
3
3
|
import { DYNAMIC_OUTPUT_PROFILES } from "./dynamic-profiles.js";
|
|
4
|
+
import { compactStrings } from "./strings.js";
|
|
4
5
|
import {
|
|
5
6
|
APPROVAL_MODES,
|
|
6
7
|
FAST_MODES,
|
|
@@ -71,10 +72,16 @@ const OUTPUT_KEYS = new Set([
|
|
|
71
72
|
"analysis",
|
|
72
73
|
"refs",
|
|
73
74
|
"maxDigestChars",
|
|
75
|
+
"partial",
|
|
74
76
|
]);
|
|
77
|
+
const OUTPUT_PARTIAL_KEYS = new Set(["paths"]);
|
|
75
78
|
const REQUIRED_FLAG_KEYS = new Set(["required"]);
|
|
76
79
|
const REFS_OUTPUT_KEYS = new Set(["required", "minItems"]);
|
|
77
|
-
const INPUT_POLICY_KEYS = new Set([
|
|
80
|
+
const INPUT_POLICY_KEYS = new Set([
|
|
81
|
+
"requiredReads",
|
|
82
|
+
"enforcement",
|
|
83
|
+
"artifactAccess",
|
|
84
|
+
]);
|
|
78
85
|
const SOURCE_PROJECTION_KEYS = new Set(["include", "maxChars"]);
|
|
79
86
|
const SUPPORT_KEYS = new Set(["uses", "options"]);
|
|
80
87
|
const DYNAMIC_STAGE_FORBIDDEN_KEYS = new Set([
|
|
@@ -185,7 +192,8 @@ const UNTIL_KEYS = new Set([
|
|
|
185
192
|
"all",
|
|
186
193
|
"any",
|
|
187
194
|
]);
|
|
188
|
-
const FOREACH_FROM_KEYS = new Set(["source", "path"]);
|
|
195
|
+
const FOREACH_FROM_KEYS = new Set(["source", "path", "streaming"]);
|
|
196
|
+
const FOREACH_STREAMING_KEYS = new Set(["enabled", "minChunk"]);
|
|
189
197
|
const NORMAL_ARTIFACT_KINDS = new Set<WorkflowArtifactKind>([
|
|
190
198
|
"control",
|
|
191
199
|
"analysis",
|
|
@@ -296,9 +304,54 @@ function validateStageArray(
|
|
|
296
304
|
for (const [index, item] of value.entries()) {
|
|
297
305
|
validateStage(item, `${path}[${index}]`, ids, sourceIds, issues);
|
|
298
306
|
}
|
|
307
|
+
validateStreamingProducerDeclarations(value, path, issues);
|
|
299
308
|
validateStageDependencyGraph(value, path, issues);
|
|
300
309
|
}
|
|
301
310
|
|
|
311
|
+
function validateStreamingProducerDeclarations(
|
|
312
|
+
stages: readonly unknown[],
|
|
313
|
+
path: string,
|
|
314
|
+
issues: ValidationIssue[],
|
|
315
|
+
): void {
|
|
316
|
+
const byId = new Map<string, Record<string, unknown>>();
|
|
317
|
+
for (const stage of stages) {
|
|
318
|
+
if (isRecord(stage) && typeof stage.id === "string") {
|
|
319
|
+
byId.set(stage.id, stage);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
for (const [index, stage] of stages.entries()) {
|
|
323
|
+
if (!isRecord(stage) || !isRecord(stage.from)) continue;
|
|
324
|
+
const from = stage.from;
|
|
325
|
+
if (!isRecord(from.streaming) || from.streaming.enabled !== true) continue;
|
|
326
|
+
const source = typeof from.source === "string" ? from.source : undefined;
|
|
327
|
+
const controlPath = typeof from.path === "string" ? from.path : undefined;
|
|
328
|
+
if (!source || !controlPath) continue;
|
|
329
|
+
const sourceStage = byId.get(source);
|
|
330
|
+
const partialPaths = outputPartialPaths(sourceStage);
|
|
331
|
+
if (!partialPaths.includes(controlPath)) {
|
|
332
|
+
issues.push({
|
|
333
|
+
path: `${path}[${index}].from.streaming`,
|
|
334
|
+
message: `source stage "${source}" must declare output.partial.paths including "${controlPath}" to use streaming`,
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function outputPartialPaths(
|
|
341
|
+
stage: Record<string, unknown> | undefined,
|
|
342
|
+
): string[] {
|
|
343
|
+
const output = isRecord(stage?.output) ? stage.output : undefined;
|
|
344
|
+
const partial = isRecord(output?.partial) ? output.partial : undefined;
|
|
345
|
+
return Array.isArray(partial?.paths)
|
|
346
|
+
? compactStrings(partial.paths, {
|
|
347
|
+
trim: false,
|
|
348
|
+
unique: false,
|
|
349
|
+
dropEmpty: false,
|
|
350
|
+
dropWhitespaceOnly: false,
|
|
351
|
+
})
|
|
352
|
+
: [];
|
|
353
|
+
}
|
|
354
|
+
|
|
302
355
|
function validateStageDependencyGraph(
|
|
303
356
|
stages: readonly unknown[],
|
|
304
357
|
path: string,
|
|
@@ -372,7 +425,12 @@ function extractDependencyRefs(value: unknown): string[] {
|
|
|
372
425
|
if (value === undefined) return [];
|
|
373
426
|
if (typeof value === "string") return [value];
|
|
374
427
|
if (Array.isArray(value))
|
|
375
|
-
return value
|
|
428
|
+
return compactStrings(value, {
|
|
429
|
+
trim: false,
|
|
430
|
+
unique: false,
|
|
431
|
+
dropEmpty: false,
|
|
432
|
+
dropWhitespaceOnly: false,
|
|
433
|
+
});
|
|
376
434
|
if (isRecord(value) && typeof value.source === "string")
|
|
377
435
|
return [value.source];
|
|
378
436
|
return [];
|
|
@@ -517,6 +575,7 @@ function validateStage(
|
|
|
517
575
|
`${path}.inputPolicy`,
|
|
518
576
|
sourceIds,
|
|
519
577
|
issues,
|
|
578
|
+
stage.sourceProjection,
|
|
520
579
|
);
|
|
521
580
|
validateOutput(stage.output, `${path}.output`, issues);
|
|
522
581
|
validateSupportStage(stage, type, path, issues);
|
|
@@ -611,6 +670,23 @@ function validateControlPathRef(
|
|
|
611
670
|
message: "must be a control JSONPath starting with $.",
|
|
612
671
|
});
|
|
613
672
|
}
|
|
673
|
+
if (value.streaming !== undefined) {
|
|
674
|
+
validateForeachStreaming(value.streaming, `${path}.streaming`, issues);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
function validateForeachStreaming(
|
|
679
|
+
value: unknown,
|
|
680
|
+
path: string,
|
|
681
|
+
issues: ValidationIssue[],
|
|
682
|
+
): void {
|
|
683
|
+
const streaming = recordAt(value, path, issues);
|
|
684
|
+
if (!streaming) return;
|
|
685
|
+
rejectUnknownKeys(streaming, FOREACH_STREAMING_KEYS, path, issues);
|
|
686
|
+
if (streaming.enabled !== true) {
|
|
687
|
+
issues.push({ path: `${path}.enabled`, message: "must be true" });
|
|
688
|
+
}
|
|
689
|
+
optionalPositiveInteger(streaming.minChunk, `${path}.minChunk`, issues);
|
|
614
690
|
}
|
|
615
691
|
|
|
616
692
|
function validateKnownStageRef(
|
|
@@ -649,6 +725,42 @@ function validateOutput(
|
|
|
649
725
|
);
|
|
650
726
|
validateRequiredFlagObject(output.analysis, `${path}.analysis`, issues);
|
|
651
727
|
validateRefsOutputObject(output.refs, `${path}.refs`, issues);
|
|
728
|
+
validatePartialOutput(output.partial, `${path}.partial`, issues);
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
function validatePartialOutput(
|
|
732
|
+
value: unknown,
|
|
733
|
+
path: string,
|
|
734
|
+
issues: ValidationIssue[],
|
|
735
|
+
): void {
|
|
736
|
+
if (value === undefined) return;
|
|
737
|
+
const partial = recordAt(value, path, issues);
|
|
738
|
+
if (!partial) return;
|
|
739
|
+
rejectUnknownKeys(partial, OUTPUT_PARTIAL_KEYS, path, issues);
|
|
740
|
+
if (!Array.isArray(partial.paths)) {
|
|
741
|
+
issues.push({ path: `${path}.paths`, message: "must be an array" });
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
if (partial.paths.length === 0) {
|
|
745
|
+
issues.push({ path: `${path}.paths`, message: "must not be empty" });
|
|
746
|
+
}
|
|
747
|
+
const seen = new Set<string>();
|
|
748
|
+
for (const [index, item] of partial.paths.entries()) {
|
|
749
|
+
const itemPath = `${path}.paths[${index}]`;
|
|
750
|
+
if (typeof item !== "string" || item.trim() === "") {
|
|
751
|
+
issues.push({ path: itemPath, message: "must be a non-empty string" });
|
|
752
|
+
continue;
|
|
753
|
+
}
|
|
754
|
+
if (!item.startsWith("$.")) {
|
|
755
|
+
issues.push({
|
|
756
|
+
path: itemPath,
|
|
757
|
+
message: "must be a control JSONPath starting with $.",
|
|
758
|
+
});
|
|
759
|
+
}
|
|
760
|
+
if (seen.has(item))
|
|
761
|
+
issues.push({ path: itemPath, message: `duplicate value "${item}"` });
|
|
762
|
+
seen.add(item);
|
|
763
|
+
}
|
|
652
764
|
}
|
|
653
765
|
|
|
654
766
|
function validateControlSchemaRef(
|
|
@@ -694,6 +806,7 @@ function validateInputPolicy(
|
|
|
694
806
|
path: string,
|
|
695
807
|
siblingIds: ReadonlySet<string>,
|
|
696
808
|
issues: ValidationIssue[],
|
|
809
|
+
sourceProjection: unknown,
|
|
697
810
|
): void {
|
|
698
811
|
if (value === undefined) return;
|
|
699
812
|
const policy = recordAt(value, path, issues);
|
|
@@ -708,6 +821,33 @@ function validateInputPolicy(
|
|
|
708
821
|
if (policy.enforcement !== undefined && policy.enforcement !== "fail") {
|
|
709
822
|
issues.push({ path: `${path}.enforcement`, message: 'must be "fail"' });
|
|
710
823
|
}
|
|
824
|
+
if (
|
|
825
|
+
policy.artifactAccess !== undefined &&
|
|
826
|
+
policy.artifactAccess !== "enabled" &&
|
|
827
|
+
policy.artifactAccess !== "none"
|
|
828
|
+
) {
|
|
829
|
+
issues.push({
|
|
830
|
+
path: `${path}.artifactAccess`,
|
|
831
|
+
message: 'must be "enabled" or "none"',
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
if (policy.artifactAccess === "none") {
|
|
835
|
+
if (
|
|
836
|
+
Array.isArray(policy.requiredReads) &&
|
|
837
|
+
policy.requiredReads.length > 0
|
|
838
|
+
) {
|
|
839
|
+
issues.push({
|
|
840
|
+
path: `${path}.requiredReads`,
|
|
841
|
+
message: 'must be empty when artifactAccess is "none"',
|
|
842
|
+
});
|
|
843
|
+
}
|
|
844
|
+
if (sourceProjection !== undefined) {
|
|
845
|
+
issues.push({
|
|
846
|
+
path: `${path}.artifactAccess`,
|
|
847
|
+
message: 'cannot be "none" when sourceProjection is declared',
|
|
848
|
+
});
|
|
849
|
+
}
|
|
850
|
+
}
|
|
711
851
|
}
|
|
712
852
|
|
|
713
853
|
function validateRequiredReads(
|
package/src/cli.mjs
CHANGED
|
@@ -53,6 +53,7 @@ const tasks = Array.isArray(run.tasks) ? run.tasks : [];
|
|
|
53
53
|
const selected = options.has("--failures")
|
|
54
54
|
? tasks.filter((task) => ["failed", "blocked", "interrupted"].includes(task.status))
|
|
55
55
|
: tasks;
|
|
56
|
+
const reliability = summarizeReliability(tasks);
|
|
56
57
|
|
|
57
58
|
const lines = [
|
|
58
59
|
`runId: ${run.runId}`,
|
|
@@ -60,6 +61,8 @@ const lines = [
|
|
|
60
61
|
`type: ${run.type}`,
|
|
61
62
|
`status: ${run.status}`,
|
|
62
63
|
`tasks: ${tasks.length}`,
|
|
64
|
+
`completion: ${reliability.health}`,
|
|
65
|
+
`retries: output=${reliability.outputRetries}, launch=${reliability.launchRetries}, resumes=${reliability.resumeEvents}, contextLimitFailures=${reliability.contextLimitFailures}`,
|
|
63
66
|
];
|
|
64
67
|
|
|
65
68
|
for (const task of selected) {
|
|
@@ -184,3 +187,52 @@ async function readJson(path) {
|
|
|
184
187
|
function indent(text, prefix) {
|
|
185
188
|
return text.split(/\r?\n/).map((line) => `${prefix}${line}`).join("\n");
|
|
186
189
|
}
|
|
190
|
+
|
|
191
|
+
function summarizeReliability(tasks) {
|
|
192
|
+
let outputRetries = 0;
|
|
193
|
+
let launchRetries = 0;
|
|
194
|
+
let resumeEvents = 0;
|
|
195
|
+
let contextLimitFailures = 0;
|
|
196
|
+
for (const task of tasks) {
|
|
197
|
+
outputRetries += positiveCount(task.outputRetry?.attempts);
|
|
198
|
+
launchRetries += positiveCount(task.launchRetry?.attempts);
|
|
199
|
+
if (hasContextLimitFailure(task)) contextLimitFailures += 1;
|
|
200
|
+
for (const event of Array.isArray(task.resumeEvents) ? task.resumeEvents : []) {
|
|
201
|
+
resumeEvents += 1;
|
|
202
|
+
outputRetries += positiveCount(event.outputRetryAttempts);
|
|
203
|
+
launchRetries += positiveCount(event.launchRetryAttempts);
|
|
204
|
+
if (hasContextLimitFailure(event)) contextLimitFailures += 1;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
const allCompleted = tasks.length > 0 && tasks.every((task) => task.status === "completed");
|
|
208
|
+
const repairEvents = outputRetries + launchRetries + resumeEvents;
|
|
209
|
+
const health = !allCompleted
|
|
210
|
+
? "incomplete"
|
|
211
|
+
: repairEvents === 0 && contextLimitFailures === 0
|
|
212
|
+
? "clean"
|
|
213
|
+
: "repaired";
|
|
214
|
+
return { health, outputRetries, launchRetries, resumeEvents, contextLimitFailures };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function positiveCount(value) {
|
|
218
|
+
return Number.isFinite(value) ? Math.max(0, Math.floor(value)) : 0;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function hasContextLimitFailure(value) {
|
|
222
|
+
return [
|
|
223
|
+
value?.statusDetail,
|
|
224
|
+
value?.fromStatusDetail,
|
|
225
|
+
value?.lastMessage,
|
|
226
|
+
value?.outputRetry?.reason,
|
|
227
|
+
value?.outputRetry?.message,
|
|
228
|
+
value?.launchRetry?.reason,
|
|
229
|
+
value?.launchRetry?.message,
|
|
230
|
+
value?.outputRetryReason,
|
|
231
|
+
value?.launchRetryReason,
|
|
232
|
+
].some(isContextLimitText);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function isContextLimitText(value) {
|
|
236
|
+
const text = String(value ?? "").toLowerCase();
|
|
237
|
+
return text.includes("context_or_request_too_large") || /context (window|length)|maximum context|request too large|token limit/.test(text);
|
|
238
|
+
}
|