Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/neat-kiwis-relate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@workflow/swc-plugin": patch
---

Fix step functions nested multiple levels deep in an object
96 changes: 96 additions & 0 deletions packages/swc-plugin-workflow/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,101 @@ example.workflowId = "workflow//./input//example";
registerStepFunction("step//./input//example/innerStep", example$innerStep);
```

### Steps in Nested Object Properties

Step functions can be defined inside deeply nested object properties, including function call arguments. The plugin recursively processes nested objects to find step functions, generating compound paths for the step IDs.

Input:
```javascript
import { agent } from "experimental-agent";

export const vade = agent({
tools: {
VercelRequest: {
execute: async (input, ctx) => {
"use step";
return 1 + 1;
},
},
},
});
```

Output (Step Mode):
```javascript
import { registerStepFunction } from "workflow/internal/private";
import { agent } from "experimental-agent";
/**__internal_workflows{"steps":{"input.js":{"vade/tools/VercelRequest/execute":{"stepId":"step//input.js//vade/tools/VercelRequest/execute"}}}}*/;
var vade$tools$VercelRequest$execute = async function(input, ctx) {
return 1 + 1;
};
export const vade = agent({
tools: {
VercelRequest: {
execute: vade$tools$VercelRequest$execute
}
}
});
registerStepFunction("step//input.js//vade/tools/VercelRequest/execute", vade$tools$VercelRequest$execute);
```

Note: Step functions are hoisted as regular function expressions (not arrow functions) to preserve `this` binding when called with `.call()` or `.apply()`. This applies even when the original step function was defined as an arrow function.

Output (Workflow Mode):
```javascript
import { agent } from "experimental-agent";
/**__internal_workflows{"steps":{"input.js":{"vade/tools/VercelRequest/execute":{"stepId":"step//input.js//vade/tools/VercelRequest/execute"}}}}*/;
export const vade = agent({
tools: {
VercelRequest: {
execute: globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//input.js//vade/tools/VercelRequest/execute")
}
}
});
```

Note: The step ID includes the full path through nested objects (`vade/tools/VercelRequest/execute`), while the hoisted variable name uses `$` as the separator (`vade$tools$VercelRequest$execute`) to create a valid JavaScript identifier.

#### Shorthand Method Syntax

Shorthand method syntax (non-arrow functions) is also supported in nested object properties:

Input:
```javascript
import { agent } from "experimental-agent";

