@cascade-flow/runner 0.1.0 → 0.2.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/README.md CHANGED
@@ -76,8 +76,13 @@ workflows/my-workflow/
76
76
  ├── input-schema.ts # Optional: Zod schema
77
77
  ├── defineStep.ts # Optional: Typed helper
78
78
  └── steps/
79
- ├── step-1/step.ts
80
- └── step-2/step.ts
79
+ ├── step-1/step.ts # Flat step
80
+ ├── step-2/step.ts # Flat step
81
+ └── data-processing/ # Optional: Group directory
82
+ ├── extract/
83
+ │ └── fetch-data/step.ts # Nested step
84
+ └── transform/
85
+ └── normalize/step.ts # Nested step
81
86
  ```
82
87
 
83
88
  ## Key Features
@@ -87,6 +92,7 @@ workflows/my-workflow/
87
92
  - **Cycle detection** - DFS validation prevents circular dependencies
88
93
  - **Resume capability** - Continue from previous runs
89
94
  - **Pluggable backend** - Abstract persistence layer
95
+ - **Nested step groups** - Organize steps in arbitrary directory hierarchies (purely organizational)
90
96
 
91
97
  ## Exports
92
98
 
@@ -17,6 +17,7 @@ export declare function loadWorkflowConfig(workflowDir: string): Promise<Workflo
17
17
  export declare function discoverWorkflows(workflowsRoot?: string): Promise<LocalWorkflowMetadata[]>;
18
18
  /**
19
19
  * Discovers and loads all steps from a directory
20
+ * Supports arbitrary deep nesting of steps in group subdirectories
20
21
  *
21
22
  * @param root - Root directory containing step subdirectories (defaults to "./steps")
22
23
  * @returns Array of loaded steps with wired dependencies
@@ -1 +1 @@
1
- {"version":3,"file":"discovery.d.ts","sourceRoot":"","sources":["../src/discovery.ts"],"names":[],"mappings":"AAMA,OAAO,EAA0C,KAAK,UAAU,EAAyB,KAAK,cAAc,EAAE,KAAK,qBAAqB,EAAE,MAAM,SAAS,CAAC;AAG1J;;;;;;GAMG;AACH,wBAAsB,kBAAkB,CACtC,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,cAAc,CAAC,CA0BzB;AAED;;;;;;GAMG;AACH,wBAAsB,iBAAiB,CACrC,aAAa,SAA4B,GACxC,OAAO,CAAC,qBAAqB,EAAE,CAAC,CAyElC;AAED;;;;;GAKG;AACH,wBAAsB,aAAa,CACjC,IAAI,SAAwB,GAC3B,OAAO,CAAC,UAAU,EAAE,CAAC,CA2FvB"}
1
+ {"version":3,"file":"discovery.d.ts","sourceRoot":"","sources":["../src/discovery.ts"],"names":[],"mappings":"AAMA,OAAO,EAA0C,KAAK,UAAU,EAAyB,KAAK,cAAc,EAAE,KAAK,qBAAqB,EAAE,MAAM,SAAS,CAAC;AAG1J;;;;;;GAMG;AACH,wBAAsB,kBAAkB,CACtC,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,cAAc,CAAC,CA0BzB;AAED;;;;;;GAMG;AACH,wBAAsB,iBAAiB,CACrC,aAAa,SAA4B,GACxC,OAAO,CAAC,qBAAqB,EAAE,CAAC,CA+ElC;AA4CD;;;;;;GAMG;AACH,wBAAsB,aAAa,CACjC,IAAI,SAAwB,GAC3B,OAAO,CAAC,UAAU,EAAE,CAAC,CAkFvB"}
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { Backend, LogEntry } from "@cascade-flow/backend-interface";
2
- import type { StepOutput, LoadedStep, RunnerContext } from "./types";
2
+ import type { StepOutput, RunnerContext } from "@cascade-flow/workflow";
3
+ import type { LoadedStep } from "./types";
3
4
  export type { LoadedStep };
4
5
  /**
5
6
  * Execute a step in an isolated child process
@@ -22,4 +23,5 @@ export declare function runAll(options: {
22
23
  input?: unknown;
23
24
  }): Promise<Record<string, StepOutput>>;
24
25
  export { discoverWorkflows, discoverSteps } from "./discovery";
26
+ export { calculateWorkflowHash, getGitInfo } from "./versioning";
25
27
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAa,QAAQ,EAAE,MAAM,iCAAiC,CAAC;AAMpF,OAAO,KAAK,EACV,UAAU,EACV,UAAU,EACV,aAAa,EACd,MAAM,SAAS,CAAC;AAGjB,YAAY,EAAE,UAAU,EAAE,CAAC;AAE3B;;;;GAIG;AACH,wBAAsB,oBAAoB,CACxC,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,EACd,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACrC,GAAG,EAAE,aAAa,EAClB,aAAa,EAAE,MAAM,EACrB,OAAO,EAAE,OAAO,EAChB,KAAK,CAAC,EAAE,CAAC,GAAG,EAAE,QAAQ,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,EAC/C,OAAO,CAAC,EAAE;IAAE,MAAM,CAAC,EAAE,WAAW,CAAA;CAAE,GACjC,OAAO,CAAC;IAAE,MAAM,EAAE,UAAU,CAAC;IAAC,IAAI,EAAE,QAAQ,EAAE,CAAA;CAAE,CAAC,CAoBnD;AAED,wBAAsB,MAAM,CAAC,OAAO,EAAE;IAClC,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,GAAG,CAAC,EAAE,OAAO,CAAC,aAAa,CAAC,CAAC;IAC7B,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,OAAO,CAAC;CACnB,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC,CA4ZtC;AAGD,OAAO,EAAE,iBAAiB,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAa,QAAQ,EAAE,MAAM,iCAAiC,CAAC;AAMpF,OAAO,KAAK,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AACxE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAI1C,YAAY,EAAE,UAAU,EAAE,CAAC;AAE3B;;;;GAIG;AACH,wBAAsB,oBAAoB,CACxC,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,EACd,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACrC,GAAG,EAAE,aAAa,EAClB,aAAa,EAAE,MAAM,EACrB,OAAO,EAAE,OAAO,EAChB,KAAK,CAAC,EAAE,CAAC,GAAG,EAAE,QAAQ,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,EAC/C,OAAO,CAAC,EAAE;IAAE,MAAM,CAAC,EAAE,WAAW,CAAA;CAAE,GACjC,OAAO,CAAC;IAAE,MAAM,EAAE,UAAU,CAAC;IAAC,IAAI,EAAE,QAAQ,EAAE,CAAA;CAAE,CAAC,CAoBnD;AAED,wBAAsB,MAAM,CAAC,OAAO,EAAE;IAClC,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,GAAG,CAAC,EAAE,OAAO,CAAC,aAAa,CAAC,CAAC;IAC7B,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,OAAO,CAAC;CACnB,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC,CAkctC;AAGD,OAAO,EAAE,iBAAiB,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAG/D,OAAO,EAAE,qBAAqB,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC"}
package/dist/index.js CHANGED
@@ -10,7 +10,7 @@ var __export = (target, all) => {
10
10
  };
11
11
 
12
12
  // src/index.ts
13
- import { join } from "node:path";
13
+ import { join as join2 } from "node:path";
14
14
 
15
15
  // src/discovery.ts
16
16
  import fs from "node:fs/promises";
@@ -12532,7 +12532,11 @@ async function discoverWorkflows(workflowsRoot = path.resolve("workflows")) {
12532
12532
  if (schemaMod?.inputSchema) {
12533
12533
  inputSchema = schemaMod.inputSchema;
12534
12534
  }
12535
- } catch {}
12535
+ } catch (error46) {
12536
+ if (error46?.code === "ENOENT" || error46?.code === "ERR_MODULE_NOT_FOUND") {} else {
12537
+ throw new Error(`Failed to load input schema at ${inputSchemaPath}: ${error46?.message || error46}`);
12538
+ }
12539
+ }
12536
12540
  workflows.push({
12537
12541
  slug: entry.name,
12538
12542
  name: config2.name,
@@ -12547,18 +12551,32 @@ async function discoverWorkflows(workflowsRoot = path.resolve("workflows")) {
12547
12551
  }
12548
12552
  return workflows;
12549
12553
  }
12550
- async function discoverSteps(root = path.resolve("steps")) {
12554
+ async function discoverStepDirectories(root, relativePath = "") {
12551
12555
  const entries = await fs.readdir(root, { withFileTypes: true });
12552
- const stepDirs = entries.filter((e) => e.isDirectory()).map((e) => path.join(root, e.name));
12553
- const loaded = [];
12554
- const byModuleStepObject = new Map;
12555
- for (const dir of stepDirs) {
12556
- const stepFile = path.join(dir, "step.ts");
12556
+ const results = [];
12557
+ for (const entry of entries) {
12558
+ if (!entry.isDirectory()) {
12559
+ continue;
12560
+ }
12561
+ const fullPath = path.join(root, entry.name);
12562
+ const stepFile = path.join(fullPath, "step.ts");
12563
+ const currentPath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
12557
12564
  try {
12558
12565
  await fs.access(stepFile);
12566
+ results.push({ dir: fullPath, stepId: currentPath });
12559
12567
  } catch {
12560
- continue;
12568
+ const nestedSteps = await discoverStepDirectories(fullPath, currentPath);
12569
+ results.push(...nestedSteps);
12561
12570
  }
12571
+ }
12572
+ return results;
12573
+ }
12574
+ async function discoverSteps(root = path.resolve("steps")) {
12575
+ const stepDirs = await discoverStepDirectories(root);
12576
+ const loaded = [];
12577
+ const byModuleStepObject = new Map;
12578
+ for (const { dir, stepId } of stepDirs) {
12579
+ const stepFile = path.join(dir, "step.ts");
12562
12580
  const modUrl = pathToFileURL(stepFile).toString();
12563
12581
  const mod = await import(modUrl);
12564
12582
  if (!mod?.step || typeof mod.step.fn !== "function") {
@@ -12569,10 +12587,9 @@ async function discoverSteps(root = path.resolve("steps")) {
12569
12587
  const errors3 = validationResult.error.issues.map((e) => `${e.path.join(".")}: ${e.message}`).join(", ");
12570
12588
  throw new Error(`Invalid step configuration at ${stepFile}: ${errors3}`);
12571
12589
  }
12572
- const dirName = path.basename(dir);
12573
12590
  const placeholder = {
12574
- id: dirName,
12575
- name: mod.step.name ?? dirName,
12591
+ id: stepId,
12592
+ name: mod.step.name ?? stepId.split("/").pop(),
12576
12593
  dir,
12577
12594
  fn: mod.step.fn,
12578
12595
  exportOutput: mod.step.exportOutput,
@@ -12635,11 +12652,10 @@ import { spawn } from "node:child_process";
12635
12652
  import { resolve, dirname } from "node:path";
12636
12653
  import { fileURLToPath } from "node:url";
12637
12654
  import { mkdir, readFile, unlink } from "node:fs/promises";
12638
- import { getMicrosecondTimestamp } from "@cascade-flow/backend-interface";
12655
+ import { getMicrosecondTimestamp, ensureErrorMessage } from "@cascade-flow/backend-interface";
12639
12656
  function createStreamHandler(streamType, attemptNumber, emitLog) {
12640
12657
  let buffer = "";
12641
12658
  const handler = (chunk) => {
12642
- const timestamp = getMicrosecondTimestamp();
12643
12659
  buffer += chunk.toString();
12644
12660
  const lines = buffer.split(`
