@agwab/pi-workflow 0.2.1 → 0.3.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/dist/compiler.js +6 -8
- package/dist/dynamic-decision.d.ts +0 -1
- package/dist/dynamic-decision.js +0 -7
- package/dist/dynamic-profiles.d.ts +0 -1
- package/dist/dynamic-profiles.js +0 -3
- package/dist/engine-run-graph.d.ts +1 -0
- package/dist/engine-run-graph.js +142 -2
- package/dist/engine.d.ts +5 -0
- package/dist/engine.js +112 -27
- package/dist/extension.d.ts +2 -1
- package/dist/extension.js +27 -6
- package/dist/index.d.ts +3 -3
- package/dist/index.js +2 -1
- package/dist/store.js +55 -11
- package/dist/subagent-backend.js +155 -29
- package/dist/types.d.ts +6 -0
- package/dist/workflow-runtime.js +10 -1
- package/dist/workflow-view.js +3 -1
- package/dist/workflow-web-source-extension.js +167 -48
- package/dist/workflow-web-source.d.ts +2 -1
- package/dist/workflow-web-source.js +84 -19
- 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 +995 -573
- 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 +1352 -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/src/compiler.ts +14 -9
- package/src/dynamic-decision.ts +0 -11
- package/src/dynamic-profiles.ts +0 -4
- package/src/engine-run-graph.ts +185 -2
- package/src/engine.ts +145 -24
- package/src/extension.ts +33 -4
- package/src/index.ts +3 -1
- package/src/store.ts +74 -11
- package/src/subagent-backend.ts +201 -28
- package/src/types.ts +6 -0
- package/src/workflow-runtime.ts +18 -2
- package/src/workflow-view.ts +2 -1
- package/src/workflow-web-source-extension.ts +621 -228
- package/src/workflow-web-source.ts +118 -28
- package/workflows/deep-research/helpers/claim-evidence-gate.mjs +56 -16
- package/workflows/deep-research/helpers/final-audit-packet.mjs +1 -4
- package/workflows/deep-research/helpers/normalize-input-packet.mjs +1 -1
- package/workflows/deep-research/helpers/render-executive.mjs +8 -21
- package/workflows/deep-research/helpers/sanitize-verification-candidates.mjs +89 -15
- package/workflows/deep-research/schemas/deep-research-executive-render-control.schema.json +0 -1
- package/workflows/deep-research/schemas/deep-research-verify-claims-control.schema.json +4 -1
- 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/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.3.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.0",
|
|
80
80
|
"pi-web-access": "^0.10.7",
|
|
81
81
|
"typebox": "^1.1.39"
|
|
82
82
|
},
|
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 { compileRole } from "./roles.js";
|
|
6
7
|
import {
|
|
7
8
|
classifyToolCapability,
|
|
8
9
|
effectiveToolClassification,
|
|
@@ -663,15 +664,19 @@ async function compileArtifactGraphPlan(
|
|
|
663
664
|
return defaultAgent;
|
|
664
665
|
};
|
|
665
666
|
const roleEntries = Object.entries(spec.roles ?? {});
|
|
666
|
-
const roles =
|
|
667
|
-
name,
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
667
|
+
const roles = await Promise.all(
|
|
668
|
+
roleEntries.map(async ([name, role]: [string, any]) => {
|
|
669
|
+
const sourceAgent = role.fromAgent
|
|
670
|
+
? await loadWorkflowAgent(
|
|
671
|
+
role.fromAgent,
|
|
672
|
+
options.cwd,
|
|
673
|
+
agentCache,
|
|
674
|
+
`$.roles.${name}.fromAgent`,
|
|
675
|
+
)
|
|
676
|
+
: undefined;
|
|
677
|
+
return compileRole(name, role, sourceAgent);
|
|
678
|
+
}),
|
|
679
|
+
);
|
|
675
680
|
const roleText = roles.length
|
|
676
681
|
? `# Role Context\n\n${roles.map((r) => `## Role: ${r.name}\n${r.content}`).join("\n\n")}`
|
|
677
682
|
: "";
|
package/src/dynamic-decision.ts
CHANGED
|
@@ -283,17 +283,6 @@ 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
|
-
|
|
297
286
|
export function hashDynamicDecision(value: unknown): string {
|
|
298
287
|
return createHash("sha256")
|
|
299
288
|
.update(stableStringify(toJsonNormalizedValue(value)))
|
package/src/dynamic-profiles.ts
CHANGED
|
@@ -44,7 +44,3 @@ 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(): string[] {
|
|
49
|
-
return [...DYNAMIC_OUTPUT_PROFILES];
|
|
50
|
-
}
|
package/src/engine-run-graph.ts
CHANGED
|
@@ -127,6 +127,135 @@ export function reconcileDynamicGeneratedRunRecords(
|
|
|
127
127
|
return changed;
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
+
export function reconcileForeachGeneratedRunRecords(
|
|
131
|
+
cwd: string,
|
|
132
|
+
run: WorkflowRunRecord,
|
|
133
|
+
compiledFlow: CompiledWorkflow,
|
|
134
|
+
): boolean {
|
|
135
|
+
let changed = false;
|
|
136
|
+
const compiledSpecIds = new Set(
|
|
137
|
+
compiledFlow.tasks.map((task) => compiledTaskSpecId(task)),
|
|
138
|
+
);
|
|
139
|
+
const placeholderToGeneratedSpecIds = new Map<string, string[]>();
|
|
140
|
+
|
|
141
|
+
for (const compiledTask of compiledFlow.tasks) {
|
|
142
|
+
const specId = compiledTaskSpecId(compiledTask);
|
|
143
|
+
const placeholderSpecId = foreachGeneratedPlaceholderSpecId(
|
|
144
|
+
compiledTask,
|
|
145
|
+
compiledFlow,
|
|
146
|
+
specId,
|
|
147
|
+
);
|
|
148
|
+
if (!placeholderSpecId) continue;
|
|
149
|
+
if (
|
|
150
|
+
compiledTask.foreachGenerated?.placeholderSpecId !== placeholderSpecId
|
|
151
|
+
) {
|
|
152
|
+
compiledTask.foreachGenerated = { placeholderSpecId };
|
|
153
|
+
changed = true;
|
|
154
|
+
}
|
|
155
|
+
const generated =
|
|
156
|
+
placeholderToGeneratedSpecIds.get(placeholderSpecId) ?? [];
|
|
157
|
+
generated.push(specId);
|
|
158
|
+
placeholderToGeneratedSpecIds.set(placeholderSpecId, generated);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (placeholderToGeneratedSpecIds.size === 0) return changed;
|
|
162
|
+
|
|
163
|
+
const filteredRunTasks: WorkflowTaskRunRecord[] = [];
|
|
164
|
+
const seenGeneratedSpecIds = new Set<string>();
|
|
165
|
+
for (const task of run.tasks) {
|
|
166
|
+
const generatedSpecIds = placeholderToGeneratedSpecIds.get(task.specId);
|
|
167
|
+
let placeholderSpecId = foreachGeneratedPlaceholderSpecId(
|
|
168
|
+
task,
|
|
169
|
+
compiledFlow,
|
|
170
|
+
task.specId,
|
|
171
|
+
);
|
|
172
|
+
if (generatedSpecIds && !placeholderSpecId) {
|
|
173
|
+
if (generatedSpecIds.includes(task.specId)) {
|
|
174
|
+
placeholderSpecId = task.specId;
|
|
175
|
+
task.foreachGenerated = { placeholderSpecId };
|
|
176
|
+
changed = true;
|
|
177
|
+
} else {
|
|
178
|
+
changed = true;
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (placeholderSpecId && !compiledSpecIds.has(task.specId)) {
|
|
183
|
+
changed = true;
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
if (placeholderSpecId && seenGeneratedSpecIds.has(task.specId)) {
|
|
187
|
+
changed = true;
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
if (placeholderSpecId) {
|
|
191
|
+
seenGeneratedSpecIds.add(task.specId);
|
|
192
|
+
if (task.foreachGenerated?.placeholderSpecId !== placeholderSpecId) {
|
|
193
|
+
task.foreachGenerated = { placeholderSpecId };
|
|
194
|
+
changed = true;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
filteredRunTasks.push(task);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const runTaskBySpecId = new Map<string, WorkflowTaskRunRecord>();
|
|
201
|
+
for (const task of filteredRunTasks) {
|
|
202
|
+
if (!runTaskBySpecId.has(task.specId))
|
|
203
|
+
runTaskBySpecId.set(task.specId, task);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const reordered: WorkflowTaskRunRecord[] = [];
|
|
207
|
+
const usedSpecIds = new Set<string>();
|
|
208
|
+
let nextIndex = nextTaskRecordIndex({ ...run, tasks: filteredRunTasks });
|
|
209
|
+
for (const compiledTask of compiledFlow.tasks) {
|
|
210
|
+
const specId = compiledTaskSpecId(compiledTask);
|
|
211
|
+
const existing = runTaskBySpecId.get(specId);
|
|
212
|
+
if (existing) {
|
|
213
|
+
const placeholderSpecId =
|
|
214
|
+
compiledTask.foreachGenerated?.placeholderSpecId;
|
|
215
|
+
if (
|
|
216
|
+
placeholderSpecId &&
|
|
217
|
+
existing.foreachGenerated?.placeholderSpecId !== placeholderSpecId
|
|
218
|
+
) {
|
|
219
|
+
existing.foreachGenerated = { placeholderSpecId };
|
|
220
|
+
changed = true;
|
|
221
|
+
}
|
|
222
|
+
reordered.push(existing);
|
|
223
|
+
usedSpecIds.add(specId);
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
if (!compiledTask.foreachGenerated) continue;
|
|
227
|
+
const created = createTaskRunRecord(
|
|
228
|
+
cwd,
|
|
229
|
+
run.runId,
|
|
230
|
+
compiledTask,
|
|
231
|
+
nextIndex,
|
|
232
|
+
);
|
|
233
|
+
nextIndex += 1;
|
|
234
|
+
reordered.push(created);
|
|
235
|
+
usedSpecIds.add(specId);
|
|
236
|
+
changed = true;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
for (const task of filteredRunTasks) {
|
|
240
|
+
if (!usedSpecIds.has(task.specId)) reordered.push(task);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (!sameTaskRecordOrder(run.tasks, reordered)) changed = true;
|
|
244
|
+
for (const task of reordered) {
|
|
245
|
+
if (!task.dependsOn) continue;
|
|
246
|
+
const replaced = replaceForeachGeneratedDependencies(
|
|
247
|
+
task.dependsOn,
|
|
248
|
+
placeholderToGeneratedSpecIds,
|
|
249
|
+
);
|
|
250
|
+
if (!sameStringList(task.dependsOn, replaced)) {
|
|
251
|
+
task.dependsOn = replaced;
|
|
252
|
+
changed = true;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
if (changed) run.tasks = reordered;
|
|
256
|
+
return changed;
|
|
257
|
+
}
|
|
258
|
+
|
|
130
259
|
export function assertRunTaskPositionalAlignment(
|
|
131
260
|
run: WorkflowRunRecord,
|
|
132
261
|
compiledFlow: CompiledWorkflow,
|
|
@@ -209,6 +338,54 @@ export function compiledTaskSpecId(task: CompiledTask): string {
|
|
|
209
338
|
return typeof specId === "string" && specId.trim() !== "" ? specId : task.id;
|
|
210
339
|
}
|
|
211
340
|
|
|
341
|
+
function foreachGeneratedPlaceholderSpecId(
|
|
342
|
+
task: CompiledTask | WorkflowTaskRunRecord,
|
|
343
|
+
compiledFlow: CompiledWorkflow,
|
|
344
|
+
specId: string,
|
|
345
|
+
): string | undefined {
|
|
346
|
+
const explicit = task.foreachGenerated?.placeholderSpecId;
|
|
347
|
+
if (typeof explicit === "string" && explicit.trim() !== "") return explicit;
|
|
348
|
+
if ((task as CompiledTask).foreach) return undefined;
|
|
349
|
+
if (task.kind !== "foreach" || !task.stageId) return undefined;
|
|
350
|
+
const placeholderSpecId = foreachPlaceholderSpecId(
|
|
351
|
+
compiledFlow,
|
|
352
|
+
task.stageId,
|
|
353
|
+
);
|
|
354
|
+
if (!placeholderSpecId || specId === placeholderSpecId) return undefined;
|
|
355
|
+
return placeholderSpecId;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function foreachPlaceholderSpecId(
|
|
359
|
+
compiledFlow: CompiledWorkflow,
|
|
360
|
+
stageId: string,
|
|
361
|
+
): string | undefined {
|
|
362
|
+
const stage = ((compiledFlow as any).stages ?? []).find(
|
|
363
|
+
(candidate: any) => candidate?.id === stageId,
|
|
364
|
+
);
|
|
365
|
+
if (stage?.type !== "foreach") return undefined;
|
|
366
|
+
return `${stageId}.item`;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function replaceForeachGeneratedDependencies(
|
|
370
|
+
dependsOn: string[],
|
|
371
|
+
placeholderToGeneratedSpecIds: Map<string, string[]>,
|
|
372
|
+
): string[] {
|
|
373
|
+
const replaced: string[] = [];
|
|
374
|
+
for (const dep of dependsOn) {
|
|
375
|
+
const generatedSpecIds = placeholderToGeneratedSpecIds.get(dep);
|
|
376
|
+
if (generatedSpecIds) replaced.push(...generatedSpecIds);
|
|
377
|
+
else replaced.push(dep);
|
|
378
|
+
}
|
|
379
|
+
return [...new Set(replaced)];
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function sameStringList(left: string[], right: string[]): boolean {
|
|
383
|
+
return (
|
|
384
|
+
left.length === right.length &&
|
|
385
|
+
left.every((value, index) => value === right[index])
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
|
|
212
389
|
function isLoopGeneratedCompiledTask(
|
|
213
390
|
task: CompiledTask,
|
|
214
391
|
loopIds: Set<string>,
|
|
@@ -330,7 +507,7 @@ export function buildForeachGeneratedTasks(
|
|
|
330
507
|
const itemText = formatForeachItem(item);
|
|
331
508
|
const instructions = template.foreach!.prompt.replace(
|
|
332
509
|
/\$\{item\}/g,
|
|
333
|
-
itemText,
|
|
510
|
+
escapeReplacementText(itemText),
|
|
334
511
|
);
|
|
335
512
|
const compiledPrompt = [
|
|
336
513
|
template.foreach!.injectRuntimeTask && runtimeTask
|
|
@@ -352,6 +529,7 @@ export function buildForeachGeneratedTasks(
|
|
|
352
529
|
compiledPrompt,
|
|
353
530
|
dependsOn: [...(template.dependsOn ?? [])],
|
|
354
531
|
foreach: undefined,
|
|
532
|
+
foreachGenerated: { placeholderSpecId: template.id },
|
|
355
533
|
} as CompiledTask);
|
|
356
534
|
}
|
|
357
535
|
return { tasks };
|
|
@@ -382,6 +560,10 @@ function formatForeachItem(item: unknown): string {
|
|
|
382
560
|
return typeof item === "string" ? item : JSON.stringify(item);
|
|
383
561
|
}
|
|
384
562
|
|
|
563
|
+
function escapeReplacementText(value: string): string {
|
|
564
|
+
return value.replace(/\$/g, "$$$$");
|
|
565
|
+
}
|
|
566
|
+
|
|
385
567
|
export function sourceStageIdsForFrom(from: unknown): string[] {
|
|
386
568
|
if (Array.isArray(from))
|
|
387
569
|
return from.filter((item): item is string => typeof item === "string");
|
|
@@ -453,7 +635,8 @@ export function markDagDependentsSkipped(
|
|
|
453
635
|
return (
|
|
454
636
|
status === "failed" ||
|
|
455
637
|
status === "interrupted" ||
|
|
456
|
-
status === "skipped"
|
|
638
|
+
status === "skipped" ||
|
|
639
|
+
status === "blocked"
|
|
457
640
|
);
|
|
458
641
|
});
|
|
459
642
|
if (!failedDep) continue;
|