@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
|
@@ -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
|
+
}
|
package/src/compiler.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { dirname, resolve } from "node:path";
|
|
|
3
3
|
|
|
4
4
|
import { loadAgentByName } from "./agents.js";
|
|
5
5
|
import { DYNAMIC_OUTPUT_PROFILES } from "./dynamic-profiles.js";
|
|
6
|
+
import { stringifyPromptJson } from "./prompt-json.js";
|
|
6
7
|
import { compileRole } from "./roles.js";
|
|
7
8
|
import {
|
|
8
9
|
classifyToolCapability,
|
|
@@ -179,7 +180,13 @@ function lowerArtifactGraphFrom(from: ArtifactGraphStageSpec["from"]): unknown {
|
|
|
179
180
|
!Array.isArray(from) &&
|
|
180
181
|
typeof from.source === "string"
|
|
181
182
|
) {
|
|
182
|
-
return {
|
|
183
|
+
return {
|
|
184
|
+
stage: from.source,
|
|
185
|
+
path: from.path,
|
|
186
|
+
...((from as { streaming?: unknown }).streaming !== undefined
|
|
187
|
+
? { streaming: (from as { streaming?: unknown }).streaming }
|
|
188
|
+
: {}),
|
|
189
|
+
};
|
|
183
190
|
}
|
|
184
191
|
return from;
|
|
185
192
|
}
|
|
@@ -210,11 +217,26 @@ function appendWorkflowOutputInstructions(
|
|
|
210
217
|
: "Use schema `stage-control-v1` unless the workflow asks for a more specific control schema.",
|
|
211
218
|
"Put detailed prose, reasoning, and evidence discussion in <analysis> only.",
|
|
212
219
|
"Put structured evidence pointers in <refs> as a JSON array; use [] if none.",
|
|
220
|
+
...partialOutputInstructions(stage.output?.partial?.paths),
|
|
213
221
|
]
|
|
214
222
|
.filter(Boolean)
|
|
215
223
|
.join("\n\n");
|
|
216
224
|
}
|
|
217
225
|
|
|
226
|
+
function partialOutputInstructions(
|
|
227
|
+
paths: readonly string[] | undefined,
|
|
228
|
+
): string[] {
|
|
229
|
+
if (!paths || paths.length === 0) return [];
|
|
230
|
+
return [
|
|
231
|
+
"# Workflow Partial Output Protocol (optional)",
|
|
232
|
+
`If a complete stable array item is ready before your final answer for one of these control paths (${paths.join(", ")}), you may emit a partial-control section before the final output:`,
|
|
233
|
+
'<partial-control>{"schema":"workflow-partial-output-v1","path":"$.items","items":[{"id":"stable-id","...":"..."}]}</partial-control>',
|
|
234
|
+
"Use the actual declared path, not the example path, and include only items that are final/stable enough to appear unchanged in your final <control> at that path.",
|
|
235
|
+
"Every partial item must be the exact JSON object that will appear in the final array and must include a stable non-empty string `id`; never revise or withdraw a published partial item.",
|
|
236
|
+
"If an item might change, do not publish it partially; wait for the final workflow output. The final answer must still include the normal <control>, <analysis>, and <refs> sections exactly once.",
|
|
237
|
+
];
|
|
238
|
+
}
|
|
239
|
+
|
|
218
240
|
function artifactGraphTaskMetadata(
|
|
219
241
|
stage: ArtifactGraphStageSpec,
|
|
220
242
|
specDir: string,
|
|
@@ -231,8 +253,12 @@ function artifactGraphTaskMetadata(
|
|
|
231
253
|
? resolve(specDir, controlSchema)
|
|
232
254
|
: undefined,
|
|
233
255
|
maxDigestChars: stage.output?.maxDigestChars,
|
|
256
|
+
partial: stage.output?.partial
|
|
257
|
+
? { paths: [...stage.output.partial.paths] }
|
|
258
|
+
: undefined,
|
|
234
259
|
},
|
|
235
260
|
requiredReads: stage.inputPolicy?.requiredReads ?? [],
|
|
261
|
+
artifactAccess: stage.inputPolicy?.artifactAccess ?? "enabled",
|
|
236
262
|
sourceProjection: stage.sourceProjection,
|
|
237
263
|
};
|
|
238
264
|
}
|
|
@@ -686,7 +712,7 @@ async function compileArtifactGraphPlan(
|
|
|
686
712
|
typeof workflowInput === "object" &&
|
|
687
713
|
!Array.isArray(workflowInput) &&
|
|
688
714
|
Object.keys(workflowInput).length > 0
|
|
689
|
-
? `# Workflow Input\n\n${
|
|
715
|
+
? `# Workflow Input\n\n${stringifyPromptJson(workflowInput)}`
|
|
690
716
|
: "";
|
|
691
717
|
const runtimeOverrides = options.runtimeOverrides;
|
|
692
718
|
const runtimeDefaults = options.runtimeDefaults;
|
|
@@ -836,15 +862,29 @@ async function compileArtifactGraphPlan(
|
|
|
836
862
|
/\$\{item\}/g,
|
|
837
863
|
"the relevant item from the dependency context",
|
|
838
864
|
);
|
|
839
|
-
const
|
|
865
|
+
const instructionText = `# Instructions\n\n${normalizedPrompt}`;
|
|
866
|
+
const stageText = `# Workflow Stage\n\nstage=${stage.id}\ntype=${runtimeStageKind}`;
|
|
867
|
+
const taskText =
|
|
840
868
|
injectRuntimeTaskInPrompt && options.task
|
|
841
869
|
? `# Task\n\n${options.task}`
|
|
842
|
-
: undefined
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
870
|
+
: undefined;
|
|
871
|
+
const compiledPrompt = (
|
|
872
|
+
runtimeStageKind === "foreach"
|
|
873
|
+
? [
|
|
874
|
+
taskText,
|
|
875
|
+
workflowInputText || undefined,
|
|
876
|
+
stageText,
|
|
877
|
+
roleText || undefined,
|
|
878
|
+
instructionText,
|
|
879
|
+
]
|
|
880
|
+
: [
|
|
881
|
+
taskText,
|
|
882
|
+
workflowInputText || undefined,
|
|
883
|
+
stageText,
|
|
884
|
+
instructionText,
|
|
885
|
+
roleText || undefined,
|
|
886
|
+
]
|
|
887
|
+
)
|
|
848
888
|
.filter(Boolean)
|
|
849
889
|
.join("\n\n");
|
|
850
890
|
const toolSelection = resolveToolSelection(
|
package/src/dynamic-decision.ts
CHANGED
|
@@ -283,6 +283,17 @@ export function validateDynamicDecision(
|
|
|
283
283
|
};
|
|
284
284
|
}
|
|
285
285
|
|
|
286
|
+
export function assertValidDynamicDecision(
|
|
287
|
+
value: unknown,
|
|
288
|
+
context: DynamicDecisionValidationContext = {},
|
|
289
|
+
): NormalizedDynamicDecision {
|
|
290
|
+
const result = validateDynamicDecision(value, context);
|
|
291
|
+
if (!result.ok || !result.decision) {
|
|
292
|
+
throw new Error(`invalid dynamic decision: ${result.errors.join("; ")}`);
|
|
293
|
+
}
|
|
294
|
+
return result.decision;
|
|
295
|
+
}
|
|
296
|
+
|
|
286
297
|
export function hashDynamicDecision(value: unknown): string {
|
|
287
298
|
return createHash("sha256")
|
|
288
299
|
.update(stableStringify(toJsonNormalizedValue(value)))
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
} from "./dynamic-profiles.js";
|
|
8
8
|
import { readOrRebuildDynamicState } from "./dynamic-state.js";
|
|
9
9
|
import { sanitizeTaskId } from "./engine-run-graph.js";
|
|
10
|
+
import { compactStrings } from "./strings.js";
|
|
10
11
|
import { fromProjectPath, isTerminalTaskStatus, readJson } from "./store.js";
|
|
11
12
|
import {
|
|
12
13
|
classifyToolCapability,
|
|
@@ -307,6 +308,7 @@ export async function buildDynamicGeneratedCompiledTask(input: {
|
|
|
307
308
|
maxDigestChars: DYNAMIC_OUTPUT_MAX_DIGEST_CHARS,
|
|
308
309
|
},
|
|
309
310
|
requiredReads: input.request.requiredReads,
|
|
311
|
+
artifactAccess: "enabled",
|
|
310
312
|
sourceProjection: dynamicInputSourceProjection(input.request.inputs),
|
|
311
313
|
},
|
|
312
314
|
dynamicGenerated: {
|
|
@@ -966,5 +968,5 @@ function dynamicOutputProfileInstructions(
|
|
|
966
968
|
}
|
|
967
969
|
|
|
968
970
|
function uniqueStrings(values: readonly string[]): string[] {
|
|
969
|
-
return
|
|
971
|
+
return compactStrings(values, { trim: false, dropWhitespaceOnly: true });
|
|
970
972
|
}
|
package/src/dynamic-profiles.ts
CHANGED
|
@@ -44,3 +44,7 @@ export function isTerminalDynamicOutputProfile(
|
|
|
44
44
|
): value is (typeof DYNAMIC_TERMINAL_OUTPUT_PROFILES)[number] {
|
|
45
45
|
return typeof value === "string" && TERMINAL_OUTPUT_PROFILE_SET.has(value);
|
|
46
46
|
}
|
|
47
|
+
|
|
48
|
+
export function dynamicOutputProfileValues(): DynamicOutputProfile[] {
|
|
49
|
+
return [...DYNAMIC_OUTPUT_PROFILES];
|
|
50
|
+
}
|
package/src/engine-run-graph.ts
CHANGED
|
@@ -136,10 +136,17 @@ export function reconcileForeachGeneratedRunRecords(
|
|
|
136
136
|
const compiledSpecIds = new Set(
|
|
137
137
|
compiledFlow.tasks.map((task) => compiledTaskSpecId(task)),
|
|
138
138
|
);
|
|
139
|
+
const compiledTaskBySpecId = new Map(
|
|
140
|
+
compiledFlow.tasks.map((task) => [compiledTaskSpecId(task), task]),
|
|
141
|
+
);
|
|
139
142
|
const placeholderToGeneratedSpecIds = new Map<string, string[]>();
|
|
143
|
+
const streamingPlaceholderSpecIds = new Set<string>();
|
|
140
144
|
|
|
141
145
|
for (const compiledTask of compiledFlow.tasks) {
|
|
142
146
|
const specId = compiledTaskSpecId(compiledTask);
|
|
147
|
+
if (compiledTask.foreach && foreachStreamingEnabled(compiledTask)) {
|
|
148
|
+
streamingPlaceholderSpecIds.add(specId);
|
|
149
|
+
}
|
|
143
150
|
const placeholderSpecId = foreachGeneratedPlaceholderSpecId(
|
|
144
151
|
compiledTask,
|
|
145
152
|
compiledFlow,
|
|
@@ -170,6 +177,14 @@ export function reconcileForeachGeneratedRunRecords(
|
|
|
170
177
|
task.specId,
|
|
171
178
|
);
|
|
172
179
|
if (generatedSpecIds && !placeholderSpecId) {
|
|
180
|
+
const compiledTask = compiledTaskBySpecId.get(task.specId);
|
|
181
|
+
if (
|
|
182
|
+
compiledTask?.foreach &&
|
|
183
|
+
streamingPlaceholderSpecIds.has(task.specId)
|
|
184
|
+
) {
|
|
185
|
+
filteredRunTasks.push(task);
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
173
188
|
if (generatedSpecIds.includes(task.specId)) {
|
|
174
189
|
placeholderSpecId = task.specId;
|
|
175
190
|
task.foreachGenerated = { placeholderSpecId };
|
|
@@ -246,6 +261,7 @@ export function reconcileForeachGeneratedRunRecords(
|
|
|
246
261
|
const replaced = replaceForeachGeneratedDependencies(
|
|
247
262
|
task.dependsOn,
|
|
248
263
|
placeholderToGeneratedSpecIds,
|
|
264
|
+
streamingPlaceholderSpecIds,
|
|
249
265
|
);
|
|
250
266
|
if (!sameStringList(task.dependsOn, replaced)) {
|
|
251
267
|
task.dependsOn = replaced;
|
|
@@ -369,12 +385,15 @@ function foreachPlaceholderSpecId(
|
|
|
369
385
|
function replaceForeachGeneratedDependencies(
|
|
370
386
|
dependsOn: string[],
|
|
371
387
|
placeholderToGeneratedSpecIds: Map<string, string[]>,
|
|
388
|
+
keepPlaceholderSpecIds = new Set<string>(),
|
|
372
389
|
): string[] {
|
|
373
390
|
const replaced: string[] = [];
|
|
374
391
|
for (const dep of dependsOn) {
|
|
375
392
|
const generatedSpecIds = placeholderToGeneratedSpecIds.get(dep);
|
|
376
|
-
if (generatedSpecIds)
|
|
377
|
-
|
|
393
|
+
if (generatedSpecIds) {
|
|
394
|
+
if (keepPlaceholderSpecIds.has(dep)) replaced.push(dep);
|
|
395
|
+
replaced.push(...generatedSpecIds);
|
|
396
|
+
} else replaced.push(dep);
|
|
378
397
|
}
|
|
379
398
|
return [...new Set(replaced)];
|
|
380
399
|
}
|
|
@@ -480,6 +499,29 @@ export function dependenciesReady(
|
|
|
480
499
|
if (deps.length === 0) return true;
|
|
481
500
|
const partial =
|
|
482
501
|
stageSourcePolicy(compiledFlow, compiledTask.stageId ?? "") === "partial";
|
|
502
|
+
if (foreachStreamingEnabled(compiledTask)) {
|
|
503
|
+
let completedDependencyReady = false;
|
|
504
|
+
let runningDependencyMayHavePartialItems = false;
|
|
505
|
+
let allKnownDependenciesTerminal = true;
|
|
506
|
+
for (const dep of deps) {
|
|
507
|
+
const status = bySpecId.get(dep)?.status;
|
|
508
|
+
if (status === "completed") {
|
|
509
|
+
completedDependencyReady = true;
|
|
510
|
+
continue;
|
|
511
|
+
}
|
|
512
|
+
if (status && isTerminalTaskStatus(status)) {
|
|
513
|
+
if (!partial) return false;
|
|
514
|
+
continue;
|
|
515
|
+
}
|
|
516
|
+
if (status === "running") runningDependencyMayHavePartialItems = true;
|
|
517
|
+
allKnownDependenciesTerminal = false;
|
|
518
|
+
}
|
|
519
|
+
return (
|
|
520
|
+
completedDependencyReady ||
|
|
521
|
+
runningDependencyMayHavePartialItems ||
|
|
522
|
+
allKnownDependenciesTerminal
|
|
523
|
+
);
|
|
524
|
+
}
|
|
483
525
|
return deps.every((dep) => {
|
|
484
526
|
const status = bySpecId.get(dep)?.status;
|
|
485
527
|
if (status === "completed") return true;
|
|
@@ -488,6 +530,22 @@ export function dependenciesReady(
|
|
|
488
530
|
});
|
|
489
531
|
}
|
|
490
532
|
|
|
533
|
+
export function foreachStreamingEnabled(compiledTask: CompiledTask): boolean {
|
|
534
|
+
const streaming = (compiledTask.foreach?.from as any)?.streaming;
|
|
535
|
+
return Boolean(
|
|
536
|
+
streaming &&
|
|
537
|
+
typeof streaming === "object" &&
|
|
538
|
+
(streaming as { enabled?: unknown }).enabled === true,
|
|
539
|
+
);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
export function foreachStreamingMinChunk(compiledTask: CompiledTask): number {
|
|
543
|
+
const value = (compiledTask.foreach?.from as any)?.streaming?.minChunk;
|
|
544
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0
|
|
545
|
+
? Math.floor(value)
|
|
546
|
+
: 1;
|
|
547
|
+
}
|
|
548
|
+
|
|
491
549
|
export function buildForeachGeneratedTasks(
|
|
492
550
|
template: CompiledTask,
|
|
493
551
|
runtimeTask: string | undefined,
|
|
@@ -513,9 +571,10 @@ export function buildForeachGeneratedTasks(
|
|
|
513
571
|
template.foreach!.injectRuntimeTask && runtimeTask
|
|
514
572
|
? `# Task\n\n${runtimeTask}`
|
|
515
573
|
: undefined,
|
|
516
|
-
`# Workflow Stage\n\nstage=${template.stageId}\ntype=foreach
|
|
517
|
-
`# Instructions\n\n${instructions}`,
|
|
574
|
+
`# Workflow Stage\n\nstage=${template.stageId}\ntype=foreach`,
|
|
518
575
|
template.foreach!.roleText || undefined,
|
|
576
|
+
`# Workflow Item\n\nitem=${taskId}`,
|
|
577
|
+
`# Instructions\n\n${instructions}`,
|
|
519
578
|
]
|
|
520
579
|
.filter(Boolean)
|
|
521
580
|
.join("\n\n");
|