@duckflux/core 0.6.8 → 0.7.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/dist/index.js CHANGED
@@ -26590,6 +26590,248 @@ var init_validate = __esm(() => {
26590
26590
  BUILTIN_ONERROR = new Set(["fail", "skip", "retry"]);
26591
26591
  });
26592
26592
 
26593
+ // src/tracer/writers/json.ts
26594
+ var exports_json = {};
26595
+ __export(exports_json, {
26596
+ JsonTraceWriter: () => JsonTraceWriter
26597
+ });
26598
+ import { writeFile } from "node:fs/promises";
26599
+ import { join } from "node:path";
26600
+
26601
+ class JsonTraceWriter {
26602
+ dir;
26603
+ filePath = "";
26604
+ trace = {
26605
+ execution: {
26606
+ id: "",
26607
+ startedAt: "",
26608
+ finishedAt: "",
26609
+ duration: 0,
26610
+ status: "running",
26611
+ inputs: null,
26612
+ output: null
26613
+ },
26614
+ steps: []
26615
+ };
26616
+ constructor(dir) {
26617
+ this.dir = dir;
26618
+ }
26619
+ async open(meta) {
26620
+ this.filePath = join(this.dir, `${meta.id}.json`);
26621
+ this.trace = {
26622
+ execution: {
26623
+ id: meta.id,
26624
+ workflowId: meta.workflowId,
26625
+ workflowName: meta.workflowName,
26626
+ workflowVersion: meta.workflowVersion,
26627
+ startedAt: meta.startedAt,
26628
+ finishedAt: "",
26629
+ duration: 0,
26630
+ status: "running",
26631
+ inputs: meta.inputs,
26632
+ output: null
26633
+ },
26634
+ steps: []
26635
+ };
26636
+ await this.flush();
26637
+ }
26638
+ writeStep(step) {
26639
+ this.trace.steps.push(step);
26640
+ this.flushSync();
26641
+ }
26642
+ async finalize(meta) {
26643
+ this.trace.execution.status = meta.status;
26644
+ this.trace.execution.output = meta.output;
26645
+ this.trace.execution.finishedAt = meta.finishedAt;
26646
+ this.trace.execution.duration = meta.duration;
26647
+ await this.flush();
26648
+ }
26649
+ flushSync() {
26650
+ this.flush().catch(() => {});
26651
+ }
26652
+ async flush() {
26653
+ if (!this.filePath)
26654
+ return;
26655
+ await writeFile(this.filePath, JSON.stringify(this.trace, null, 2), "utf-8");
26656
+ }
26657
+ }
26658
+ var init_json = () => {};
26659
+
26660
+ // src/tracer/writers/txt.ts
26661
+ var exports_txt = {};
26662
+ __export(exports_txt, {
26663
+ TxtTraceWriter: () => TxtTraceWriter
26664
+ });
26665
+ import { appendFile, readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
26666
+ import { join as join2 } from "node:path";
26667
+ function serializeValue(value) {
26668
+ if (value === undefined || value === null)
26669
+ return "none";
26670
+ if (typeof value === "string")
26671
+ return value;
26672
+ return JSON.stringify(value, null, 2);
26673
+ }
26674
+ function formatStep(step) {
26675
+ const lines = [
26676
+ `## [${step.seq}] ${step.name} (${step.type})`,
26677
+ `startedAt: ${step.startedAt}`
26678
+ ];
26679
+ if (step.finishedAt)
26680
+ lines.push(`finishedAt: ${step.finishedAt}`);
26681
+ if (step.duration !== undefined)
26682
+ lines.push(`duration: ${step.duration}ms`);
26683
+ lines.push(`status: ${step.status}`);
26684
+ if (step.loopIndex !== undefined)
26685
+ lines.push(`loopIndex: ${step.loopIndex}`);
26686
+ if (step.retries !== undefined && step.retries > 0)
26687
+ lines.push(`retries: ${step.retries}`);
26688
+ lines.push(`input: ${serializeValue(step.input)}`);
26689
+ lines.push(`output: ${serializeValue(step.output)}`);
26690
+ if (step.error)
26691
+ lines.push(`error: ${step.error}`);
26692
+ lines.push("");
26693
+ return lines.join(`
26694
+ `);
26695
+ }
26696
+
26697
+ class TxtTraceWriter {
26698
+ dir;
26699
+ filePath = "";
26700
+ constructor(dir) {
26701
+ this.dir = dir;
26702
+ }
26703
+ async open(meta) {
26704
+ this.filePath = join2(this.dir, `${meta.id}.txt`);
26705
+ const versionStr = meta.workflowVersion !== undefined ? ` (v${meta.workflowVersion})` : "";
26706
+ const workflowLabel = meta.workflowName ?? meta.workflowId ?? "unnamed";
26707
+ const header = [
26708
+ "# execution",
26709
+ `id: ${meta.id}`,
26710
+ `workflow: ${workflowLabel}${versionStr}`,
26711
+ `startedAt: ${meta.startedAt}`,
26712
+ "status: running",
26713
+ "",
26714
+ "# inputs",
26715
+ serializeValue(meta.inputs),
26716
+ "",
26717
+ "# steps",
26718
+ ""
26719
+ ].join(`
26720
+ `);
26721
+ await writeFile2(this.filePath, header, "utf-8");
26722
+ }
26723
+ async writeStep(step) {
26724
+ if (!this.filePath)
26725
+ return;
26726
+ await appendFile(this.filePath, formatStep(step), "utf-8");
26727
+ }
26728
+ async finalize(meta) {
26729
+ if (!this.filePath)
26730
+ return;
26731
+ const outputSection = [
26732
+ "# output",
26733
+ serializeValue(meta.output),
26734
+ ""
26735
+ ].join(`
26736
+ `);
26737
+ await appendFile(this.filePath, outputSection, "utf-8");
26738
+ const content = await readFile2(this.filePath, "utf-8");
26739
+ const updated = content.replace(/^status: running$/m, `status: ${meta.status}
26740
+ finishedAt: ${meta.finishedAt}
26741
+ duration: ${meta.duration}ms`);
26742
+ await writeFile2(this.filePath, updated, "utf-8");
26743
+ }
26744
+ }
26745
+ var init_txt = () => {};
26746
+
26747
+ // src/tracer/writers/sqlite.ts
26748
+ var exports_sqlite = {};
26749
+ __export(exports_sqlite, {
26750
+ SqliteTraceWriter: () => SqliteTraceWriter
26751
+ });
26752
+ import { join as join3 } from "node:path";
26753
+ function toJson(value) {
26754
+ if (value === undefined || value === null)
26755
+ return null;
26756
+ if (typeof value === "string")
26757
+ return value;
26758
+ return JSON.stringify(value);
26759
+ }
26760
+
26761
+ class SqliteTraceWriter {
26762
+ dir;
26763
+ db = null;
26764
+ executionId = "";
26765
+ constructor(dir) {
26766
+ this.dir = dir;
26767
+ }
26768
+ async open(meta) {
26769
+ this.executionId = meta.id;
26770
+ const filePath = join3(this.dir, `${meta.id}.sqlite`);
26771
+ const { Database } = await import("bun:sqlite");
26772
+ this.db = new Database(filePath);
26773
+ this.db.exec(`
26774
+ CREATE TABLE IF NOT EXISTS executions (
26775
+ id TEXT PRIMARY KEY,
26776
+ workflow_id TEXT,
26777
+ workflow_name TEXT,
26778
+ workflow_version TEXT,
26779
+ started_at TEXT NOT NULL,
26780
+ finished_at TEXT,
26781
+ duration_ms INTEGER,
26782
+ status TEXT NOT NULL,
26783
+ inputs TEXT,
26784
+ output TEXT
26785
+ );
26786
+
26787
+ CREATE TABLE IF NOT EXISTS steps (
26788
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
26789
+ execution_id TEXT NOT NULL REFERENCES executions(id),
26790
+ seq INTEGER NOT NULL,
26791
+ name TEXT NOT NULL,
26792
+ type TEXT NOT NULL,
26793
+ started_at TEXT,
26794
+ finished_at TEXT,
26795
+ duration_ms INTEGER,
26796
+ status TEXT NOT NULL,
26797
+ input TEXT,
26798
+ output TEXT,
26799
+ error TEXT,
26800
+ retries INTEGER,
26801
+ loop_index INTEGER
26802
+ );
26803
+ `);
26804
+ const insert = this.db.prepare(`
26805
+ INSERT INTO executions (id, workflow_id, workflow_name, workflow_version, started_at, status, inputs)
26806
+ VALUES (?, ?, ?, ?, ?, 'running', ?)
26807
+ `);
26808
+ insert.run(meta.id, meta.workflowId ?? null, meta.workflowName ?? null, meta.workflowVersion !== undefined ? String(meta.workflowVersion) : null, meta.startedAt, toJson(meta.inputs));
26809
+ }
26810
+ writeStep(step) {
26811
+ if (!this.db)
26812
+ return;
26813
+ const insert = this.db.prepare(`
26814
+ INSERT INTO steps
26815
+ (execution_id, seq, name, type, started_at, finished_at, duration_ms, status, input, output, error, retries, loop_index)
26816
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
26817
+ `);
26818
+ insert.run(this.executionId, step.seq, step.name, step.type, step.startedAt ?? null, step.finishedAt ?? null, step.duration ?? null, step.status, toJson(step.input), toJson(step.output), step.error ?? null, step.retries ?? null, step.loopIndex ?? null);
26819
+ }
26820
+ async finalize(meta) {
26821
+ if (!this.db)
26822
+ return;
26823
+ const update = this.db.prepare(`
26824
+ UPDATE executions
26825
+ SET status = ?, output = ?, finished_at = ?, duration_ms = ?
26826
+ WHERE id = ?
26827
+ `);
26828
+ update.run(meta.status, toJson(meta.output), meta.finishedAt, Math.round(meta.duration), this.executionId);
26829
+ this.db.close();
26830
+ this.db = null;
26831
+ }
26832
+ }
26833
+ var init_sqlite = () => {};
26834
+
26593
26835
  // src/engine/errors.ts
26594
26836
  function sleep(ms) {
26595
26837
  return new Promise((resolve2) => setTimeout(resolve2, ms));
@@ -27189,6 +27431,10 @@ async function executeStep(workflow, state, step, basePath = process.cwd(), engi
27189
27431
  output: "",
27190
27432
  duration: 0
27191
27433
  });
27434
+ const loopIndex2 = state.isInsideLoop() ? state.currentLoopIndex() : undefined;
27435
+ const skippedSeq = state.tracer?.startStep(stepName, participant.type, undefined, loopIndex2);
27436
+ if (skippedSeq !== undefined)
27437
+ state.tracer?.endStep(skippedSeq, "skipped");
27192
27438
  }
