Agents spend most of their time waiting on a model or a network request, which is exactly the work you want to overlap. Orchard does this with structured concurrency: spawn starts work, await joins it, and parallel runs a fixed set of branches together. The structure is the point. A spawned task cannot outlive the scope that created it, so there are no leaked tasks and no futures floating loose past the code that owns them.
spawn and await
spawn e immediately schedules e on the executor and hands you back a future<T>, where T is the static type of e. The work runs concurrently; you keep going.
let h = spawn delegate "Research angle: {a}" // future<str>
let report = await h // structured joinawait f joins the future. It blocks the awaiting task until the future resolves, then returns its value. A future is single-shot in the sense that awaiting the same one twice returns the cached result. If the spawned work threw, the Throw is captured in the future and re-raised at the await, not at the spawn: the error surfaces where you join, which is where you can catch it.
parallel
When you have a known, fixed set of concurrent work, parallel is clearer than spawning by hand. It is a brace block of named branches, and it is a barrier: it returns only once every branch has settled.
let results = parallel {
weather: get_weather(city: c)
news: fetch_page(url: feed)
}
// results : a record { weather: ..., news: ... }The result maps each label to its branch's value, in declaration order. That ordering is deterministic and does not depend on which branch finished first, which is what makes parallel safe to use in code you test with scripted replay. Branch labels must be unique; the checker rejects duplicates.
The map fan-out pattern
The common shape is fanning one input out into many concurrent spans, then joining them all. You spawn across a list with map, get back a list of futures, then map(await) to collect the results.
skill survey(topic: str) -> str {
let angles = gen as list<str> "Three angles on: {topic}"
let hs = angles |> map((a) => spawn delegate "Research: {a}")
let findings = hs |> map(await)
return gen "Synthesize:\n{findings}"
}Each angle runs as its own autonomous delegate loop, all of them in flight at once, and the synthesis step runs only after every one has joined. This is the central concurrency idiom: deterministic glue around a set of autonomous spans.
Scoped lifetimes
Every spawned future is bound to the lexical scope that created it, the enclosing block, skill, or handler. This is what makes the model structured rather than a free-for-all of detached tasks.
- A future that is never awaited is cancelled at scope exit. Its task is dropped at the next await point and no new tool or model call is started, so it cannot keep running after the code that spawned it has returned.
- A future may not escape its scope. Storing one in a
varand returning it past its lexical owner is a checker error: the analysis is done at compile time, so you find out before you run. - Cancellation never leaves
statehalf-written. A cancelled in-runtime task's uncommitted writes are discarded, because they were never committed in the first place.
The practical consequence: you cannot leak a task. If you spawn work, you either await it within the scope or it is cleanly cancelled when the scope ends.
Errors and budgets
Error propagation is deterministic. If a parallel branch throws, the join throws the first branch's error by source order, after all branches have settled or been cancelled. Branches that had not yet started when the first error surfaced are cancelled; branches already running are allowed to finish their current step rather than being killed mid-tool. Wrap a parallel in try to handle the error.
Budgets are shared safely across concurrent spans. Accounting is atomic and pre-reserved: at spawn an estimate is debited from the parent's remaining allowance, and at await (or on cancellation) the reservation is reconciled, with unused budget refunded and actual usage committed. The atomic check-and-debit guarantees that no two concurrent tasks both slip past a cap they jointly exceed, so a fan-out cannot quietly overspend. A budget(...) block around concurrent work tightens the shared cap for the whole scope, and every branch draws from that one pool.
spawn schedules a task, await yields, and parallel interleaves branches at their await points. The observable semantics, ordering, budget, errors, and cancellation, are identical to native. Only wall-clock parallelism differs.Next. The fan-out above spawned delegate loops without saying what bounds them. Continue to the autonomous loop and policy to see how the loop runs and the safety envelope that contains it.