12645
12661
  `);
@@ -12647,6 +12663,7 @@ function createStreamHandler(streamType, attemptNumber, emitLog) {
12647
12663
  for (const line of lines) {
12648
12664
  if (!line.trim())
12649
12665
  continue;
12666
+ const timestamp = getMicrosecondTimestamp();
12650
12667
  emitLog({
12651
12668
  timestamp,
12652
12669
  stream: streamType,
@@ -12670,7 +12687,7 @@ function createStreamHandler(streamType, attemptNumber, emitLog) {
12670
12687
  return { handler, getBuffer, flushBuffer };
12671
12688
  }
12672
12689
  async function executeStepInSubprocess(stepFile, stepId, dependencies, ctx, attemptNumber, outputPath, onLog, options) {
12673
- const executorPath = resolve(dirname(fileURLToPath(import.meta.url)), "step-executor.ts");
12690
+ const executorPath = resolve(dirname(fileURLToPath(import.meta.url)), "step-executor");
12674
12691
  await mkdir(dirname(outputPath), { recursive: true });
12675
12692
  return new Promise((resolve2, reject) => {
12676
12693
  const child = spawn("bun", [executorPath], {
@@ -12762,7 +12779,8 @@ async function executeStepInSubprocess(stepFile, stepId, dependencies, ctx, atte
12762
12779
  if (lastStderrLog) {
12763
12780
  try {
12764
12781
  const errorObj = JSON.parse(lastStderrLog.message);
12765
- const error46 = new Error(errorObj.message);
12782
+ const errorMessage = ensureErrorMessage(errorObj.message);
12783
+ const error46 = new Error(errorMessage);
12766
12784
  error46.stack = errorObj.stack;
12767
12785
  error46.name = errorObj.name || "Error";
12768
12786
  reject(error46);
@@ -12785,6 +12803,87 @@ async function executeStepInSubprocess(stepFile, stepId, dependencies, ctx, atte
12785
12803
  // src/index.ts
12786
12804
  import { getMicrosecondTimestamp as getMicrosecondTimestamp2 } from "@cascade-flow/backend-interface";
12787
12805
  import { Skip, isOptional as isOptional2 } from "@cascade-flow/workflow";
12806
+
12807
+ // src/versioning.ts
12808
+ import { createHash } from "node:crypto";
12809
+ import { readFile as readFile2, readdir } from "node:fs/promises";
12810
+ import { join } from "node:path";
12811
+ import { exec } from "node:child_process";
12812
+ import { promisify } from "node:util";
12813
+ var execAsync = promisify(exec);
12814
+ async function calculateWorkflowHash(workflow) {
12815
+ const hash2 = createHash("sha256");
12816
+ try {
12817
+ const workflowJsonPath = join(workflow.dir, "workflow.json");
12818
+ const workflowJsonContent = await readFile2(workflowJsonPath, "utf-8");
12819
+ hash2.update(`workflow.json:${workflowJsonContent}`);
12820
+ } catch (error46) {
12821
+ throw new Error(`Failed to read workflow.json for ${workflow.slug}: ${error46 instanceof Error ? error46.message : "Unknown error"}`);
12822
+ }
12823
+ try {
12824
+ const inputSchemaPath = join(workflow.dir, "input-schema.ts");
12825
+ const inputSchemaContent = await readFile2(inputSchemaPath, "utf-8");
12826
+ hash2.update(`input-schema.ts:${inputSchemaContent}`);
12827
+ } catch (error46) {
12828
+ if (error46 && typeof error46 === "object" && "code" in error46 && error46.code !== "ENOENT") {
12829
+ throw error46;
12830
+ }
12831
+ }
12832
+ const stepFiles = await collectStepFiles(workflow.stepsDir);
12833
+ stepFiles.sort();
12834
+ for (const stepFilePath of stepFiles) {
12835
+ try {
12836
+ const content = await readFile2(stepFilePath, "utf-8");
12837
+ const relativePath = stepFilePath.replace(workflow.stepsDir + "/", "");
12838
+ hash2.update(`step:${relativePath}:${content}`);
12839
+ } catch (error46) {
12840
+ throw new Error(`Failed to read step file ${stepFilePath}: ${error46 instanceof Error ? error46.message : "Unknown error"}`);
12841
+ }
12842
+ }
12843
+ const digest = hash2.digest("hex");
12844
+ return `sha256:${digest}`;
12845
+ }
12846
+ async function collectStepFiles(stepsDir) {
12847
+ const files = [];
12848
+ async function scan(dir) {
12849
+ const entries = await readdir(dir, { withFileTypes: true });
12850
+ for (const entry of entries) {
12851
+ const fullPath = join(dir, entry.name);
12852
+ if (entry.isDirectory()) {
12853
+ await scan(fullPath);
12854
+ } else if (entry.isFile() && entry.name === "step.ts") {
12855
+ files.push(fullPath);
12856
+ }
12857
+ }
12858
+ }
12859
+ try {
12860
+ await scan(stepsDir);
12861
+ } catch (error46) {
12862
+ throw new Error(`Failed to scan steps directory ${stepsDir}: ${error46 instanceof Error ? error46.message : "Unknown error"}`);
12863
+ }
12864
+ return files;
12865
+ }
12866
+ async function getGitInfo(workflowDir) {
12867
+ try {
12868
+ const { stdout: commit } = await execAsync("git rev-parse HEAD", {
12869
+ cwd: workflowDir
12870
+ });
12871
+ const { stdout: branch } = await execAsync("git rev-parse --abbrev-ref HEAD", { cwd: workflowDir });
12872
+ const { stdout: status } = await execAsync("git status --porcelain", {
12873
+ cwd: workflowDir
12874
+ });
12875
+ const dirty = status.trim().length > 0;
12876
+ return {
12877
+ commit: commit.trim(),
12878
+ branch: branch.trim(),
12879
+ dirty
12880
+ };
12881
+ } catch (error46) {
12882
+ return;
12883
+ }
12884
+ }
12885
+
12886
+ // src/index.ts
12788
12887
  async function executeStepInProcess(stepFile, stepId, dependencies, ctx, attemptNumber, backend, onLog, options) {
12789
12888
  const outputPath = backend.getStepOutputPath(ctx.workflow.slug, ctx.runId, stepId, attemptNumber);
12790
12889
  return executeStepInSubprocess(stepFile, stepId, dependencies, ctx, attemptNumber, outputPath, onLog, options);
@@ -12825,9 +12924,21 @@ async function runAll(options) {
12825
12924
  };
12826
12925
  const workflowStartTime = getMicrosecondTimestamp2();
12827
12926
  await backend.initializeRun(workflowSlug, runId);
12927
+ const versionId = await calculateWorkflowHash(workflow);
12928
+ const git = await getGitInfo(workflow.dir);
12929
+ const stepManifest = steps.map((s) => s.id);
12930
+ await backend.createWorkflowVersion({
12931
+ workflowSlug,
12932
+ versionId,
12933
+ createdAt: getMicrosecondTimestamp2(),
12934
+ stepManifest,
12935
+ totalSteps: steps.length,
12936
+ git
12937
+ });
12828
12938
  const hasInputSchema = workflow.inputSchema !== undefined;
12829
12939
  const hasInput = options.input !== undefined;
12830
12940
  await backend.saveWorkflowStart(workflowSlug, runId, {
12941
+ versionId,
12831
12942
  workflowAttemptNumber: 1,
12832
12943
  hasInputSchema,
12833
12944
  hasInput
@@ -12876,10 +12987,25 @@ ${errorMessage}`
12876
12987
  }
