codewhale_workflow_js/lib.rs
1//! Dynamic Workflow runtime for CodeWhale.
2//!
3//! This crate is the imperative half of Workflow: a sandboxed QuickJS
4//! (rquickjs) runtime that executes a model-authored JS program which
5//! dispatches fleet-routed subagents via `task()`, fans out with
6//! `parallel()`/`pipeline()`, reports progress with `log()`/`phase()`, and
7//! scales itself to a token pool via the `budget` global. The static,
8//! declarative IR (record/replay, model policy) stays in `codewhale-workflow`;
9//! this crate only speaks to the outside world through the
10//! [`WorkflowDriver`] seam, so it is fully testable without spawning a real
11//! subagent (see [`testing::FakeDriver`]).
12//!
13//! # Script surface
14//!
15//! Every script runs inside an async function with these globals:
16//!
17//! * `args` — the invocation input, verbatim.
18//! * `await task(opts)` — dispatch one subagent; resolves to the full result
19//! text, or to a parsed + schema-validated object when `opts.responseSchema`
20//! is set. Throws on rejection, failure, cancellation, budget exhaustion,
21//! or once [`WORKFLOW_LIFETIME_CAP`] spawn attempts have been made.
22//! * `parallel(thunks)` — all-settled fan-out; an ordinary failed slot becomes
23//! `null`; schema-contract failures and run cancellation still fail the run;
24//! at most [`PARALLEL_MAX_ITEMS`] items.
25//! * `pipeline(items, ...stages)` — per-item stage chains with no barrier
26//! between stages; an ordinary stage error drops that item to `null`, while
27//! schema-contract failures and cancellation still fail the run; same cap.
28//! * `log(msg)` / `phase(title)` — progress events forwarded to the driver.
29//! * `budget.total` / `budget.spent()` / `budget.remaining()` — live driver
30//! snapshots (`total` is `null` and `remaining()` is `Infinity` when no
31//! ceiling is configured).
32//!
33//! `Date.now()`, `new Date()`, `Date.parse/UTC`, and `Math.random()` throw:
34//! runs must be deterministic so recorded traces can be replayed.
35//!
36//! # Ownership boundaries
37//!
38//! Token accounting and admission belong to the driver; the VM only reads
39//! snapshots and fast-fails a spawn when the shared pool is already exhausted.
40//! Already-running parallel children can reconcile above the hint because
41//! provider usage arrives at response boundaries, not token-by-token. Fleet
42//! roster resolution for `profile` also happens driver-side; this crate
43//! normalizes and token-validates the profile string, nothing more.
44
45mod driver;
46mod error;
47mod schema;
48pub mod testing;
49mod vm;
50
51pub use driver::{
52 BudgetSnapshot, ProgressEvent, SpawnedTask, TaskCompletion, TaskRequest, WorkflowDriver,
53 normalize_profile,
54};
55pub use error::{DriverError, WorkflowJsError};
56pub use vm::{VmLimits, WorkflowRunCancel, WorkflowVm};
57
58/// Maximum `task()` spawn attempts per run (design §4.3). Counted in the VM
59/// before the driver is consulted, so a runaway `loop-until-dry` terminates
60/// even if the driver would keep admitting work.
61///
62/// Product scale: up to 1_000 agents per Workflow run.
63pub const WORKFLOW_LIFETIME_CAP: u64 = 1000;
64
65/// Maximum concurrently executing agents within one Workflow run.
66///
67/// Fan-out may *declare* more work via `parallel()` / `pipeline()`, but the
68/// host admits at most this many live `task()` children at once; additional
69/// spawns wait for a slot.
70pub const WORKFLOW_MAX_CONCURRENT: usize = 16;
71
72/// Maximum items per `parallel()` or `pipeline()` call (design §4.2).
73/// Kept at the per-run agent ceiling so a single fan-out cannot declare more
74/// work than the lifetime cap can ever complete.
75pub const PARALLEL_MAX_ITEMS: usize = 1000;