@hyperdrive.bot/paseo-protocol 0.3.42 → 0.3.43
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/generated/validation/ws-outbound.aot.js +12252 -12039
- package/dist/messages.d.ts +295 -0
- package/dist/messages.js +44 -0
- package/dist/validation/ws-outbound-schema-metadata.d.ts +33 -0
- package/dist/workflow/compile.d.ts +33 -0
- package/dist/workflow/compile.js +172 -0
- package/dist/workflow/definition.d.ts +134 -0
- package/dist/workflow/definition.js +362 -0
- package/package.json +1 -1
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { defaultLoopParams, validateLoopParams } from "../fleet/params.js";
|
|
2
|
+
import { createWorkflowTokenPattern, renderWorkflowInputValue, SUBSTITUTABLE_STEP_FIELDS, validateWorkflowDefinition, WorkflowDefinitionError, } from "./definition.js";
|
|
3
|
+
/**
|
|
4
|
+
* Compile one definition against one set of input values.
|
|
5
|
+
*
|
|
6
|
+
* Validates BEFORE it emits, and reports EVERY issue in one throw rather than stopping
|
|
7
|
+
* at the first: a definition can carry a bad input value, an undeclared token and an
|
|
8
|
+
* unknown agent name simultaneously, and three sequential throws would hide the second
|
|
9
|
+
* and third until the first is fixed. On any issue nothing is returned and no partial
|
|
10
|
+
* graph is observable.
|
|
11
|
+
*
|
|
12
|
+
* @throws {WorkflowDefinitionError} carrying the union of the value issues, the
|
|
13
|
+
* definition issues, and the unresolved-reference issues this module adds.
|
|
14
|
+
*/
|
|
15
|
+
export function compileWorkflowDefinition(args) {
|
|
16
|
+
const { definition, session, values } = args;
|
|
17
|
+
// Defaults on the LEFT so a supplied value always wins over a declared default.
|
|
18
|
+
// `defaultLoopParams` only emits a key when the field's `default` parses as a
|
|
19
|
+
// `LoopParamValue`, so a field with no default contributes nothing here - which is
|
|
20
|
+
// exactly the gap the declared-but-unresolved issue below closes.
|
|
21
|
+
const merged = {
|
|
22
|
+
...defaultLoopParams({ fields: definition.inputs }),
|
|
23
|
+
...values,
|
|
24
|
+
};
|
|
25
|
+
const declaredKeys = new Set(definition.inputs.map((field) => field.key));
|
|
26
|
+
const valueIssues = validateLoopParams({ fields: definition.inputs }, merged);
|
|
27
|
+
// Each of the three failure shapes has exactly ONE reporter. A required input that is
|
|
28
|
+
// missing is already located at `inputs[<key>]`, so the reference pass below stays
|
|
29
|
+
// silent about it rather than restating the same root cause at every step that
|
|
30
|
+
// mentions it.
|
|
31
|
+
const reportedKeys = new Set(valueIssues.map((issue) => issue.key));
|
|
32
|
+
const rendered = definition.steps.map((step) => renderStepFields({ step, values: merged, declaredKeys, reportedKeys }));
|
|
33
|
+
const issues = [
|
|
34
|
+
...valueIssues.map((issue) => ({
|
|
35
|
+
path: `inputs[${issue.key}]`,
|
|
36
|
+
message: issue.message,
|
|
37
|
+
})),
|
|
38
|
+
// Deliberately duplicated with `parseWorkflowDefinition`, and it stays: the two
|
|
39
|
+
// guard different entry points - a CLI that parsed a file, versus any caller
|
|
40
|
+
// invoking this compiler directly with a hand-built object.
|
|
41
|
+
...validateWorkflowDefinition(definition),
|
|
42
|
+
...rendered.flatMap((entry) => entry.issues),
|
|
43
|
+
];
|
|
44
|
+
if (issues.length > 0) {
|
|
45
|
+
throw new WorkflowDefinitionError(issues);
|
|
46
|
+
}
|
|
47
|
+
const graph = {
|
|
48
|
+
// What the manager's `title ?? graph.goal` default reads, so `--title` stays optional.
|
|
49
|
+
goal: definition.name,
|
|
50
|
+
// Ported ballast: the CLI's reusable master-prompt template. Zero readers on
|
|
51
|
+
// 25fe57635 (only the TS interface and the Zod schema mention it).
|
|
52
|
+
masterPrompt: "",
|
|
53
|
+
metadata: {
|
|
54
|
+
// Emitted EMPTY on purpose. `resolveExecutionLayers` honours a declared list only
|
|
55
|
+
// when non-empty, then falls through to Kahn layering over `dependencies`. A
|
|
56
|
+
// precomputed list here would be a second derivation that can disagree with `needs`.
|
|
57
|
+
executionLayers: [],
|
|
58
|
+
// Upper bound, not a cap - nothing enforces it. Zero readers.
|
|
59
|
+
maxParallelism: definition.steps.length,
|
|
60
|
+
totalTasks: definition.steps.length,
|
|
61
|
+
// Unknown. Zero readers.
|
|
62
|
+
estimatedDuration: 0,
|
|
63
|
+
// Per-file task generation is a CLI concept with no paseo reader.
|
|
64
|
+
perFileMode: false,
|
|
65
|
+
// `storyFormat` and `totalFiles` are omitted, not defaulted: both are optional and
|
|
66
|
+
// neither has a definition-side vocabulary, so `false`/`0` would be a claim where
|
|
67
|
+
// an absent key is honestly "not specified".
|
|
68
|
+
},
|
|
69
|
+
// Ambient run identity, supplied by the caller so this stays a pure function.
|
|
70
|
+
// `outputDirectory` has zero readers on 25fe57635 but is schema-required.
|
|
71
|
+
session,
|
|
72
|
+
tasks: rendered.map(buildTask),
|
|
73
|
+
};
|
|
74
|
+
// Shallow copy: a caller mutating the returned presets must not reach back into the
|
|
75
|
+
// definition. Populating them onto the wire is Story 3.3's job.
|
|
76
|
+
return { graph, agentPresets: { ...definition.agents } };
|
|
77
|
+
}
|
|
78
|
+
/** Substitute every token in one field's text, iterating Story 3.1's scanner. */
|
|
79
|
+
function substituteTokens(args) {
|
|
80
|
+
const issues = [];
|
|
81
|
+
// A FRESH pattern per call. A module-level `/g` regex carries `lastIndex` between
|
|
82
|
+
// calls, which would make the result depend on scan order.
|
|
83
|
+
const text = args.text.replace(createWorkflowTokenPattern(), (raw, body) => {
|
|
84
|
+
const name = body.trim();
|
|
85
|
+
if (!args.declaredKeys.has(name)) {
|
|
86
|
+
// Undeclared, or malformed (a body that is not a bare identifier is never a
|
|
87
|
+
// declared key). `validateWorkflowDefinition` already reports both, located at
|
|
88
|
+
// this same path, so reporting again here would double-count one mistake. The
|
|
89
|
+
// raw token is returned unrendered; the union above is non-empty, so the caller
|
|
90
|
+
// throws and this interim string is never observable.
|
|
91
|
+
return raw;
|
|
92
|
+
}
|
|
93
|
+
const value = args.values[name];
|
|
94
|
+
if (value === undefined) {
|
|
95
|
+
if (args.reportedKeys.has(name)) {
|
|
96
|
+
// Already located at `inputs[<name>]` by the value pass (required and missing).
|
|
97
|
+
return raw;
|
|
98
|
+
}
|
|
99
|
+
// Declared, referenced, and has no value. `validateLoopParams` cannot catch this
|
|
100
|
+
// (a non-required field with no value is legal to it) and Story 3.1 cannot
|
|
101
|
+
// (the input IS declared). Only the compiler knows the token is referenced.
|
|
102
|
+
// Reporting it is what keeps the token from rendering as "" or "undefined".
|
|
103
|
+
issues.push({
|
|
104
|
+
path: args.path,
|
|
105
|
+
message: `step "${args.stepId}" references ${raw}, which is a declared input with no value: supply --input ${name}=... or give the field a default`,
|
|
106
|
+
});
|
|
107
|
+
return raw;
|
|
108
|
+
}
|
|
109
|
+
return renderWorkflowInputValue(value);
|
|
110
|
+
});
|
|
111
|
+
return { text, issues };
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Render every substitutable field of one step.
|
|
115
|
+
*
|
|
116
|
+
* Iterates `SUBSTITUTABLE_STEP_FIELDS` rather than a second hard-coded list, so this
|
|
117
|
+
* module and Story 3.1's validator cannot disagree about which fields accept a token.
|
|
118
|
+
* `id` and `needs` are absent from that constant on purpose: a topology that depends on
|
|
119
|
+
* runtime values could not be validated at authoring time.
|
|
120
|
+
*/
|
|
121
|
+
function renderStepFields(args) {
|
|
122
|
+
const fields = {};
|
|
123
|
+
const issues = [];
|
|
124
|
+
for (const field of SUBSTITUTABLE_STEP_FIELDS) {
|
|
125
|
+
const text = args.step[field];
|
|
126
|
+
if (text === undefined) {
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
const result = substituteTokens({
|
|
130
|
+
text,
|
|
131
|
+
values: args.values,
|
|
132
|
+
declaredKeys: args.declaredKeys,
|
|
133
|
+
reportedKeys: args.reportedKeys,
|
|
134
|
+
path: `steps[${args.step.id}].${field}`,
|
|
135
|
+
stepId: args.step.id,
|
|
136
|
+
});
|
|
137
|
+
fields[field] = result.text;
|
|
138
|
+
issues.push(...result.issues);
|
|
139
|
+
}
|
|
140
|
+
return { step: args.step, fields, issues };
|
|
141
|
+
}
|
|
142
|
+
/** One task node per step, in definition order. */
|
|
143
|
+
function buildTask(rendered) {
|
|
144
|
+
const { fields, step } = rendered;
|
|
145
|
+
// `step.id` never carries a token, so a fallback that came from it needs no rendering.
|
|
146
|
+
const title = fields.title ?? step.id;
|
|
147
|
+
return {
|
|
148
|
+
id: step.id, // never substituted
|
|
149
|
+
dependencies: step.needs, // never substituted
|
|
150
|
+
// Falls back through the SUBSTITUTED title before the id, so a token in the title
|
|
151
|
+
// reaches the description rather than the description inheriting an unrendered one.
|
|
152
|
+
description: fields.description ?? title,
|
|
153
|
+
// `prompt` is required by `WorkflowStepSchema`, so `renderStepFields` always renders
|
|
154
|
+
// it; the fallback exists only to keep the expression total.
|
|
155
|
+
prompt: fields.prompt ?? step.prompt,
|
|
156
|
+
title,
|
|
157
|
+
// `""` is the schema-satisfying empty, and `outputFile` has no reader anywhere.
|
|
158
|
+
outputFile: fields.outputFile ?? "",
|
|
159
|
+
// Ported ballast from the bmad-workflow CLI. Verified zero readers on 25fe57635:
|
|
160
|
+
// the only occurrences of `parallelizable` in the tree are its two declarations.
|
|
161
|
+
parallelizable: true,
|
|
162
|
+
// Same: no reader. The definition has no duration vocabulary and this epic does not
|
|
163
|
+
// invent one.
|
|
164
|
+
estimatedMinutes: 0,
|
|
165
|
+
// Conditional spread so the key is ABSENT rather than present-with-`undefined`: the
|
|
166
|
+
// schema says optional, and absent is the honest encoding of "this step names no agent".
|
|
167
|
+
...(step.agent === undefined ? {} : { agentType: step.agent }),
|
|
168
|
+
// `targetFiles` is never emitted: the definition has no vocabulary for it and this
|
|
169
|
+
// epic does not invent one.
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
//# sourceMappingURL=compile.js.map
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { type LoopParamValue } from "../fleet/params.js";
|
|
3
|
+
/** Step fields that accept `{{VAR}}` substitution. NEVER `id` and NEVER `needs`: a
|
|
4
|
+
* topology that depends on runtime values could not be validated at parse time. */
|
|
5
|
+
export declare const SUBSTITUTABLE_STEP_FIELDS: readonly ["prompt", "title", "description", "outputFile"];
|
|
6
|
+
export type SubstitutableStepField = (typeof SUBSTITUTABLE_STEP_FIELDS)[number];
|
|
7
|
+
export interface WorkflowDefinitionIssue {
|
|
8
|
+
/** `steps[<id>].<field>` | `steps[#<index>]` | `inputs[<key>]` | `agents[<name>]` | `steps` | `version` */
|
|
9
|
+
path: string;
|
|
10
|
+
message: string;
|
|
11
|
+
}
|
|
12
|
+
export declare class WorkflowDefinitionError extends Error {
|
|
13
|
+
readonly issues: WorkflowDefinitionIssue[];
|
|
14
|
+
constructor(issues: WorkflowDefinitionIssue[]);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* One step. `.strict()` rather than Zod's default strip: a key this version does not
|
|
18
|
+
* support must be REJECTED by name, because a silently stripped `forEach:` reads as
|
|
19
|
+
* configured and behaves as absent.
|
|
20
|
+
*/
|
|
21
|
+
export declare const WorkflowStepSchema: z.ZodObject<{
|
|
22
|
+
id: z.ZodString;
|
|
23
|
+
needs: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
24
|
+
agent: z.ZodOptional<z.ZodString>;
|
|
25
|
+
prompt: z.ZodString;
|
|
26
|
+
title: z.ZodOptional<z.ZodString>;
|
|
27
|
+
description: z.ZodOptional<z.ZodString>;
|
|
28
|
+
outputFile: z.ZodOptional<z.ZodString>;
|
|
29
|
+
}, z.core.$strict>;
|
|
30
|
+
/**
|
|
31
|
+
* The whole definition. `inputs` reuses the Loops parameter vocabulary rather than
|
|
32
|
+
* declaring a second one; a new input type is added in ../fleet/params.ts, never here.
|
|
33
|
+
*/
|
|
34
|
+
export declare const WorkflowDefinitionSchema: z.ZodObject<{
|
|
35
|
+
version: z.ZodLiteral<1>;
|
|
36
|
+
name: z.ZodString;
|
|
37
|
+
inputs: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
38
|
+
key: z.ZodString;
|
|
39
|
+
label: z.ZodString;
|
|
40
|
+
type: z.ZodEnum<{
|
|
41
|
+
string: "string";
|
|
42
|
+
number: "number";
|
|
43
|
+
boolean: "boolean";
|
|
44
|
+
enum: "enum";
|
|
45
|
+
"string-list": "string-list";
|
|
46
|
+
}>;
|
|
47
|
+
description: z.ZodOptional<z.ZodString>;
|
|
48
|
+
required: z.ZodDefault<z.ZodBoolean>;
|
|
49
|
+
options: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
50
|
+
min: z.ZodOptional<z.ZodNumber>;
|
|
51
|
+
max: z.ZodOptional<z.ZodNumber>;
|
|
52
|
+
default: z.ZodOptional<z.ZodUnknown>;
|
|
53
|
+
}, z.core.$strip>>>;
|
|
54
|
+
agents: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
55
|
+
provider: z.ZodOptional<z.ZodString>;
|
|
56
|
+
model: z.ZodOptional<z.ZodString>;
|
|
57
|
+
modeId: z.ZodOptional<z.ZodString>;
|
|
58
|
+
systemPrompt: z.ZodOptional<z.ZodString>;
|
|
59
|
+
thinkingOptionId: z.ZodOptional<z.ZodString>;
|
|
60
|
+
approvalPolicy: z.ZodOptional<z.ZodString>;
|
|
61
|
+
sandboxMode: z.ZodOptional<z.ZodString>;
|
|
62
|
+
networkAccess: z.ZodOptional<z.ZodBoolean>;
|
|
63
|
+
webSearch: z.ZodOptional<z.ZodBoolean>;
|
|
64
|
+
}, z.core.$strict>>>;
|
|
65
|
+
steps: z.ZodArray<z.ZodObject<{
|
|
66
|
+
id: z.ZodString;
|
|
67
|
+
needs: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
68
|
+
agent: z.ZodOptional<z.ZodString>;
|
|
69
|
+
prompt: z.ZodString;
|
|
70
|
+
title: z.ZodOptional<z.ZodString>;
|
|
71
|
+
description: z.ZodOptional<z.ZodString>;
|
|
72
|
+
outputFile: z.ZodOptional<z.ZodString>;
|
|
73
|
+
}, z.core.$strict>>;
|
|
74
|
+
}, z.core.$strict>;
|
|
75
|
+
export type WorkflowStep = z.infer<typeof WorkflowStepSchema>;
|
|
76
|
+
export type WorkflowDefinition = z.infer<typeof WorkflowDefinitionSchema>;
|
|
77
|
+
/** A `{{ ... }}` occurrence, matched LOOSELY on purpose: a malformed token must be
|
|
78
|
+
* reported, not silently ignored and passed through into the rendered prompt.
|
|
79
|
+
* Fresh instance per call so `lastIndex` is never shared. */
|
|
80
|
+
export declare function createWorkflowTokenPattern(): RegExp;
|
|
81
|
+
export interface WorkflowTokenMatch {
|
|
82
|
+
/** The full literal token as written, e.g. `{{ PRD }}`. */
|
|
83
|
+
raw: string;
|
|
84
|
+
/** The trimmed body, e.g. `PRD`. */
|
|
85
|
+
name: string;
|
|
86
|
+
/** True when `name` is a bare identifier, i.e. inside the closed grammar. */
|
|
87
|
+
wellFormed: boolean;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Every `{{ ... }}` occurrence in `text`, in source order.
|
|
91
|
+
*
|
|
92
|
+
* The grammar is closed: `{{`, optional whitespace, ONE identifier, optional
|
|
93
|
+
* whitespace, `}}`. No dotted paths, no filters, no expressions, no nesting. Anything
|
|
94
|
+
* else comes back with `wellFormed: false` so the caller can report it rather than let
|
|
95
|
+
* it survive into a rendered prompt as literal text.
|
|
96
|
+
*/
|
|
97
|
+
export declare function scanWorkflowTokens(text: string): WorkflowTokenMatch[];
|
|
98
|
+
/**
|
|
99
|
+
* Render one input value for interpolation into a PROMPT.
|
|
100
|
+
*
|
|
101
|
+
* Deliberately NOT `loopParamsToEnv`: booleans render as "true"/"false" here and
|
|
102
|
+
* "1"/"0" there, because the consumer here is an LLM prompt and there a bash env
|
|
103
|
+
* var. The two renderers share no consumer, so the divergence is safe; the spec
|
|
104
|
+
* asserts them side by side so it stays deliberate.
|
|
105
|
+
*/
|
|
106
|
+
export declare function renderWorkflowInputValue(value: LoopParamValue): string;
|
|
107
|
+
/**
|
|
108
|
+
* Semantic checks Zod cannot see: ids, edges, cycles, agent names and tokens.
|
|
109
|
+
*
|
|
110
|
+
* Takes an already-parsed definition, returns EVERY issue, never throws and never
|
|
111
|
+
* returns early. Each pass is separate so the issue order is stable and each rule is
|
|
112
|
+
* readable on its own.
|
|
113
|
+
*/
|
|
114
|
+
export declare function validateWorkflowDefinition(definition: WorkflowDefinition): WorkflowDefinitionIssue[];
|
|
115
|
+
export type WorkflowDefinitionParseResult = {
|
|
116
|
+
ok: true;
|
|
117
|
+
definition: WorkflowDefinition;
|
|
118
|
+
} | {
|
|
119
|
+
ok: false;
|
|
120
|
+
issues: WorkflowDefinitionIssue[];
|
|
121
|
+
};
|
|
122
|
+
/**
|
|
123
|
+
* Parse an untrusted object into a definition.
|
|
124
|
+
*
|
|
125
|
+
* Never throws. `WorkflowDefinitionError` exists for callers that want a throwing
|
|
126
|
+
* boundary; this function returns a discriminated result instead.
|
|
127
|
+
*
|
|
128
|
+
* `version` is checked BY HAND before the Zod parse. A file declaring a version this
|
|
129
|
+
* build does not know has a shape this build does not know, so parsing it against the
|
|
130
|
+
* v1 schema buries the one useful fact under a cascade of field errors. `z.literal(1)`
|
|
131
|
+
* stays in the schema as the structural guard for direct `.parse` callers.
|
|
132
|
+
*/
|
|
133
|
+
export declare function parseWorkflowDefinition(raw: unknown): WorkflowDefinitionParseResult;
|
|
134
|
+
//# sourceMappingURL=definition.d.ts.map
|
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { LoopParamFieldSchema } from "../fleet/params.js";
|
|
3
|
+
import { WorkflowAgentPresetSchema } from "../messages.js";
|
|
4
|
+
/**
|
|
5
|
+
* A workflow DEFINITION: the authored, versioned description of a pipeline, as opposed
|
|
6
|
+
* to a `TaskGraph`, which is one pre-built run of one.
|
|
7
|
+
*
|
|
8
|
+
* The point of this module is that a definition is either provably valid or carries a
|
|
9
|
+
* list of located errors. Every check reports EVERY issue in one pass, mirroring
|
|
10
|
+
* `validateLoopParams` in ../fleet/params.ts, so an author fixes a file once instead of
|
|
11
|
+
* resubmitting to discover the next mistake.
|
|
12
|
+
*
|
|
13
|
+
* Pure by construction: no I/O, no `node:` imports, no clock, no RNG. The same input
|
|
14
|
+
* always produces the same issue list in the same order.
|
|
15
|
+
*/
|
|
16
|
+
// Same rule as `LoopParamFieldSchema.key` in ../fleet/params.ts. Duplicated (not
|
|
17
|
+
// imported) because that module does not export the pattern and this epic's
|
|
18
|
+
// compatibility requirements forbid editing it. The drift guard is a test that runs
|
|
19
|
+
// one sample table through both schemas and asserts identical accept/reject.
|
|
20
|
+
const IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
21
|
+
/** The only definition version this build understands. */
|
|
22
|
+
const SUPPORTED_VERSION = 1;
|
|
23
|
+
/** Step fields that accept `{{VAR}}` substitution. NEVER `id` and NEVER `needs`: a
|
|
24
|
+
* topology that depends on runtime values could not be validated at parse time. */
|
|
25
|
+
export const SUBSTITUTABLE_STEP_FIELDS = ["prompt", "title", "description", "outputFile"];
|
|
26
|
+
export class WorkflowDefinitionError extends Error {
|
|
27
|
+
constructor(issues) {
|
|
28
|
+
super(`Workflow definition is invalid: ${issues.map((i) => `${i.path}: ${i.message}`).join("; ")}`);
|
|
29
|
+
this.issues = issues;
|
|
30
|
+
this.name = "WorkflowDefinitionError";
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* One step. `.strict()` rather than Zod's default strip: a key this version does not
|
|
35
|
+
* support must be REJECTED by name, because a silently stripped `forEach:` reads as
|
|
36
|
+
* configured and behaves as absent.
|
|
37
|
+
*/
|
|
38
|
+
export const WorkflowStepSchema = z
|
|
39
|
+
.object({
|
|
40
|
+
id: z.string().trim().min(1).regex(IDENTIFIER, "step id must be a valid identifier"),
|
|
41
|
+
needs: z.array(z.string()).default([]),
|
|
42
|
+
agent: z.string().optional(),
|
|
43
|
+
prompt: z.string().min(1),
|
|
44
|
+
title: z.string().optional(),
|
|
45
|
+
description: z.string().optional(),
|
|
46
|
+
outputFile: z.string().optional(),
|
|
47
|
+
})
|
|
48
|
+
.strict();
|
|
49
|
+
/**
|
|
50
|
+
* The whole definition. `inputs` reuses the Loops parameter vocabulary rather than
|
|
51
|
+
* declaring a second one; a new input type is added in ../fleet/params.ts, never here.
|
|
52
|
+
*/
|
|
53
|
+
export const WorkflowDefinitionSchema = z
|
|
54
|
+
.object({
|
|
55
|
+
version: z.literal(1),
|
|
56
|
+
name: z.string().trim().min(1),
|
|
57
|
+
inputs: z.array(LoopParamFieldSchema).default([]),
|
|
58
|
+
agents: z.record(z.string(), WorkflowAgentPresetSchema).default({}),
|
|
59
|
+
steps: z.array(WorkflowStepSchema).min(1),
|
|
60
|
+
})
|
|
61
|
+
.strict();
|
|
62
|
+
/** A `{{ ... }}` occurrence, matched LOOSELY on purpose: a malformed token must be
|
|
63
|
+
* reported, not silently ignored and passed through into the rendered prompt.
|
|
64
|
+
* Fresh instance per call so `lastIndex` is never shared. */
|
|
65
|
+
export function createWorkflowTokenPattern() {
|
|
66
|
+
return /\{\{([^}]*)\}\}/g;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Every `{{ ... }}` occurrence in `text`, in source order.
|
|
70
|
+
*
|
|
71
|
+
* The grammar is closed: `{{`, optional whitespace, ONE identifier, optional
|
|
72
|
+
* whitespace, `}}`. No dotted paths, no filters, no expressions, no nesting. Anything
|
|
73
|
+
* else comes back with `wellFormed: false` so the caller can report it rather than let
|
|
74
|
+
* it survive into a rendered prompt as literal text.
|
|
75
|
+
*/
|
|
76
|
+
export function scanWorkflowTokens(text) {
|
|
77
|
+
const matches = [];
|
|
78
|
+
for (const match of text.matchAll(createWorkflowTokenPattern())) {
|
|
79
|
+
const name = (match[1] ?? "").trim();
|
|
80
|
+
matches.push({ raw: match[0], name, wellFormed: IDENTIFIER.test(name) });
|
|
81
|
+
}
|
|
82
|
+
return matches;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Render one input value for interpolation into a PROMPT.
|
|
86
|
+
*
|
|
87
|
+
* Deliberately NOT `loopParamsToEnv`: booleans render as "true"/"false" here and
|
|
88
|
+
* "1"/"0" there, because the consumer here is an LLM prompt and there a bash env
|
|
89
|
+
* var. The two renderers share no consumer, so the divergence is safe; the spec
|
|
90
|
+
* asserts them side by side so it stays deliberate.
|
|
91
|
+
*/
|
|
92
|
+
export function renderWorkflowInputValue(value) {
|
|
93
|
+
if (Array.isArray(value)) {
|
|
94
|
+
return value.join(",");
|
|
95
|
+
}
|
|
96
|
+
if (typeof value === "boolean") {
|
|
97
|
+
return value ? "true" : "false";
|
|
98
|
+
}
|
|
99
|
+
return String(value);
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Semantic checks Zod cannot see: ids, edges, cycles, agent names and tokens.
|
|
103
|
+
*
|
|
104
|
+
* Takes an already-parsed definition, returns EVERY issue, never throws and never
|
|
105
|
+
* returns early. Each pass is separate so the issue order is stable and each rule is
|
|
106
|
+
* readable on its own.
|
|
107
|
+
*/
|
|
108
|
+
export function validateWorkflowDefinition(definition) {
|
|
109
|
+
return [
|
|
110
|
+
...collectDuplicateStepIds(definition.steps),
|
|
111
|
+
...collectUnknownNeeds(definition.steps),
|
|
112
|
+
...collectCycle(definition.steps),
|
|
113
|
+
...collectUnknownAgents(definition),
|
|
114
|
+
...collectTokenIssues(definition),
|
|
115
|
+
];
|
|
116
|
+
}
|
|
117
|
+
/** One issue per repeated id, not one per extra occurrence. */
|
|
118
|
+
function collectDuplicateStepIds(steps) {
|
|
119
|
+
const seen = new Set();
|
|
120
|
+
const reported = new Set();
|
|
121
|
+
const issues = [];
|
|
122
|
+
for (const step of steps) {
|
|
123
|
+
if (!seen.has(step.id)) {
|
|
124
|
+
seen.add(step.id);
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if (reported.has(step.id)) {
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
reported.add(step.id);
|
|
131
|
+
issues.push({ path: "steps", message: `duplicate step id: "${step.id}"` });
|
|
132
|
+
}
|
|
133
|
+
return issues;
|
|
134
|
+
}
|
|
135
|
+
function collectUnknownNeeds(steps) {
|
|
136
|
+
const known = new Set(steps.map((step) => step.id));
|
|
137
|
+
const issues = [];
|
|
138
|
+
for (const step of steps) {
|
|
139
|
+
for (const need of step.needs) {
|
|
140
|
+
if (known.has(need)) {
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
issues.push({
|
|
144
|
+
path: `steps[${step.id}].needs`,
|
|
145
|
+
message: `step "${step.id}" needs "${need}", which is not a step in this definition`,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return issues;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Kahn-style layering, mirroring `computeLayersFromDependencies`
|
|
153
|
+
* (packages/server/src/server/workflow/workflow-progress.ts:255-283) including its rule
|
|
154
|
+
* that a dependency outside the graph counts as already satisfied. Honouring that rule
|
|
155
|
+
* is what keeps an unknown `needs` target from being double-reported as a cycle.
|
|
156
|
+
*
|
|
157
|
+
* Collects an issue where the server function throws, because this pass must report
|
|
158
|
+
* every issue in one run.
|
|
159
|
+
*/
|
|
160
|
+
function collectCycle(steps) {
|
|
161
|
+
const byId = new Map(steps.map((step) => [step.id, step]));
|
|
162
|
+
const resolved = new Set();
|
|
163
|
+
let remaining = steps.map((step) => step.id);
|
|
164
|
+
while (remaining.length > 0) {
|
|
165
|
+
const layer = remaining.filter((id) => {
|
|
166
|
+
const node = byId.get(id);
|
|
167
|
+
if (!node) {
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
// A dependency outside the graph is treated as already satisfied.
|
|
171
|
+
return node.needs.every((dep) => resolved.has(dep) || !byId.has(dep));
|
|
172
|
+
});
|
|
173
|
+
if (layer.length === 0) {
|
|
174
|
+
const unresolved = [...new Set(remaining)].sort();
|
|
175
|
+
return [
|
|
176
|
+
{
|
|
177
|
+
path: "steps",
|
|
178
|
+
message: `steps form a dependency cycle: ${unresolved.join(", ")}`,
|
|
179
|
+
},
|
|
180
|
+
];
|
|
181
|
+
}
|
|
182
|
+
for (const id of layer) {
|
|
183
|
+
resolved.add(id);
|
|
184
|
+
}
|
|
185
|
+
remaining = remaining.filter((id) => !resolved.has(id));
|
|
186
|
+
}
|
|
187
|
+
return [];
|
|
188
|
+
}
|
|
189
|
+
function collectUnknownAgents(definition) {
|
|
190
|
+
const declared = Object.keys(definition.agents).sort();
|
|
191
|
+
const known = declared.length === 0 ? "no agents are declared" : `declared agents: ${declared.join(", ")}`;
|
|
192
|
+
const issues = [];
|
|
193
|
+
for (const step of definition.steps) {
|
|
194
|
+
const agent = step.agent;
|
|
195
|
+
if (agent === undefined || Object.hasOwn(definition.agents, agent)) {
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
issues.push({
|
|
199
|
+
path: `steps[${step.id}].agent`,
|
|
200
|
+
message: `step "${step.id}" names agent "${agent}", which is not declared (${known})`,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
return issues;
|
|
204
|
+
}
|
|
205
|
+
function collectTokenIssues(definition) {
|
|
206
|
+
const declared = new Set(definition.inputs.map((field) => field.key));
|
|
207
|
+
const issues = [];
|
|
208
|
+
for (const step of definition.steps) {
|
|
209
|
+
for (const field of SUBSTITUTABLE_STEP_FIELDS) {
|
|
210
|
+
const text = step[field];
|
|
211
|
+
if (text === undefined) {
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
issues.push(...collectFieldTokenIssues(step.id, field, text, declared));
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return issues;
|
|
218
|
+
}
|
|
219
|
+
function collectFieldTokenIssues(stepId, field, text, declared) {
|
|
220
|
+
const path = `steps[${stepId}].${field}`;
|
|
221
|
+
const issues = [];
|
|
222
|
+
for (const token of scanWorkflowTokens(text)) {
|
|
223
|
+
if (!token.wellFormed) {
|
|
224
|
+
issues.push({
|
|
225
|
+
path,
|
|
226
|
+
message: `step "${stepId}" has a malformed token ${token.raw} in ${field}: only {{NAME}} naming a declared input is supported`,
|
|
227
|
+
});
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
if (declared.has(token.name)) {
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
issues.push({
|
|
234
|
+
path,
|
|
235
|
+
message: `step "${stepId}" references ${token.raw} in ${field}, which is not a declared input`,
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
return issues;
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Parse an untrusted object into a definition.
|
|
242
|
+
*
|
|
243
|
+
* Never throws. `WorkflowDefinitionError` exists for callers that want a throwing
|
|
244
|
+
* boundary; this function returns a discriminated result instead.
|
|
245
|
+
*
|
|
246
|
+
* `version` is checked BY HAND before the Zod parse. A file declaring a version this
|
|
247
|
+
* build does not know has a shape this build does not know, so parsing it against the
|
|
248
|
+
* v1 schema buries the one useful fact under a cascade of field errors. `z.literal(1)`
|
|
249
|
+
* stays in the schema as the structural guard for direct `.parse` callers.
|
|
250
|
+
*/
|
|
251
|
+
export function parseWorkflowDefinition(raw) {
|
|
252
|
+
const versionIssue = checkVersionGate(raw);
|
|
253
|
+
if (versionIssue) {
|
|
254
|
+
return { ok: false, issues: [versionIssue] };
|
|
255
|
+
}
|
|
256
|
+
const parsed = WorkflowDefinitionSchema.safeParse(raw);
|
|
257
|
+
if (!parsed.success) {
|
|
258
|
+
const issues = parsed.error.issues.flatMap((issue) => {
|
|
259
|
+
const unrecognizedKeys = issue.code === "unrecognized_keys" ? issue.keys : [];
|
|
260
|
+
return locateZodIssue({ path: issue.path, message: issue.message, unrecognizedKeys }, raw);
|
|
261
|
+
});
|
|
262
|
+
return { ok: false, issues };
|
|
263
|
+
}
|
|
264
|
+
const issues = validateWorkflowDefinition(parsed.data);
|
|
265
|
+
if (issues.length > 0) {
|
|
266
|
+
return { ok: false, issues };
|
|
267
|
+
}
|
|
268
|
+
return { ok: true, definition: parsed.data };
|
|
269
|
+
}
|
|
270
|
+
function checkVersionGate(raw) {
|
|
271
|
+
const record = RawObjectSchema.safeParse(raw);
|
|
272
|
+
if (!record.success) {
|
|
273
|
+
return { path: "version", message: "expected a workflow definition object" };
|
|
274
|
+
}
|
|
275
|
+
if (!Object.hasOwn(record.data, "version")) {
|
|
276
|
+
return {
|
|
277
|
+
path: "version",
|
|
278
|
+
message: 'missing "version": a file without "version" is treated as a task graph, not a workflow definition',
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
const version = record.data.version;
|
|
282
|
+
if (version !== SUPPORTED_VERSION) {
|
|
283
|
+
return {
|
|
284
|
+
path: "version",
|
|
285
|
+
message: `unsupported definition version: ${String(version)} (this build supports version ${SUPPORTED_VERSION})`,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
return null;
|
|
289
|
+
}
|
|
290
|
+
const RawObjectSchema = z.record(z.string(), z.unknown());
|
|
291
|
+
/** Loose view of the raw input, used only to recover the ids Zod's paths do not carry. */
|
|
292
|
+
const RawLocatorSchema = z.object({
|
|
293
|
+
steps: z.array(z.unknown()).optional(),
|
|
294
|
+
inputs: z.array(z.unknown()).optional(),
|
|
295
|
+
});
|
|
296
|
+
const RawStepIdSchema = z.object({ id: z.string().trim().min(1) });
|
|
297
|
+
const RawInputKeySchema = z.object({ key: z.string().trim().min(1) });
|
|
298
|
+
function locateZodIssue(issue, raw) {
|
|
299
|
+
if (issue.unrecognizedKeys.length > 0) {
|
|
300
|
+
return issue.unrecognizedKeys.map((key) => describeUnrecognizedKey(key, issue.path, raw));
|
|
301
|
+
}
|
|
302
|
+
return [{ path: locatePath(issue.path, raw), message: issue.message }];
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* One issue PER unrecognized key, not one lumped issue, and with a purpose-built
|
|
306
|
+
* message that names the version so the author knows the key is unsupported here rather
|
|
307
|
+
* than misspelled.
|
|
308
|
+
*/
|
|
309
|
+
function describeUnrecognizedKey(key, path, raw) {
|
|
310
|
+
const index = path[1];
|
|
311
|
+
if (path[0] !== "steps" || typeof index !== "number") {
|
|
312
|
+
return {
|
|
313
|
+
path: key,
|
|
314
|
+
message: `"${key}" is not a supported definition key in version: ${SUPPORTED_VERSION}`,
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
const stepId = readRawStepId(raw, index);
|
|
318
|
+
const located = stepId === null ? `steps[#${index}]` : `steps[${stepId}]`;
|
|
319
|
+
const owner = stepId === null ? `step #${index}` : `step "${stepId}"`;
|
|
320
|
+
return {
|
|
321
|
+
path: located,
|
|
322
|
+
message: `${owner}: "${key}" is not a supported step key in version: ${SUPPORTED_VERSION}`,
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Zod reports `steps[2]`, which names the array index. An author reads step ids, so
|
|
327
|
+
* recover the id from the raw input and degrade to `steps[#<index>]` only when the step
|
|
328
|
+
* cannot name itself.
|
|
329
|
+
*/
|
|
330
|
+
function locatePath(path, raw) {
|
|
331
|
+
const [head, index, field] = path;
|
|
332
|
+
if (head === "steps" && typeof index === "number") {
|
|
333
|
+
const stepId = readRawStepId(raw, index);
|
|
334
|
+
if (stepId === null) {
|
|
335
|
+
return `steps[#${index}]`;
|
|
336
|
+
}
|
|
337
|
+
return field === undefined ? `steps[${stepId}]` : `steps[${stepId}].${String(field)}`;
|
|
338
|
+
}
|
|
339
|
+
if (head === "inputs" && typeof index === "number") {
|
|
340
|
+
const key = readRawInputKey(raw, index);
|
|
341
|
+
return key === null ? `inputs[#${index}]` : `inputs[${key}]`;
|
|
342
|
+
}
|
|
343
|
+
const joined = path.map((segment) => String(segment)).join(".");
|
|
344
|
+
return joined.length > 0 ? joined : "definition";
|
|
345
|
+
}
|
|
346
|
+
function readRawStepId(raw, index) {
|
|
347
|
+
const view = RawLocatorSchema.safeParse(raw);
|
|
348
|
+
if (!view.success) {
|
|
349
|
+
return null;
|
|
350
|
+
}
|
|
351
|
+
const step = RawStepIdSchema.safeParse(view.data.steps?.[index]);
|
|
352
|
+
return step.success ? step.data.id : null;
|
|
353
|
+
}
|
|
354
|
+
function readRawInputKey(raw, index) {
|
|
355
|
+
const view = RawLocatorSchema.safeParse(raw);
|
|
356
|
+
if (!view.success) {
|
|
357
|
+
return null;
|
|
358
|
+
}
|
|
359
|
+
const input = RawInputKeySchema.safeParse(view.data.inputs?.[index]);
|
|
360
|
+
return input.success ? input.data.key : null;
|
|
361
|
+
}
|
|
362
|
+
//# sourceMappingURL=definition.js.map
|