返回 CodeWhale
lib.rs
根目录 / crates / workflow-js / src / 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. A reply
22 //! that fails `responseSchema` gets up to
23 //! [`SCHEMA_REPAIR_MAX_ATTEMPTS`] bounded repairs (#5583) before it throws;
24 //! the failed attempts are reported as driver receipts either way.
25 //! * `parallel(thunks, opts)` — fan-out, at most [`PARALLEL_MAX_ITEMS`] items.
26 //! `opts.mode` is `settled` (default), `fail-fast`, or `partial`; an
27 //! unrecognized mode throws. `settled` keeps today's ergonomics — an
28 //! ordinary failed slot resolves to `null` — while schema-contract failures
29 //! and run cancellation still fail the run. `fail-fast` rejects the fan-out
30 //! with the first non-fatal slot error instead of nulling it. `partial`
31 //! resolves every non-cancellation failure to
32 //! `{ __taskError: { index, kind, message } }`.
33 //! * `pipeline(items, ...stages)` — per-item stage chains with no barrier
34 //! between stages; same cap and the same three modes via the
35 //! `pipeline(items, { stages, mode })` overload.
36 //!
37 //! Both fan-outs attach a non-enumerable `errors` array to the resolved
38 //! result — `[{ index, kind, message }]`, ordered by index, empty when
39 //! nothing failed. The array's own contents, length, and JSON encoding are
40 //! unchanged, so a dropped slot is now inspectable instead of erased.
41 //!
42 //! `kind` is a [`TaskErrorKind`] wire string (`admission`, `budget`,
43 //! `cancelled`, `agent`, `schema`, `driver`) assigned by the host where the
44 //! failure happened, or `script` for an error the script threw itself. It is
45 //! read from the thrown `Error`'s `.kind`, never inferred from message text.
46 //! * `log(msg)` / `phase(title)` — progress events forwarded to the driver.
47 //! * `budget.total` / `budget.spent()` / `budget.remaining()` — live driver
48 //! snapshots (`total` is `null` and `remaining()` is `Infinity` when no
49 //! ceiling is configured).
50 //!
51 //! `Date.now()`, `new Date()`, `Date.parse/UTC`, and `Math.random()` throw:
52 //! runs must be deterministic so recorded traces can be replayed.
53 //!
54 //! # Ownership boundaries
55 //!
56 //! Token accounting and admission belong to the driver; the VM only reads
57 //! snapshots and fast-fails a spawn when the shared pool is already exhausted.
58 //! Already-running parallel children can reconcile above the hint because
59 //! provider usage arrives at response boundaries, not token-by-token. Fleet
60 //! roster resolution for `profile` also happens driver-side; this crate
61 //! normalizes and token-validates the profile string, nothing more.
62
63 mod driver;
64 mod error;
65 mod schema;
66 pub mod testing;
67 mod vm;
68
69 pub use driver::{
70 BudgetSnapshot, ProgressEvent, SpawnedTask, TaskCompletion, TaskRequest, ToolCallRequest,
71 ToolCallResponse, ToolInvoker, WorkflowDriver, normalize_profile,
72 };
73 pub use error::{DriverError, TaskErrorKind, WorkflowJsError};
74 pub use schema::{SCHEMA_RAW_CARRY_CHARS, SCHEMA_RAW_PREVIEW_CHARS, SCHEMA_REPAIR_MAX_ATTEMPTS};
75 pub use vm::{VmLimits, WorkflowRunCancel, WorkflowVm};
76
77 /// Maximum `task()` spawn attempts per run (design §4.3). Counted in the VM
78 /// before the driver is consulted, so a runaway `loop-until-dry` terminates
79 /// even if the driver would keep admitting work.
80 ///
81 /// Product scale: up to 1_000 agents per Workflow run.
82 pub const WORKFLOW_LIFETIME_CAP: u64 = 1000;
83
84 /// Maximum concurrently executing agents within one Workflow run.
85 ///
86 /// Fan-out may *declare* more work via `parallel()` / `pipeline()`, but the
87 /// host admits at most this many live `task()` children at once; additional
88 /// spawns wait for a slot.
89 pub const WORKFLOW_MAX_CONCURRENT: usize = 16;
90
91 /// Maximum items per `parallel()` or `pipeline()` call (design §4.2).
92 /// Kept at the per-run agent ceiling so a single fan-out cannot declare more
93 /// work than the lifetime cap can ever complete.
94 pub const PARALLEL_MAX_ITEMS: usize = 1000;
95
96 /// Maximum `tools.call()` invocations per code-mode run.
97 ///
98 /// Counted in the VM before the invoker is consulted, so a runaway loop
99 /// terminates even if the invoker would keep admitting calls. Concurrency,
100 /// deadlines, and result sizes are enforced host-side by the invoker.
101 pub const CODEMODE_MAX_TOOL_CALLS: u64 = 50;
102
102 lines RUST