flow-lang 0.1.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/LICENSE +21 -0
- package/README.md +256 -0
- package/dist/analyzer/index.d.ts +3 -0
- package/dist/analyzer/index.d.ts.map +1 -0
- package/dist/analyzer/index.js +268 -0
- package/dist/analyzer/index.js.map +1 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +241 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/errors/index.d.ts +40 -0
- package/dist/errors/index.d.ts.map +1 -0
- package/dist/errors/index.js +110 -0
- package/dist/errors/index.js.map +1 -0
- package/dist/lexer/index.d.ts +7 -0
- package/dist/lexer/index.d.ts.map +1 -0
- package/dist/lexer/index.js +443 -0
- package/dist/lexer/index.js.map +1 -0
- package/dist/parser/index.d.ts +11 -0
- package/dist/parser/index.d.ts.map +1 -0
- package/dist/parser/index.js +942 -0
- package/dist/parser/index.js.map +1 -0
- package/dist/runtime/index.d.ts +92 -0
- package/dist/runtime/index.d.ts.map +1 -0
- package/dist/runtime/index.js +972 -0
- package/dist/runtime/index.js.map +1 -0
- package/dist/types/index.d.ts +269 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +29 -0
- package/dist/types/index.js.map +1 -0
- package/package.json +59 -0
|
@@ -0,0 +1,972 @@
|
|
|
1
|
+
import { createError } from "../errors/index.js";
|
|
2
|
+
// ============================================================
|
|
3
|
+
// FlowValue constructors
|
|
4
|
+
// ============================================================
|
|
5
|
+
export function text(value) {
|
|
6
|
+
return { type: "text", value };
|
|
7
|
+
}
|
|
8
|
+
export function num(value) {
|
|
9
|
+
return { type: "number", value };
|
|
10
|
+
}
|
|
11
|
+
export function bool(value) {
|
|
12
|
+
return { type: "boolean", value };
|
|
13
|
+
}
|
|
14
|
+
export function list(items) {
|
|
15
|
+
return { type: "list", value: items };
|
|
16
|
+
}
|
|
17
|
+
export function record(entries) {
|
|
18
|
+
const map = new Map();
|
|
19
|
+
for (const [k, v] of Object.entries(entries)) {
|
|
20
|
+
map.set(k, v);
|
|
21
|
+
}
|
|
22
|
+
return { type: "record", value: map };
|
|
23
|
+
}
|
|
24
|
+
export const EMPTY = { type: "empty" };
|
|
25
|
+
// ============================================================
|
|
26
|
+
// FlowValue helpers
|
|
27
|
+
// ============================================================
|
|
28
|
+
export function toDisplay(value) {
|
|
29
|
+
switch (value.type) {
|
|
30
|
+
case "text": return value.value;
|
|
31
|
+
case "number": return String(value.value);
|
|
32
|
+
case "boolean": return value.value ? "true" : "false";
|
|
33
|
+
case "list": return "[" + value.value.map(toDisplay).join(", ") + "]";
|
|
34
|
+
case "record": {
|
|
35
|
+
const entries = [];
|
|
36
|
+
for (const [k, v] of value.value) {
|
|
37
|
+
entries.push(`${k}: ${toDisplay(v)}`);
|
|
38
|
+
}
|
|
39
|
+
return "{ " + entries.join(", ") + " }";
|
|
40
|
+
}
|
|
41
|
+
case "empty": return "(empty)";
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
export function isTruthy(value) {
|
|
45
|
+
switch (value.type) {
|
|
46
|
+
case "text": return value.value.length > 0;
|
|
47
|
+
case "number": return value.value !== 0;
|
|
48
|
+
case "boolean": return value.value;
|
|
49
|
+
case "list": return value.value.length > 0;
|
|
50
|
+
case "record": return value.value.size > 0;
|
|
51
|
+
case "empty": return false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
export function flowValuesEqual(a, b) {
|
|
55
|
+
if (a.type !== b.type)
|
|
56
|
+
return false;
|
|
57
|
+
switch (a.type) {
|
|
58
|
+
case "text": return a.value === b.value;
|
|
59
|
+
case "number": return a.value === b.value;
|
|
60
|
+
case "boolean": return a.value === b.value;
|
|
61
|
+
case "empty": return true;
|
|
62
|
+
case "list": {
|
|
63
|
+
const bList = b;
|
|
64
|
+
if (a.value.length !== bList.value.length)
|
|
65
|
+
return false;
|
|
66
|
+
return a.value.every((item, i) => flowValuesEqual(item, bList.value[i]));
|
|
67
|
+
}
|
|
68
|
+
case "record": {
|
|
69
|
+
const bRec = b;
|
|
70
|
+
if (a.value.size !== bRec.value.size)
|
|
71
|
+
return false;
|
|
72
|
+
for (const [k, v] of a.value) {
|
|
73
|
+
const bVal = bRec.value.get(k);
|
|
74
|
+
if (!bVal || !flowValuesEqual(v, bVal))
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function asNumber(value, loc, ctx) {
|
|
82
|
+
if (value.type === "number")
|
|
83
|
+
return value.value;
|
|
84
|
+
throw new RuntimeError(`I expected a number here, but got ${value.type} (${toDisplay(value)}).`, loc, ctx.source, ctx.fileName);
|
|
85
|
+
}
|
|
86
|
+
// ============================================================
|
|
87
|
+
// JSON <-> FlowValue conversion
|
|
88
|
+
// ============================================================
|
|
89
|
+
export function jsonToFlowValue(data) {
|
|
90
|
+
if (data === null || data === undefined)
|
|
91
|
+
return EMPTY;
|
|
92
|
+
if (typeof data === "string")
|
|
93
|
+
return text(data);
|
|
94
|
+
if (typeof data === "number")
|
|
95
|
+
return num(data);
|
|
96
|
+
if (typeof data === "boolean")
|
|
97
|
+
return bool(data);
|
|
98
|
+
if (Array.isArray(data))
|
|
99
|
+
return list(data.map(jsonToFlowValue));
|
|
100
|
+
if (typeof data === "object") {
|
|
101
|
+
const map = new Map();
|
|
102
|
+
for (const [k, v] of Object.entries(data)) {
|
|
103
|
+
map.set(k, jsonToFlowValue(v));
|
|
104
|
+
}
|
|
105
|
+
return { type: "record", value: map };
|
|
106
|
+
}
|
|
107
|
+
return EMPTY;
|
|
108
|
+
}
|
|
109
|
+
export function flowValueToJson(value) {
|
|
110
|
+
switch (value.type) {
|
|
111
|
+
case "text": return value.value;
|
|
112
|
+
case "number": return value.value;
|
|
113
|
+
case "boolean": return value.value;
|
|
114
|
+
case "list": return value.value.map(flowValueToJson);
|
|
115
|
+
case "record": {
|
|
116
|
+
const obj = {};
|
|
117
|
+
for (const [k, v] of value.value) {
|
|
118
|
+
obj[k] = flowValueToJson(v);
|
|
119
|
+
}
|
|
120
|
+
return obj;
|
|
121
|
+
}
|
|
122
|
+
case "empty": return null;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
// ============================================================
|
|
126
|
+
// Runtime Error
|
|
127
|
+
// ============================================================
|
|
128
|
+
export class RuntimeError extends Error {
|
|
129
|
+
flowError;
|
|
130
|
+
constructor(message, loc, source, fileName) {
|
|
131
|
+
super(message);
|
|
132
|
+
this.name = "RuntimeError";
|
|
133
|
+
this.flowError = createError(fileName, loc.line, loc.column, message, source);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
// ============================================================
|
|
137
|
+
// Environment (scope/variable store)
|
|
138
|
+
// ============================================================
|
|
139
|
+
export class Environment {
|
|
140
|
+
variables = new Map();
|
|
141
|
+
parent;
|
|
142
|
+
constructor(parent = null) {
|
|
143
|
+
this.parent = parent;
|
|
144
|
+
}
|
|
145
|
+
get(name) {
|
|
146
|
+
const val = this.variables.get(name);
|
|
147
|
+
if (val !== undefined)
|
|
148
|
+
return val;
|
|
149
|
+
if (this.parent)
|
|
150
|
+
return this.parent.get(name);
|
|
151
|
+
return undefined;
|
|
152
|
+
}
|
|
153
|
+
set(name, value) {
|
|
154
|
+
// If the variable already exists in a parent scope, update it there
|
|
155
|
+
// (so `set x to ...` inside a loop updates the outer x, not a new local x)
|
|
156
|
+
let current = this.parent;
|
|
157
|
+
while (current) {
|
|
158
|
+
if (current.variables.has(name)) {
|
|
159
|
+
current.variables.set(name, value);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
current = current.parent;
|
|
163
|
+
}
|
|
164
|
+
// Otherwise define it in the current scope
|
|
165
|
+
this.variables.set(name, value);
|
|
166
|
+
}
|
|
167
|
+
createChild() {
|
|
168
|
+
return new Environment(this);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
export class MockAPIConnector {
|
|
172
|
+
callCount = 0;
|
|
173
|
+
failCount;
|
|
174
|
+
constructor(options) {
|
|
175
|
+
this.failCount = options?.failCount ?? 0;
|
|
176
|
+
}
|
|
177
|
+
async call(verb, description, _params) {
|
|
178
|
+
this.callCount++;
|
|
179
|
+
if (this.callCount <= this.failCount) {
|
|
180
|
+
throw new Error(`Service call failed: ${verb} ${description} (mock failure)`);
|
|
181
|
+
}
|
|
182
|
+
return record({
|
|
183
|
+
status: text("ok"),
|
|
184
|
+
data: text(`mock response for: ${verb} ${description}`),
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
export class MockAIConnector {
|
|
189
|
+
callCount = 0;
|
|
190
|
+
failCount;
|
|
191
|
+
constructor(options) {
|
|
192
|
+
this.failCount = options?.failCount ?? 0;
|
|
193
|
+
}
|
|
194
|
+
async call(_verb, description, _params) {
|
|
195
|
+
this.callCount++;
|
|
196
|
+
if (this.callCount <= this.failCount) {
|
|
197
|
+
throw new Error(`AI service failed: ${description} (mock failure)`);
|
|
198
|
+
}
|
|
199
|
+
return record({
|
|
200
|
+
result: text(`mock AI response for: ${description}`),
|
|
201
|
+
confidence: num(0.85),
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
export class MockPluginConnector {
|
|
206
|
+
callCount = 0;
|
|
207
|
+
failCount;
|
|
208
|
+
constructor(options) {
|
|
209
|
+
this.failCount = options?.failCount ?? 0;
|
|
210
|
+
}
|
|
211
|
+
async call(verb, description, _params) {
|
|
212
|
+
this.callCount++;
|
|
213
|
+
if (this.callCount <= this.failCount) {
|
|
214
|
+
throw new Error(`Plugin failed: ${verb} ${description} (mock failure)`);
|
|
215
|
+
}
|
|
216
|
+
return record({
|
|
217
|
+
status: text("ok"),
|
|
218
|
+
data: text(`mock plugin response for: ${verb} ${description}`),
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
export class MockWebhookConnector {
|
|
223
|
+
callCount = 0;
|
|
224
|
+
failCount;
|
|
225
|
+
constructor(options) {
|
|
226
|
+
this.failCount = options?.failCount ?? 0;
|
|
227
|
+
}
|
|
228
|
+
async call(verb, description, _params) {
|
|
229
|
+
this.callCount++;
|
|
230
|
+
if (this.callCount <= this.failCount) {
|
|
231
|
+
throw new Error(`Webhook failed: ${verb} ${description} (mock failure)`);
|
|
232
|
+
}
|
|
233
|
+
return record({
|
|
234
|
+
status: text("ok"),
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
export function createMockConnector(serviceType, options) {
|
|
239
|
+
switch (serviceType) {
|
|
240
|
+
case "api": return new MockAPIConnector(options);
|
|
241
|
+
case "ai": return new MockAIConnector(options);
|
|
242
|
+
case "plugin": return new MockPluginConnector(options);
|
|
243
|
+
case "webhook": return new MockWebhookConnector(options);
|
|
244
|
+
default: return new MockAPIConnector(options);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
// ============================================================
|
|
248
|
+
// Real Connectors
|
|
249
|
+
// ============================================================
|
|
250
|
+
// Verb-to-HTTP-method mapping
|
|
251
|
+
const GET_VERBS = new Set(["get", "fetch", "retrieve", "check", "pull", "list", "find", "search"]);
|
|
252
|
+
const POST_VERBS = new Set(["create", "send", "submit", "add", "post", "charge", "notify", "record", "verify"]);
|
|
253
|
+
const PUT_VERBS = new Set(["update", "modify", "change", "edit"]);
|
|
254
|
+
const DELETE_VERBS = new Set(["delete", "remove", "cancel"]);
|
|
255
|
+
export function inferHTTPMethod(verb) {
|
|
256
|
+
const lower = verb.toLowerCase();
|
|
257
|
+
if (GET_VERBS.has(lower))
|
|
258
|
+
return "GET";
|
|
259
|
+
if (POST_VERBS.has(lower))
|
|
260
|
+
return "POST";
|
|
261
|
+
if (PUT_VERBS.has(lower))
|
|
262
|
+
return "PUT";
|
|
263
|
+
if (DELETE_VERBS.has(lower))
|
|
264
|
+
return "DELETE";
|
|
265
|
+
return "POST"; // default
|
|
266
|
+
}
|
|
267
|
+
export class HTTPAPIConnector {
|
|
268
|
+
baseUrl;
|
|
269
|
+
constructor(baseUrl) {
|
|
270
|
+
this.baseUrl = baseUrl.replace(/\/$/, ""); // strip trailing slash
|
|
271
|
+
}
|
|
272
|
+
async call(verb, description, params, path) {
|
|
273
|
+
const method = inferHTTPMethod(verb);
|
|
274
|
+
let url = this.baseUrl;
|
|
275
|
+
if (path) {
|
|
276
|
+
url += path.startsWith("/") ? path : "/" + path;
|
|
277
|
+
}
|
|
278
|
+
// Serialize params
|
|
279
|
+
const serialized = {};
|
|
280
|
+
for (const [k, v] of params) {
|
|
281
|
+
serialized[k] = flowValueToJson(v);
|
|
282
|
+
}
|
|
283
|
+
const controller = new AbortController();
|
|
284
|
+
const timeout = setTimeout(() => controller.abort(), 30000);
|
|
285
|
+
try {
|
|
286
|
+
let response;
|
|
287
|
+
if (method === "GET" || method === "DELETE") {
|
|
288
|
+
// Params become query string
|
|
289
|
+
const queryParts = [];
|
|
290
|
+
for (const [k, v] of Object.entries(serialized)) {
|
|
291
|
+
queryParts.push(`${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`);
|
|
292
|
+
}
|
|
293
|
+
if (queryParts.length > 0) {
|
|
294
|
+
url += (url.includes("?") ? "&" : "?") + queryParts.join("&");
|
|
295
|
+
}
|
|
296
|
+
response = await fetch(url, {
|
|
297
|
+
method,
|
|
298
|
+
headers: { "Accept": "application/json" },
|
|
299
|
+
signal: controller.signal,
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
else {
|
|
303
|
+
// POST/PUT: params become JSON body
|
|
304
|
+
response = await fetch(url, {
|
|
305
|
+
method,
|
|
306
|
+
headers: {
|
|
307
|
+
"Content-Type": "application/json",
|
|
308
|
+
"Accept": "application/json",
|
|
309
|
+
},
|
|
310
|
+
body: JSON.stringify({
|
|
311
|
+
verb,
|
|
312
|
+
description,
|
|
313
|
+
...serialized,
|
|
314
|
+
}),
|
|
315
|
+
signal: controller.signal,
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
if (!response.ok) {
|
|
319
|
+
const body = await response.text().catch(() => "");
|
|
320
|
+
throw new Error(`Service returned error ${response.status}: ${body || response.statusText}`);
|
|
321
|
+
}
|
|
322
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
323
|
+
if (contentType.includes("application/json")) {
|
|
324
|
+
const data = await response.json();
|
|
325
|
+
return jsonToFlowValue(data);
|
|
326
|
+
}
|
|
327
|
+
// Non-JSON response: return as text
|
|
328
|
+
const textBody = await response.text();
|
|
329
|
+
return text(textBody);
|
|
330
|
+
}
|
|
331
|
+
catch (err) {
|
|
332
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
333
|
+
throw new Error(`Request to ${this.baseUrl} timed out after 30 seconds`);
|
|
334
|
+
}
|
|
335
|
+
throw err;
|
|
336
|
+
}
|
|
337
|
+
finally {
|
|
338
|
+
clearTimeout(timeout);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
export class WebhookConnector {
|
|
343
|
+
url;
|
|
344
|
+
constructor(url) {
|
|
345
|
+
this.url = url;
|
|
346
|
+
}
|
|
347
|
+
async call(verb, description, params) {
|
|
348
|
+
const serialized = {};
|
|
349
|
+
for (const [k, v] of params) {
|
|
350
|
+
serialized[k] = flowValueToJson(v);
|
|
351
|
+
}
|
|
352
|
+
const controller = new AbortController();
|
|
353
|
+
const timeout = setTimeout(() => controller.abort(), 30000);
|
|
354
|
+
try {
|
|
355
|
+
const response = await fetch(this.url, {
|
|
356
|
+
method: "POST",
|
|
357
|
+
headers: { "Content-Type": "application/json" },
|
|
358
|
+
body: JSON.stringify({ verb, description, ...serialized }),
|
|
359
|
+
signal: controller.signal,
|
|
360
|
+
});
|
|
361
|
+
if (!response.ok) {
|
|
362
|
+
const body = await response.text().catch(() => "");
|
|
363
|
+
throw new Error(`Webhook returned error ${response.status}: ${body || response.statusText}`);
|
|
364
|
+
}
|
|
365
|
+
return record({ status: text("ok") });
|
|
366
|
+
}
|
|
367
|
+
catch (err) {
|
|
368
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
369
|
+
throw new Error(`Webhook at ${this.url} timed out after 30 seconds`);
|
|
370
|
+
}
|
|
371
|
+
throw err;
|
|
372
|
+
}
|
|
373
|
+
finally {
|
|
374
|
+
clearTimeout(timeout);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
export class PluginStubConnector {
|
|
379
|
+
async call(verb, description, _params) {
|
|
380
|
+
// Plugin connectors are not yet implemented — fall back to mock behavior
|
|
381
|
+
return record({
|
|
382
|
+
status: text("ok"),
|
|
383
|
+
data: text(`mock plugin response for: ${verb} ${description}`),
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
// ============================================================
|
|
388
|
+
// AI Connectors
|
|
389
|
+
// ============================================================
|
|
390
|
+
const AI_SYSTEM_PROMPT = `You are a workflow assistant. Respond ONLY with a JSON object containing:
|
|
391
|
+
- "result": your response as a string
|
|
392
|
+
- "confidence": a number between 0 and 1 indicating your confidence
|
|
393
|
+
|
|
394
|
+
Example: {"result": "The application looks good", "confidence": 0.92}`;
|
|
395
|
+
function buildAIContext(params) {
|
|
396
|
+
if (params.size === 0)
|
|
397
|
+
return "";
|
|
398
|
+
const parts = [];
|
|
399
|
+
for (const [k, v] of params) {
|
|
400
|
+
parts.push(`${k}: ${toDisplay(v)}`);
|
|
401
|
+
}
|
|
402
|
+
return "\n\nContext:\n" + parts.join("\n");
|
|
403
|
+
}
|
|
404
|
+
function parseAIResponse(responseText) {
|
|
405
|
+
try {
|
|
406
|
+
const parsed = JSON.parse(responseText);
|
|
407
|
+
return record({
|
|
408
|
+
result: text(String(parsed["result"] ?? responseText)),
|
|
409
|
+
confidence: num(Number(parsed["confidence"] ?? 0.5)),
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
catch {
|
|
413
|
+
return record({
|
|
414
|
+
result: text(responseText),
|
|
415
|
+
confidence: num(0.5),
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
export class AnthropicConnector {
|
|
420
|
+
model;
|
|
421
|
+
apiKey;
|
|
422
|
+
constructor(target, apiKey) {
|
|
423
|
+
this.model = target.startsWith("anthropic/") ? target.slice(10) : target;
|
|
424
|
+
this.apiKey = apiKey;
|
|
425
|
+
}
|
|
426
|
+
getModel() {
|
|
427
|
+
return this.model;
|
|
428
|
+
}
|
|
429
|
+
async call(_verb, description, params) {
|
|
430
|
+
if (!this.apiKey) {
|
|
431
|
+
throw new Error("Missing API key. Set ANTHROPIC_API_KEY in your .env file.");
|
|
432
|
+
}
|
|
433
|
+
const { default: Anthropic } = await import("@anthropic-ai/sdk");
|
|
434
|
+
const client = new Anthropic({ apiKey: this.apiKey });
|
|
435
|
+
const userMessage = description + buildAIContext(params);
|
|
436
|
+
const message = await client.messages.create({
|
|
437
|
+
model: this.model,
|
|
438
|
+
max_tokens: 1024,
|
|
439
|
+
system: AI_SYSTEM_PROMPT,
|
|
440
|
+
messages: [{ role: "user", content: userMessage }],
|
|
441
|
+
});
|
|
442
|
+
const textBlock = message.content.find((b) => b.type === "text");
|
|
443
|
+
const responseText = textBlock && "text" in textBlock ? textBlock.text : "";
|
|
444
|
+
return parseAIResponse(responseText);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
export class OpenAIConnector {
|
|
448
|
+
model;
|
|
449
|
+
apiKey;
|
|
450
|
+
constructor(target, apiKey) {
|
|
451
|
+
this.model = target.startsWith("openai/") ? target.slice(7) : target;
|
|
452
|
+
this.apiKey = apiKey;
|
|
453
|
+
}
|
|
454
|
+
getModel() {
|
|
455
|
+
return this.model;
|
|
456
|
+
}
|
|
457
|
+
async call(_verb, description, params) {
|
|
458
|
+
if (!this.apiKey) {
|
|
459
|
+
throw new Error("Missing API key. Set OPENAI_API_KEY in your .env file.");
|
|
460
|
+
}
|
|
461
|
+
const { default: OpenAI } = await import("openai");
|
|
462
|
+
const client = new OpenAI({ apiKey: this.apiKey });
|
|
463
|
+
const userMessage = description + buildAIContext(params);
|
|
464
|
+
const response = await client.chat.completions.create({
|
|
465
|
+
model: this.model,
|
|
466
|
+
max_tokens: 1024,
|
|
467
|
+
messages: [
|
|
468
|
+
{ role: "system", content: AI_SYSTEM_PROMPT },
|
|
469
|
+
{ role: "user", content: userMessage },
|
|
470
|
+
],
|
|
471
|
+
});
|
|
472
|
+
const responseText = response.choices[0]?.message?.content ?? "";
|
|
473
|
+
return parseAIResponse(responseText);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
// ============================================================
|
|
477
|
+
// Flow control signals
|
|
478
|
+
// ============================================================
|
|
479
|
+
class CompleteSignal {
|
|
480
|
+
outputs;
|
|
481
|
+
constructor(outputs) {
|
|
482
|
+
this.outputs = outputs;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
class RejectSignal {
|
|
486
|
+
message;
|
|
487
|
+
constructor(message) {
|
|
488
|
+
this.message = message;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
// ============================================================
|
|
492
|
+
// Expression evaluator (stays synchronous)
|
|
493
|
+
// ============================================================
|
|
494
|
+
function evaluateExpression(expr, ctx) {
|
|
495
|
+
switch (expr.kind) {
|
|
496
|
+
case "StringLiteral":
|
|
497
|
+
return text(expr.value);
|
|
498
|
+
case "NumberLiteral":
|
|
499
|
+
return num(expr.value);
|
|
500
|
+
case "BooleanLiteral":
|
|
501
|
+
return bool(expr.value);
|
|
502
|
+
case "Identifier": {
|
|
503
|
+
const val = ctx.env.get(expr.name);
|
|
504
|
+
if (val === undefined) {
|
|
505
|
+
throw new RuntimeError(`The variable "${expr.name}" hasn't been set yet.`, expr.loc, ctx.source, ctx.fileName);
|
|
506
|
+
}
|
|
507
|
+
return val;
|
|
508
|
+
}
|
|
509
|
+
case "DotAccess":
|
|
510
|
+
return evaluateDotAccess(expr, ctx);
|
|
511
|
+
case "InterpolatedString":
|
|
512
|
+
return evaluateInterpolatedString(expr, ctx);
|
|
513
|
+
case "MathExpression":
|
|
514
|
+
return evaluateMath(expr, ctx);
|
|
515
|
+
case "ComparisonExpression":
|
|
516
|
+
return evaluateComparison(expr, ctx);
|
|
517
|
+
case "LogicalExpression":
|
|
518
|
+
return evaluateLogical(expr, ctx);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
function evaluateDotAccess(expr, ctx) {
|
|
522
|
+
if (expr.kind !== "DotAccess") {
|
|
523
|
+
return evaluateExpression(expr, ctx);
|
|
524
|
+
}
|
|
525
|
+
// Build the full dot-access chain
|
|
526
|
+
const parts = [expr.property];
|
|
527
|
+
let current = expr.object;
|
|
528
|
+
while (current.kind === "DotAccess") {
|
|
529
|
+
parts.unshift(current.property);
|
|
530
|
+
current = current.object;
|
|
531
|
+
}
|
|
532
|
+
// The root should be an identifier
|
|
533
|
+
if (current.kind !== "Identifier") {
|
|
534
|
+
throw new RuntimeError("I can only access fields on variables, not on other expressions.", expr.loc, ctx.source, ctx.fileName);
|
|
535
|
+
}
|
|
536
|
+
const rootName = current.name;
|
|
537
|
+
let value = ctx.env.get(rootName);
|
|
538
|
+
if (value === undefined) {
|
|
539
|
+
// Return empty for undefined root (lenient for trigger data)
|
|
540
|
+
return EMPTY;
|
|
541
|
+
}
|
|
542
|
+
// Traverse the chain
|
|
543
|
+
const isEnvAccess = rootName === "env";
|
|
544
|
+
for (const part of parts) {
|
|
545
|
+
if (value.type === "record") {
|
|
546
|
+
const field = value.value.get(part);
|
|
547
|
+
if (field === undefined) {
|
|
548
|
+
if (isEnvAccess && ctx.strictEnv) {
|
|
549
|
+
throw new RuntimeError(`The environment variable "${part}" is not set. Add it to your .env file or set it in your system environment.`, expr.loc, ctx.source, ctx.fileName);
|
|
550
|
+
}
|
|
551
|
+
if (isEnvAccess && ctx.verbose) {
|
|
552
|
+
ctx.log.push({
|
|
553
|
+
timestamp: new Date(),
|
|
554
|
+
step: ctx.currentStep,
|
|
555
|
+
action: "env warning",
|
|
556
|
+
result: "skipped",
|
|
557
|
+
details: { message: `Environment variable "${part}" is not set` },
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
return EMPTY;
|
|
561
|
+
}
|
|
562
|
+
value = field;
|
|
563
|
+
}
|
|
564
|
+
else if (value.type === "empty") {
|
|
565
|
+
return EMPTY;
|
|
566
|
+
}
|
|
567
|
+
else {
|
|
568
|
+
throw new RuntimeError(`I can't access ".${part}" on a ${value.type} value. Only records have fields.`, expr.loc, ctx.source, ctx.fileName);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
return value;
|
|
572
|
+
}
|
|
573
|
+
function evaluateInterpolatedString(expr, ctx) {
|
|
574
|
+
let result = "";
|
|
575
|
+
for (const part of expr.parts) {
|
|
576
|
+
if (part.kind === "text") {
|
|
577
|
+
result += part.value;
|
|
578
|
+
}
|
|
579
|
+
else {
|
|
580
|
+
const val = evaluateExpression(part.value, ctx);
|
|
581
|
+
result += toDisplay(val);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
return text(result);
|
|
585
|
+
}
|
|
586
|
+
function evaluateMath(expr, ctx) {
|
|
587
|
+
const left = evaluateExpression(expr.left, ctx);
|
|
588
|
+
const right = evaluateExpression(expr.right, ctx);
|
|
589
|
+
// Special case: text + text with "plus" means concatenation
|
|
590
|
+
if (expr.operator === "plus" && left.type === "text" && right.type === "text") {
|
|
591
|
+
return text(left.value + right.value);
|
|
592
|
+
}
|
|
593
|
+
const leftNum = asNumber(left, expr.left.loc, ctx);
|
|
594
|
+
const rightNum = asNumber(right, expr.right.loc, ctx);
|
|
595
|
+
switch (expr.operator) {
|
|
596
|
+
case "plus": return num(leftNum + rightNum);
|
|
597
|
+
case "minus": return num(leftNum - rightNum);
|
|
598
|
+
case "times": return num(leftNum * rightNum);
|
|
599
|
+
case "divided by": {
|
|
600
|
+
if (rightNum === 0) {
|
|
601
|
+
throw new RuntimeError("I can't divide by zero.", expr.right.loc, ctx.source, ctx.fileName);
|
|
602
|
+
}
|
|
603
|
+
return num(leftNum / rightNum);
|
|
604
|
+
}
|
|
605
|
+
case "rounded to": return num(Number(leftNum.toFixed(rightNum)));
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
function evaluateComparison(expr, ctx) {
|
|
609
|
+
const left = evaluateExpression(expr.left, ctx);
|
|
610
|
+
switch (expr.operator) {
|
|
611
|
+
// Unary operators
|
|
612
|
+
case "is empty":
|
|
613
|
+
return bool(!isTruthy(left));
|
|
614
|
+
case "is not empty":
|
|
615
|
+
return bool(isTruthy(left));
|
|
616
|
+
case "exists":
|
|
617
|
+
return bool(left.type !== "empty");
|
|
618
|
+
case "does not exist":
|
|
619
|
+
return bool(left.type === "empty");
|
|
620
|
+
// Binary operators
|
|
621
|
+
case "is": {
|
|
622
|
+
const right = evaluateExpression(expr.right, ctx);
|
|
623
|
+
return bool(flowValuesEqual(left, right));
|
|
624
|
+
}
|
|
625
|
+
case "is not": {
|
|
626
|
+
const right = evaluateExpression(expr.right, ctx);
|
|
627
|
+
return bool(!flowValuesEqual(left, right));
|
|
628
|
+
}
|
|
629
|
+
case "is above": {
|
|
630
|
+
const right = evaluateExpression(expr.right, ctx);
|
|
631
|
+
return bool(asNumber(left, expr.left.loc, ctx) > asNumber(right, expr.right.loc, ctx));
|
|
632
|
+
}
|
|
633
|
+
case "is below": {
|
|
634
|
+
const right = evaluateExpression(expr.right, ctx);
|
|
635
|
+
return bool(asNumber(left, expr.left.loc, ctx) < asNumber(right, expr.right.loc, ctx));
|
|
636
|
+
}
|
|
637
|
+
case "is at least": {
|
|
638
|
+
const right = evaluateExpression(expr.right, ctx);
|
|
639
|
+
return bool(asNumber(left, expr.left.loc, ctx) >= asNumber(right, expr.right.loc, ctx));
|
|
640
|
+
}
|
|
641
|
+
case "is at most": {
|
|
642
|
+
const right = evaluateExpression(expr.right, ctx);
|
|
643
|
+
return bool(asNumber(left, expr.left.loc, ctx) <= asNumber(right, expr.right.loc, ctx));
|
|
644
|
+
}
|
|
645
|
+
case "contains": {
|
|
646
|
+
const right = evaluateExpression(expr.right, ctx);
|
|
647
|
+
if (left.type === "text" && right.type === "text") {
|
|
648
|
+
return bool(left.value.includes(right.value));
|
|
649
|
+
}
|
|
650
|
+
if (left.type === "list") {
|
|
651
|
+
return bool(left.value.some(item => flowValuesEqual(item, right)));
|
|
652
|
+
}
|
|
653
|
+
throw new RuntimeError(`I can't use "contains" on a ${left.type} value. It works with text and lists.`, expr.loc, ctx.source, ctx.fileName);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
function evaluateLogical(expr, ctx) {
|
|
658
|
+
const left = evaluateExpression(expr.left, ctx);
|
|
659
|
+
switch (expr.operator) {
|
|
660
|
+
case "not":
|
|
661
|
+
return bool(!isTruthy(left));
|
|
662
|
+
case "and": {
|
|
663
|
+
if (!isTruthy(left))
|
|
664
|
+
return bool(false);
|
|
665
|
+
const right = evaluateExpression(expr.right, ctx);
|
|
666
|
+
return bool(isTruthy(right));
|
|
667
|
+
}
|
|
668
|
+
case "or": {
|
|
669
|
+
if (isTruthy(left))
|
|
670
|
+
return bool(true);
|
|
671
|
+
const right = evaluateExpression(expr.right, ctx);
|
|
672
|
+
return bool(isTruthy(right));
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
// ============================================================
|
|
677
|
+
// Statement executor (async — service calls return promises)
|
|
678
|
+
// ============================================================
|
|
679
|
+
async function executeStatements(stmts, ctx) {
|
|
680
|
+
for (const stmt of stmts) {
|
|
681
|
+
await executeStatement(stmt, ctx);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
async function executeStatement(stmt, ctx) {
|
|
685
|
+
switch (stmt.kind) {
|
|
686
|
+
case "SetStatement":
|
|
687
|
+
executeSetStatement(stmt, ctx);
|
|
688
|
+
break;
|
|
689
|
+
case "IfStatement":
|
|
690
|
+
await executeIfStatement(stmt, ctx);
|
|
691
|
+
break;
|
|
692
|
+
case "ForEachStatement":
|
|
693
|
+
await executeForEachStatement(stmt, ctx);
|
|
694
|
+
break;
|
|
695
|
+
case "ServiceCall":
|
|
696
|
+
await executeServiceCall(stmt, ctx);
|
|
697
|
+
break;
|
|
698
|
+
case "AskStatement":
|
|
699
|
+
await executeAskStatement(stmt, ctx);
|
|
700
|
+
break;
|
|
701
|
+
case "LogStatement":
|
|
702
|
+
executeLogStatement(stmt, ctx);
|
|
703
|
+
break;
|
|
704
|
+
case "CompleteStatement":
|
|
705
|
+
executeCompleteStatement(stmt, ctx);
|
|
706
|
+
break;
|
|
707
|
+
case "RejectStatement":
|
|
708
|
+
executeRejectStatement(stmt, ctx);
|
|
709
|
+
break;
|
|
710
|
+
case "StepBlock":
|
|
711
|
+
await executeStepBlock(stmt, ctx);
|
|
712
|
+
break;
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
function executeSetStatement(stmt, ctx) {
|
|
716
|
+
const value = evaluateExpression(stmt.value, ctx);
|
|
717
|
+
ctx.env.set(stmt.variable, value);
|
|
718
|
+
}
|
|
719
|
+
async function executeIfStatement(stmt, ctx) {
|
|
720
|
+
const condValue = evaluateExpression(stmt.condition, ctx);
|
|
721
|
+
if (isTruthy(condValue)) {
|
|
722
|
+
await executeStatements(stmt.body, ctx);
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
for (const oi of stmt.otherwiseIfs) {
|
|
726
|
+
const oiValue = evaluateExpression(oi.condition, ctx);
|
|
727
|
+
if (isTruthy(oiValue)) {
|
|
728
|
+
await executeStatements(oi.body, ctx);
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
if (stmt.otherwise) {
|
|
733
|
+
await executeStatements(stmt.otherwise, ctx);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
async function executeForEachStatement(stmt, ctx) {
|
|
737
|
+
const collection = evaluateExpression(stmt.collection, ctx);
|
|
738
|
+
if (collection.type !== "list") {
|
|
739
|
+
throw new RuntimeError(`I expected a list to loop over, but got ${collection.type} (${toDisplay(collection)}).`, stmt.collection.loc, ctx.source, ctx.fileName);
|
|
740
|
+
}
|
|
741
|
+
for (const item of collection.value) {
|
|
742
|
+
const childEnv = ctx.env.createChild();
|
|
743
|
+
childEnv.set(stmt.itemName, item);
|
|
744
|
+
const childCtx = { ...ctx, env: childEnv };
|
|
745
|
+
await executeStatements(stmt.body, childCtx);
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
async function executeServiceCall(stmt, ctx) {
|
|
749
|
+
const connector = ctx.connectors.get(stmt.service);
|
|
750
|
+
if (!connector) {
|
|
751
|
+
throw new RuntimeError(`No connector found for service "${stmt.service}".`, stmt.loc, ctx.source, ctx.fileName);
|
|
752
|
+
}
|
|
753
|
+
// Evaluate parameters
|
|
754
|
+
const params = new Map();
|
|
755
|
+
for (const param of stmt.parameters) {
|
|
756
|
+
params.set(param.name, evaluateExpression(param.value, ctx));
|
|
757
|
+
}
|
|
758
|
+
// Evaluate path if present
|
|
759
|
+
let path;
|
|
760
|
+
if (stmt.path) {
|
|
761
|
+
const pathValue = evaluateExpression(stmt.path, ctx);
|
|
762
|
+
path = toDisplay(pathValue);
|
|
763
|
+
}
|
|
764
|
+
if (stmt.errorHandler) {
|
|
765
|
+
await executeWithErrorHandler(async () => {
|
|
766
|
+
await connector.call(stmt.verb, stmt.description, params, path);
|
|
767
|
+
addLogEntry(ctx, stmt.verb + " " + stmt.description, "success", { service: stmt.service });
|
|
768
|
+
}, stmt.errorHandler, ctx, stmt.loc, { service: stmt.service, verb: stmt.verb, description: stmt.description });
|
|
769
|
+
}
|
|
770
|
+
else {
|
|
771
|
+
try {
|
|
772
|
+
await connector.call(stmt.verb, stmt.description, params, path);
|
|
773
|
+
addLogEntry(ctx, stmt.verb + " " + stmt.description, "success", { service: stmt.service });
|
|
774
|
+
}
|
|
775
|
+
catch (err) {
|
|
776
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
777
|
+
addLogEntry(ctx, stmt.verb + " " + stmt.description, "failure", { service: stmt.service, error: message });
|
|
778
|
+
throw new RuntimeError(`The service "${stmt.service}" failed: ${message}`, stmt.loc, ctx.source, ctx.fileName);
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
async function executeAskStatement(stmt, ctx) {
|
|
783
|
+
const connector = ctx.connectors.get(stmt.agent);
|
|
784
|
+
if (!connector) {
|
|
785
|
+
throw new RuntimeError(`No connector found for agent "${stmt.agent}".`, stmt.loc, ctx.source, ctx.fileName);
|
|
786
|
+
}
|
|
787
|
+
try {
|
|
788
|
+
const response = await connector.call("ask", stmt.instruction, new Map());
|
|
789
|
+
addLogEntry(ctx, "ask " + stmt.agent, "success", { agent: stmt.agent, instruction: stmt.instruction });
|
|
790
|
+
// Extract result and confidence from the response
|
|
791
|
+
if (stmt.resultVar) {
|
|
792
|
+
if (response.type === "record") {
|
|
793
|
+
const resultVal = response.value.get("result") ?? response;
|
|
794
|
+
ctx.env.set(stmt.resultVar, resultVal);
|
|
795
|
+
}
|
|
796
|
+
else {
|
|
797
|
+
ctx.env.set(stmt.resultVar, response);
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
if (stmt.confidenceVar) {
|
|
801
|
+
if (response.type === "record") {
|
|
802
|
+
const confVal = response.value.get("confidence") ?? EMPTY;
|
|
803
|
+
ctx.env.set(stmt.confidenceVar, confVal);
|
|
804
|
+
}
|
|
805
|
+
else {
|
|
806
|
+
ctx.env.set(stmt.confidenceVar, EMPTY);
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
catch (err) {
|
|
811
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
812
|
+
addLogEntry(ctx, "ask " + stmt.agent, "failure", { agent: stmt.agent, error: message });
|
|
813
|
+
throw new RuntimeError(`The agent "${stmt.agent}" failed: ${message}`, stmt.loc, ctx.source, ctx.fileName);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
function executeLogStatement(stmt, ctx) {
|
|
817
|
+
const value = evaluateExpression(stmt.expression, ctx);
|
|
818
|
+
const displayed = toDisplay(value);
|
|
819
|
+
addLogEntry(ctx, "log", "success", { message: displayed });
|
|
820
|
+
}
|
|
821
|
+
function executeCompleteStatement(stmt, ctx) {
|
|
822
|
+
const outputs = {};
|
|
823
|
+
for (const param of stmt.outputs) {
|
|
824
|
+
outputs[param.name] = evaluateExpression(param.value, ctx);
|
|
825
|
+
}
|
|
826
|
+
throw new CompleteSignal(outputs);
|
|
827
|
+
}
|
|
828
|
+
function executeRejectStatement(stmt, ctx) {
|
|
829
|
+
const value = evaluateExpression(stmt.message, ctx);
|
|
830
|
+
throw new RejectSignal(toDisplay(value));
|
|
831
|
+
}
|
|
832
|
+
async function executeStepBlock(stmt, ctx) {
|
|
833
|
+
const prevStep = ctx.currentStep;
|
|
834
|
+
ctx.currentStep = stmt.name;
|
|
835
|
+
addLogEntry(ctx, `step "${stmt.name}" started`, "success", {});
|
|
836
|
+
await executeStatements(stmt.body, ctx);
|
|
837
|
+
addLogEntry(ctx, `step "${stmt.name}" completed`, "success", {});
|
|
838
|
+
ctx.currentStep = prevStep;
|
|
839
|
+
}
|
|
840
|
+
// ============================================================
|
|
841
|
+
// Error handling (retry logic)
|
|
842
|
+
// ============================================================
|
|
843
|
+
async function executeWithErrorHandler(action, handler, ctx, loc, details) {
|
|
844
|
+
const maxAttempts = 1 + (handler.retryCount ?? 0);
|
|
845
|
+
let lastError = null;
|
|
846
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
847
|
+
try {
|
|
848
|
+
await action();
|
|
849
|
+
return; // Success
|
|
850
|
+
}
|
|
851
|
+
catch (err) {
|
|
852
|
+
lastError = err;
|
|
853
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
854
|
+
if (attempt < maxAttempts) {
|
|
855
|
+
addLogEntry(ctx, `retry ${attempt}/${handler.retryCount}`, "failure", { ...details, error: message });
|
|
856
|
+
// In a real runtime we'd wait handler.retryWaitSeconds here
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
// All attempts failed
|
|
861
|
+
if (handler.fallback) {
|
|
862
|
+
addLogEntry(ctx, "executing fallback", "success", details);
|
|
863
|
+
await executeStatements(handler.fallback, ctx);
|
|
864
|
+
}
|
|
865
|
+
else {
|
|
866
|
+
const message = lastError instanceof Error ? lastError.message : String(lastError);
|
|
867
|
+
addLogEntry(ctx, "all retries failed", "failure", { ...details, error: message });
|
|
868
|
+
throw new RuntimeError(`All ${maxAttempts} attempts failed: ${message}`, loc, ctx.source, ctx.fileName);
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
// ============================================================
|
|
872
|
+
// Log helper
|
|
873
|
+
// ============================================================
|
|
874
|
+
function addLogEntry(ctx, action, result, details) {
|
|
875
|
+
ctx.log.push({
|
|
876
|
+
timestamp: new Date(),
|
|
877
|
+
step: ctx.currentStep,
|
|
878
|
+
action,
|
|
879
|
+
result,
|
|
880
|
+
details,
|
|
881
|
+
});
|
|
882
|
+
}
|
|
883
|
+
export async function execute(program, source, options) {
|
|
884
|
+
const fileName = "<input>";
|
|
885
|
+
const log = [];
|
|
886
|
+
if (!program.workflow) {
|
|
887
|
+
return {
|
|
888
|
+
result: { status: "completed", outputs: {} },
|
|
889
|
+
log,
|
|
890
|
+
};
|
|
891
|
+
}
|
|
892
|
+
// Set up the global environment
|
|
893
|
+
const globalEnv = new Environment();
|
|
894
|
+
// Add env variable (wraps environment variables)
|
|
895
|
+
const envEntries = {};
|
|
896
|
+
const envSource = options?.envVars ?? {};
|
|
897
|
+
for (const [k, v] of Object.entries(envSource)) {
|
|
898
|
+
envEntries[k] = text(v);
|
|
899
|
+
}
|
|
900
|
+
globalEnv.set("env", record(envEntries));
|
|
901
|
+
// Add input data to the environment
|
|
902
|
+
if (options?.input) {
|
|
903
|
+
for (const [key, value] of Object.entries(options.input)) {
|
|
904
|
+
globalEnv.set(key, jsonToFlowValue(value));
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
// Set up connectors
|
|
908
|
+
const connectors = new Map();
|
|
909
|
+
if (options?.connectors) {
|
|
910
|
+
for (const [name, connector] of options.connectors) {
|
|
911
|
+
connectors.set(name, connector);
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
// Create mock connectors for any services not already provided
|
|
915
|
+
if (program.services) {
|
|
916
|
+
for (const decl of program.services.declarations) {
|
|
917
|
+
if (!connectors.has(decl.name)) {
|
|
918
|
+
connectors.set(decl.name, createMockConnector(decl.serviceType));
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
// Build execution context
|
|
923
|
+
const ctx = {
|
|
924
|
+
env: globalEnv,
|
|
925
|
+
connectors,
|
|
926
|
+
log,
|
|
927
|
+
currentStep: null,
|
|
928
|
+
source,
|
|
929
|
+
fileName,
|
|
930
|
+
verbose: options?.verbose ?? false,
|
|
931
|
+
strictEnv: options?.strictEnv ?? false,
|
|
932
|
+
};
|
|
933
|
+
// Execute the workflow
|
|
934
|
+
try {
|
|
935
|
+
await executeStatements(program.workflow.body, ctx);
|
|
936
|
+
// If we get here, no complete/reject was hit
|
|
937
|
+
return {
|
|
938
|
+
result: { status: "completed", outputs: {} },
|
|
939
|
+
log,
|
|
940
|
+
};
|
|
941
|
+
}
|
|
942
|
+
catch (signal) {
|
|
943
|
+
if (signal instanceof CompleteSignal) {
|
|
944
|
+
return {
|
|
945
|
+
result: { status: "completed", outputs: signal.outputs },
|
|
946
|
+
log,
|
|
947
|
+
};
|
|
948
|
+
}
|
|
949
|
+
if (signal instanceof RejectSignal) {
|
|
950
|
+
return {
|
|
951
|
+
result: { status: "rejected", message: signal.message },
|
|
952
|
+
log,
|
|
953
|
+
};
|
|
954
|
+
}
|
|
955
|
+
if (signal instanceof RuntimeError) {
|
|
956
|
+
return {
|
|
957
|
+
result: { status: "error", error: signal.flowError },
|
|
958
|
+
log,
|
|
959
|
+
};
|
|
960
|
+
}
|
|
961
|
+
// Unexpected error
|
|
962
|
+
const message = signal instanceof Error ? signal.message : String(signal);
|
|
963
|
+
return {
|
|
964
|
+
result: {
|
|
965
|
+
status: "error",
|
|
966
|
+
error: createError(fileName, 1, 1, `Unexpected error: ${message}`, source),
|
|
967
|
+
},
|
|
968
|
+
log,
|
|
969
|
+
};
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
//# sourceMappingURL=index.js.map
|