smol-workflow-engine 0.2.1-alpha.2

Rust implementation of the smol-workflows engine.
Documentation

smol-workflow-engine

Rust implementation of the smol-workflows engine.

This crate contains the Rust port of the TypeScript workflow engine core, including the native QuickJS workflow runtime and built-in agent providers.

Workflow execution and async model

workflow::run_workflow is async. Callers should run it inside a Tokio runtime.

The workflow coordinator runs inside a Tokio LocalSet and starts a local JavaScript runtime actor. That actor owns the QuickJS execution object and is the only task that polls QuickJS or resolves JavaScript promises. The coordinator communicates with the actor over Tokio channels, while agent-provider tasks run independently as async Tokio tasks. Budget updates, phase/log collection, and child workflow orchestration remain serialized in the coordinator.

Agent calls are asynchronous. When workflow JavaScript calls agent(...), the QuickJS actor queues a request event and suspends that JavaScript promise. The Rust coordinator starts provider work asynchronously and sends a resolution command back to the QuickJS actor when the provider completes.

For parallel([...agent(...)]), the scheduler behaves like a dynamic event loop rather than a fixed batch executor:

  1. drain currently queued JavaScript requests,
  2. start provider calls up to RunWorkflowOptions::max_parallel_agent_requests,
  3. wait for one provider completion,
  4. resolve that one request back into QuickJS,
  5. the QuickJS actor immediately polls QuickJS again so follow-up agent(...) calls can be discovered and scheduled while other provider calls are still running.

max_parallel_agent_requests controls only concurrent agent-provider calls for a workflow run:

  • None: no engine-imposed per-run cap,
  • Some(1): serial agent execution,
  • Some(n): at most n in-flight agent calls.

Child workflow calls are still handled by the scheduler as workflow requests and retain the existing nesting limit.

JavaScript runtime

src/js_runtime defines the engine-independent boundary for executing workflow-shaped JavaScript modules. The first implementation is js_runtime::rquickjs::RQuickJSWorkflowRuntime, a restricted QuickJS runtime used by the native Rust workflow runner backend.

Runtime implementations are synchronous behind the actor boundary: they expose polling and request-resolution methods, but do not own provider execution. The local actor isolates JavaScript engine access from async provider tasks and avoids cross-task/cross-thread access to QuickJS values/contexts.

Agent providers

src/agent_providers ports the TypeScript engine's built-in providers:

  • debug
  • claude-code
  • codex
  • opencode
  • pi

Providers implement an async AgentProvider::run method and must be Send + Sync, because the workflow scheduler may call a provider concurrently for parallel workflow agent calls.

Provider expectations:

  • Do not block the Tokio runtime thread. Use async process/IO APIs where possible.
  • If blocking work is unavoidable, isolate it with tokio::task::spawn_blocking.
  • Honor AgentProviderRunInput::context.cwd for command execution and file-relative behavior.
  • Return the workflow-visible value in AgentProviderResult::output.
  • Put backend diagnostics/raw events in AgentProviderResult::raw.
  • Normalize token/cost data into AgentUsage when available; the workflow budget currently counts usage.output_tokens.

The CLI-backed providers mirror the TypeScript command construction, output extraction, structured-output handling, usage normalization, and error formatting. Provider tests use the TypeScript fake CLI fixtures to compare runtime behavior.

Integration-test feature

The crate has an integration-test feature for real-provider e2e tests that run in temporary workspaces. It enables test-only provider conveniences such as passing Codex --skip-git-repo-check automatically. This feature should not be enabled for normal builds unless that behavior is desired.

The Codex provider invokes the real Codex CLI through the non-interactive codex exec subcommand by default. Tests that supply a custom CodexAgentProviderOptions::subcommand keep full control of the command arguments, which is how the fake Codex fixtures are exercised.