12877
12988
  if (options?.resume) {
12878
12989
  defaultCtx.log(`Resuming run ${defaultCtx.runId}...`);
12990
+ const workflowEvents = await backend.loadEvents(workflowSlug, defaultCtx.runId, { category: "workflow" });
12991
+ const workflowStartedEvent = workflowEvents.find((e) => e.type === "WorkflowStarted");
12992
+ if (workflowStartedEvent && workflowStartedEvent.type === "WorkflowStarted") {
12993
+ const previousVersionId = workflowStartedEvent.versionId;
12994
+ if (previousVersionId !== versionId) {
12995
+ const previousVersion = await backend.getWorkflowVersion(workflowSlug, previousVersionId);
12996
+ const currentVersion = await backend.getWorkflowVersion(workflowSlug, versionId);
12997
+ defaultCtx.log(`⚠️ Workflow definition changed since original run`);
12998
+ defaultCtx.log(` Original: ${previousVersionId}`);
12999
+ defaultCtx.log(` Current: ${versionId}`);
13000
+ if (previousVersion?.git && currentVersion?.git) {
13001
+ defaultCtx.log(` Git: ${previousVersion.git.commit} → ${currentVersion.git.commit}`);
13002
+ }
13003
+ }
13004
+ }
12879
13005
  const existingRecords = await backend.loadRun(workflowSlug, defaultCtx.runId);
12880
13006
  let resumedSteps = 0;
12881
13007
  for (const record2 of existingRecords) {
12882
- if (record2.status === "completed" && record2.output) {
13008
+ if (record2.status === "completed" && record2.output !== undefined && record2.output !== null) {
12883
13009
  try {
12884
13010
  const output = JSON.parse(record2.output);
12885
13011
  cache.set(record2.stepId, Promise.resolve(output));
@@ -12941,7 +13067,7 @@ ${errorMessage}`
12941
13067
  defaultCtx.log(`⊘ ${step.name} (skipped: dependency '${cascadedFromStep.name}' was skipped)`);
12942
13068
  return {};
12943
13069
  }
12944
- const stepFile = join(step.dir, "step.ts");
13070
+ const stepFile = join2(step.dir, "step.ts");
12945
13071
  const maxRetries = step.maxRetries ?? 0;
12946
13072
  let lastError = null;
12947
13073
  for (let attemptNumber = 1;attemptNumber <= maxRetries + 1; attemptNumber++) {
@@ -13077,9 +13203,11 @@ ${errorMessage}`
13077
13203
  }
13078
13204
  export {
13079
13205
  runAll,
13206
+ getGitInfo,
13080
13207
  executeStepInProcess,
13081
13208
  discoverWorkflows,
13082
- discoverSteps
13209
+ discoverSteps,
13210
+ calculateWorkflowHash
13083
13211
  };
13084
13212
 
13085
- //# debugId=A5F45B2C15F619F364756E2164756E21
13213
+ //# debugId=0A6F5DFCD4D7954F64756E2164756E21