27193
27439
  return chain;
27194
27440
  }
@@ -27197,6 +27443,8 @@ async function executeStep(workflow, state, step, basePath = process.cwd(), engi
27197
27443
  const overrideInput = override?.input !== undefined ? resolveParticipantInput(override.input, state) : undefined;
27198
27444
  const mergedWithBase = mergeChainedInput(chain, baseInput);
27199
27445
  const mergedInput = mergeChainedInput(mergedWithBase, overrideInput);
27446
+ const loopIndex = state.isInsideLoop() ? state.currentLoopIndex() : undefined;
27447
+ const traceSeq = state.tracer?.startStep(stepName ?? "<anonymous>", participant.type, mergedInput, loopIndex);
27200
27448
  state.currentInput = mergedInput;
27201
27449
  const strategy = resolveErrorStrategy(override ?? null, participant, workflow.defaults ?? null);
27202
27450
  const timeoutMs = resolveTimeout(override ?? null, participant, workflow.defaults ?? null);
@@ -27274,6 +27522,8 @@ async function executeStep(workflow, state, step, basePath = process.cwd(), engi
27274
27522
  }
27275
27523
  const outputValue = result.parsedOutput ?? result.output;
27276
27524
  state.currentOutput = outputValue;
27525
+ if (traceSeq !== undefined)
27526
+ state.tracer?.endStep(traceSeq, result.status, outputValue, undefined, retries);
27277
27527
  return outputValue;
27278
27528
  } catch (error) {
27279
27529
  const message = String(error?.message ?? error);
@@ -27299,6 +27549,8 @@ async function executeStep(workflow, state, step, basePath = process.cwd(), engi
27299
27549
  if (stepName) {
27300
27550
  state.setResult(stepName, skipResult);
27301
27551
  }
27552
+ if (traceSeq !== undefined)
27553
+ state.tracer?.endStep(traceSeq, "skipped", undefined, message);
27302
27554
  return chain;
27303
27555
  }
27304
27556
  if (strategy !== "fail" && strategy !== "retry") {
@@ -27317,6 +27569,8 @@ async function executeStep(workflow, state, step, basePath = process.cwd(), engi
27317
27569
  ...httpMeta
27318
27570
  });
27319
27571
  }
