lilflow 0.1.0 → 0.2.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 +178 -51
- package/package.json +4 -3
- package/plugin/plugin.json +7 -0
- package/plugin/skills/lilflow-workflow-driver/SKILL.md +110 -0
- package/src/agents/index.js +41 -1
- package/src/agents/output-file.js +204 -0
- package/src/cli.js +17 -9
- package/src/run-workflow.js +202 -1
- package/src/session-bridge.js +644 -0
- package/src/session-prompt.js +59 -0
- package/src/session-runner.js +150 -0
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Error thrown when post-processing agent output fails (invalid JSON, write
|
|
6
|
+
* failure, etc.). The dispatcher converts these to step failures.
|
|
7
|
+
*/
|
|
8
|
+
export class AgentOutputError extends Error {
|
|
9
|
+
/**
|
|
10
|
+
* @param {string} message - Human-readable error.
|
|
11
|
+
* @param {string} code - Stable machine-readable code.
|
|
12
|
+
*/
|
|
13
|
+
constructor(message, code) {
|
|
14
|
+
super(message);
|
|
15
|
+
this.name = "AgentOutputError";
|
|
16
|
+
this.code = code;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Write an agent step's captured stdout to disk.
|
|
22
|
+
*
|
|
23
|
+
* When `format === 'json'`, strip surrounding markdown fences / prose, extract
|
|
24
|
+
* the first balanced JSON object or array, validate it parses, and write the
|
|
25
|
+
* cleaned JSON. When `format === 'text'` (or omitted), write raw stdout.
|
|
26
|
+
*
|
|
27
|
+
* Failures (missing JSON, invalid JSON, unwritable path) throw AgentOutputError
|
|
28
|
+
* so the dispatcher can fail the step cleanly.
|
|
29
|
+
*
|
|
30
|
+
* @param {object} options - Write options.
|
|
31
|
+
* @param {string} options.outputFile - Target file path (relative or absolute).
|
|
32
|
+
* @param {"text" | "json"} [options.format="text"] - Output format.
|
|
33
|
+
* @param {string} options.stdout - Captured agent stdout.
|
|
34
|
+
* @param {string} options.cwd - Workflow working directory (for relative paths).
|
|
35
|
+
* @param {string} options.stepName - Step name used in error messages.
|
|
36
|
+
* @returns {Promise<{bytesWritten: number, path: string}>} Write result.
|
|
37
|
+
*/
|
|
38
|
+
export async function writeAgentOutput(options) {
|
|
39
|
+
const { outputFile, format = "text", stdout, cwd, stepName } = options;
|
|
40
|
+
const absolute = path.isAbsolute(outputFile) ? outputFile : path.join(cwd, outputFile);
|
|
41
|
+
const content = format === "json"
|
|
42
|
+
? extractAndValidateJson(stdout, stepName)
|
|
43
|
+
: stdout;
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
await mkdir(path.dirname(absolute), { recursive: true });
|
|
47
|
+
await writeFile(absolute, content, "utf8");
|
|
48
|
+
} catch (error) {
|
|
49
|
+
throw new AgentOutputError(
|
|
50
|
+
`Agent step '${stepName}' failed to write output_file '${outputFile}': ${error.message}`,
|
|
51
|
+
"AGENT_OUTPUT_WRITE_FAILED"
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return { bytesWritten: Buffer.byteLength(content, "utf8"), path: absolute };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Strip markdown fences and prose, extract the first balanced JSON object or
|
|
60
|
+
* array, validate it parses, and return the cleaned JSON text.
|
|
61
|
+
*
|
|
62
|
+
* @param {string} raw - Captured agent stdout.
|
|
63
|
+
* @param {string} stepName - Step name used in error messages.
|
|
64
|
+
* @returns {string} Cleaned JSON text (guaranteed to JSON.parse).
|
|
65
|
+
*/
|
|
66
|
+
export function extractAndValidateJson(raw, stepName) {
|
|
67
|
+
if (typeof raw !== "string") {
|
|
68
|
+
throw new AgentOutputError(
|
|
69
|
+
`Agent step '${stepName}' produced no stdout to write as JSON.`,
|
|
70
|
+
"AGENT_OUTPUT_EMPTY"
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const stripped = stripMarkdownFences(raw).trim();
|
|
75
|
+
|
|
76
|
+
if (stripped === "") {
|
|
77
|
+
throw new AgentOutputError(
|
|
78
|
+
`Agent step '${stepName}' produced empty stdout — no JSON to extract.`,
|
|
79
|
+
"AGENT_OUTPUT_EMPTY"
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const extracted = extractBalancedJson(stripped);
|
|
84
|
+
|
|
85
|
+
if (extracted === null) {
|
|
86
|
+
throw new AgentOutputError(
|
|
87
|
+
`Agent step '${stepName}' stdout contains no balanced JSON object or array.`,
|
|
88
|
+
"AGENT_OUTPUT_NO_JSON"
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
JSON.parse(extracted);
|
|
94
|
+
} catch (error) {
|
|
95
|
+
throw new AgentOutputError(
|
|
96
|
+
`Agent step '${stepName}' stdout is not valid JSON: ${error.message}`,
|
|
97
|
+
"AGENT_OUTPUT_INVALID_JSON"
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return extracted;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Strip leading/trailing markdown fences (``` or ```json) from a string.
|
|
106
|
+
*
|
|
107
|
+
* @param {string} raw - Raw text.
|
|
108
|
+
* @returns {string} Text with outer fences removed.
|
|
109
|
+
*/
|
|
110
|
+
function stripMarkdownFences(raw) {
|
|
111
|
+
const fencePattern = /^```(?:json|JSON|jsonc)?\s*\n([\s\S]*?)\n```\s*$/;
|
|
112
|
+
const match = fencePattern.exec(raw.trim());
|
|
113
|
+
|
|
114
|
+
if (match) {
|
|
115
|
+
return match[1];
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Also handle fences that are anywhere in the string — take the content of the
|
|
119
|
+
// first fenced block.
|
|
120
|
+
const innerPattern = /```(?:json|JSON|jsonc)?\s*\n([\s\S]*?)\n```/;
|
|
121
|
+
const innerMatch = innerPattern.exec(raw);
|
|
122
|
+
|
|
123
|
+
if (innerMatch) {
|
|
124
|
+
return innerMatch[1];
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return raw;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Find and return the first balanced `{...}` or `[...]` substring that parses.
|
|
132
|
+
*
|
|
133
|
+
* Scans left-to-right, tracking brace depth, respecting strings (including
|
|
134
|
+
* escape sequences) so that braces inside string literals don't affect depth.
|
|
135
|
+
*
|
|
136
|
+
* @param {string} input - Input text.
|
|
137
|
+
* @returns {string | null} Balanced JSON substring or null when absent.
|
|
138
|
+
*/
|
|
139
|
+
export function extractBalancedJson(input) {
|
|
140
|
+
const openers = /[{[]/g;
|
|
141
|
+
let match;
|
|
142
|
+
|
|
143
|
+
while ((match = openers.exec(input)) !== null) {
|
|
144
|
+
const start = match.index;
|
|
145
|
+
const end = findBalancedEnd(input, start);
|
|
146
|
+
|
|
147
|
+
if (end === -1) continue;
|
|
148
|
+
|
|
149
|
+
const candidate = input.slice(start, end + 1);
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
JSON.parse(candidate);
|
|
153
|
+
return candidate;
|
|
154
|
+
} catch {
|
|
155
|
+
// try next opener
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Find the index of the balanced closer for the opener at `startIndex`.
|
|
164
|
+
*
|
|
165
|
+
* @param {string} input - Input text.
|
|
166
|
+
* @param {number} startIndex - Index of the `{` or `[` opener.
|
|
167
|
+
* @returns {number} Index of the matching closer, or -1 if unbalanced.
|
|
168
|
+
*/
|
|
169
|
+
function findBalancedEnd(input, startIndex) {
|
|
170
|
+
const opener = input[startIndex];
|
|
171
|
+
const closer = opener === "{" ? "}" : "]";
|
|
172
|
+
let depth = 0;
|
|
173
|
+
let inString = false;
|
|
174
|
+
let escape = false;
|
|
175
|
+
|
|
176
|
+
for (let i = startIndex; i < input.length; i += 1) {
|
|
177
|
+
const ch = input[i];
|
|
178
|
+
|
|
179
|
+
if (inString) {
|
|
180
|
+
if (escape) {
|
|
181
|
+
escape = false;
|
|
182
|
+
} else if (ch === "\\") {
|
|
183
|
+
escape = true;
|
|
184
|
+
} else if (ch === "\"") {
|
|
185
|
+
inString = false;
|
|
186
|
+
}
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (ch === "\"") {
|
|
191
|
+
inString = true;
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (ch === opener) {
|
|
196
|
+
depth += 1;
|
|
197
|
+
} else if (ch === closer) {
|
|
198
|
+
depth -= 1;
|
|
199
|
+
if (depth === 0) return i;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return -1;
|
|
204
|
+
}
|
package/src/cli.js
CHANGED
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
runWorkflowSignalCommand,
|
|
30
30
|
runWorkflowStatusCommand
|
|
31
31
|
} from "./run-workflow.js";
|
|
32
|
+
import { runSessionBridgeCommand, getSessionBridgeHelpText } from "./session-bridge.js";
|
|
32
33
|
|
|
33
34
|
/**
|
|
34
35
|
* Return the CLI help text for `flow`.
|
|
@@ -48,18 +49,20 @@ Usage:
|
|
|
48
49
|
flow status
|
|
49
50
|
flow list
|
|
50
51
|
flow logs
|
|
52
|
+
flow session-bridge <subcommand>
|
|
51
53
|
flow --help
|
|
52
54
|
|
|
53
55
|
Commands:
|
|
54
|
-
init
|
|
55
|
-
config
|
|
56
|
-
run
|
|
57
|
-
resume
|
|
58
|
-
set-step
|
|
59
|
-
signal
|
|
60
|
-
status
|
|
61
|
-
list
|
|
62
|
-
logs
|
|
56
|
+
init Initialize flow in the current repository or from a reusable template
|
|
57
|
+
config Show or initialize flow configuration
|
|
58
|
+
run Execute a workflow from YAML
|
|
59
|
+
resume Resume a failed persisted workflow run
|
|
60
|
+
set-step Manually set which persisted workflow step to resume from
|
|
61
|
+
signal Deliver a signal to a waiting workflow step
|
|
62
|
+
status Show the status of a persisted workflow run
|
|
63
|
+
list List persisted workflow runs
|
|
64
|
+
logs Show persisted logs for a workflow run
|
|
65
|
+
session-bridge Agent-facing bridge commands for session-mode workflows`;
|
|
63
66
|
}
|
|
64
67
|
|
|
65
68
|
/**
|
|
@@ -123,6 +126,10 @@ export async function runCli(argv, stdout = console.log, stderr = console.error,
|
|
|
123
126
|
return runWorkflowSignalCommand(commandArgs, stdout, stderr, { cwd, env, homeDir });
|
|
124
127
|
}
|
|
125
128
|
|
|
129
|
+
if (command === "session-bridge") {
|
|
130
|
+
return runSessionBridgeCommand(commandArgs, stdout, stderr, { cwd, env });
|
|
131
|
+
}
|
|
132
|
+
|
|
126
133
|
stderr(`Unknown command: ${command}`);
|
|
127
134
|
stderr("Run 'flow --help' to see available commands.");
|
|
128
135
|
return 1;
|
|
@@ -193,6 +200,7 @@ export {
|
|
|
193
200
|
getLogsHelpText,
|
|
194
201
|
getResumeHelpText,
|
|
195
202
|
getRunHelpText,
|
|
203
|
+
getSessionBridgeHelpText,
|
|
196
204
|
getSetStepHelpText,
|
|
197
205
|
getSignalHelpText,
|
|
198
206
|
getStatusHelpText
|
package/src/run-workflow.js
CHANGED
|
@@ -8,6 +8,7 @@ import readline from "node:readline/promises";
|
|
|
8
8
|
import yaml from "js-yaml";
|
|
9
9
|
import { ConfigError, loadConfig } from "./config.js";
|
|
10
10
|
import { AgentDispatchError, executeAgentStep } from "./agents/index.js";
|
|
11
|
+
import { executeSessionWorkflow } from "./session-runner.js";
|
|
11
12
|
|
|
12
13
|
const ANSI_RESET = "\u001B[0m";
|
|
13
14
|
const ANSI_RED = "\u001B[31m";
|
|
@@ -596,6 +597,8 @@ export function parseWorkflowContent(content, sourceLabel) {
|
|
|
596
597
|
throw new WorkflowError(`${sourceLabel} must define a non-empty steps array.`);
|
|
597
598
|
}
|
|
598
599
|
|
|
600
|
+
const mode = validateWorkflowMode(parsed.mode, sourceLabel);
|
|
601
|
+
const session = validateWorkflowSession(parsed.session, mode, sourceLabel);
|
|
599
602
|
const parameters = validateWorkflowParameters(parsed.parameters, sourceLabel);
|
|
600
603
|
const defaults = validateWorkflowDefaults(parsed.defaults, sourceLabel);
|
|
601
604
|
const authoredSteps = parsed.steps.map((step, index) => validateStep(step, index, sourceLabel));
|
|
@@ -607,12 +610,110 @@ export function parseWorkflowContent(content, sourceLabel) {
|
|
|
607
610
|
return {
|
|
608
611
|
name: parsed.name.trim(),
|
|
609
612
|
version: parsed.version?.trim(),
|
|
613
|
+
mode,
|
|
614
|
+
...(session === undefined ? {} : { session }),
|
|
610
615
|
parameters,
|
|
611
616
|
defaults,
|
|
612
617
|
steps
|
|
613
618
|
};
|
|
614
619
|
}
|
|
615
620
|
|
|
621
|
+
/**
|
|
622
|
+
* Validate the workflow-level `mode` field.
|
|
623
|
+
*
|
|
624
|
+
* @param {unknown} mode - Raw mode value from the YAML document.
|
|
625
|
+
* @param {string} sourceLabel - Error label.
|
|
626
|
+
* @returns {"classic" | "session"} Normalized mode, defaulting to "classic".
|
|
627
|
+
*/
|
|
628
|
+
function validateWorkflowMode(mode, sourceLabel) {
|
|
629
|
+
if (mode === undefined) {
|
|
630
|
+
return "classic";
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
if (mode !== "classic" && mode !== "session") {
|
|
634
|
+
throw new WorkflowError(`${sourceLabel} mode must be 'classic' or 'session' when provided.`);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
return mode;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
/**
|
|
641
|
+
* Validate and normalize the workflow-level `session` block. Required when
|
|
642
|
+
* `mode: session`; forbidden when `mode: classic`.
|
|
643
|
+
*
|
|
644
|
+
* @param {unknown} session - Raw session block from the YAML document.
|
|
645
|
+
* @param {"classic" | "session"} mode - Resolved workflow mode.
|
|
646
|
+
* @param {string} sourceLabel - Error label.
|
|
647
|
+
* @returns {object | undefined} Normalized session config or undefined in classic mode.
|
|
648
|
+
*/
|
|
649
|
+
function validateWorkflowSession(session, mode, sourceLabel) {
|
|
650
|
+
if (mode === "classic") {
|
|
651
|
+
if (session !== undefined) {
|
|
652
|
+
throw new WorkflowError(`${sourceLabel} session must be omitted when mode is 'classic'.`);
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
return undefined;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
if (session === undefined) {
|
|
659
|
+
throw new WorkflowError(`${sourceLabel} session is required when mode is 'session'.`);
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
if (session == null || typeof session !== "object" || Array.isArray(session)) {
|
|
663
|
+
throw new WorkflowError(`${sourceLabel} session must be a YAML object.`);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
if (session.provider !== "claude-code" && session.provider !== "opencode") {
|
|
667
|
+
throw new WorkflowError(`${sourceLabel} session.provider must be 'claude-code' or 'opencode'.`);
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
if (session.model !== undefined && (typeof session.model !== "string" || session.model.trim() === "")) {
|
|
671
|
+
throw new WorkflowError(`${sourceLabel} session.model must be a non-empty string when provided.`);
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
if (session.system_prompt !== undefined
|
|
675
|
+
&& (typeof session.system_prompt !== "string" || session.system_prompt.trim() === "")) {
|
|
676
|
+
throw new WorkflowError(`${sourceLabel} session.system_prompt must be a non-empty string when provided.`);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
if (session.compact_after !== undefined
|
|
680
|
+
&& (typeof session.compact_after !== "number"
|
|
681
|
+
|| !Number.isFinite(session.compact_after)
|
|
682
|
+
|| session.compact_after <= 0
|
|
683
|
+
|| !Number.isInteger(session.compact_after))) {
|
|
684
|
+
throw new WorkflowError(`${sourceLabel} session.compact_after must be a positive integer when provided.`);
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
if (session.allow_tools !== undefined) {
|
|
688
|
+
if (!Array.isArray(session.allow_tools) || session.allow_tools.some((entry) => typeof entry !== "string")) {
|
|
689
|
+
throw new WorkflowError(`${sourceLabel} session.allow_tools must be an array of strings when provided.`);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
if (session.allow_tools.some((entry) => entry.startsWith("-"))) {
|
|
693
|
+
throw new WorkflowError(`${sourceLabel} session.allow_tools entries must not start with '-'.`);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
if (session.plugins !== undefined) {
|
|
698
|
+
if (!Array.isArray(session.plugins) || session.plugins.some((entry) => typeof entry !== "string")) {
|
|
699
|
+
throw new WorkflowError(`${sourceLabel} session.plugins must be an array of strings when provided.`);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
if (session.plugins.some((entry) => entry.startsWith("-"))) {
|
|
703
|
+
throw new WorkflowError(`${sourceLabel} session.plugins entries must not start with '-'.`);
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
return {
|
|
708
|
+
provider: session.provider,
|
|
709
|
+
...(session.model === undefined ? {} : { model: session.model.trim() }),
|
|
710
|
+
...(session.system_prompt === undefined ? {} : { systemPrompt: session.system_prompt.trim() }),
|
|
711
|
+
...(session.compact_after === undefined ? {} : { compactAfter: session.compact_after }),
|
|
712
|
+
...(session.allow_tools === undefined ? {} : { allowTools: [...session.allow_tools] }),
|
|
713
|
+
...(session.plugins === undefined ? {} : { plugins: [...session.plugins] })
|
|
714
|
+
};
|
|
715
|
+
}
|
|
716
|
+
|
|
616
717
|
/**
|
|
617
718
|
* Validate and normalize the workflow-level `defaults` block.
|
|
618
719
|
*
|
|
@@ -722,6 +823,20 @@ export async function runWorkflowCommand(args, stdout, stderr, options = {}) {
|
|
|
722
823
|
stdout("Warning: .flow/ directory not found. Running in standalone mode.");
|
|
723
824
|
}
|
|
724
825
|
|
|
826
|
+
if (workflow.mode === "session") {
|
|
827
|
+
const sessionResult = await executeSessionWorkflow({
|
|
828
|
+
workflow,
|
|
829
|
+
workflowPath,
|
|
830
|
+
cwd,
|
|
831
|
+
env,
|
|
832
|
+
stdout,
|
|
833
|
+
stderr,
|
|
834
|
+
flowConfig: config,
|
|
835
|
+
eventLogger
|
|
836
|
+
});
|
|
837
|
+
return sessionResult.exitCode === 0 ? 0 : 1;
|
|
838
|
+
}
|
|
839
|
+
|
|
725
840
|
await executeWorkflow({
|
|
726
841
|
workflow,
|
|
727
842
|
workflowPath,
|
|
@@ -2906,6 +3021,37 @@ function validateStep(step, index, sourceLabel) {
|
|
|
2906
3021
|
throw new WorkflowError(`${sourceLabel} step '${step.name}' agent.interactive must be a boolean when provided.`);
|
|
2907
3022
|
}
|
|
2908
3023
|
|
|
3024
|
+
if (step.agent.mode !== undefined && step.agent.mode !== "autonomous" && step.agent.mode !== "conversational") {
|
|
3025
|
+
throw new WorkflowError(`${sourceLabel} step '${step.name}' agent.mode must be 'autonomous' or 'conversational' when provided.`);
|
|
3026
|
+
}
|
|
3027
|
+
|
|
3028
|
+
if (step.agent.mode !== undefined && step.agent.interactive !== undefined) {
|
|
3029
|
+
throw new WorkflowError(
|
|
3030
|
+
`${sourceLabel} step '${step.name}' cannot define both agent.mode and agent.interactive. Use agent.mode (agent.interactive is deprecated).`
|
|
3031
|
+
);
|
|
3032
|
+
}
|
|
3033
|
+
|
|
3034
|
+
if (step.agent.output_file !== undefined
|
|
3035
|
+
&& (typeof step.agent.output_file !== "string" || step.agent.output_file.trim() === "")) {
|
|
3036
|
+
throw new WorkflowError(
|
|
3037
|
+
`${sourceLabel} step '${step.name}' agent.output_file must be a non-empty string when provided.`
|
|
3038
|
+
);
|
|
3039
|
+
}
|
|
3040
|
+
|
|
3041
|
+
if (step.agent.output_format !== undefined
|
|
3042
|
+
&& step.agent.output_format !== "text"
|
|
3043
|
+
&& step.agent.output_format !== "json") {
|
|
3044
|
+
throw new WorkflowError(
|
|
3045
|
+
`${sourceLabel} step '${step.name}' agent.output_format must be 'text' or 'json' when provided.`
|
|
3046
|
+
);
|
|
3047
|
+
}
|
|
3048
|
+
|
|
3049
|
+
if (step.agent.output_format !== undefined && step.agent.output_file === undefined) {
|
|
3050
|
+
throw new WorkflowError(
|
|
3051
|
+
`${sourceLabel} step '${step.name}' agent.output_format requires agent.output_file.`
|
|
3052
|
+
);
|
|
3053
|
+
}
|
|
3054
|
+
|
|
2909
3055
|
if (step.agent.allow_tools !== undefined) {
|
|
2910
3056
|
if (!Array.isArray(step.agent.allow_tools) || step.agent.allow_tools.some((entry) => typeof entry !== "string")) {
|
|
2911
3057
|
throw new WorkflowError(`${sourceLabel} step '${step.name}' agent.allow_tools must be an array of strings when provided.`);
|
|
@@ -3119,9 +3265,18 @@ function resolveProviderDefaults(agentDefaults, providerName) {
|
|
|
3119
3265
|
* @returns {object} Normalized agent spec with trimmed strings and defaults.
|
|
3120
3266
|
*/
|
|
3121
3267
|
function normalizeAgentSpec(agent) {
|
|
3268
|
+
// Back-compat: agent.interactive: true maps to agent.mode: conversational.
|
|
3269
|
+
// Mutual exclusion is enforced earlier in validateStep.
|
|
3270
|
+
const resolvedMode = agent.mode !== undefined
|
|
3271
|
+
? agent.mode
|
|
3272
|
+
: agent.interactive === true
|
|
3273
|
+
? "conversational"
|
|
3274
|
+
: "autonomous";
|
|
3275
|
+
|
|
3122
3276
|
return {
|
|
3123
3277
|
provider: agent.provider,
|
|
3124
3278
|
prompt: agent.prompt.trim(),
|
|
3279
|
+
mode: resolvedMode,
|
|
3125
3280
|
...(agent.session === undefined ? {} : { session: agent.session }),
|
|
3126
3281
|
...(agent.model === undefined ? {} : { model: agent.model.trim() }),
|
|
3127
3282
|
...(agent.source_access === undefined ? {} : { source_access: agent.source_access }),
|
|
@@ -3129,7 +3284,9 @@ function normalizeAgentSpec(agent) {
|
|
|
3129
3284
|
...(agent.allow_tools === undefined ? {} : { allow_tools: [...agent.allow_tools] }),
|
|
3130
3285
|
...(agent.plugins === undefined ? {} : { plugins: [...agent.plugins] }),
|
|
3131
3286
|
...(agent.append_system_prompt === undefined ? {} : { append_system_prompt: agent.append_system_prompt.trim() }),
|
|
3132
|
-
...(agent.max_budget_usd === undefined ? {} : { max_budget_usd: agent.max_budget_usd })
|
|
3287
|
+
...(agent.max_budget_usd === undefined ? {} : { max_budget_usd: agent.max_budget_usd }),
|
|
3288
|
+
...(agent.output_file === undefined ? {} : { output_file: agent.output_file.trim() }),
|
|
3289
|
+
...(agent.output_format === undefined ? {} : { output_format: agent.output_format })
|
|
3133
3290
|
};
|
|
3134
3291
|
}
|
|
3135
3292
|
|
|
@@ -3906,6 +4063,50 @@ function quoteConditionLiteral(value) {
|
|
|
3906
4063
|
* @param {Record<string, string | number | boolean | null>} [options.parameters={}] - Resolved workflow parameters.
|
|
3907
4064
|
* @returns {{shouldRun: boolean, resolvedValues: {reference: string, value: string}[]}} Evaluation result.
|
|
3908
4065
|
*/
|
|
4066
|
+
/**
|
|
4067
|
+
* Evaluate a gate step against persisted run events. Reconstructs the
|
|
4068
|
+
* `completedStepOutputs` shape that `evaluateStepCondition` expects and returns
|
|
4069
|
+
* a plain pass/fail result for the session bridge.
|
|
4070
|
+
*
|
|
4071
|
+
* @param {object} step - Normalized gate step (must have `stepType === 'gate'`).
|
|
4072
|
+
* @param {object[]} events - All persisted run events.
|
|
4073
|
+
* @param {object} env - Environment variables available to the gate expression.
|
|
4074
|
+
* @returns {{passed: boolean, message: string | null}} Gate evaluation result.
|
|
4075
|
+
*/
|
|
4076
|
+
export function evaluateSessionGate(step, events, env) {
|
|
4077
|
+
const completedStepOutputs = [];
|
|
4078
|
+
|
|
4079
|
+
for (const event of events) {
|
|
4080
|
+
if (event.type === "step_completed" || event.type === "step_failed") {
|
|
4081
|
+
completedStepOutputs.push({
|
|
4082
|
+
name: event.step_name,
|
|
4083
|
+
exitCode: event.exit_code ?? (event.type === "step_failed" ? 1 : 0),
|
|
4084
|
+
stdout: event.stdout ?? "",
|
|
4085
|
+
stderr: event.stderr ?? ""
|
|
4086
|
+
});
|
|
4087
|
+
}
|
|
4088
|
+
}
|
|
4089
|
+
|
|
4090
|
+
try {
|
|
4091
|
+
const { shouldRun } = evaluateStepCondition({
|
|
4092
|
+
condition: step.gate,
|
|
4093
|
+
stepName: step.name,
|
|
4094
|
+
env: env ?? {},
|
|
4095
|
+
completedStepOutputs,
|
|
4096
|
+
parameters: {}
|
|
4097
|
+
});
|
|
4098
|
+
|
|
4099
|
+
return {
|
|
4100
|
+
passed: shouldRun,
|
|
4101
|
+
message: shouldRun
|
|
4102
|
+
? (step.message ?? null)
|
|
4103
|
+
: (step.message ?? `Gate '${step.name}' failed: ${step.gate}`)
|
|
4104
|
+
};
|
|
4105
|
+
} catch (error) {
|
|
4106
|
+
return { passed: false, message: error.message };
|
|
4107
|
+
}
|
|
4108
|
+
}
|
|
4109
|
+
|
|
3909
4110
|
function evaluateStepCondition(options) {
|
|
3910
4111
|
const {
|
|
3911
4112
|
condition,
|