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.
@@ -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