27572
+ if (traceSeq !== undefined)
27573
+ state.tracer?.endStep(traceSeq, "failure", undefined, message);
27320
27574
  const fallbackResult = await executeStep(workflow, state, fallbackName, basePath, engineExecutor, [...fallbackStack, stepName ?? "<anonymous>"], chain, hub);
27321
27575
  return fallbackResult;
27322
27576
  }
@@ -27331,6 +27585,8 @@ async function executeStep(workflow, state, step, basePath = process.cwd(), engi
27331
27585
  ...httpMeta
27332
27586
  });
27333
27587
  }
27588
+ if (traceSeq !== undefined)
27589
+ state.tracer?.endStep(traceSeq, "failure", undefined, message);
27334
27590
  throw error;
27335
27591
  }
27336
27592
  }
@@ -27443,21 +27699,38 @@ async function executeControlStep(workflow, state, step, basePath = process.cwd(
27443
27699
  }
27444
27700
  const obj = step;
27445
27701
  if ("wait" in obj && Object.keys(obj).length === 1) {
27446
- const { executeWait: executeWait2 } = await Promise.resolve().then(() => (init_wait(), exports_wait));
27447
- return executeWait2(state, obj.wait, chain, hub, signal);
27702
+ const loopIndex = state.isInsideLoop() ? state.currentLoopIndex() : undefined;
27703
+ const traceSeq = state.tracer?.startStep("wait", "wait", undefined, loopIndex);
27704
+ try {
27705
+ const { executeWait: executeWait2 } = await Promise.resolve().then(() => (init_wait(), exports_wait));
27706
+ const result = await executeWait2(state, obj.wait, chain, hub, signal);
27707
+ if (traceSeq !== undefined)
27708
+ state.tracer?.endStep(traceSeq, "success", result);
27709
+ return result;
27710
+ } catch (err) {
27711
+ if (traceSeq !== undefined)
27712
+ state.tracer?.endStep(traceSeq, "failure", undefined, String(err?.message ?? err));
27713
+ throw err;
27714
+ }
27448
27715
  }
27449
27716
  if ("set" in obj && Object.keys(obj).length === 1) {
27450
27717
  const setDef = obj.set;
27451
27718
  const ctx = state.toCelContext();
27719
+ const loopIndex = state.isInsideLoop() ? state.currentLoopIndex() : undefined;
27720
+ const traceSeq = state.tracer?.startStep("set", "set", setDef, loopIndex);
27452
27721
  if (!state.executionMeta.context) {
27453
27722
  state.executionMeta.context = {};
27454
27723
  }
27455
27724
  for (const [key, expr] of Object.entries(setDef)) {
27456
27725
  if (RESERVED_SET_KEYS.has(key)) {
27726
+ if (traceSeq !== undefined)
27727
+ state.tracer?.endStep(traceSeq, "failure", undefined, `set key '${key}' uses a reserved name`);
27457
27728
  throw new Error(`set key '${key}' uses a reserved name`);
27458
27729
  }
27459
27730
  state.executionMeta.context[key] = evaluateCel(expr, ctx);
27460
27731
  }
27732
+ if (traceSeq !== undefined)
27733
+ state.tracer?.endStep(traceSeq, "success", state.executionMeta.context);
27461
27734
  return chain;
27462
27735
  }
27463
27736
  if ("loop" in obj && Object.keys(obj).length === 1) {
@@ -27474,6 +27747,8 @@ async function executeControlStep(workflow, state, step, basePath = process.cwd(
27474
27747
  maxIterations = loopDef.max ?? Number.POSITIVE_INFINITY;
27475
27748
  }
27476
27749
  const hasMax = loopDef.max !== undefined;
27750
+ const outerLoopIndex = state.isInsideLoop() ? state.currentLoopIndex() : undefined;
27751
+ const loopTraceSeq = state.tracer?.startStep(loopAs ?? "loop", "loop", undefined, outerLoopIndex);
27477
27752
  state.pushLoop(loopAs);
27478
27753
  let loopChain = chain;
27479
27754
  try {
@@ -27497,6 +27772,12 @@ async function executeControlStep(workflow, state, step, basePath = process.cwd(
27497
27772
  iterations += 1;
27498
27773
  state.incrementLoop();
27499
27774
  }
27775
+ if (loopTraceSeq !== undefined)
27776
+ state.tracer?.endStep(loopTraceSeq, "success", loopChain);
27777
+ } catch (err) {
27778
+ if (loopTraceSeq !== undefined)
27779
+ state.tracer?.endStep(loopTraceSeq, "failure", undefined, String(err?.message ?? err));
27780
+ throw err;
27500
27781
  } finally {
27501
27782
  state.popLoop();
27502
27783
  }
@@ -27505,29 +27786,54 @@ async function executeControlStep(workflow, state, step, basePath = process.cwd(
27505
27786
  if ("parallel" in obj && Object.keys(obj).length === 1) {
27506
27787
  const parallelSteps = obj.parallel;
27507
27788
  const controller = new AbortController;
27789
+ const loopIndex = state.isInsideLoop() ? state.currentLoopIndex() : undefined;
27790
+ const parallelTraceSeq = state.tracer?.startStep("parallel", "parallel", undefined, loopIndex);
27508
27791
  const branchSignal = signal ? AbortSignal.any([signal, controller.signal]) : controller.signal;
27509
- const results = await Promise.all(parallelSteps.map(async (parallelStep) => {
27510
- try {
27511
- return await executeControlStep(workflow, state, parallelStep, basePath, engineExecutor, chain, hub, branchSignal);
27512
- } catch (error) {
27513
- controller.abort();
27514
- throw error;
27515
- }
27516
- }));
27517
- return results;
27792
+ try {
27793
+ const results = await Promise.all(parallelSteps.map(async (parallelStep) => {
27794
+ try {
27795
+ return await executeControlStep(workflow, state, parallelStep, basePath, engineExecutor, chain, hub, branchSignal);
27796
+ } catch (error) {
27797
+ controller.abort();
27798
+ throw error;
27799
+ }
27800
+ }));
27801
+ if (parallelTraceSeq !== undefined)
27802
+ state.tracer?.endStep(parallelTraceSeq, "success", results);
27803
+ return results;
27804
+ } catch (err) {
27805
+ if (parallelTraceSeq !== undefined)
27806
+ state.tracer?.endStep(parallelTraceSeq, "failure", undefined, String(err?.message ?? err));
27807
+ throw err;
27808
+ }
27518
27809
  }
27519
27810
  if ("if" in obj && Object.keys(obj).length === 1) {
27520
27811
  const ifDef = obj.if;
27521
27812
  const condition = evaluateCel(ifDef.condition, state.toCelContext());
27813
+ const loopIndex = state.isInsideLoop() ? state.currentLoopIndex() : undefined;
27814
+ const ifTraceSeq = state.tracer?.startStep("if", "if", { condition: ifDef.condition }, loopIndex);
27522
27815
  if (typeof condition !== "boolean") {
27816
+ if (ifTraceSeq !== undefined)
27817
+ state.tracer?.endStep(ifTraceSeq, "failure", undefined, `if.condition must evaluate to boolean, got ${typeof condition}`);
27523
27818
  throw new Error(`if.condition must evaluate to boolean, got ${typeof condition}`);
27524
27819
  }
27525
- if (condition) {
27526
- return executeSequential(workflow, state, ifDef.then, basePath, engineExecutor, chain, hub, signal);
27527
- } else if (ifDef.else) {
27528
- return executeSequential(workflow, state, ifDef.else, basePath, engineExecutor, chain, hub, signal);
27820
+ try {
27821
+ let result;
27822
+ if (condition) {
27823
+ result = await executeSequential(workflow, state, ifDef.then, basePath, engineExecutor, chain, hub, signal);
27824
+ } else if (ifDef.else) {
27825
+ result = await executeSequential(workflow, state, ifDef.else, basePath, engineExecutor, chain, hub, signal);
27826
+ } else {
27827
+ result = chain;
27828
+ }
27829
+ if (ifTraceSeq !== undefined)
27830
+ state.tracer?.endStep(ifTraceSeq, "success", result);
27831
+ return result;
27832
+ } catch (err) {
27833
+ if (ifTraceSeq !== undefined)
27834
+ state.tracer?.endStep(ifTraceSeq, "failure", undefined, String(err?.message ?? err));
27835
+ throw err;
27529
27836
  }
27530
- return chain;
27531
27837
  }
27532
27838
  return executeStep(workflow, state, step, basePath, engineExecutor, [], chain, hub, signal);
27533
27839
  }
@@ -27814,6 +28120,82 @@ init_parser3();
27814
28120
  init_schema();
27815
28121
  init_validate();
27816
28122
  import { dirname as dirname2, resolve as resolve3 } from "node:path";
28123
+
28124
+ // src/tracer/index.ts
28125
+ import { mkdir } from "node:fs/promises";
28126
+
28127
+ class TraceCollector {
28128
+ openSteps = new Map;
28129
+ seq = 0;
28130
+ truncateAt;
28131
+ writer;
28132
+ constructor(truncateAt = 1e6) {
28133
+ this.truncateAt = truncateAt;
28134
+ }
28135
+ startStep(name, type3, input, loopIndex) {
28136
+ this.seq += 1;
28137
+ this.openSteps.set(this.seq, {
28138
+ name,
28139
+ type: type3,
28140
+ startedAt: new Date().toISOString(),
28141
+ startMs: performance.now(),
28142
+ input: input !== undefined ? this.truncate(input) : undefined,
28143
+ loopIndex
28144
+ });
28145
+ return this.seq;
28146
+ }
28147
+ endStep(seq, status, output, error, retries) {
28148
+ const open = this.openSteps.get(seq);
28149
+ if (!open)
28150
+ return;
28151
+ this.openSteps.delete(seq);
28152
+ const finishedAt = new Date().toISOString();
28153
+ const duration = Math.max(0, performance.now() - open.startMs);
28154
+ const step = {
28155
+ seq,
28156
+ name: open.name,
28157
+ type: open.type,
28158
+ startedAt: open.startedAt,
28159
+ finishedAt,
28160
+ duration: Math.round(duration),
28161
+ status,
28162
+ ...open.input !== undefined ? { input: open.input } : {},
28163
+ ...output !== undefined ? { output: this.truncate(output) } : {},
28164
+ ...error !== undefined ? { error } : {},
28165
+ ...retries !== undefined && retries > 0 ? { retries } : {},
28166
+ ...open.loopIndex !== undefined ? { loopIndex: open.loopIndex } : {}
28167
+ };
28168
+ this.writer?.writeStep(step);
28169
+ }
28170
+ truncate(value) {
28171
+ if (value == null)
28172
+ return value;
28173
+ const str = typeof value === "string" ? value : JSON.stringify(value);
28174
+ const bytes = new TextEncoder().encode(str);
28175
+ if (bytes.length <= this.truncateAt)
28176
+ return value;
28177
+ const cut = new TextDecoder().decode(bytes.slice(0, this.truncateAt));
28178
+ return cut + "...[truncated]";
28179
+ }
28180
+ }
28181
+ async function createTraceWriter(dir, format) {
28182
+ await mkdir(dir, { recursive: true });
28183
+ if (format === "json") {
28184
+ const { JsonTraceWriter: JsonTraceWriter2 } = await Promise.resolve().then(() => (init_json(), exports_json));
28185
+ return new JsonTraceWriter2(dir);
28186
+ }
28187
+ if (format === "txt") {
28188
+ const { TxtTraceWriter: TxtTraceWriter2 } = await Promise.resolve().then(() => (init_txt(), exports_txt));
28189
+ return new TxtTraceWriter2(dir);
28190
+ }
28191
+ if (format === "sqlite") {
28192
+ const { SqliteTraceWriter: SqliteTraceWriter2 } = await Promise.resolve().then(() => (init_sqlite(), exports_sqlite));
28193
+ return new SqliteTraceWriter2(dir);
28194
+ }
28195
+ throw new Error(`unknown trace format: ${format}`);
28196
+ }
28197
+
28198
+ // src/engine/engine.ts
27817
28199
  init_control();
27818
28200
 
27819
28201
  // src/engine/state.ts
@@ -27831,6 +28213,7 @@ class WorkflowState {
27831
28213
  chainValue;
27832
28214
  eventPayload;
27833
28215
  ancestorPaths;
28216
+ tracer;
27834
28217
  constructor(inputs = {}) {
27835
28218
  this.inputs = { ...inputs };
27836
28219
  this.workflowInputs = { ...inputs };
@@ -27881,6 +28264,9 @@ class WorkflowState {
27881
28264
  if (top)
27882
28265
  top.last = last2;
27883
28266
  }
28267
+ isInsideLoop() {
28268
+ return this.loopStack.length > 0;
28269
+ }
27884
28270
  currentLoopContext() {
27885
28271
  const top = this.loopStack[this.loopStack.length - 1];
27886
28272
  if (!top)
@@ -27971,38 +28357,66 @@ async function executeWorkflow(workflow, inputs = {}, basePath = process.cwd(),
27971
28357
  if (options.cwd) {
27972
28358
  state.executionMeta.cwd = options.cwd;
27973
28359
  }
27974
- const startedAt = performance.now();
28360
+ const startedAtMs = performance.now();
28361
+ const startedAtIso = state.executionMeta.startedAt;
27975
28362
  if (options._ancestorPaths) {
27976
28363
  state.ancestorPaths = options._ancestorPaths;
27977
28364
  }
28365
+ if (options.traceDir && !options._ancestorPaths) {
28366
+ const collector = new TraceCollector;
28367
+ const writer = await createTraceWriter(options.traceDir, options.traceFormat ?? "json");
28368
+ collector.writer = writer;
28369
+ state.tracer = collector;
28370
+ await writer.open({
28371
+ id: state.executionMeta.id,
28372
+ workflowId: workflow.id,
28373
+ workflowName: workflow.name,
28374
+ workflowVersion: workflow.version,
28375
+ startedAt: startedAtIso,
28376
+ inputs: resolved
28377
+ });
28378
+ }
27978
28379
  const engineExecutor = async (subWorkflow, subInputs, subBasePath) => {
27979
28380
  return executeWorkflow(subWorkflow, subInputs, subBasePath, {
27980
28381
  ...options,
27981
28382
  _ancestorPaths: state.ancestorPaths
27982
28383
  });
27983
28384
  };
27984
- let chain;
27985
- for (const step of workflow.flow) {
27986
- chain = await executeControlStep(workflow, state, step, basePath, engineExecutor, chain, options.hub);
27987
- }
27988
28385
  let output;
27989
- if (workflow.output !== undefined) {
27990
- output = state.resolveOutput(workflow.output, evaluateCel);
27991
- if (typeof workflow.output === "object" && "schema" in workflow.output && "map" in workflow.output && typeof output === "object" && output !== null) {
27992
- validateOutputSchema(workflow.output.schema, output);
28386
+ let success = false;
28387
+ try {
28388
+ let chain;
28389
+ for (const step of workflow.flow) {
28390
+ chain = await executeControlStep(workflow, state, step, basePath, engineExecutor, chain, options.hub);
28391
+ }
28392
+ if (workflow.output !== undefined) {
28393
+ output = state.resolveOutput(workflow.output, evaluateCel);
28394
+ if (typeof workflow.output === "object" && "schema" in workflow.output && "map" in workflow.output && typeof output === "object" && output !== null) {
28395
+ validateOutputSchema(workflow.output.schema, output);
28396
+ }
28397
+ } else {
28398
+ output = chain;
28399
+ }
28400
+ const steps = state.getAllResults();
28401
+ success = !Object.values(steps).some((step) => step.status === "failure");
28402
+ state.executionMeta.status = success ? "success" : "failure";
28403
+ return {
28404
+ success,
28405
+ output,
28406
+ steps,
28407
+ duration: Math.max(0, performance.now() - startedAtMs)
28408
+ };
28409
+ } finally {
28410
+ if (state.tracer?.writer) {
28411
+ const duration = Math.max(0, performance.now() - startedAtMs);
28412
+ await state.tracer.writer.finalize({
28413
+ status: success ? "success" : "failure",
28414
+ output: output ?? null,
28415
+ finishedAt: new Date().toISOString(),
28416
+ duration
28417
+ }).catch(() => {});
27993
28418
  }
27994
- } else {
27995
- output = chain;
27996
28419
  }
27997
- const steps = state.getAllResults();
27998
- const success = !Object.values(steps).some((step) => step.status === "failure");
27999
- state.executionMeta.status = success ? "success" : "failure";
28000
- return {
28001
- success,
28002
- output,
28003
- steps,
28004
- duration: Math.max(0, performance.now() - startedAt)
28005
- };
28006
28420
  }
28007
28421
  async function runWorkflowFromFile(filePath, inputs = {}, options = {}) {
28008
28422
  const workflow = await parseWorkflowFile(filePath);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@duckflux/core",
3
- "version": "0.6.8",
3
+ "version": "0.7.0",
4
4
  "type": "module",
5
5
  "main": "src/index.ts",
6
6
  "exports": {