Lloyal does not have its own concurrency model. Every harness runs on Effection, the structured concurrency library for JavaScript made by Frontside, and the ideas on this page — generators as operations, yield* in place of await, scopes that own every lifetime — are Effection's. Lloyal builds on them, and adds the few pieces that live model state needs.
What Effection guarantees
From Frontside's Thinking in Effection:
- No operation runs longer than its parent. When a parent completes or is halted, Effection tears down its children — the way memory is released when nothing refers to it.
- Every operation exits fully. Cleanup runs, whether the work returned, threw or was stopped.
- It's just JavaScript.
let,const,if,for,try/catch/finallyall work as you expect; generators take the place ofasync/await, because async/await cannot model structured concurrency.
That is why a Stop in a Lloyal app works in the middle of anything, and why there is almost no teardown code to write: whatever a piece of work starts — a tool call, a pool of agents, a fork of the model's state — is finished or cleaned up when that work ends, however it ends.
The rules below are few, and they matter: code that breaks them usually compiles, runs, and leaks. Thinking in Lloyal is how the same ownership extends to the model's live state.
From async/await to Effection
If you know how to do it in JavaScript, you know how to do it in Effection. These rows follow Frontside's Async Rosetta Stone, with what each one means for ownership:
| JavaScript | Effection | Ownership meaning |
|---|---|---|
async function |
function*(): Operation<T> |
A composable scoped program |
await work() |
yield* work() |
Perform work under the current owner |
Promise<T> |
Operation<T> |
Work waiting to be placed in a scope |
Promise.all(...) |
yield* all(...) |
Run and join an owned cohort |
Promise.race(...) |
yield* race(...) |
Race owned alternatives and halt the losers |
| fire-and-forget Promise | yield* spawn(...) |
Start a child that cannot outlive this scope |
| call an async API | yield* call(() => ...) |
Cross deliberately into Promise code |
finally cleanup |
ensure() or a resource |
Bind cleanup to scope exit |
for await |
for (... of yield* each(stream)) |
Consume a scoped subscription |
| global dependency | Effection Context | Inherit a capability inside a scope |
What Lloyal adds
Two forms are Lloyal's, because live model state has lifetimes of its own:
| Situation | Lloyal form | Ownership meaning |
|---|---|---|
write into the model — commitTurn, commit, prefill, promote |
yield* waitUntilSettled(session.commitTurn(...)) |
Finish the native write, even when halted |
| a temporary workspace for agents | yield* withSpine(options, body) |
Borrow live inference state and reclaim it when the body ends |
Both are built from Effection's own primitives — scoped, ensure, until — and they are in @lloyal-labs/lloyal-agents.
Four lines that carry the model
const value = yield* operation;Perform owned work and resume with its result.
const task = yield* spawn(operation);Start concurrent work without detaching it from the current scope.
const findings = yield* withSpine(options, body);Borrow live inference state, return durable data, and reclaim the temporary subtree.
yield* waitUntilSettled(session.commitTurn(query, answer));Make the result durable, explicitly — and let the write finish even if the owner is halted.
Operator ownership reference
yield* performs the Operation it receives. Different Operations complete at different moments.
| Code | Meaning |
|---|---|
yield* operation |
Perform scoped work and receive its result |
yield* spawn(operation) |
Attach and start a concurrent child |
yield* all(operations) |
Run and join an owned cohort |
yield* race(operations) |
Race owned alternatives and halt losers |
yield* call(() => promise) |
Cross deliberately into Promise-based code |
yield* waitUntilSettled(promise) |
Await a native write into the model; exit only once it has settled, even on halt |
yield* ensure(cleanup) |
Register cleanup for scope exit |
for (... of yield* each(stream)) |
Consume a subscription owned by the scope |
yield* context.expect() |
Read an inherited scoped capability |
yield* resourceOperation |
Acquire a live capability whose provider remains beneath you |
This matters most with spawn and resources: yield* does not always mean “wait for every activity underneath this call to finish”.
Follow the return type
Two APIs may share a method name but have different semantics.
events.send(event);may be synchronous.
yield* channel.send(event);may return Operation<void> and require yield*.
Do not infer from names such as send, read, or close.
Let TypeScript tell you whether an API is:
- synchronous;
- Promise-returning;
- or Operation-returning.
A floating-Operation lint rule would be valuable for harness projects.
Common mistakes
Converting an Operation to async
Avoid:
async function runResearch() {
// ...
}when the function must compose Lloyal Operations.
Use:
function* runResearch(): Operation<Result> {
// ...
}Calling an Operation without performing it
Avoid:
ctx.spawn(task);Use:
const agent = yield* ctx.spawn(task);Treating an Agent as a Task
Avoid:
yield* agent;Use:
yield* ctx.waitFor(agent);Returning a scoped branch
Avoid relying on:
const spine = yield* withSpine(options, function* (spine) {
return spine;
});Return findings or another explicitly durable value.
Detached background loops
Avoid:
void listenForever();Use:
yield* spawn(listenForever);Wrapping a native write in call()
Avoid:
yield* call(() => session.commitTurn(query, answer));Use:
import { waitUntilSettled } from "@lloyal-labs/lloyal-agents";
yield* waitUntilSettled(session.commitTurn(query, answer));A native decode is queued on a worker thread and cannot be recalled. If the owner is halted — the reader presses Stop — call abandons the promise while the decode keeps writing the model's memory, and whatever cleans up next touches the same state: a corrupted branch, or a crash. waitUntilSettled waits for that one call to finish before the scope exits. It applies to every call that writes into the model: commitTurn, commit, the prefill family, promote and retainOnly.
Assuming call() cancels every Promise
Bind provider cancellation into the scope where available.
Marking a native Tool as off-loop
Do not set Tool.fanout = true when the Tool may touch the main SessionContext, a branch, or a nested Agent runtime.
Repository invariants
## Lloyal structured-concurrency invariants
- A harness is a long-lived Effection scope.
- Read `yield*` as “perform this operation here, under this owner.”
- Do not convert Operation generators into async functions.
- Do not call and ignore a value of type `Operation<T>`.
- Perform Operations with `yield*`, `all`, `race`, `spawn`, or return them.
- Use `spawn` only for concurrent children owned by the current scope.
- Use `call()` at Promise or async-library boundaries.
- Await a native write into the model (`commitTurn`, `commit`, `prefill*`, `promote`) with `waitUntilSettled`, never `call()`.
- An Agent is not an Effection Task; the pool advances Agents.
- Agent concurrency is executed by the pool over BranchStore.
- A Branch or spine normally cannot outlive the scope that created it.
- Return durable findings from `withSpine`, not live branch handles.
- Effection Context values are scoped capabilities, not globals.
- Follow TypeScript return types: some `send()` methods are synchronous,
while Channels return Operations that must be yielded.
- Do not mark a Tool as `fanout` if it may touch the main SessionContext.Learn Effection
Effection's guides are the reasoning behind everything on this page, and the contract Lloyal's own code is held to:
- Thinking in Effection — the three guarantees, and why generators.
- Async Rosetta Stone — every async/await form and its Effection equivalent.
- The Effection guides — scopes, resources, actions, events, collections and errors.
Next
- Build your first harness — these forms in a real project.
- Thinking in Lloyal — the execution model they describe.