* refactor: use `this` value serialization for builtin step functions
Refactor builtin step functions (__builtin_response_json, _text,
_array_buffer) to use `this` instead of an explicit parameter,
leveraging the useStep() proxy's this value serialization support.
- Assign useStep() proxies directly onto Request.prototype and
Response.prototype instead of wrapping them in class methods
- Update builtin function signatures to use `this: Request | Response`
- Remove unused duplicate builtins file from @workflow/core
(the canonical copy lives in workflow/src/internal/builtins.ts)
* use Object.defineProperties for non-enumerable prototype methods
* fix(swc-plugin): closure variable detection for `new` expressions and module-level declarations
Fix two SWC compiler plugin bugs related to closure variable detection:
1. Add Expr::New handling to ClosureVariableCollector so `new Class(...args)`
properly captures both the callee and arguments as closure variables.
2. Exclude module-level declarations (functions, variables, classes) from
closure variable detection, preventing over-capturing of identifiers
that are already available in all bundles. This also allows DCE to
properly remove step-only helpers and their imports from the workflow
bundle.
Fixes#1365
* fix(swc-plugin): handle additional expression/statement types in closure variable collector
Expand closure variable detection to cover more AST patterns:
Expressions: Seq (comma), Yield, OptChain, Prop::Shorthand,
computed property keys, Prop::Assign defaults, Class (skip bodies)
Statements: Throw, Try/Catch/Finally, Switch, ForIn, ForOf,
DoWhile, Labeled
Also fix existing Prop::Shorthand bug where object shorthand
properties like { url } were not being collected as closure vars.
Extend test fixture with cases for all newly handled patterns.
Fix spec.md wording per review feedback.
* fix(swc-plugin): preserve original step function bodies in enclosing functions
In step mode, nested step functions were replaced with bare references
to the hoisted copy (e.g., `return hoisted$fn;`). This broke direct
calls because the hoisted copy uses `__private_getClosureVars()` which
only works in workflow context.
Now the original function body is preserved inline with just the
directive stripped, so JavaScript's normal closure semantics work for
direct calls. The hoisted copy with `__private_getClosureVars()` is
still registered for workflow-driven execution.
Fixes#1369
* fix(swc-plugin): restore metadata tracking for object property steps in step mode
The previous commit accidentally removed the
object_property_workflow_conversions tracking from the step mode path,
causing __internal_workflows metadata to be stripped from step bundle
output for object property step functions.
* fix(swc-plugin): detect closure variables inside nested function/method bodies
The closure variable collector was skipping nested function expressions,
arrow functions, and method bodies entirely. This meant closure
variables used deep inside inner functions (e.g., a variable used inside
a ReadableStream's start() method) were not captured.
Now the collector walks into nested function/arrow/method/getter/setter
bodies while adding their parameters to the local var set, so only
truly free variables from the outer step scope are captured.
Also add ReadableStream, WritableStream, TransformStream, and other
common Web API globals to the known globals list.
* update changeset to include Bug 4
* fix(swc-plugin): handle TypeScript expression wrappers and class bodies in closure detection
After comparing with Next.js's SWC plugin closure detection approach,
identified and fixed remaining gaps:
- TypeScript expression wrappers (as, satisfies, !, type assertions,
const assertions, instantiation expressions) now traverse to the
inner expression instead of being silently skipped
- Class expressions and declarations now walk their body members
(methods, properties, constructors, static blocks) to detect
closure variables used inside them
- Document all remaining safe-to-skip Expr variants (This, Lit,
SuperProp, MetaProp, PrivateName, Invalid, JSX)
* test: add fixture cases for TypeScript wrappers and class body closure detection
* test: add TypeScript fixture for closure detection through TS expression wrappers
Add a proper input.ts fixture that tests closure variable detection
through real TypeScript syntax: `as`, `satisfies`, `!` (non-null),
angle-bracket type assertions, `as const`, and generic function calls.
Update test harness to support input.ts files by adding swc_ecma_parser
dev-dependency and auto-detecting TypeScript syntax from file extension.
Remove the incorrectly placed TypeScript-related test cases from the JS
fixture (they were using plain JS syntax, not actual TS wrappers).
Apply messages override before system override so that the system
message is prepended to the new prompt instead of being discarded.
Previously, system was applied first then messages replaced the
entire conversation prompt, losing the system message.
Add tests for prepareStep system/messages ordering:
- system only: prepended to prompt
- system + messages: system prepended to replaced messages
- system + messages with existing system: replaces existing
- system updates across multi-step tool call rounds
* fix trace viewer for v1 events
* add changeset
* add a test
* add a test for web-shared
* add lock file
* update logic to look for run_created event first
* handle step created
* add changeset
* fix trace viewer for v1 events
* add changeset
* add a test
* add a test for web-shared
* add lock file
* update logic to look for run_created event first
* add stepName with events
* add changeset
* add workflowname to run created
* add postgres migration
* update world-local
* update world-local
* preserve the fields in the original shape
* fix tests
* strip only ref/payload fields
* stub the helper into world
* add test coverage
* fix web package
* fix web package to not pass withData: true
* Add DurableAgent compat tests, e2e agent tests, and migrate to AI SDK v6
- Port ToolLoopAgent test suite as DurableAgent compatibility spec (34 tests,
all expected to fail — each maps to a feature gap to implement)
- Add e2e workflow definitions using mock LLM providers (no API keys needed)
- Add e2e test file for DurableAgent workflows
- Migrate all AI SDK types from V2 to V3 (LanguageModelV2 → V3, etc.)
- Drop AI SDK v5 support: ai peer dep ^5||^6 → ^6, @ai-sdk/provider ^2||^3 → ^3
- Update ai catalog version from 5.0.104 to 6.0.116
- Simplify CompatibleLanguageModel to just LanguageModelV3
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Address PR review feedback
- Remove providerExecuted guard on tool-result stream parts (V3: all
tool-results are provider-executed by definition)
- Remove providerExecuted spread from tool-output-available UI chunks
- Replace inline MockLanguageModelV3 with import from ai/test (works
without msw in AI SDK v6)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Remove streamTextIterator mock from compat tests, use it.fails for gaps
Tests now exercise the real DurableAgent code path instead of mocking
the core iterator. 5 tests pass (features DurableAgent already has),
29 are marked it.fails() for known API gaps that will alert when fixed.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Implement Tier 1+2 gaps, add @workflow/ai/test mock provider, wire e2e in CI
DurableAgent API additions:
- Add `instructions` (string | SystemModelMessage | SystemModelMessage[])
as alias for deprecated `system` on constructor
- Add `onStepFinish` and `onFinish` on constructor, merged with stream
options (constructor first, then stream — matching ToolLoopAgent)
- Add `timeout` on stream options (converted to AbortSignal)
- Add `text`, `finishReason`, `totalUsage` to onFinish event
Test infrastructure:
- Add @workflow/ai/test export with `mockModel()` wrapper that wraps
MockLanguageModelV3 from ai/test as an async step function
- E2e workflows now use mockModel() + convertArrayToReadableStream
from @workflow/ai/test instead of inline V2 mock models
- Add e2e-agent.test.ts to test:e2e script so it runs in CI
- Flip 6 compat tests from it.fails → it (now passing)
Score: 11 passing / 23 it.fails (was 5/29)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Remove e2e agent tests — mock models can't serialize across step boundary
The workflow runtime serializes step arguments, and function closures
(like mock model doStream callbacks) aren't serializable. Mock models
only work in unit tests where 'use step' is a no-op. Real e2e agent
tests would need either a mock HTTP server or real provider credentials.
Also removes 'use step' from mockModel wrapper (closures aren't
serializable) and reverts test:e2e script and example workbench dep.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Add working e2e agent tests with mock model step factories
Mock model factories use the same 'use step' pattern as real providers
(anthropic, openai). Closure variables are bound to locals at the step
body level so the SWC plugin detects them via __private_getClosureVars.
All 4 e2e tests pass against local dev server:
- agentBasicE2e: text response (11s)
- agentToolCallE2e: single tool call + text (11s)
- agentMultiStepE2e: 3 sequential tool calls (12s)
- agentErrorToolE2e: FatalError recovery (11s)
Also adds e2e-agent.test.ts to test:e2e script for CI.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Use @workflow/ai/test package imports for e2e mock models
Split mock provider into two files to work around SWC constructor
closure bug: mock-create.ts has the model creation logic,
mock.ts has the 'use step' wrappers that capture only serializable
args (strings, plain object arrays).
Exports mockTextModel(text) and mockSequenceModel(responses) —
same 'use step' pattern as real providers (anthropic, openai, etc.).
E2e workflows now import directly from @workflow/ai/test.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Simplify mock provider, add comprehensive e2e tests for all features + gaps
Mock provider:
- Replace mock-create.ts with mock-function-wrapper.ts that simply wraps
MockLanguageModelV3 constructor in a function (SWC class closure bug)
- mockTextModel/mockSequenceModel use mockProvider() from wrapper file
- Bind closure vars at step body level (_text = text) for SWC detection
- Fix AbortController not available in workflow VM sandbox
E2e tests (13 total, all passing):
- Core: basic text, tool call, multi-step, error recovery (4)
- Callbacks: onStepFinish constructor+stream, onFinish constructor+stream (2)
- Features: instructions, timeout (2)
- GAPs documented: onStart, onStepStart, onToolCallStart,
onToolCallFinish, prepareCall (5 — complete but callbacks not called)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Add tool approval (needsApproval) gap tests, fix SWC closure var binding
Unit tests: 2 new it.fails() tests for tool approval
- needsApproval: true should pause agent (pending tool call, no result)
- needsApproval as function should receive tool input
E2e tests: 1 new test for tool approval gap
- Documents that needsApproval is currently ignored (tool executes anyway)
Also fixes:
- Bind closure vars at step body level in mock provider (_text = text,
_responses = responses) so SWC plugin detects them
- Guard AbortController usage in workflow VM (not available in sandbox)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Add default args for agent e2e workflows in UI definitions
The nextjs-turbopack UI calls workflows with hardcoded default args.
Without these entries, agent workflows were called with no args,
causing prompt=undefined → ModelMessage validation failure.
Also removes debug logging.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Add DurableAgent chat UI with tools, update docs for AI SDK v6
Chat UI:
- Tab-based layout with Workflows (existing) and DurableAgent Chat tabs
- Chat powered by DurableAgent + WorkflowChatTransport + ai-elements
- Tools: getWeather (fake data), calculate (math expressions)
- Uses createUIMessageStreamResponse for proper stream serialization
- Reconnect route at /api/chat/[runId]/stream
- ai-elements components: conversation, message, prompt-input, tool
- onStepFinish + onFinish callbacks with console logging
Docs (AI SDK v6 migration):
- system → instructions in DurableAgent constructor examples (10 places)
- LanguageModelV2Prompt → LanguageModelV3Prompt in type references (3 places)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix tool rendering, add reasoning support, model picker, observability links
- Fix tool part rendering: use `input`/`output` props (not `args`/`result`)
and `tool-{name}` part type (AI SDK v6 format)
- Add reasoning support for Opus 4.5 via providerOptions
- Model picker: Haiku 4.5, Sonnet 4, Opus 4.5 (reasoning), GPT-5.2, GPT-5.3
- Fix observability links: localhost:3456 for local, Vercel dashboard for prod
- Add suggestions above prompt input
- Add MessageParts component handling text, tool, reasoning, step-start
- Add loading spinner for submitted state
- Update docs: system → instructions, LanguageModelV2Prompt → V3
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix tool output rendering: use input/output props on ToolInput/ToolOutput
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix: Documentation for `PrepareStepInfo` and `PrepareStepResult` interfaces references the obsolete `LanguageModelV2` type while the codebase has fully migrated to `LanguageModelV3`.
This commit fixes the issue reported at docs/content/docs/ai/message-queueing.mdx:36
**Bug explanation:**
The codebase migrated from AI SDK V2 to V3. In `packages/ai/src/agent/types.ts`, `CompatibleLanguageModel` is defined as `LanguageModelV3` (from `@ai-sdk/provider`). The actual TypeScript interfaces in `packages/ai/src/agent/durable-agent.ts` use `string | (() => Promise<CompatibleLanguageModel>)` which resolves to `LanguageModelV3`.
However, the documentation in `docs/content/docs/ai/message-queueing.mdx` at lines 36 and 43 still referenced `LanguageModelV2` for the `model` field in both `PrepareStepInfo` and `PrepareStepResult`. This is inconsistent because:
1. The `messages` fields in the same interfaces were correctly updated to `LanguageModelV3Prompt`
2. The actual source code uses `LanguageModelV3` via `CompatibleLanguageModel`
3. There is no `LanguageModelV2` type anywhere in the codebase
This would mislead developers reading the documentation into using the wrong type.
**Fix explanation:**
Changed both `LanguageModelV2` references to `LanguageModelV3` on lines 36 and 43 of the documentation file, matching the actual codebase types. Verified no other stale `LanguageModelV2` references remain in the docs directory.
Co-authored-by: Vercel <vercel[bot]@users.noreply.github.com>
Co-authored-by: pranaygp <pranay.gp@gmail.com>
* Fix instructions tests: flip from it.fails to it, update snapshots
The 3 instructions tests (string, SystemModelMessage, array) now pass.
The snapshots include the assistant reply message from the agent loop,
which is a behavioral difference from ToolLoopAgent (DurableAgent
captures the prompt after the full loop iteration).
Score: 14 passing / 22 it.fails (was 11/25)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix getReadable call: pass startIndex as options object
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix docs type errors and turbopack build
Docs:
- Add await to convertToModelMessages() calls (now async in AI SDK v6)
- Change LanguageModelV3Prompt → ModelMessage[] in type references
- Change LanguageModelV3 → LanguageModel in PrepareStepInfo
- Update docs-globals.d.ts convertToModelMessages return type
- Add LanguageModel to import inference map
DurableAgent:
- Update OutputSpecification to match AI SDK v6 Output interface
(type→name, parsePartial→parsePartialOutput, parseOutput→parseCompleteOutput,
responseFormat now PromiseLike)
Turbopack build:
- Remove streamdown plugins from MessageResponse (plugins prop API
changed in streamdown 2.4.0, causing type mismatch in CI)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix pnpm-workspace.yaml: use double quotes for catalog entries
The stage-workbench-with-tarballs.mjs script only strips double quotes
when parsing catalog keys. Single-quoted @-scoped entries (e.g.,
'@types/node') weren't matched, causing "unresolved catalog dependencies"
errors in CI.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix CI build failures, add changeset
- Remove streamdown plugins from reasoning.tsx (same CI type mismatch)
- Cast ToolHeader type prop and WorkflowChatTransport to fix type errors
- Fix pnpm-workspace.yaml single→double quotes for staging script
- Add minor changeset for @workflow/ai (breaking: AI SDK v6 migration)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Sync webpack workbench with turbopack: add chat UI deps and symlinks
- Symlink app-shell.tsx, chat-client.tsx, agent_chat workflow,
chat API routes into nextjs-webpack
- Add matching deps: streamdown, @streamdown/*, shiki, cmdk, nanoid,
motion, @radix-ui/react-use-controllable-state
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix circular symlink: restore chat-client.tsx as real file in turbopack
The previous commit accidentally converted turbopack's chat-client.tsx
into a circular symlink pointing to itself. Webpack's symlink to it
then couldn't resolve, breaking both builds on Vercel.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Add missing deps to webpack: use-stick-to-bottom, radix-ui, @vercel/blob
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix model picker: merge body params via prepareSendMessagesRequest
WorkflowChatTransport sends { messages } by default, ignoring the
body option from ChatRequestOptions. Use prepareSendMessagesRequest
to merge { messages, ...body } so the model selection reaches the API.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix WorkflowChatTransport: forward body/headers from ChatRequestOptions
The transport hardcoded body: undefined when calling
prepareSendMessagesRequest, so extra body params (like model selection)
from sendMessage({ body: { model } }) were silently dropped.
Now forwards options.body and options.headers to both
prepareSendMessagesRequest and the default request body.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix model IDs: use real AI Gateway model names
gpt-5.2 and gpt-5.3 don't exist in the AI Gateway.
Replace with gpt-4o and gpt-4o-mini.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Use correct AI Gateway model IDs: Opus 4.5, GPT-5.2, GPT-5.3
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Enable reasoning for all model providers
- Anthropic: thinking.type='enabled' with 10k token budget
- OpenAI: reasoningEffort='high'
- Instructions kept for all models (no longer conditionally removed)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix OpenAI reasoning: use 'medium' effort (GPT-5.3 doesn't support 'high')
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Address all PR review comments
- Change changeset from minor to patch (repo convention)
- Use ?? instead of || for system/instructions fallback
- Clean up timeout: store ID, clearTimeout in finally, { once: true } listeners
- Update class docstring example to use instructions
- Map unrecognized finish reasons to 'other' with validation
- Fix duplicate test, align assertion for unrecognized type
- Support ^ exponentiation in calculate tool
- Remove debug console.log from chat client
- Fix ReactNode/ComponentProps type imports in UI components
- Remove unused MockLanguageModelV3 re-exports from mock.ts
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Remove accidentally created empty mock2.ts
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Change changeset back to minor for breaking AI SDK v6 migration
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix agent e2e tests: add Vercel world setup for CI
The agent e2e tests only configured the local filesystem world but not
the Vercel world backend. On CI (Vercel prod tests), this caused
VercelOidcTokenError because the world wasn't initialized.
Now matches the setup pattern from e2e.test.ts: configures Vercel world
with OIDC token and project config from CI environment variables.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Deduplicate e2e test utilities: extract shared code to utils.ts
Extract manifest fetching, workflow lookup, world setup, and types
into shared utils.ts. Both e2e.test.ts and e2e-agent.test.ts now
import from the same source, eliminating ~200 lines of duplication.
Shared utilities:
- WorkflowManifest interface
- fetchManifest() with caching
- getWorkflowMetadata() with retry and fallback
- setupWorld() handling local/Vercel/Postgres backends
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix missing deploymentUrl args in e2e.test.ts after utils refactor
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Add @workflow/ai dep to all workbenches for agent e2e tests
All workbenches now have @workflow/ai as a dependency and the
100_durable_agent_e2e.ts symlink, so agent e2e tests run everywhere.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix missing imports in e2e.test.ts: add fetchManifest and sleep
The utils refactor removed these imports but they're still used:
- fetchManifest: used in stepFunctionAsStartArgWorkflow test
- sleep (setTimeout): used in webhookWorkflow test
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Vercel <vercel[bot]@users.noreply.github.com>
* Skip OIDC call in fetchRunKey when token already available
getVercelOidcToken() was called unconditionally before checking
options.token or VERCEL_TOKEN. In the CLI/e2e context where a token
is always provided, this is a wasted call that can hang in CI.
Reorder the fallback chain: options.token -> VERCEL_TOKEN -> OIDC.
* Add changeset for OIDC skip fix
* Update changeset and comments to document token precedence order
* Update event list to show decrypt button when a row is expanded
* improve loading skeleton
* add timestamp tooltip
* add a toast adapter
* add changeset
* remove noop timestamp tool tip
* add toast for decryption
* add toast for decryption
* fix: separate infrastructure vs user code error handling in runtime and step handler
Transient network errors (ECONNRESET, etc.) during infrastructure calls
(event listing, event creation) were caught by a shared try/catch that
also handles user code errors, incorrectly marking runs as run_failed
or steps as step_failed instead of letting the queue redeliver.
- runtime.ts: Move infrastructure calls outside the user-code try/catch
so errors propagate to the queue handler for automatic retry
- step-handler.ts: Same structural separation — only stepFn.apply() is
wrapped in the try/catch that produces step_failed/step_retrying
- helpers.ts: Add isTransientNetworkError() and update withServerErrorRetry
to retry network errors in addition to 5xx responses
- helpers.test.ts: Add tests for network error detection and retry
* add changeset
* remove withServerErrorRetry and isTransientNetworkError
Redundant with undici RetryAgent which already handles 5xx retries
and network error retries at the HTTP dispatcher level.
* address review feedback: move getEncryptionKeyForRun out of user-code try/catch, re-add 5xx/410 safety net in step handler, relax e2e test assertion
* remove serverError5xxRetryWorkflow e2e test
This test validated withServerErrorRetry's in-process retry behavior,
which was removed. Queue-level retry with process-scoped fault injection
is unreliable across serverless instances and too slow for e2e timeouts.
* remove serverError5xxRetryWorkflow and fault injection helpers from e2e workflows
* remove inline comment about deleted test
spawn({timeout}) sends SIGTERM by default, but the CLI process ignores it
(likely due to undici keeping active connections alive). The process then
runs for minutes until vitest or GitHub Actions eventually kills it.
Using killSignal: 'SIGKILL' ensures the process is forcefully terminated
when the 20s timeout fires, since SIGKILL cannot be caught or ignored.
The health command's `endpoint` flag and the shared `env` flag both
declared `char: 'e'`, causing ambiguity. Remove the short flag from
`endpoint` so `-e` unambiguously maps to `--env`.
The e2e tests spawn a CLI subprocess for every inspect/cancel/health call.
Each subprocess performs an npm registry version check on startup, which
can hang under load and exceed the 20s spawn timeout, causing SIGTERM.
Add WORKFLOW_NO_UPDATE_CHECK=1 env var support to skip the check, and
set it in the e2e test harness.
The warning about WORKFLOW_VERCEL_* env vars having no effect at runtime
was incorrectly shown when the Workflow CLI or web observability app was
running. These tools intentionally set those env vars. Gate the warning
on VERCEL=1 so it only fires inside Vercel serverless functions where the
env vars are truly unnecessary.
* Fix connector line showing for run events
* Improve row toggle
* improve error reporting
* add sorting for events
* add changeset
* update search to be best match
* feat: add `exists` getter to `Run` class
Allow checking if a workflow run exists without catching errors.
Returns `true` if the run is found, `false` for WorkflowRunNotFoundError,
and re-throws any other errors.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* address PR feedback: use resolveData: 'none' in exists getter, fix docs wording
Pass { resolveData: 'none' } to world.runs.get() in the exists getter
to skip fetching input/output payloads. Update docs to say "without
throwing when the run is not found" instead of "without throwing an error".
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Support client-side tools in DurableAgent
Tools without an `execute` function now pause the agent loop and return
`clientToolCalls` in the result, enabling human-in-the-loop and
client-side tool execution patterns. Fixes#847.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Match AI SDK result shape for client-side tools
Use `toolCalls` and `toolResults` on DurableAgentStreamResult to match
the AI SDK's GenerateTextResult convention. Consumers find unresolved
client-side tool calls by diffing toolCalls vs toolResults — the same
pattern used with useChat/addToolOutput.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Address PR review feedback
- Use iterMessages (not options.messages) so returned messages include
the assistant tool-call message, enabling callers to resume
- Add resolved server/provider tool results to returned messages as a
tool role message so callers don't re-execute when resuming
- Treat missing tools (!tool) as errors (will throw in executeTool),
only tools that exist but lack execute are client-side
- Track tool-output-available chunks in allUIChunks when
collectUIMessages is enabled so uiMessages is complete
- Add message assertions to tests
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Clean up type casts in client-side tools path
Push directly to iterMessages (LanguageModelV2Prompt) instead of
double-casting through ModelMessage[]. Single cast at the return
point matches the existing pattern at the end of stream().
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Reverse check in resolveProviderToolResult for early exit
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: support `deploymentId: 'latest'` in `start()` to resolve most recent deployment
Add resolveLatestDeploymentId() to the World interface and implement it
in world-vercel, which calls the Vercel API to resolve the latest
deployment for the current environment. When start() receives
deploymentId: 'latest', it invokes this method and replaces the sentinel
with the resolved deployment ID before passing it to event creation,
queue dispatch, and encryption key derivation.
* fix: address PR review feedback
- Use encodeURIComponent() for deployment ID in API URL path
- Avoid mutating caller-provided opts; pass resolved deploymentId via
spread into getEncryptionKeyForRun context instead
- Add test asserting getEncryptionKeyForRun receives resolved ID
- Add test for OIDC token success path in resolve-latest-deployment
* first pass
* fix failing test
* Update changeset to fix SWC compiler issue
Fix bug where the SWC compiler bug prunes step-only imports in the client-mode transformation
Signed-off-by: Nathan Rajlich <n@n8.io>
* DCO Remediation Commit for nathancolosimo <nathancolosimo@gmail.com>
I, nathancolosimo <nathancolosimo@gmail.com>, hereby add my Signed-off-by to this commit: 150d175e7d
I, nathancolosimo <nathancolosimo@gmail.com>, hereby add my Signed-off-by to this commit: 429747fcc3
Signed-off-by: nathancolosimo <nathancolosimo@gmail.com>
---------
Signed-off-by: Nathan Rajlich <n@n8.io>
Signed-off-by: nathancolosimo <nathancolosimo@gmail.com>
Co-authored-by: Nathan Rajlich <n@n8.io>
* fix(web): move react-router deps to devDependencies
Add a custom entry.server.tsx (based on the default react-router template)
so that the @react-router/dev build plugin no longer requires
@react-router/node and isbot to be in the dependencies field of
package.json. This allows all react-router related packages to be
devDependencies since they are fully bundled at build time, leaving
express as the sole production dependency.
* Update packages/web/app/entry.server.tsx
Co-authored-by: Peter Wielander <mittgfu@gmail.com>
Signed-off-by: Nathan Rajlich <n@n8.io>
---------
Signed-off-by: Nathan Rajlich <n@n8.io>
Co-authored-by: Peter Wielander <mittgfu@gmail.com>
* fix(ai): use workspace:* for workflow peer dependency
The hardcoded version range in peerDependencies would fall out of sync
with the actual workflow package version, causing changeset warnings.
Using workspace:* lets pnpm resolve the correct version at publish time.
* Apply suggestion from @Copilot
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Nathan Rajlich <n@n8.io>
* add changeset for @workflow/ai peer dependency fix
---------
Signed-off-by: Nathan Rajlich <n@n8.io>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
The start() function resolved deploymentId from world.getDeploymentId() but
then passed the original opts (without the resolved value) to
getEncryptionKeyForRun. In external contexts (e2e tests, CLI) where
deploymentId is inferred from the environment rather than explicitly provided,
this caused getEncryptionKeyForRun to receive undefined deploymentId and
silently skip encryption for the initial workflow input.
Multiple layered issues caused encryption key 429 errors to produce
confusing, unrelated-looking test failures:
- awaitCommand() in e2e utils now rejects on non-zero exit codes
instead of silently resolving (the most critical fix)
- cliInspectJson/cliHealthJson throw on empty stdout instead of
falling back to '{}'
- maybeDecryptFields re-throws HTTP errors instead of silently
falling back to encrypted placeholders
- fetchRunKey includes response body and status text in errors
- Added 429 to CLI status text map
* fix(world-local): return HTTP 200 instead of 503 for queue timeout re-enqueue signals
The local queue used HTTP 503 (Service Unavailable) as an internal signal
for re-enqueueing after a timeout delay. This is misleading since 503 is
an error status, causing confusion when inspecting logs or network traffic.
Changed to return 200 with timeoutSeconds in the body, and updated the
consumer to detect the re-enqueue signal from the response body instead
of the status code.
* fix(world-local): handle timeoutSeconds: 0 and add queue re-enqueue tests
Address review feedback: timeoutSeconds === 0 should trigger an immediate
re-enqueue without delay, not be treated as a normal success. Also adds
test coverage for the handler response codes and re-enqueue behavior.
* test: enable encryption on nextjs-turbopack workbench
Set VERCEL_DEPLOYMENT_KEY to enable e2e encryption for the
nextjs-turbopack workbench project. This allows the e2e test
suite to exercise encrypted workflow runs alongside existing
unencrypted runs for backwards compatibility testing.
* fix: fetch encryption key from API in external contexts (e2e, CLI)
Use VERCEL=1 env var to distinguish serverless runtime (can use local
HKDF derivation) from external contexts like e2e test runners, CLI,
and web dashboard (must fetch derived key from Vercel API).
Previously, when VERCEL_DEPLOYMENT_ID matched but VERCEL_DEPLOYMENT_KEY
was absent (e.g., e2e runner has the deployment ID from CI but not the
key), getEncryptionKeyForRun returned undefined — silently skipping
decryption. Now external contexts always use the API fetch path.
* fix: add 429 retry with exponential backoff to fetchRunKey
The e2e runner calls fetchRunKey for every run, which can exceed the
API rate limit (30/min per team) when many tests run in parallel.
Add retry logic with exponential backoff + jitter (up to 5 retries)
and respect the Retry-After header when present.
* Fix: Missing NaN guard on `retry-after` header parsing causes `setTimeout(r, NaN)` to resolve immediately (0ms), triggering rapid-fire retries against a rate-limited API endpoint.
This commit fixes the issue reported at packages/world-vercel/src/encryption.ts:138
**Bug Analysis:**
In `packages/world-vercel/src/encryption.ts`, the `fetchRunKey` function handles HTTP 429 (rate limited) responses by reading the `retry-after` header and computing a delay. The original code was:
```js
const retryAfter = response.headers.get('retry-after');
const delay = retryAfter
? Number.parseInt(retryAfter, 10) * 1000
: BASE_DELAY_MS * 2 ** attempt + Math.random() * 500;
```
Per RFC 9110, the `Retry-After` header can contain either a number of seconds OR an HTTP date string (e.g., `"Wed, 21 Oct 2015 07:28:00 GMT"`). When the header contains a date string or any non-numeric value, `Number.parseInt(retryAfter, 10)` returns `NaN`. Since the header string is truthy (non-empty), the ternary takes the parseInt path rather than the exponential backoff fallback. `NaN * 1000` evaluates to `NaN`, and `setTimeout(r, NaN)` resolves immediately (~0ms, confirmed by testing). This causes rapid-fire retries (up to 5 iterations with zero delay) against a rate-limited API endpoint, wasting resources and potentially worsening the rate-limiting situation.
**Fix:**
Parse the integer first, then check if the result is NaN before using it. If NaN, fall back to exponential backoff with jitter. This matches the established pattern already used in `packages/world-vercel/src/utils.ts` (lines 309-315) where the same header is properly guarded with `!Number.isNaN(parsed)`.
```js
const retryAfterHeader = response.headers.get('retry-after');
const parsedRetryAfter = retryAfterHeader
? Number.parseInt(retryAfterHeader, 10)
: NaN;
const delay = !Number.isNaN(parsedRetryAfter)
? parsedRetryAfter * 1000
: BASE_DELAY_MS * 2 ** attempt + Math.random() * 500;
```
Co-authored-by: Vercel <vercel[bot]@users.noreply.github.com>
Co-authored-by: TooTallNate <n@n8.io>
* fix: add --decrypt to e2e CLI inspect calls
With encryption enabled on the nextjs-turbopack workbench, CLI inspect
output shows encrypted data as placeholders. The e2e tests check for
actual output values, so they need --decrypt to see the real data.
* fix: remove manual 429 retry, rely on undici RetryAgent; add changeset
The shared RetryAgent from getDispatcher() already handles 429/5xx
retries with exponential backoff and Retry-After header support.
The manual retry loop in fetchRunKey was redundant.
Also add changeset for the VERCEL=1 external context fix.
* refactor: use @ts-expect-error on dispatcher instead of as any cast
---------
Co-authored-by: Vercel <vercel[bot]@users.noreply.github.com>
* Stop reading WORKFLOW_VERCEL_* env vars at runtime to prevent unintended proxy routing
createWorld() in core no longer reads WORKFLOW_VERCEL_PROJECT, WORKFLOW_VERCEL_TEAM,
WORKFLOW_VERCEL_AUTH_TOKEN, WORKFLOW_VERCEL_ENV, or WORKFLOW_VERCEL_PROJECT_NAME from
process.env. These env vars are intended for CLI/observability tooling only, and
reading them at runtime caused all workflow traffic to route through the
api.vercel.com proxy when users mistakenly set them as Vercel project env vars.
The Vercel runtime already provides everything needed: OIDC tokens for auth,
VERCEL_PROJECT_ID for encryption context, and VERCEL_DEPLOYMENT_ID for world
selection. createWorld() now calls createVercelWorld() with no config.
A warning is emitted if the env vars are detected at runtime, telling users to
remove them.
The CLI and e2e tests are updated to call createVercelWorld() directly with an
explicit config object and inject via setWorld(), keeping the WORKFLOW_VERCEL_*
env vars scoped to tooling contexts only.
* Refactor inferVercelEnvVars to return config instead of relying on process.env
inferVercelEnvVars() now returns a VercelEnvVars object with the resolved
config values. setup.ts uses the returned object directly for
createVercelWorld() instead of re-reading from process.env via getEnvVars().
The writeEnvVars() calls inside inferVercelEnvVars are consolidated into a
single call at the end, retained only for the embedded web UI which reads
process.env as a fallback in its server actions. The scattered writeEnvVars
calls after each inference step are removed.
* Address review: consistent e2e gate and expanded warning
- Use WORKFLOW_VERCEL_ENV as the gate in both e2e.test.ts and
bench.bench.ts for consistency (was WORKFLOW_VERCEL_AUTH_TOKEN in
e2e.test.ts, WORKFLOW_VERCEL_ENV in bench.bench.ts)
- Expand the misconfiguration warning to also detect
WORKFLOW_VERCEL_AUTH_TOKEN and WORKFLOW_VERCEL_ENV, listing the
specific env vars that are set
* Add changeset
Signed-off-by: Nathan Rajlich <n@n8.io>
---------
Signed-off-by: Nathan Rajlich <n@n8.io>