@reasonlayer/sdk 0.0.1
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/LICENSE +201 -0
- package/README.md +35 -0
- package/bin/rl.mjs +75 -0
- package/dist/builder.d.ts +38 -0
- package/dist/builder.js +41 -0
- package/dist/bundler/index.d.ts +32 -0
- package/dist/bundler/index.js +143 -0
- package/dist/bundler/lint.d.ts +13 -0
- package/dist/bundler/lint.js +74 -0
- package/dist/bundler/node.d.ts +10 -0
- package/dist/bundler/node.js +55 -0
- package/dist/bundler/smoke.d.ts +12 -0
- package/dist/bundler/smoke.js +70 -0
- package/dist/bundler/worker.d.ts +31 -0
- package/dist/bundler/worker.js +214 -0
- package/dist/cli/args.d.ts +9 -0
- package/dist/cli/args.js +71 -0
- package/dist/cli/config.d.ts +38 -0
- package/dist/cli/config.js +94 -0
- package/dist/cli/convex.d.ts +9 -0
- package/dist/cli/convex.js +25 -0
- package/dist/cli/format.d.ts +11 -0
- package/dist/cli/format.js +140 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +721 -0
- package/dist/cli/keychain.d.ts +21 -0
- package/dist/cli/keychain.js +100 -0
- package/dist/cli/login-browser.d.ts +22 -0
- package/dist/cli/login-browser.js +106 -0
- package/dist/cli/mcp.d.ts +3 -0
- package/dist/cli/mcp.js +36 -0
- package/dist/cli/operations.d.ts +106 -0
- package/dist/cli/operations.js +623 -0
- package/dist/cli/sdk-package.d.ts +18 -0
- package/dist/cli/sdk-package.js +45 -0
- package/dist/cli/storage-upload.d.ts +9 -0
- package/dist/cli/storage-upload.js +21 -0
- package/dist/cli/watcher-compile.d.ts +1 -0
- package/dist/cli/watcher-compile.js +20 -0
- package/dist/cli/watcher-process.d.ts +1 -0
- package/dist/cli/watcher-process.js +312 -0
- package/dist/cli/worker-host.d.ts +10 -0
- package/dist/cli/worker-host.js +120 -0
- package/dist/cli/workspace-files.d.ts +12 -0
- package/dist/cli/workspace-files.js +91 -0
- package/dist/cli/workspace-local.d.ts +27 -0
- package/dist/cli/workspace-local.js +310 -0
- package/dist/cli/workspace-session.d.ts +13 -0
- package/dist/cli/workspace-session.js +12 -0
- package/dist/compile.d.ts +27 -0
- package/dist/compile.js +834 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +5 -0
- package/dist/schedule-validation.d.ts +1 -0
- package/dist/schedule-validation.js +16 -0
- package/dist/selector-schema.d.ts +26 -0
- package/dist/selector-schema.js +780 -0
- package/dist/selectors.d.ts +25 -0
- package/dist/selectors.js +99 -0
- package/dist/testkit.d.ts +32 -0
- package/dist/testkit.js +161 -0
- package/dist/types.d.ts +184 -0
- package/dist/types.js +110 -0
- package/node_modules/@reasonlayer/integrations/dist/catalog.d.ts +4937 -0
- package/node_modules/@reasonlayer/integrations/dist/catalog.js +20 -0
- package/node_modules/@reasonlayer/integrations/dist/generated/slack.d.ts +3014 -0
- package/node_modules/@reasonlayer/integrations/dist/generated/slack.js +3101 -0
- package/node_modules/@reasonlayer/integrations/dist/index.d.ts +2 -0
- package/node_modules/@reasonlayer/integrations/dist/index.js +1 -0
- package/node_modules/@reasonlayer/integrations/dist/internal/definitions.d.ts +44 -0
- package/node_modules/@reasonlayer/integrations/dist/internal/definitions.js +52 -0
- package/node_modules/@reasonlayer/integrations/dist/internal/transport.d.ts +7 -0
- package/node_modules/@reasonlayer/integrations/dist/internal/transport.js +48 -0
- package/node_modules/@reasonlayer/integrations/dist/testkit.d.ts +1 -0
- package/node_modules/@reasonlayer/integrations/dist/testkit.js +1 -0
- package/node_modules/@reasonlayer/integrations/package.json +26 -0
- package/node_modules/@reasonlayer/protocol/dist/dispatch.d.ts +10 -0
- package/node_modules/@reasonlayer/protocol/dist/dispatch.js +58 -0
- package/node_modules/@reasonlayer/protocol/dist/envelope.d.ts +9 -0
- package/node_modules/@reasonlayer/protocol/dist/envelope.js +1 -0
- package/node_modules/@reasonlayer/protocol/dist/environments.d.ts +7 -0
- package/node_modules/@reasonlayer/protocol/dist/environments.js +7 -0
- package/node_modules/@reasonlayer/protocol/dist/events.d.ts +313 -0
- package/node_modules/@reasonlayer/protocol/dist/events.js +20 -0
- package/node_modules/@reasonlayer/protocol/dist/index.d.ts +18 -0
- package/node_modules/@reasonlayer/protocol/dist/index.js +9 -0
- package/node_modules/@reasonlayer/protocol/dist/ir.d.ts +252 -0
- package/node_modules/@reasonlayer/protocol/dist/ir.js +116 -0
- package/node_modules/@reasonlayer/protocol/dist/permissions.d.ts +5 -0
- package/node_modules/@reasonlayer/protocol/dist/permissions.js +30 -0
- package/node_modules/@reasonlayer/protocol/dist/readmodels.d.ts +617 -0
- package/node_modules/@reasonlayer/protocol/dist/readmodels.js +36 -0
- package/node_modules/@reasonlayer/protocol/dist/runtime.d.ts +67 -0
- package/node_modules/@reasonlayer/protocol/dist/runtime.js +66 -0
- package/node_modules/@reasonlayer/protocol/dist/scheduling.d.ts +69 -0
- package/node_modules/@reasonlayer/protocol/dist/scheduling.js +126 -0
- package/node_modules/@reasonlayer/protocol/dist/selector.d.ts +23 -0
- package/node_modules/@reasonlayer/protocol/dist/selector.js +77 -0
- package/node_modules/@reasonlayer/protocol/dist/tasks.d.ts +69 -0
- package/node_modules/@reasonlayer/protocol/dist/tasks.js +6 -0
- package/node_modules/@reasonlayer/protocol/dist/validate.d.ts +6 -0
- package/node_modules/@reasonlayer/protocol/dist/validate.js +18 -0
- package/node_modules/@reasonlayer/protocol/package.json +21 -0
- package/node_modules/@reasonlayer/runner/dist/environment-materializer.d.ts +13 -0
- package/node_modules/@reasonlayer/runner/dist/environment-materializer.js +92 -0
- package/node_modules/@reasonlayer/runner/dist/flue-transcript.d.ts +17 -0
- package/node_modules/@reasonlayer/runner/dist/flue-transcript.js +182 -0
- package/node_modules/@reasonlayer/runner/dist/index.d.ts +184 -0
- package/node_modules/@reasonlayer/runner/dist/index.js +884 -0
- package/node_modules/@reasonlayer/runner/dist/mcp-supervisor.d.ts +15 -0
- package/node_modules/@reasonlayer/runner/dist/mcp-supervisor.js +142 -0
- package/node_modules/@reasonlayer/runner/dist/sandbox-env.d.ts +36 -0
- package/node_modules/@reasonlayer/runner/dist/sandbox-env.js +171 -0
- package/node_modules/@reasonlayer/runner/dist/task-child.d.ts +1 -0
- package/node_modules/@reasonlayer/runner/dist/task-child.js +72 -0
- package/node_modules/@reasonlayer/runner/package.json +21 -0
- package/package.json +65 -0
package/dist/compile.js
ADDED
|
@@ -0,0 +1,834 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* compileWorkflow (spec §4.4): lower a WorkflowDefinition to WorkflowIR.
|
|
3
|
+
* Every selector is validated against the step graph; wrong wiring is a
|
|
4
|
+
* compile error, not a runtime surprise. Returns { ok, ir, codeRefs,
|
|
5
|
+
* agentRefs } or { ok: false, errors } — it never throws for authoring
|
|
6
|
+
* mistakes.
|
|
7
|
+
*/
|
|
8
|
+
import { createHash } from "node:crypto";
|
|
9
|
+
import { readFileSync, statSync } from "node:fs";
|
|
10
|
+
import { isAbsolute, normalize, relative, resolve } from "node:path";
|
|
11
|
+
import { toJsonSchema } from "@valibot/to-json-schema";
|
|
12
|
+
import { resolveCatalogActionDescriptor } from "@reasonlayer/integrations";
|
|
13
|
+
import { STEP_ID_PATTERN, canonicalEnvironmentDeclarationJson, parseExactNpmPackage, parseExactPipPackage, mapChildId, } from "@reasonlayer/protocol";
|
|
14
|
+
import { WiringError, inputProxy, lower, mapItemProxy, referencedStepIds, stepsProxy, usesMapItem, } from "./selectors.js";
|
|
15
|
+
import { checkSelectorSatisfiesSchema, resolveSelectorArrayItemSchema, } from "./selector-schema.js";
|
|
16
|
+
import { validateSchedule } from "./schedule-validation.js";
|
|
17
|
+
import { variableDefinitionForCompile } from "./types.js";
|
|
18
|
+
/** IR step ids must be safe as JS export names in the emitted bundles. */
|
|
19
|
+
export function exportNameFor(stepId) {
|
|
20
|
+
return `step__${stepId.replace(/[^a-zA-Z0-9_]/g, "_")}`;
|
|
21
|
+
}
|
|
22
|
+
function secretNames(secrets) {
|
|
23
|
+
if (!secrets || secrets.length === 0)
|
|
24
|
+
return undefined;
|
|
25
|
+
return secrets.map((s) => s.__rlSecret);
|
|
26
|
+
}
|
|
27
|
+
function collectVariableNames(selector) {
|
|
28
|
+
switch (selector.kind) {
|
|
29
|
+
case "variable":
|
|
30
|
+
return [selector.name];
|
|
31
|
+
case "object":
|
|
32
|
+
return Object.values(selector.fields).flatMap(collectVariableNames);
|
|
33
|
+
case "array":
|
|
34
|
+
return selector.items.flatMap(collectVariableNames);
|
|
35
|
+
default:
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function stepSelectors(step) {
|
|
40
|
+
if (step.kind === "map")
|
|
41
|
+
return [step.over, step.key, step.step.input];
|
|
42
|
+
return [step.input];
|
|
43
|
+
}
|
|
44
|
+
function variableSchemasFor(selectors) {
|
|
45
|
+
const schemas = Object.create(null);
|
|
46
|
+
for (const name of new Set(selectors.flatMap(collectVariableNames))) {
|
|
47
|
+
const definition = variableDefinitionForCompile(name);
|
|
48
|
+
if (definition)
|
|
49
|
+
schemas[name] = definition.schemaJson;
|
|
50
|
+
}
|
|
51
|
+
return schemas;
|
|
52
|
+
}
|
|
53
|
+
function isSecretRef(value) {
|
|
54
|
+
return (typeof value === "object" &&
|
|
55
|
+
value !== null &&
|
|
56
|
+
typeof value.__rlSecret === "string" &&
|
|
57
|
+
value.__rlSecret.length > 0);
|
|
58
|
+
}
|
|
59
|
+
function envHashFor(declaration) {
|
|
60
|
+
return createHash("sha256")
|
|
61
|
+
.update(canonicalEnvironmentDeclarationJson({ envHash: "", ...declaration }))
|
|
62
|
+
.digest("hex");
|
|
63
|
+
}
|
|
64
|
+
function normalizeSecretRefs(stepId, serverName, label, values, errors) {
|
|
65
|
+
if (!values || Object.keys(values).length === 0)
|
|
66
|
+
return undefined;
|
|
67
|
+
const out = {};
|
|
68
|
+
for (const [key, value] of Object.entries(values).sort(([a], [b]) => a.localeCompare(b))) {
|
|
69
|
+
if (!isSecretRef(value)) {
|
|
70
|
+
errors.push({
|
|
71
|
+
stepId,
|
|
72
|
+
message: `environment mcp.${serverName}.${label}.${key} must use secret("name"); inline secret values are not allowed`,
|
|
73
|
+
});
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
out[key] = { kind: "secret", name: value.__rlSecret };
|
|
77
|
+
}
|
|
78
|
+
return out;
|
|
79
|
+
}
|
|
80
|
+
function normalizeMcpServer(stepId, name, config, errors) {
|
|
81
|
+
const hasCommand = "command" in config && typeof config.command === "string";
|
|
82
|
+
const hasUrl = "url" in config && typeof config.url === "string";
|
|
83
|
+
if (hasCommand === hasUrl) {
|
|
84
|
+
errors.push({
|
|
85
|
+
stepId,
|
|
86
|
+
message: `environment mcp.${name} must declare exactly one of command or url`,
|
|
87
|
+
});
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
if (hasCommand) {
|
|
91
|
+
if (!config.command.trim()) {
|
|
92
|
+
errors.push({ stepId, message: `environment mcp.${name}.command cannot be empty` });
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
if (config.transport !== "http") {
|
|
96
|
+
errors.push({
|
|
97
|
+
stepId,
|
|
98
|
+
message: `environment mcp.${name}.command servers must declare transport: "http"; stdio MCP commands need an HTTP bridge`,
|
|
99
|
+
});
|
|
100
|
+
return undefined;
|
|
101
|
+
}
|
|
102
|
+
const env = normalizeSecretRefs(stepId, name, "env", config.env, errors);
|
|
103
|
+
return {
|
|
104
|
+
kind: "command",
|
|
105
|
+
command: config.command,
|
|
106
|
+
transport: "http",
|
|
107
|
+
...(env !== undefined ? { env } : {}),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
try {
|
|
111
|
+
new URL(config.url);
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
errors.push({ stepId, message: `environment mcp.${name}.url must be an absolute URL` });
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
const headers = normalizeSecretRefs(stepId, name, "headers", config.headers, errors);
|
|
118
|
+
return {
|
|
119
|
+
kind: "url",
|
|
120
|
+
url: config.url,
|
|
121
|
+
...(headers !== undefined ? { headers } : {}),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
function sortedNonEmptyStrings(stepId, label, values, errors) {
|
|
125
|
+
if (!values || values.length === 0)
|
|
126
|
+
return undefined;
|
|
127
|
+
const out = [];
|
|
128
|
+
for (const value of values) {
|
|
129
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
130
|
+
errors.push({ stepId, message: `environment ${label} entries must be non-empty strings` });
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
out.push(value);
|
|
134
|
+
}
|
|
135
|
+
return out.sort((a, b) => a.localeCompare(b));
|
|
136
|
+
}
|
|
137
|
+
function isInsideDirectory(child, parent) {
|
|
138
|
+
const rel = relative(parent, child);
|
|
139
|
+
return rel === "" || (!!rel && !rel.startsWith("..") && !isAbsolute(rel));
|
|
140
|
+
}
|
|
141
|
+
function lowerEnvironmentFiles(stepId, files, options, errors) {
|
|
142
|
+
if (!files || Object.keys(files).length === 0)
|
|
143
|
+
return undefined;
|
|
144
|
+
const baseDir = options.environmentFileBaseDir
|
|
145
|
+
? resolve(options.environmentFileBaseDir)
|
|
146
|
+
: undefined;
|
|
147
|
+
const out = [];
|
|
148
|
+
for (const [src, dest] of Object.entries(files).sort(([a], [b]) => a.localeCompare(b))) {
|
|
149
|
+
if (!src.trim() || !dest.trim() || !dest.startsWith("/")) {
|
|
150
|
+
errors.push({
|
|
151
|
+
stepId,
|
|
152
|
+
message: `environment file mapping "${src}" must have a non-empty relative source and absolute destination`,
|
|
153
|
+
});
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
if (isAbsolute(src) || normalize(src).split(/[\\/]/).includes("..")) {
|
|
157
|
+
errors.push({
|
|
158
|
+
stepId,
|
|
159
|
+
message: `environment file source "${src}" must be relative to the workflow entry directory`,
|
|
160
|
+
});
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
if (!options.inlineEnvironmentFiles)
|
|
164
|
+
continue;
|
|
165
|
+
if (!baseDir) {
|
|
166
|
+
errors.push({
|
|
167
|
+
stepId,
|
|
168
|
+
message: "environment files require an environmentFileBaseDir when inlineEnvironmentFiles is enabled",
|
|
169
|
+
});
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
const sourcePath = resolve(baseDir, src);
|
|
173
|
+
if (!isInsideDirectory(sourcePath, baseDir)) {
|
|
174
|
+
errors.push({
|
|
175
|
+
stepId,
|
|
176
|
+
message: `environment file source "${src}" escapes the workflow entry directory`,
|
|
177
|
+
});
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
let bytes;
|
|
181
|
+
try {
|
|
182
|
+
const stat = statSync(sourcePath);
|
|
183
|
+
if (!stat.isFile()) {
|
|
184
|
+
errors.push({ stepId, message: `environment file source "${src}" must be a regular file` });
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
bytes = readFileSync(sourcePath);
|
|
188
|
+
}
|
|
189
|
+
catch (err) {
|
|
190
|
+
errors.push({
|
|
191
|
+
stepId,
|
|
192
|
+
message: `environment file source "${src}" cannot be read: ${err.message}`,
|
|
193
|
+
});
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
out.push({
|
|
197
|
+
path: dest,
|
|
198
|
+
sha256: createHash("sha256").update(bytes).digest("hex"),
|
|
199
|
+
contentBase64: bytes.toString("base64"),
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
return out.length > 0 ? out.sort((a, b) => a.path.localeCompare(b.path)) : undefined;
|
|
203
|
+
}
|
|
204
|
+
function lowerEnvironment(stepId, environment, host, stepKind, errors, options) {
|
|
205
|
+
if (!environment)
|
|
206
|
+
return undefined;
|
|
207
|
+
if (environment.__rlEnvironment !== true) {
|
|
208
|
+
errors.push({ stepId, message: "environment must be created with defineEnvironment(...)" });
|
|
209
|
+
return undefined;
|
|
210
|
+
}
|
|
211
|
+
if (host.kind === "device") {
|
|
212
|
+
const disallowed = [];
|
|
213
|
+
if (environment.config.npm?.length)
|
|
214
|
+
disallowed.push("npm");
|
|
215
|
+
if (environment.config.pip?.length)
|
|
216
|
+
disallowed.push("pip");
|
|
217
|
+
if (environment.config.apt?.length)
|
|
218
|
+
disallowed.push("apt");
|
|
219
|
+
if (Object.keys(environment.config.files ?? {}).length > 0)
|
|
220
|
+
disallowed.push("files");
|
|
221
|
+
if (environment.config.setup?.length)
|
|
222
|
+
disallowed.push("setup");
|
|
223
|
+
if (stepKind === "code" && Object.keys(environment.config.mcp ?? {}).length > 0)
|
|
224
|
+
disallowed.push("mcp");
|
|
225
|
+
if (disallowed.length > 0) {
|
|
226
|
+
const allowed = stepKind === "agent" ? "binaries or MCP servers" : "binaries";
|
|
227
|
+
errors.push({
|
|
228
|
+
stepId,
|
|
229
|
+
message: `device-hosted environments are verification-only and may only declare ${allowed}; remove ${disallowed.join(", ")}`,
|
|
230
|
+
});
|
|
231
|
+
return undefined;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
const npm = sortedNonEmptyStrings(stepId, "npm", environment.config.npm, errors);
|
|
235
|
+
for (const spec of npm ?? []) {
|
|
236
|
+
if (!parseExactNpmPackage(spec)) {
|
|
237
|
+
errors.push({
|
|
238
|
+
stepId,
|
|
239
|
+
message: `environment npm package "${spec}" must include an exact semver version, e.g. name@1.2.3`,
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
const pip = sortedNonEmptyStrings(stepId, "pip", environment.config.pip, errors);
|
|
244
|
+
for (const spec of pip ?? []) {
|
|
245
|
+
if (!parseExactPipPackage(spec)) {
|
|
246
|
+
errors.push({
|
|
247
|
+
stepId,
|
|
248
|
+
message: `environment pip package "${spec}" must use an exact == version, e.g. name==1.2.3`,
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
const apt = sortedNonEmptyStrings(stepId, "apt", environment.config.apt, errors);
|
|
253
|
+
const binaries = sortedNonEmptyStrings(stepId, "binaries", environment.config.binaries, errors);
|
|
254
|
+
const setup = environment.config.setup?.filter((cmd) => {
|
|
255
|
+
if (typeof cmd === "string" && cmd.trim())
|
|
256
|
+
return true;
|
|
257
|
+
errors.push({ stepId, message: "environment setup entries must be non-empty strings" });
|
|
258
|
+
return false;
|
|
259
|
+
});
|
|
260
|
+
const files = lowerEnvironmentFiles(stepId, environment.config.files, options, errors);
|
|
261
|
+
const mcp = environment.config.mcp && Object.keys(environment.config.mcp).length > 0
|
|
262
|
+
? Object.fromEntries(Object.entries(environment.config.mcp)
|
|
263
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
264
|
+
.flatMap(([name, server]) => {
|
|
265
|
+
if (!STEP_ID_PATTERN.test(name)) {
|
|
266
|
+
errors.push({
|
|
267
|
+
stepId,
|
|
268
|
+
message: `environment MCP server name "${name}" must match ${STEP_ID_PATTERN}`,
|
|
269
|
+
});
|
|
270
|
+
return [];
|
|
271
|
+
}
|
|
272
|
+
const lowered = normalizeMcpServer(stepId, name, server, errors);
|
|
273
|
+
return lowered ? [[name, lowered]] : [];
|
|
274
|
+
}))
|
|
275
|
+
: undefined;
|
|
276
|
+
const declaration = {
|
|
277
|
+
...(npm !== undefined ? { npm } : {}),
|
|
278
|
+
...(pip !== undefined ? { pip } : {}),
|
|
279
|
+
...(apt !== undefined ? { apt } : {}),
|
|
280
|
+
...(binaries !== undefined ? { binaries } : {}),
|
|
281
|
+
...(files !== undefined ? { files } : {}),
|
|
282
|
+
...(setup !== undefined && setup.length > 0 ? { setup } : {}),
|
|
283
|
+
...(mcp !== undefined ? { mcp } : {}),
|
|
284
|
+
};
|
|
285
|
+
return { envHash: envHashFor(declaration), ...declaration };
|
|
286
|
+
}
|
|
287
|
+
function schemaToJson(schema, where, errors, stepId) {
|
|
288
|
+
try {
|
|
289
|
+
return toJsonSchema(schema);
|
|
290
|
+
}
|
|
291
|
+
catch (err) {
|
|
292
|
+
errors.push({
|
|
293
|
+
...(stepId !== undefined ? { stepId } : {}),
|
|
294
|
+
message: `${where}: schema cannot convert to JSON Schema — ${err.message}`,
|
|
295
|
+
});
|
|
296
|
+
return {};
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
function maybeLowerEnvironment(stepId, environment, host, stepKind, errors, options) {
|
|
300
|
+
if (options.omitEnvironments)
|
|
301
|
+
return undefined;
|
|
302
|
+
return lowerEnvironment(stepId, environment, host, stepKind, errors, options);
|
|
303
|
+
}
|
|
304
|
+
function lowerCatalogTools(stepId, tools, errors) {
|
|
305
|
+
if (tools === undefined)
|
|
306
|
+
return [];
|
|
307
|
+
if (!Array.isArray(tools)) {
|
|
308
|
+
errors.push({ stepId, message: "agent tools must be an array of catalog action descriptors" });
|
|
309
|
+
return [];
|
|
310
|
+
}
|
|
311
|
+
const actionIds = [];
|
|
312
|
+
const seen = new Set();
|
|
313
|
+
for (const [index, value] of tools.entries()) {
|
|
314
|
+
const action = resolveCatalogActionDescriptor(value);
|
|
315
|
+
if (!action) {
|
|
316
|
+
errors.push({
|
|
317
|
+
stepId,
|
|
318
|
+
message: `agent tool at index ${index} must use a descriptor exported by @reasonlayer/sdk`,
|
|
319
|
+
});
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
if (seen.has(action.id))
|
|
323
|
+
continue;
|
|
324
|
+
seen.add(action.id);
|
|
325
|
+
actionIds.push(action.id);
|
|
326
|
+
}
|
|
327
|
+
return actionIds;
|
|
328
|
+
}
|
|
329
|
+
function lowerExecutableStep(args) {
|
|
330
|
+
const irStepId = args.irStepId ?? args.stepId;
|
|
331
|
+
if (!args.config.host) {
|
|
332
|
+
args.errors.push({
|
|
333
|
+
stepId: args.stepId,
|
|
334
|
+
message: args.missingHostMessage ?? `${args.kind} steps require a host`,
|
|
335
|
+
});
|
|
336
|
+
return undefined;
|
|
337
|
+
}
|
|
338
|
+
if (args.config.host.kind === "device" && !args.config.host.handle) {
|
|
339
|
+
args.errors.push({
|
|
340
|
+
stepId: args.stepId,
|
|
341
|
+
message: `${args.kind} device hosts require a handle`,
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
if (args.config.browserProfile !== undefined) {
|
|
345
|
+
if (!args.config.browserProfile.trim()) {
|
|
346
|
+
args.errors.push({ stepId: args.stepId, message: "browserProfile must be a non-empty string" });
|
|
347
|
+
}
|
|
348
|
+
if (args.config.host.kind !== "device") {
|
|
349
|
+
args.errors.push({ stepId: args.stepId, message: "browserProfile requires a device host" });
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
args.checkAtLeastOne(args.stepId, args.config.retries?.maxAttempts, args.retriesLabel ?? "retries.maxAttempts");
|
|
353
|
+
const secrets = secretNames(args.config.secrets);
|
|
354
|
+
const environment = maybeLowerEnvironment(args.stepId, args.config.environment, args.config.host, args.kind, args.errors, args.options);
|
|
355
|
+
const catalogTools = args.kind === "agent" ? lowerCatalogTools(args.stepId, args.config.tools, args.errors) : undefined;
|
|
356
|
+
const base = {
|
|
357
|
+
id: irStepId,
|
|
358
|
+
input: args.input,
|
|
359
|
+
outputSchemaJson: args.outputSchemaJson,
|
|
360
|
+
moduleRef: args.kind === "code" ? "steps.js" : "agents.js",
|
|
361
|
+
exportName: exportNameFor(irStepId),
|
|
362
|
+
host: args.config.host,
|
|
363
|
+
...(args.config.browserProfile !== undefined
|
|
364
|
+
? { browserProfile: args.config.browserProfile }
|
|
365
|
+
: {}),
|
|
366
|
+
...(args.config.timeoutMs !== undefined ? { timeoutMs: args.config.timeoutMs } : {}),
|
|
367
|
+
...(args.config.retries !== undefined ? { retries: args.config.retries } : {}),
|
|
368
|
+
...(secrets !== undefined ? { secrets } : {}),
|
|
369
|
+
...(environment !== undefined ? { environment } : {}),
|
|
370
|
+
};
|
|
371
|
+
return args.kind === "code"
|
|
372
|
+
? { ...base, kind: "code" }
|
|
373
|
+
: { ...base, kind: "agent", framework: "flue", catalogTools: catalogTools };
|
|
374
|
+
}
|
|
375
|
+
export function compileWorkflow(definition, options = {}) {
|
|
376
|
+
const errors = [];
|
|
377
|
+
const { config, steps } = definition;
|
|
378
|
+
const devices = [];
|
|
379
|
+
const declaredDeviceHandles = new Set();
|
|
380
|
+
for (const ref of config.devices ?? []) {
|
|
381
|
+
const handle = ref.__rlDeviceHandle;
|
|
382
|
+
if (typeof handle !== "string" || !STEP_ID_PATTERN.test(handle)) {
|
|
383
|
+
errors.push({ message: `device handle "${String(handle)}" must match ${STEP_ID_PATTERN}` });
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
386
|
+
if (declaredDeviceHandles.has(handle)) {
|
|
387
|
+
errors.push({ message: `duplicate device handle "${handle}"` });
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
declaredDeviceHandles.add(handle);
|
|
391
|
+
devices.push({ kind: "device", handle });
|
|
392
|
+
}
|
|
393
|
+
const secrets = [];
|
|
394
|
+
const declaredSecretNames = new Set();
|
|
395
|
+
for (const ref of config.secrets ?? []) {
|
|
396
|
+
const name = ref.__rlSecret;
|
|
397
|
+
if (typeof name !== "string" || !/^[A-Z_][A-Z0-9_]*$/.test(name)) {
|
|
398
|
+
errors.push({ message: `secret name "${String(name)}" must match /^[A-Z_][A-Z0-9_]*$/` });
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
if (declaredSecretNames.has(name)) {
|
|
402
|
+
errors.push({ message: `duplicate secret handle "${name}"` });
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
405
|
+
declaredSecretNames.add(name);
|
|
406
|
+
secrets.push({ kind: "secret", name });
|
|
407
|
+
}
|
|
408
|
+
const triggers = [];
|
|
409
|
+
const triggerNames = new Set();
|
|
410
|
+
for (const trigger of config.triggers ?? []) {
|
|
411
|
+
if (!STEP_ID_PATTERN.test(trigger.name)) {
|
|
412
|
+
errors.push({ message: `trigger name "${trigger.name}" must match ${STEP_ID_PATTERN}` });
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
if (triggerNames.has(trigger.name)) {
|
|
416
|
+
errors.push({ message: `duplicate trigger name "${trigger.name}"` });
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
419
|
+
triggerNames.add(trigger.name);
|
|
420
|
+
const timezone = trigger.timezone ?? "UTC";
|
|
421
|
+
try {
|
|
422
|
+
validateSchedule(trigger.cron, timezone);
|
|
423
|
+
}
|
|
424
|
+
catch (error) {
|
|
425
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
426
|
+
errors.push({
|
|
427
|
+
message: `trigger "${trigger.name}" has invalid schedule in timezone "${timezone}": ${message}`,
|
|
428
|
+
});
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
triggers.push({
|
|
432
|
+
kind: "schedule",
|
|
433
|
+
name: trigger.name,
|
|
434
|
+
cron: trigger.cron,
|
|
435
|
+
timezone,
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
if (!config.id || !STEP_ID_PATTERN.test(config.id)) {
|
|
439
|
+
errors.push({ message: `workflow id "${config.id}" must match ${STEP_ID_PATTERN}` });
|
|
440
|
+
}
|
|
441
|
+
const inputSchemaErrorCount = errors.length;
|
|
442
|
+
const inputSchemaJson = schemaToJson(config.input, "workflow input", errors);
|
|
443
|
+
const inputSchemaValid = errors.length === inputSchemaErrorCount;
|
|
444
|
+
const outputSchemaJson = schemaToJson(config.output, "workflow output", errors);
|
|
445
|
+
const irSteps = {};
|
|
446
|
+
const codeRefs = {};
|
|
447
|
+
const agentRefs = {};
|
|
448
|
+
const knownIds = [];
|
|
449
|
+
const stepSchemas = Object.create(null);
|
|
450
|
+
const invalidStepSchemas = new Set();
|
|
451
|
+
const runWiring = (stepId, wiring) => {
|
|
452
|
+
if (!wiring)
|
|
453
|
+
return { kind: "input", path: [] };
|
|
454
|
+
try {
|
|
455
|
+
return lower(wiring(stepsProxy(knownIds), inputProxy()));
|
|
456
|
+
}
|
|
457
|
+
catch (err) {
|
|
458
|
+
if (err instanceof WiringError) {
|
|
459
|
+
errors.push({ stepId, message: err.message });
|
|
460
|
+
return { kind: "literal", value: null };
|
|
461
|
+
}
|
|
462
|
+
throw err;
|
|
463
|
+
}
|
|
464
|
+
};
|
|
465
|
+
const checkSelector = (stepId, selector, mapScope) => {
|
|
466
|
+
for (const ref of referencedStepIds(selector)) {
|
|
467
|
+
if (!knownIds.includes(ref)) {
|
|
468
|
+
errors.push({ stepId, message: `selector references undeclared step "${ref}"` });
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
for (const name of new Set(collectVariableNames(selector))) {
|
|
472
|
+
if (!variableDefinitionForCompile(name)) {
|
|
473
|
+
errors.push({
|
|
474
|
+
stepId,
|
|
475
|
+
message: `variable "${name}" is referenced but not defined via defineVariable()`,
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
if (!mapScope && usesMapItem(selector)) {
|
|
480
|
+
errors.push({ stepId, message: "mapItem is only valid inside a map step's child" });
|
|
481
|
+
}
|
|
482
|
+
};
|
|
483
|
+
const selectorSourcesAreUsable = (selector, mapItemSchema) => {
|
|
484
|
+
switch (selector.kind) {
|
|
485
|
+
case "input":
|
|
486
|
+
return inputSchemaValid;
|
|
487
|
+
case "step":
|
|
488
|
+
return (Object.prototype.hasOwnProperty.call(stepSchemas, selector.stepId) &&
|
|
489
|
+
!invalidStepSchemas.has(selector.stepId));
|
|
490
|
+
case "variable":
|
|
491
|
+
return variableDefinitionForCompile(selector.name) !== undefined;
|
|
492
|
+
case "mapItem":
|
|
493
|
+
return mapItemSchema !== undefined;
|
|
494
|
+
case "object":
|
|
495
|
+
return Object.values(selector.fields).every((field) => selectorSourcesAreUsable(field, mapItemSchema));
|
|
496
|
+
case "array":
|
|
497
|
+
return selector.items.every((item) => selectorSourcesAreUsable(item, mapItemSchema));
|
|
498
|
+
case "literal":
|
|
499
|
+
return true;
|
|
500
|
+
}
|
|
501
|
+
};
|
|
502
|
+
const checkReviewCompatibility = (stepId, input, outputSchema, mapItemSchema) => {
|
|
503
|
+
if (!selectorSourcesAreUsable(input, mapItemSchema))
|
|
504
|
+
return;
|
|
505
|
+
const context = {
|
|
506
|
+
inputSchema: inputSchemaJson,
|
|
507
|
+
stepSchemas,
|
|
508
|
+
variableSchemas: variableSchemasFor([input]),
|
|
509
|
+
...(mapItemSchema !== undefined ? { mapItemSchema } : {}),
|
|
510
|
+
};
|
|
511
|
+
const mismatches = checkSelectorSatisfiesSchema(input, outputSchema, context);
|
|
512
|
+
if (mismatches.length === 0)
|
|
513
|
+
return;
|
|
514
|
+
const conclusiveMismatches = mismatches.filter((mismatch) => !mismatch.startsWith("compatibility cannot be proven"));
|
|
515
|
+
const detail = (conclusiveMismatches.length > 0 ? conclusiveMismatches : mismatches)
|
|
516
|
+
.slice(0, 5)
|
|
517
|
+
.join("; ");
|
|
518
|
+
const guidance = "Review steps approve or edit their input; select an output-shaped value (e.g. { send: steps.draft.emails }).";
|
|
519
|
+
if (conclusiveMismatches.length === 0) {
|
|
520
|
+
errors.push({
|
|
521
|
+
stepId,
|
|
522
|
+
message: `review input compatibility cannot be proven — ${detail.replace(/^compatibility cannot be proven\s*/, "")}. ${guidance}`,
|
|
523
|
+
});
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
errors.push({
|
|
527
|
+
stepId,
|
|
528
|
+
message: `review input does not satisfy the review output schema — ${detail}. ${guidance}`,
|
|
529
|
+
});
|
|
530
|
+
};
|
|
531
|
+
const checkAtLeastOne = (stepId, value, label) => {
|
|
532
|
+
if (value !== undefined && value < 1) {
|
|
533
|
+
errors.push({ stepId, message: `${label} must be ≥ 1` });
|
|
534
|
+
}
|
|
535
|
+
};
|
|
536
|
+
const lowerChild = (mapId, child, mapItemSchema) => {
|
|
537
|
+
const childId = mapChildId(mapId);
|
|
538
|
+
const cfg = child.config;
|
|
539
|
+
const inputErrorCount = errors.length;
|
|
540
|
+
let input;
|
|
541
|
+
try {
|
|
542
|
+
input = lower(cfg.input);
|
|
543
|
+
}
|
|
544
|
+
catch (err) {
|
|
545
|
+
if (err instanceof WiringError) {
|
|
546
|
+
errors.push({ stepId: mapId, message: `map child input: ${err.message}` });
|
|
547
|
+
return undefined;
|
|
548
|
+
}
|
|
549
|
+
throw err;
|
|
550
|
+
}
|
|
551
|
+
checkSelector(mapId, input, true);
|
|
552
|
+
const inputValid = errors.length === inputErrorCount;
|
|
553
|
+
const outputSchemaErrorCount = errors.length;
|
|
554
|
+
const outputJson = schemaToJson(cfg.output, "map child output", errors, mapId);
|
|
555
|
+
const outputSchemaValid = errors.length === outputSchemaErrorCount;
|
|
556
|
+
if (child.__rlChildStep === "review") {
|
|
557
|
+
const review = cfg;
|
|
558
|
+
if (inputValid && outputSchemaValid) {
|
|
559
|
+
checkReviewCompatibility(mapId, input, outputJson, mapItemSchema);
|
|
560
|
+
}
|
|
561
|
+
checkAtLeastOne(mapId, review.deadlineMs, "map child: deadlineMs");
|
|
562
|
+
return {
|
|
563
|
+
step: {
|
|
564
|
+
kind: "review",
|
|
565
|
+
id: childId,
|
|
566
|
+
input,
|
|
567
|
+
outputSchemaJson: outputJson,
|
|
568
|
+
view: review.view ?? { kind: "json" },
|
|
569
|
+
...(review.deadlineMs !== undefined ? { deadlineMs: review.deadlineMs } : {}),
|
|
570
|
+
},
|
|
571
|
+
outputSchemaJson: outputJson,
|
|
572
|
+
outputSchemaValid,
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
const common = cfg;
|
|
576
|
+
const childKind = child.__rlChildStep === "code" ? "code" : "agent";
|
|
577
|
+
const lowered = lowerExecutableStep({
|
|
578
|
+
stepId: mapId,
|
|
579
|
+
irStepId: childId,
|
|
580
|
+
kind: childKind,
|
|
581
|
+
input,
|
|
582
|
+
outputSchemaJson: outputJson,
|
|
583
|
+
config: common,
|
|
584
|
+
errors,
|
|
585
|
+
options,
|
|
586
|
+
checkAtLeastOne,
|
|
587
|
+
missingHostMessage: "map child: executable steps require a host",
|
|
588
|
+
retriesLabel: "map child: retries.maxAttempts",
|
|
589
|
+
});
|
|
590
|
+
if (!lowered)
|
|
591
|
+
return { outputSchemaJson: outputJson, outputSchemaValid };
|
|
592
|
+
if (child.__rlChildStep === "code") {
|
|
593
|
+
codeRefs[childId] = cfg.run;
|
|
594
|
+
return { step: lowered, outputSchemaJson: outputJson, outputSchemaValid };
|
|
595
|
+
}
|
|
596
|
+
agentRefs[childId] = cfg.agent;
|
|
597
|
+
return { step: lowered, outputSchemaJson: outputJson, outputSchemaValid };
|
|
598
|
+
};
|
|
599
|
+
for (const def of steps) {
|
|
600
|
+
if (!STEP_ID_PATTERN.test(def.id)) {
|
|
601
|
+
errors.push({
|
|
602
|
+
stepId: def.id,
|
|
603
|
+
message: `step id "${def.id}" must match ${STEP_ID_PATTERN} (":" is reserved for map children)`,
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
if (knownIds.includes(def.id)) {
|
|
607
|
+
errors.push({ stepId: def.id, message: "duplicate step id" });
|
|
608
|
+
continue;
|
|
609
|
+
}
|
|
610
|
+
switch (def.kind) {
|
|
611
|
+
case "code": {
|
|
612
|
+
const cfg = def.config;
|
|
613
|
+
const input = runWiring(def.id, cfg.input);
|
|
614
|
+
checkSelector(def.id, input, false);
|
|
615
|
+
const schemaErrorCount = errors.length;
|
|
616
|
+
const stepOutputSchema = schemaToJson(cfg.output, "step output", errors, def.id);
|
|
617
|
+
stepSchemas[def.id] = stepOutputSchema;
|
|
618
|
+
if (errors.length !== schemaErrorCount)
|
|
619
|
+
invalidStepSchemas.add(def.id);
|
|
620
|
+
const lowered = lowerExecutableStep({
|
|
621
|
+
stepId: def.id,
|
|
622
|
+
kind: "code",
|
|
623
|
+
input,
|
|
624
|
+
outputSchemaJson: stepOutputSchema,
|
|
625
|
+
config: cfg,
|
|
626
|
+
errors,
|
|
627
|
+
options,
|
|
628
|
+
checkAtLeastOne,
|
|
629
|
+
});
|
|
630
|
+
if (lowered) {
|
|
631
|
+
codeRefs[def.id] = cfg.run;
|
|
632
|
+
irSteps[def.id] = lowered;
|
|
633
|
+
}
|
|
634
|
+
break;
|
|
635
|
+
}
|
|
636
|
+
case "agent": {
|
|
637
|
+
const cfg = def.config;
|
|
638
|
+
const input = runWiring(def.id, cfg.input);
|
|
639
|
+
checkSelector(def.id, input, false);
|
|
640
|
+
const schemaErrorCount = errors.length;
|
|
641
|
+
const stepOutputSchema = schemaToJson(cfg.output, "step output", errors, def.id);
|
|
642
|
+
stepSchemas[def.id] = stepOutputSchema;
|
|
643
|
+
if (errors.length !== schemaErrorCount)
|
|
644
|
+
invalidStepSchemas.add(def.id);
|
|
645
|
+
const lowered = lowerExecutableStep({
|
|
646
|
+
stepId: def.id,
|
|
647
|
+
kind: "agent",
|
|
648
|
+
input,
|
|
649
|
+
outputSchemaJson: stepOutputSchema,
|
|
650
|
+
config: cfg,
|
|
651
|
+
errors,
|
|
652
|
+
options,
|
|
653
|
+
checkAtLeastOne,
|
|
654
|
+
});
|
|
655
|
+
if (lowered) {
|
|
656
|
+
agentRefs[def.id] = cfg.agent;
|
|
657
|
+
irSteps[def.id] = lowered;
|
|
658
|
+
}
|
|
659
|
+
break;
|
|
660
|
+
}
|
|
661
|
+
case "action": {
|
|
662
|
+
const cfg = def.config;
|
|
663
|
+
const action = resolveCatalogActionDescriptor(cfg.action);
|
|
664
|
+
if (!action) {
|
|
665
|
+
errors.push({
|
|
666
|
+
stepId: def.id,
|
|
667
|
+
message: "action must use a descriptor exported by @reasonlayer/sdk",
|
|
668
|
+
});
|
|
669
|
+
break;
|
|
670
|
+
}
|
|
671
|
+
if (typeof cfg.input !== "function") {
|
|
672
|
+
errors.push({ stepId: def.id, message: "action input wiring is required" });
|
|
673
|
+
break;
|
|
674
|
+
}
|
|
675
|
+
const input = runWiring(def.id, cfg.input);
|
|
676
|
+
checkSelector(def.id, input, false);
|
|
677
|
+
checkAtLeastOne(def.id, cfg.timeoutMs, "timeoutMs");
|
|
678
|
+
checkAtLeastOne(def.id, cfg.retries?.maxAttempts, "retries.maxAttempts");
|
|
679
|
+
const actionStep = {
|
|
680
|
+
kind: "action",
|
|
681
|
+
id: def.id,
|
|
682
|
+
actionId: action.id,
|
|
683
|
+
actionVersion: action.version,
|
|
684
|
+
input,
|
|
685
|
+
inputSchemaJson: action.inputSchemaJson,
|
|
686
|
+
outputSchemaJson: action.outputSchemaJson,
|
|
687
|
+
...(cfg.timeoutMs !== undefined ? { timeoutMs: cfg.timeoutMs } : {}),
|
|
688
|
+
...(cfg.retries !== undefined ? { retries: cfg.retries } : {}),
|
|
689
|
+
};
|
|
690
|
+
stepSchemas[def.id] = action.outputSchemaJson;
|
|
691
|
+
irSteps[def.id] = actionStep;
|
|
692
|
+
break;
|
|
693
|
+
}
|
|
694
|
+
case "review": {
|
|
695
|
+
const cfg = def.config;
|
|
696
|
+
const inputErrorCount = errors.length;
|
|
697
|
+
const input = runWiring(def.id, cfg.input);
|
|
698
|
+
checkSelector(def.id, input, false);
|
|
699
|
+
const inputValid = errors.length === inputErrorCount;
|
|
700
|
+
const schemaErrorCount = errors.length;
|
|
701
|
+
const stepOutputSchema = schemaToJson(cfg.output, "step output", errors, def.id);
|
|
702
|
+
const outputSchemaValid = errors.length === schemaErrorCount;
|
|
703
|
+
stepSchemas[def.id] = stepOutputSchema;
|
|
704
|
+
if (!outputSchemaValid)
|
|
705
|
+
invalidStepSchemas.add(def.id);
|
|
706
|
+
if (inputValid && outputSchemaValid) {
|
|
707
|
+
checkReviewCompatibility(def.id, input, stepOutputSchema);
|
|
708
|
+
}
|
|
709
|
+
checkAtLeastOne(def.id, cfg.deadlineMs, "deadlineMs");
|
|
710
|
+
irSteps[def.id] = {
|
|
711
|
+
kind: "review",
|
|
712
|
+
id: def.id,
|
|
713
|
+
input,
|
|
714
|
+
outputSchemaJson: stepOutputSchema,
|
|
715
|
+
view: cfg.view ?? { kind: "json" },
|
|
716
|
+
...(cfg.deadlineMs !== undefined ? { deadlineMs: cfg.deadlineMs } : {}),
|
|
717
|
+
};
|
|
718
|
+
break;
|
|
719
|
+
}
|
|
720
|
+
case "map": {
|
|
721
|
+
const cfg = def.config;
|
|
722
|
+
const overErrorCount = errors.length;
|
|
723
|
+
const over = runWiring(def.id, cfg.over);
|
|
724
|
+
checkSelector(def.id, over, false);
|
|
725
|
+
const overValid = errors.length === overErrorCount;
|
|
726
|
+
const mapItemResolution = overValid && selectorSourcesAreUsable(over)
|
|
727
|
+
? resolveSelectorArrayItemSchema(over, {
|
|
728
|
+
inputSchema: inputSchemaJson,
|
|
729
|
+
stepSchemas,
|
|
730
|
+
variableSchemas: variableSchemasFor([over]),
|
|
731
|
+
})
|
|
732
|
+
: { issues: [] };
|
|
733
|
+
if (mapItemResolution.issues.length > 0) {
|
|
734
|
+
errors.push({
|
|
735
|
+
stepId: def.id,
|
|
736
|
+
message: `map over: ${mapItemResolution.issues.slice(0, 5).join("; ")}`,
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
let key = { kind: "mapItem", path: [] };
|
|
740
|
+
try {
|
|
741
|
+
key = lower(cfg.key(mapItemProxy()));
|
|
742
|
+
}
|
|
743
|
+
catch (err) {
|
|
744
|
+
if (err instanceof WiringError) {
|
|
745
|
+
errors.push({ stepId: def.id, message: `map key: ${err.message}` });
|
|
746
|
+
}
|
|
747
|
+
else
|
|
748
|
+
throw err;
|
|
749
|
+
}
|
|
750
|
+
if (key.kind !== "mapItem" && key.kind !== "variable") {
|
|
751
|
+
errors.push({
|
|
752
|
+
stepId: def.id,
|
|
753
|
+
message: "map key must select a field off the item or use a variable that resolves to a string",
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
checkSelector(def.id, key, true);
|
|
757
|
+
checkAtLeastOne(def.id, cfg.concurrency, "concurrency");
|
|
758
|
+
checkAtLeastOne(def.id, cfg.retries?.maxAttempts, "retries.maxAttempts");
|
|
759
|
+
const loweredChild = lowerChild(def.id, cfg.step(mapItemProxy()), mapItemResolution.schema);
|
|
760
|
+
if (!loweredChild)
|
|
761
|
+
break;
|
|
762
|
+
stepSchemas[def.id] = { type: "array", items: loweredChild.outputSchemaJson };
|
|
763
|
+
if (!loweredChild.outputSchemaValid)
|
|
764
|
+
invalidStepSchemas.add(def.id);
|
|
765
|
+
const child = loweredChild.step;
|
|
766
|
+
if (!child)
|
|
767
|
+
break;
|
|
768
|
+
irSteps[def.id] = {
|
|
769
|
+
kind: "map",
|
|
770
|
+
id: def.id,
|
|
771
|
+
over,
|
|
772
|
+
key,
|
|
773
|
+
step: child,
|
|
774
|
+
...(cfg.concurrency !== undefined ? { concurrency: cfg.concurrency } : {}),
|
|
775
|
+
...(cfg.retries !== undefined ? { retries: cfg.retries } : {}),
|
|
776
|
+
};
|
|
777
|
+
break;
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
knownIds.push(def.id);
|
|
781
|
+
}
|
|
782
|
+
let output = { kind: "input", path: [] };
|
|
783
|
+
try {
|
|
784
|
+
output = lower(definition.outputWiring(stepsProxy(knownIds), inputProxy()));
|
|
785
|
+
}
|
|
786
|
+
catch (err) {
|
|
787
|
+
if (err instanceof WiringError) {
|
|
788
|
+
errors.push({ message: `workflow output: ${err.message}` });
|
|
789
|
+
}
|
|
790
|
+
else
|
|
791
|
+
throw err;
|
|
792
|
+
}
|
|
793
|
+
checkSelector("(output)", output, false);
|
|
794
|
+
const referencedVariables = new Set(collectVariableNames(output));
|
|
795
|
+
for (const step of Object.values(irSteps)) {
|
|
796
|
+
for (const selector of stepSelectors(step)) {
|
|
797
|
+
for (const name of collectVariableNames(selector))
|
|
798
|
+
referencedVariables.add(name);
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
const variables = [...referencedVariables]
|
|
802
|
+
.sort((a, b) => a.localeCompare(b))
|
|
803
|
+
.flatMap((name) => {
|
|
804
|
+
const definition = variableDefinitionForCompile(name);
|
|
805
|
+
return definition ? [{ name, ...definition }] : [];
|
|
806
|
+
});
|
|
807
|
+
for (const step of Object.values(irSteps)) {
|
|
808
|
+
const executable = step.kind === "map" ? step.step : step;
|
|
809
|
+
if ((executable.kind === "code" || executable.kind === "agent") &&
|
|
810
|
+
executable.host.kind === "device" &&
|
|
811
|
+
!declaredDeviceHandles.has(executable.host.handle)) {
|
|
812
|
+
errors.push({
|
|
813
|
+
stepId: step.id,
|
|
814
|
+
message: `device handle "${executable.host.handle}" must be declared in workflow devices`,
|
|
815
|
+
});
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
if (errors.length > 0)
|
|
819
|
+
return { ok: false, errors };
|
|
820
|
+
const ir = {
|
|
821
|
+
schemaVersion: 1,
|
|
822
|
+
id: config.id,
|
|
823
|
+
...(config.description !== undefined ? { description: config.description } : {}),
|
|
824
|
+
...(devices.length > 0 ? { devices } : {}),
|
|
825
|
+
...(secrets.length > 0 ? { secrets } : {}),
|
|
826
|
+
...(variables.length > 0 ? { variables } : {}),
|
|
827
|
+
...(triggers.length > 0 ? { triggers } : {}),
|
|
828
|
+
inputSchemaJson,
|
|
829
|
+
outputSchemaJson,
|
|
830
|
+
steps: irSteps,
|
|
831
|
+
output,
|
|
832
|
+
};
|
|
833
|
+
return { ok: true, ir, codeRefs, agentRefs };
|
|
834
|
+
}
|