export const vade = agent({
tools: {
VercelRequest: {
async execute(input, { experimental_context }) {
"use step";
return 1 + 1;
},
},
},
});
```

Output (Step Mode):
```javascript
import { registerStepFunction } from "workflow/internal/private";
import { agent } from "experimental-agent";
/**__internal_workflows{"steps":{"input.js":{"vade/tools/VercelRequest/execute":{"stepId":"step//input.js//vade/tools/VercelRequest/execute"}}}}*/;
var vade$tools$VercelRequest$execute = async function(input, { experimental_context }) {
return 1 + 1;
};
export const vade = agent({
tools: {
VercelRequest: {
execute: vade$tools$VercelRequest$execute
}
}
});
registerStepFunction("step//input.js//vade/tools/VercelRequest/execute", vade$tools$VercelRequest$execute);
```

Note: Shorthand methods are hoisted as regular function expressions (not arrow functions) to preserve `this` binding when called with `.call()` or `.apply()`. Closure variables are handled the same way as other step functions.

### Closure Variables

When nested steps capture closure variables, they are extracted using `__private_getClosureVars()`:
Expand Down Expand Up @@ -701,6 +796,7 @@ The plugin supports various function declaration styles:
- `var name = async () => { "use step"; }` - Arrow function with var
- `const name = async function() { "use step"; }` - Function expression
- `{ async method() { "use step"; } }` - Object method
- `{ nested: { execute: async () => { "use step"; } } }` - Nested object property
- `static async method() { "use step"; }` - Static class method
- `async method() { "use step"; }` - Instance class method (requires custom serialization)

Expand Down
176 changes: 98 additions & 78 deletions packages/swc-plugin-workflow/transform/src/lib.rs

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { agent } from "experimental-agent";

export const vade = agent({
tools: {
VercelRequest: {
async execute(input, { experimental_context }) {
"use step";
return 1+1
},
},
},
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { agent } from "experimental-agent";
export const vade = agent({
tools: {
VercelRequest: {
async execute (input, { experimental_context }) {
return 1 + 1;
}
}
}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { registerStepFunction } from "workflow/internal/private";
import { agent } from "experimental-agent";
/**__internal_workflows{"steps":{"input.js":{"vade/tools/VercelRequest/execute":{"stepId":"step//./input//vade/tools/VercelRequest/execute"}}}}*/;
var vade$tools$VercelRequest$execute = async function(input, { experimental_context }) {
return 1 + 1;
};
export const vade = agent({
tools: {
VercelRequest: {
execute: vade$tools$VercelRequest$execute
}
}
});
registerStepFunction("step//./input//vade/tools/VercelRequest/execute", vade$tools$VercelRequest$execute);
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { agent } from "experimental-agent";
/**__internal_workflows{"steps":{"input.js":{"vade/tools/VercelRequest/execute":{"stepId":"step//./input//vade/tools/VercelRequest/execute"}}}}*/;
export const vade = agent({
tools: {
VercelRequest: {
execute: globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//./input//vade/tools/VercelRequest/execute")
}
}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { agent } from "experimental-agent";

export const vade = agent({
tools: {
VercelRequest: {
execute: async (input, { experimental_context }) => {
"use step";
return 1+1
},
},
},
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { agent } from "experimental-agent";
export const vade = agent({
tools: {
VercelRequest: {
execute: async (input, { experimental_context })=>{
return 1 + 1;
}
Comment on lines +5 to +7
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

interestingly I guess the function body doesn't actually matter in client mode?
since it won't be called here

but I guess it's okay to just leave it as is?

}
}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { registerStepFunction } from "workflow/internal/private";
import { agent } from "experimental-agent";
/**__internal_workflows{"steps":{"input.js":{"vade/tools/VercelRequest/execute":{"stepId":"step//./input//vade/tools/VercelRequest/execute"}}}}*/;
var vade$tools$VercelRequest$execute = async function(input, { experimental_context }) {
return 1 + 1;
};
export const vade = agent({
tools: {
VercelRequest: {
execute: vade$tools$VercelRequest$execute
}
}
});
registerStepFunction("step//./input//vade/tools/VercelRequest/execute", vade$tools$VercelRequest$execute);
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { agent } from "experimental-agent";
/**__internal_workflows{"steps":{"input.js":{"vade/tools/VercelRequest/execute":{"stepId":"step//./input//vade/tools/VercelRequest/execute"}}}}*/;
export const vade = agent({
tools: {
VercelRequest: {
execute: globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//./input//vade/tools/VercelRequest/execute")
}
}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { createConfig } from "some-library";

// Test deeply nested step functions (4 levels deep)
export const config = createConfig({
level1: {
level2: {
level3: {
myStep: async (input) => {
"use step";
return input * 2;
},
},
},
},
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { createConfig } from "some-library";
// Test deeply nested step functions (4 levels deep)
export const config = createConfig({
level1: {
level2: {
level3: {
myStep: async (input)=>{
return input * 2;
}
}
}
}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { registerStepFunction } from "workflow/internal/private";
import { createConfig } from "some-library";
/**__internal_workflows{"steps":{"input.js":{"config/level1/level2/level3/myStep":{"stepId":"step//./input//config/level1/level2/level3/myStep"}}}}*/;
var config$level1$level2$level3$myStep = async function(input) {
return input * 2;
};
// Test deeply nested step functions (4 levels deep)
export const config = createConfig({
level1: {
level2: {
level3: {
myStep: config$level1$level2$level3$myStep
}
}
}
});
registerStepFunction("step//./input//config/level1/level2/level3/myStep", config$level1$level2$level3$myStep);
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { createConfig } from "some-library";
/**__internal_workflows{"steps":{"input.js":{"config/level1/level2/level3/myStep":{"stepId":"step//./input//config/level1/level2/level3/myStep"}}}}*/;
// Test deeply nested step functions (4 levels deep)
export const config = createConfig({
level1: {
level2: {
level3: {
myStep: globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//./input//config/level1/level2/level3/myStep")
}
}
}
});
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { registerStepFunction } from "workflow/internal/private";
import fs from 'fs/promises';
/**__internal_workflows{"steps":{"input.js":{"myFactory/myStep":{"stepId":"step//./input//myFactory/myStep"}}}}*/;
var myFactory$myStep = async ()=>{
var myFactory$myStep = async function() {
await fs.mkdir('test');
};
const myFactory = ()=>({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ async function example$step(a, b) {
var example$arrowStep = async (x, y)=>x * y;
var example$letArrowStep = async (x, y)=>x - y;
var example$varArrowStep = async (x, y)=>x / y;
var example$helpers$objectStep = async (x, y)=>{
var example$helpers$objectStep = async function(x, y) {
return x + y + 10;
};
export async function example(a, b) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,18 @@ import { registerStepFunction } from "workflow/internal/private";
import * as z from 'zod';
import { tool } from 'ai';
/**__internal_workflows{"steps":{"input.js":{"timeTool/execute":{"stepId":"step//./input//timeTool/execute"},"weatherTool/execute":{"stepId":"step//./input//weatherTool/execute"},"weatherTool2/execute":{"stepId":"step//./input//weatherTool2/execute"}}}}*/;
var weatherTool$execute = async ({ location })=>{
var weatherTool$execute = async function({ location }) {
return {
location,
temperature: 72 + Math.floor(Math.random() * 21) - 10
};
};
var timeTool$execute = async ()=>{
var timeTool$execute = async function timeToolImpl() {
return {
time: new Date().toISOString()
};
};
var weatherTool2$execute = async ({ location })=>{
var weatherTool2$execute = async function({ location }) {
return {
location,
temperature: 72 + Math.floor(Math.random() * 21) - 10
Expand Down