The execution stack
A Lloyal harness programs intelligence over a branch-aware inference stack.
@lloyal-labs/sdkLive inference-state primitives used by the HDK@lloyal-labs/lloyal-agentsAgents · Tools · orchestration · policy · grants · replay@lloyal-labs/lloyal.nodeNode.js · desktop · server@lloyal-labs/lloyal-react-nativeReact Native · JSI · mobile (planned)Each layer establishes a different part of the execution model.
- llama.cpp executes the model across CPU and accelerator backends.
- liblloyal gives resident inference state identity, lineage, KV tenancy, fork-and-prune semantics, and batched execution across a live branch tree.
- Platform bindings preserve those semantics in each host runtime through
@lloyal-labs/lloyal.nodetoday, with@lloyal-labs/lloyal-react-nativeplanned for mobile. - The HDK combines
@lloyal-labs/sdk,@lloyal-labs/lloyal-agents, and Effection into the public harness programming model. - The harness defines the application procedure: what work exists, what state it inherits, what may happen next, and what becomes durable.
The package names identify the layers that make the HDK work; harness authors normally program its high-level surfaces rather than composing @lloyal-labs/sdk directly.
Effection is the structured-concurrency library used by the HDK. It gives asynchronous work an ownership tree: Operations run inside scopes, child work cannot silently outlive its owner, cancellation propagates through descendants, and cleanup belongs to the scope that acquired the resource.
Generator functions and yield* are how Effection represents scoped Operations in TypeScript. They are the syntax of the lifetime model, not Lloyal's architectural idea.
Harness developers program the application procedure through the HDK; the layers beneath it preserve those semantics down to resident model execution.
What happens when a query runs
Session
└── harness stays alive and owns the work
└── query Operation
├── withSpine borrows shared inference state
│ └── AgentPool owns Agent execution
│ ├── orchestrator declares task relationships
│ ├── Agents branches with intent
│ └── AgentPolicy decides what happens next
└── answer
└── session.commitTurn(...) makes the result durable
A command arrives while the harness is alive.
The query runs beneath that harness, so its lifetime is already accounted for. It may fork a spine from the Session trunk, giving a cohort of Agents a shared line of attention. The orchestrator declares which tasks can proceed together and which depend on earlier findings. The AgentPool advances the resulting branches as a cohort. Policy is consulted at explicit lifecycle boundaries, while Tools and application state supply evidence and domain meaning.
While the query is underway, ordinary application code can observe branch and Agent state, resource pressure, Tool history, validators, and product state. It can then expand or prune topology, change retrieval mode, admit different evidence, continue or recover selected lineages, permit or reject consequential actions, and decide which result becomes continuing state.
When the run finishes, findings leave the temporary inference tree as ordinary data. The accepted answer is committed to the Session. The spine and worker branches are reclaimed.
If the Session disappears before that happens, the ownership tree unwinds the work instead.
That is the core model. The rest of the guide gives each relationship a precise shape.
Three structures, one control loop
The same query looks different depending on the question being asked.
Ownership: what ends together?
Session
└── harness
├── event forwarder
└── query
├── pool
└── synthesiser
This is the lifetime tree, managed by Effection. It determines cancellation and cleanup.
Inference: what attention state is inherited?
session trunk
└── query spine
├── researcher branch A
└── researcher branch B
This is the inference-state tree. It determines sharing, lineage, and pruning.
Workflow: what depends on what?
landscape
├── domain A
├── domain B
└── domain C
↓
synthesis
This is the orchestration graph, programmed by the harness. It determines collaboration and dependency.
Control: what can change next?
observe live execution
↓
combine with application state
↓
make a decision
↓
change topology · evidence · inference
lifecycle · authority · continuity
↓
observe the resulting execution
This is not a fourth graph. It is the harness's control loop over the other three structures.
The structures often line up, but they are not interchangeable. An Agent can inherit from one branch, depend logically on another task, and still be owned by a broader query scope. The control loop may observe all three, but it acts through explicit surfaces and lifecycle boundaries rather than through one omnipresent supervisor.
Six questions for reading any Lloyal program
- Who owns this work?
- Which live state does it inherit?
- What may enter or transform that state?
- Who decides what happens next, and at which boundary?
- What may become an accepted result or external consequence?
- What survives when the scope ends?
Those questions scale from a single Tool call to an adaptive multi-stage harness.
Five key concepts
Work always belongs somewhere
An Operation<T> does not float like an eager Promise. It runs beneath a scope that owns its completion, failure, children, and cleanup.
const result = yield* operation;
Read yield* as:
Perform this work here, under the current owner.
Live attention is a resource
A Branch represents a live line of model state, not a transcript reference. A spine can be borrowed, extended, inherited, transformed, and pruned. Its lifetime therefore belongs in the program's structure.
const findings = yield* withSpine(options, function* (spine) {
return yield* runResearch(spine);
});
Agents are managed, not launched
An Agent is a branch with intent, policy-relevant state, history, and a result. It is not one Effection Task and not one remote model request.
The AgentPool owns Agents and advances all runnable branches through a shared inference loop.
Control happens at explicit boundaries
Different parts of the harness control different dimensions of the execution:
- orchestration decides which lineages exist, where they inherit from, and what depends on what;
AgentPolicydecides at production, dispatch, exit, settlement, retry, and recovery boundaries;- Tools acquire and transform external information for a selected lineage;
- direct Branch operations can inspect or shape next-token inference when a specialised operator needs them;
- Session and spine operations decide what becomes inherited or continuing state.
There is no single “reasoning policy” object that secretly owns the whole program. The behaviour emerges from ordinary code composed across these boundaries.
Finality, authority, and durability are explicit
model proposal
≠ accepted application result
≠ permitted external action
≠ continuing Session state
A terminal Tool or policy decision may accept a result. A guard or grant may determine whether a proposed action is allowed. The application may require additional evidence, validation, or approval. The resulting state becomes durable only through an explicit continuity decision.
yield* call(() => session.commitTurn(query, answer));
Data can leave a scope. Owned runtime state normally cannot.
withSpine: borrow live attention, return durable findings
withSpine is the clearest expression of the Lloyal model:
const notes = yield* withSpine(
{
parent: session.trunk ?? undefined,
systemPrompt,
tools,
},
function* (spine) {
const pool = yield* agentPool({
parent: spine,
tools,
terminal: reportTool,
orchestrate: parallel(tasks),
});
return pool.agents
.map((agent) => agent.result)
.filter((result): result is string => Boolean(result));
},
);
Read it as:
Borrow a shared line of live attention, perform owned work inside it, return durable findings, then reclaim the temporary inference subtree.
withSpine creates or forks a spine, performs its body, and prunes the spine subtree when the body returns, throws, or is halted.
Data may leave a scope. Owned runtime state normally may not.
This is valid:
return agents.map((agent) => agent.result);
The strings survive.
This is normally wrong in intent:
return spine;
The JavaScript handle may escape, but the branch is disposed when withSpine closes.
An API that genuinely transfers branch ownership should say so explicitly.
What this changes
- Concurrent Agents inherit shared attention instead of reconstructing context.
- Cancellation reclaims both application work and its inference subtree.
- Findings can survive without keeping worker branches alive.
- Durable conversation state changes only through explicit commit.
- Application code can change topology, evidence flow, lifecycle, and continuity while the execution is underway.
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* call(() => session.commitTurn(query, answer));
Cross into a Promise boundary and persist the result explicitly.
How the guide unfolds
The execution stack establishes the architectural boundary. From there, the guide deepens the model in the same order a harness executes it:
- Own the work — harness scopes, Operations,
yield*,spawn, and cohorts. - Own the state — Agents, branches, spines, trunks, Context, and resources.
- Compose behaviour — Tools, orchestration, policy boundaries, and consequence.
- Exit cleanly — completion, failure, halt, cleanup, and Promise integration.
- Read it in code — the basic harness with the ownership model annotated.
- Advanced patterns — adaptive control, direct inference programming, acceptance, continuation, and replay.
The first sections establish the mental model. Advanced patterns shows what qualitatively different programs those relationships can express together.
Own the work
A harness is a scope
The application contract is deliberately small:
export function* harness(
ctx: SessionContext,
events: EventBus<WorkflowEvent>,
commands: Signal<Command, void>,
): Operation<void> {
// Your intelligent application lives here.
}
The function remains alive for the Session.
Its arguments establish the application boundary:
ctxis the resident model and native Session context;eventscarries application events down to whichever surface is mounted;commandscarries typed commands from that surface back into the harness;Operation<void>says the harness is a scoped asynchronous program.
A typical lifetime looks like this:
served Session
└── harness
├── enabled AgentApps
├── event-forwarding task
└── command loop
└── query
├── temporary spine
│ └── agent pool
│ ├── researcher
│ └── researcher
└── synthesiser
When the Session is released, the harness scope ends. Its child tasks, subscriptions, Tool executions, pools, and temporary inference branches unwind with it.
You do not need to manually enumerate every outstanding child and cancel it.
The ownership tree already knows.
Operation<T>: work waiting for an owner
An Effection Operation<T> is not an eager Promise<T>.
It is a description of asynchronous work that can be performed inside a scope:
function* lookupCustomer(id: string): Operation<Customer> {
// ...
}
Calling an operation-producing function does not yet say what should own it:
const operation = lookupCustomer(id);
An Operation must be:
- performed with
yield*; - started as a child with
spawn; - joined with
allorrace; - returned to the caller;
- or handed to another API that explicitly owns Operations.
Every
Operationmust be yielded, spawned, joined, raced, returned, or deliberately transferred to another owner. Never leave one floating.
What yield* means in Lloyal
The operative definition is:
yield*performs an Operation here, as part of the current scope, and resumes with its result.
Read:
const result = yield* operation;
as:
Do this work here, under the current owner.
Three outcomes are possible:
current scope
└── operation
├── returns a value → resume with the value
├── throws an error → propagate the error here
└── owner is halted → unwind the operation with its owner
yield* resembles await because it suspends and later resumes with a value.
But “yield* is Effection’s spelling of await” is incomplete. It omits the ownership guarantee.
Do not teach bare yield
Harness authors use yield*, not bare yield.
Bare yield belongs to JavaScript’s generator protocol. The Lloyal programming model composes Operations with yield*.
spawn: concurrency without orphaned work
The basic harness forwards agent events while it separately listens for commands:
const { session, events: agentEvents } = yield* initAgents(ctx);
yield* spawn(function* () {
for (const event of yield* each(agentEvents)) {
events.send(event);
yield* each.next();
}
});
for (const command of yield* each(commands)) {
// ...
yield* each.next();
}
yield* spawn(...) does not wait for the child body to finish.
spawn() completes after creating and attaching a live child Task. The parent continues while the child runs concurrently.
harness
├── event forwarder
└── command loop
The child remains owned. If the harness exits, the forwarder is halted automatically.
Avoid fire-and-forget
This work has no visible structured owner:
void someAsyncLoop();
or:
somePromiseReturningFunction();
Use spawn() when work should continue concurrently under the current scope.
all: a concurrent cohort
Orchestrators use all() when several Operations should run concurrently and complete as a cohort:
const agents = yield* all(
tasks.map((task) => ctx.spawn(task)),
);
yield* all(
agents.map((agent) => ctx.waitFor(agent)),
);
This means:
- create the Agents as a cohort;
- wait for every Agent to finish;
- propagate failure through the same structured scope.
It does not imply one model request or one JavaScript fiber per Agent.
Own the state
An Agent is not an Effection task
An Agent is a branch with intent:
Branch
+ task
+ policy
+ grammar and sampler state
+ tool history
+ result provenance
= Agent
Agents are plain objects managed by the AgentPool.
They are not:
- independent Effection resources;
- one fiber each;
- one HTTP request each;
- one model endpoint call each.
The pool advances all runnable branches through a phased loop:
SPAWN + EXTEND
↓
PRODUCE
↓
COMMIT
↓
DRAIN
↓
SETTLE
↓
DISPATCH
The AgentPool owns execution. BranchStore batches inference. An orchestrator determines which Agents should exist and which dependencies must be satisfied.
Agents sit across three structures
An Agent's owner, inherited attention, and logical dependencies are separate facts. Use the three-structures model before inferring one tree from another.
A spine is not the Session trunk
These branches have different jobs.
Session trunk
The trunk represents durable conversational state:
yield* call(() => session.commitTurn(query, answer));
A later query can inherit the committed turn.
Temporary spine
A spine is a scoped workspace:
- it may fork from the trunk;
- it may hold a shared system-and-tools header;
chainmay extend it between tasks;- nested Agents may inherit from it;
- it is reclaimed when its scope ends.
The common pattern is:
durable trunk
└── temporary query spine
├── temporary worker A
└── temporary worker B
worker findings escape as data
↓
answer is committed to trunk
Do not use branch survival as persistence.
Context: inherited capability, not global state
Lloyal uses Effection Context so descendant Operations can access runtime capabilities without manually threading every value through every call.
initAgents() installs capabilities such as:
- the active
SessionContext; - the active
BranchStore; - the Agent event channel;
- the trace writer.
Descendants can read them:
const ctx = yield* Ctx.expect();
const store = yield* Store.expect();
const events = yield* Events.expect();
During Tool dispatch, the runtime can install narrower facts:
CallingAgent;TraceParent;- current grants;
- the current shared spine format.
The rule is:
Ambient, but not global. Inherited, but only inside the owning scope.
A nested scope may shadow a Context value for its own descendants without mutating unrelated siblings.
Context answers:
What is available here?
Scope answers:
How long is it available?
Resources: acquire a live capability
Some Operations do not merely compute a value and finish. They expose a live capability while keeping backing work alive beneath the caller.
useAgentPool() is a resource. It can expose a Subscription while the pool and its branches remain live.
yield* resource
↓
receive live capability
↓
use it inside this scope
↓
scope exits
↓
provider halts and resources clean up
So yield* can mean:
Acquire this capability and keep its provider alive beneath me.
Higher-level agentPool() composes the resource and returns a completed result with branches cleaned up. Most harness authors should prefer it unless they deliberately need the lower-level live pool.
Compose behaviour
Post-training produces a tendency. A harness produces a procedure.
The model supplies language, representation, judgement, and learned competence. The harness supplies the application-specific procedure: which state exists, what evidence enters it, when work changes course, what counts as completion, and what may become consequence.
Tools are scoped evidence programs
A Tool returns an Operation:
class SearchTool extends Tool<{ query: string }> {
readonly name = "search";
readonly description = "Search the corpus";
readonly parameters = {
type: "object",
properties: {
query: { type: "string" },
},
required: ["query"],
};
*execute(args: { query: string }): Operation<unknown> {
return yield* call(() => this.search(args.query));
}
}
A Tool runs for a particular Agent. Its result returns to that Agent's continuing lineage, where inference resumes with the new information.
A Tool can:
- acquire information from an external service or local source;
- score, filter, redact, validate, or transform material before returning it;
- emit progress and read scoped Context;
- inspect pressure, exploration mode, and peer Tool history through its
ToolContext; - call
withSpineor start a nested AgentPool; - fork nested work from the calling Agent's branch so it inherits the caller's live state.
Acquisition and admission are separate concerns:
retrieve or calculate evidence
↓
rank · filter · redact · validate
↓
return to the calling Agent
↓
prefill into that lineage
↓
continue inference from transformed state
The Tool runs inside the pool's scope. It is not ownerless background work, and its result is not broadcast to every lineage unless the harness explicitly makes it shared state.
Structured ownership does not imply every operation is concurrency-safe
The pool protects the main llama_context with a single-loop discipline.
By default, a Tool executes inline on the pool loop. This is safe even when it:
- invokes
useAgent; - starts a nested pool;
- touches the calling branch;
- performs native work on the main context.
A Tool may opt into off-loop execution only when it guarantees that it performs no native operation on the main context:
readonly fanout = true;
Appropriate cases include:
- network I/O;
- pure CPU work;
- a separate reranker context that serialises itself.
A wrong false may reduce throughput.
A wrong true may violate native safety.
Keep these concepts separate:
- agent fan-out: an orchestration shape;
- off-loop Tool execution: an execution-safety declaration.
Control is split across explicit boundaries
The harness can govern a continuing execution without pretending every decision belongs in one abstraction.
| Surface | The decision it owns |
|---|---|
| Orchestrator | Which lineages exist, where they fork from, what depends on what, and what findings extend the spine |
AgentPolicy |
Whether output is accepted, a Tool is dispatched, retrieval explores or exploits, a lineage exits, failed work retries, or incomplete work recovers |
| Tool or scorer | What information is acquired, transformed, admitted, withheld, or returned |
| Branch operations | How a specialised program samples, validates, steers, constrains, forks, or merges inference state |
| Session and spine | Which accepted state becomes inherited, durable, or canonical |
| Guards and grants | Whether a proposed Tool call is permitted to create external consequence |
Use the highest-level surface that expresses the invariant. Drop to direct Branch operations when the requirement is inherently about token selection, logits, search, or state promotion—not merely because a lower-level operation exists.
In Lloyal, control flow is orchestration
An orchestrator is an ordinary Operation over PoolContext:
type Orchestrator = (ctx: PoolContext) => Operation<void>;
JavaScript control flow remains the language of orchestration.
Parallel breadth
const agents = yield* all(
tasks.map((task) => ctx.spawn(task)),
);
yield* all(
agents.map((agent) => ctx.waitFor(agent)),
);
Sequential depth
for (const step of steps) {
const agent = yield* ctx.waitFor(
yield* ctx.spawn(step.task),
);
if (agent.result && step.userContent) {
yield* ctx.extendSpine(step.userContent, agent.result);
}
}
Conditional work
if (ctx.canFit(estimatedTokens)) {
yield* ctx.waitFor(
yield* ctx.spawn(task),
);
}
Dependencies
A dependent node can simply perform the Task representing its dependency before it spawns.
You do not need to encode every workflow into a generic graph DSL.
Loops, conditions, errors, and lexical scopes are orchestration primitives.
Encode invariants as structure, not mode flags
A production harness should branch because the work has a different structural requirement, not because a generic mode string happened to select another path.
Examples:
- a warm Session trunk enables continuation because there is state to inherit;
- multiple independent findings justify synthesis because there is something to aggregate;
- one finding can become the answer directly;
- dependent work uses a chain because later tasks must inherit earlier accepted findings;
- independent work uses parallel execution because no dependency exists;
- only accepted findings extend the spine.
The structure explains the behaviour and makes the invariant reviewable in code.
Finality, authority, and continuity are different decisions
model proposes
↓
policy and completion conditions
↓
application accepts
↓
guard · grant · approval where required
↓
external consequence
↓
explicit continuity decision
A model proposal is only a candidate. Policy can reject it or require more evidence. A protected Tool can still be denied when the Session lacks a grant. An allowed Tool call can still fail. A successful action does not automatically decide which branch or answer becomes continuing state.
Keeping these boundaries distinct prevents a schema-valid model output from silently becoming an institutionally wrong action.
Exit cleanly
Normal completion, failure, and halt
These endings are different.
return
The Operation completed normally:
return answer;
Its parent resumes with the value.
throw
The Operation failed:
throw new Error("No AgentApp is enabled");
The error propagates to the nearest recovery boundary:
try {
const answer = yield* runQuery(query);
} catch (error) {
events.send({
type: "error",
message: String(error),
});
}
Parent halt
The owner disappears:
connection closes
↓
Session is released
↓
harness scope halts
↓
children and resources unwind
This is lifetime termination, not a domain-level AgentPolicy decision.
Graceful wind-down, Agent recovery, and per-Agent cancellation occur inside a still-live scope and are covered in the AgentPolicy guide.
ensure: cleanup belongs to the owner
Use ensure() when a scope must perform cleanup as it exits:
let tookPermit = false;
yield* ensure(() => {
if (tookPermit) permits.release();
});
yield* permits.acquire();
tookPermit = true;
Cleanup runs when the scope:
- returns normally;
- throws;
- or is halted by its parent.
The same principle is embedded throughout the framework:
- Agent setup registers branch pruning;
withSpineprunes its subtree;- bindings register disposers;
- subscriptions end with their scopes.
Do not design around remembering cleanup later.
Make cleanup structurally inseparable from ownership.
call: crossing the Promise boundary
Lloyal code often invokes ordinary synchronous or Promise-returning APIs:
const modelPath = yield* call(() =>
resolveModel({ projectRoot, role: "llm", spec }),
);
Use call() for:
- filesystem APIs;
- network clients;
- Promise-based SDKs;
- async Session methods;
- native wrappers returning Promises.
call() does not make external work magically cancellable
When the owner ends, Effection can stop waiting and unwind its side.
Whether the underlying activity physically stops depends on the integration:
- an abort-aware fetch can stop;
- a child process can be killed if cleanup is registered;
- an arbitrary third-party Promise may continue internally.
For long-running work, bind the provider’s AbortSignal, disposer, or cancellation API to the Effection scope.
Read it in code
The basic harness, annotated
export function* harness(ctx, events, commands): Operation<void> {
const { session, events: agentEvents } = yield* initAgents(ctx);
// Install the Agent runtime in this harness scope.
yield* spawn(function* () {
for (const event of yield* each(agentEvents)) {
events.send(event);
yield* each.next();
}
});
// Start an event-forwarding child owned by the harness.
const registry = yield* createAppRegistry({ configStore });
// Acquire the AgentApp registry and its scoped capabilities.
for (const app of apps) {
yield* registry.enable(app);
}
// Enable each capability under the registry’s ownership.
for (const command of yield* each(commands)) {
// Own a command subscription for the Session lifetime.
const notes = yield* withSpine(options, function* (spine) {
// Borrow temporary shared inference state.
const pool = yield* agentPool({
parent: spine,
orchestrate: parallel(tasks),
});
// Run and join a managed Agent cohort.
return pool.agents.flatMap((agent) =>
agent.result ? [agent.result] : []
);
// Return durable data; temporary branches remain scoped.
});
const synth = yield* useAgent({
parent: session.trunk ?? undefined,
task: renderSynthesis(notes),
});
// Perform one managed synthesis Agent.
yield* call(() =>
session.commitTurn(command.query, synth.result ?? "")
);
// Persist the durable turn explicitly.
}
}
The code visibly answers:
- who owns each activity;
- which work is concurrent;
- which capabilities are inherited;
- which inference state is temporary;
- which value becomes durable;
- what unwinds when the Session ends.
Advanced patterns
The preceding sections teach how to see a Lloyal execution. These patterns show what the same primitives can express when topology, observation, evidence flow, inference shaping, lifecycle policy, authority, and continuity are composed deliberately.
They are not built-in modes. Each is an application procedure assembled from ordinary TypeScript and the public inference primitives. Most harnesses should stay at the Agent, Tool, orchestrator, and policy level. Direct Branch operations are appropriate when the invariant is genuinely about token selection, search, logits, or state continuity.
Begin with the invariant
Do not start by choosing an orchestration factory or a number of Agents.
Start with:
What must remain true while this intelligent application investigates, concludes, acts, and continues?
For each pattern, identify:
invariant
→ required live states
→ ownership and inheritance
→ observations
→ allowed interventions
→ acceptance and authority
→ continuity
→ observable test
The runtime owns safe execution, cancellation, branch cleanup, batching, and grant enforcement. The harness owns the semantic decisions: what evidence means, which contradiction is material, what burden of proof applies, which state is canonical, and when consequence is acceptable.
Adaptive harness patterns
Contradiction-triggered expansion
Invariant: no final result while a material contradiction remains unresolved.
State shape: preserve the current interpretation and the conflicting observation as distinct live states. Create only the focused work needed to resolve their disagreement.
current interpretation
│
└── conflicting observation
│
preserve current lineage
+
provenance challenge
+
applicability challenge
+
alternative explanation
│
reconcile
│
extend only resolved finding
A custom orchestrator can inspect harness-owned contradiction state, conditionally spawn challenge Agents, waitFor them, and extend the spine only after the resolution passes the application's completion gate. AgentPolicy can keep the unresolved lineage alive or prevent terminal acceptance.
Why it is different: the graph is not declared once and then executed blindly. New topology appears because evidence changed the application’s understanding of the workload.
Minority-lineage preservation
Invariant: majority convergence must not erase unique, higher-authority, or high-consequence evidence.
majority convergence
│
├── minority evidence immaterial → prune
│
└── minority evidence material
→ preserve
→ seek corroboration
→ reconcile
→ accept or block completion
The harness records provenance and materiality outside the model. It may preserve a minority Agent, spawn a verification lineage, withhold completion, or prune the branch when the unique evidence proves immaterial.
Why it is different: the application owns the survival rule. “Consensus” is not whatever the model or the largest cohort happens to prefer.
Dynamic retrieval phase switching
Invariant: retrieval should pursue the question the workload currently needs answered, not one static search objective.
discovery
→ governing facts found
→ applicability search
→ candidate outcome
→ contradiction search
→ materiality check
→ explanation-quality retrieval
AgentPolicy.shouldExplore() supplies a live explore/exploit decision at Tool dispatch. A Tool receives that decision through ToolContext.explore and can combine it with application-owned phase state, root intent, local branch state, pressure, and peer history.
class PhasePolicy extends DefaultAgentPolicy {
constructor(private readonly state: WorkloadState) {
super({ terminalToolName: "report" });
}
override shouldExplore(agent: Agent, pressure: ContextPressure): boolean {
const phase = this.state.forAgent(agent.id).phase;
if (phase !== "discovery") return false;
return pressure.percentAvailable > 40;
}
}
The boolean boundary is intentionally small. Richer semantics—verification, falsification, applicability, or explanation—remain application and Tool concerns.
Materiality-aware compute allocation
Invariant: spend further inference only on uncertainty capable of changing the useful outcome.
uncertainty exists
→ could resolving it change the outcome?
├── no → stop or prune
└── yes → continue, fork, retrieve, or recover
Pressure reports what the runtime can afford. Materiality reports what the application considers worth affording.
class MaterialityPolicy extends DefaultAgentPolicy {
constructor(private readonly state: WorkloadState) {
super({ terminalToolName: "report" });
}
override shouldExit(agent: Agent, pressure: ContextPressure): boolean {
const s = this.state.forAgent(agent.id);
if (pressure.critical) return true;
if (!s.uncertaintyCouldChangeOutcome) return true;
return s.noMaterialProgressTurns >= 3;
}
}
A custom orchestrator can also use ctx.canFit(...) before spawning focused work. Recovery policy decides whether an incomplete but material branch deserves a final extraction rather than silent pruning.
Programming the inference trajectory
These patterns move below Agent lifecycle and operate directly on live Branch state. They are the clearest proof that the harness can program inference rather than only arrange calls around it.
Verifier-before-commit generation
Invariant: a candidate continuation must pass an application validator before it becomes branch state.
sample candidate
↓
application validates
┌──┴──────────────┐
accept reject
↓ ↓
commit alter distribution
and resample
The critical distinction is:
A sampled candidate is not yet committed inference state.
Branch.produce() samples without advancing the branch. Branch.commit() accepts and decodes the chosen token. Between them, application code can validate, steer, replace logits, or change grammar.
function* commitVerifiedToken(
branch: Branch,
accepts: (candidate: { token: number; text: string }) => boolean,
): Operation<boolean> {
const rejected = new Set<number>();
while (true) {
const candidate = yield* call(() => branch.produce());
if (candidate.isStop) return false;
if (accepts(candidate)) {
branch.clearSteer();
yield* call(() => branch.commit(candidate.token));
return true;
}
rejected.add(candidate.token);
branch.steer(
[...rejected].map((token) => ({ token, bias: -Infinity })),
);
}
}
Use this for local syntax or semantic validators, impossible world-state transitions, phase-specific structure, or specialised token-level scoring. Grammar remains preferable when the whole valid language can be expressed structurally; verifier-before-commit is valuable when validity depends on live application state.
Diverse tree search
Invariant: preserve several genuinely different continuations, spend work on promising paths, and remove dominated subtrees without reconstructing the shared prefix.
shared live state
↓
fork alternatives
↓
reseed · steer · produce · score
↓
expand promising paths
prune dominated subtrees
↓
retain or promote winner
Branches can fork from roots or intermediate branches. Sampler reseeding prevents identical stochastic continuations. Path-local steering can penalise sibling choices. produce() separates proposal from advancement, while cohort commit() advances the active frontier together.
const frontier = yield* all(
seeds.map((seed) => call(async () => {
const branch = await root.fork();
branch.reseedSampler(seed);
return branch;
})),
);
for (let depth = 0; depth < maxDepth; depth++) {
const proposals = frontier.map((branch) => [
branch,
branch.produceSync(),
] as const);
const live = proposals.filter(([, p]) => !p.isStop);
if (live.length === 0) break;
yield* call(() => store.commit(
live.map(([branch, p]) => [branch, p.token]),
));
// Application-owned scoring decides which paths expand or prune.
pruneDominated(frontier, scoreBranch);
}
A real search owns cleanup explicitly and promotes or retains the winner before the candidate scope closes.
Why it is different: the application is not asking for several detached answers. It is maintaining and governing a search tree of continuing model states.
Expert-state synthesis
Invariant: expert lineages should remain distinct while contributing directly to a common synthesis, rather than first being flattened into one textual summary.
expert A state ─┐
expert B state ─┼─ align to one synthesis task
expert C state ─┘
↓
capture token distributions
↓
merge into destination
↓
decode under destination grammar
and sampler policy
Session.prefillAligned() gives the trunk and experts the same next task while preserving each lineage's prior state. BranchStore.mergeLogits() adds expert distributions into the destination. The destination's grammar and sampler still determine the selected token.
function* synthesizeFromExperts(
session: Session,
store: BranchStore,
experts: Branch[],
task: string,
alpha = 0.25,
): Operation<string> {
yield* call(() => session.prefillAligned(task, experts));
const destination = session.trunk;
if (!destination) throw new Error("Expert synthesis requires a trunk");
let output = "";
while (true) {
store.mergeLogits(destination, experts, alpha);
const next = destination.produceSync();
if (next.isStop) break;
output += next.text;
// Advance every lineage with the selected synthesis token so their
// distinct histories remain aligned for the next distribution merge.
yield* call(() => store.commit([
[destination, next.token],
...experts.map((expert) => [expert, next.token] as [Branch, number]),
]));
}
return output;
}
The built-in merge applies one equal alpha to each expert. A custom weighting operator can read and write captured logits when experts need different weights.
Why it is different: the experts preserve their own evidence and attention histories while contributing to each next-token decision.
Acceptance and continuity patterns
Burden-of-proof scaling
Invariant: the evidence and validation required must increase with the consequence of the proposed result or action.
advisory output
→ modest evidence threshold
persistent recommendation
→ stronger evidence + provenance + validation
consequential action
→ corroboration + deterministic checks + grant + approval
The harness owns risk tier and evidence-completeness state. AgentPolicy.onProduced() can reject premature terminal output. Deterministic validators can check structure and domain rules. ToolGuard, protected Tools, Session grants, and human commands keep permission distinct from model preference.
This pattern preserves four separate facts:
the model proposed it
≠ the result is accepted
≠ the Session is authorised
≠ the application should execute it now
Verified-state continuation
Invariant: later work must inherit the state the application actually accepted, not merely a summary of whichever candidate happened to finish last.
candidate lineages
↓
validator or human selection
↓
accepted winner
↓
Session.promote(...)
↓
later work forks from accepted state
const winner = candidates.find((candidate) =>
validator.accepts(candidate.result)
);
if (!winner) throw new Error("No candidate satisfied the invariant");
// Promote while the candidate branch is still live in its owning scope.
yield* call(() => session.promote(winner.branch));
Promotion is both a topological operation and a product decision. The candidate scope must remain alive until selection and promotion complete.
Why it is different: the accepted inference state itself becomes the basis for follow-up work. Continuity is not reconstructed later from a lossy prose summary.
Counterfactual policy replay
Invariant: a changed policy, prompt, validator, model, or stage should be evaluated from equivalent inherited state.
historical inherited state
→ reconstruct
→ replace policy or stage
→ rerun
→ compare outcome and trace
const checkpoint = extractSpineCheckpoint(traceEvents, {
poolTraceId,
});
const spine = yield* reconstructBranch(checkpoint);
const result = yield* agentPool({
parent: spine,
tools,
policy: revisedPolicy,
orchestrate: replacementStage,
});
Replay reconstructs the seed prompt and ordered spine extensions. It does not promise bit-identical future generation when sampler state or other nondeterministic inputs differ. The object under comparison is the application procedure and its trace, not only the final prose.
Additional compositions
The same grammar supports further patterns that can be added when a product needs them:
- Cross-source invariant transfer: establish governing constraints in one stage, extend only the accepted constraints into the spine, and make later source work inherit them.
- Source-diverse evidence portfolio: treat independent source classes as an application invariant rather than trusting top-K relevance alone.
- Entitlement-aware evidence admission: allow private evidence to influence an authorised lineage while filtering or withholding its representation from other lineages and public output.
- Temporal applicability: keep publication date, effective date, jurisdiction, supersession, and event time in application state so current validity is tested rather than inferred from semantic relevance.
These are not special framework modes. They are different combinations of topology, Tool semantics, policy, application state, and continuity.
Design your own advanced pattern
Use this worksheet.
State
What live interpretations, candidates, or expert states must exist?
Ownership
What work ends together, and which temporary branches must remain alive until selection or promotion?
Inheritance
What state should each lineage receive? Which findings are allowed to extend the spine?
Observation
Which model, pressure, evidence, domain, external-system, and human signals matter?
Intervention
Can the harness spawn, retrieve, steer, constrain, validate, retry, recover, prune, merge, or promote?
Acceptance and authority
What separates a model proposal, an accepted result, a permitted action, and an executed consequence?
Continuity
What becomes shared state, the Session trunk, or a reconstructable checkpoint?
Test
What trace and application state prove the invariant held even when the final answer looks plausible?
Start from the application invariant, not from the number of Agents.
Advanced-pattern failure modes
Creating fan-out without a survival policy
More branches are not automatically better. Define what preserves, expands, recovers, prunes, and wins before creating a large frontier.
Confusing permission with authority
A grant means the Session may invoke a protected Tool. It does not mean the application must accept every proposed invocation or that evidence and approval conditions are satisfied.
Treating summaries as the only persistent state
Summaries are useful data. They are not equivalent to promoting an accepted live state or deliberately extending a spine with selected findings.
Assuming replay means bit-identical output
Reconstructed inherited state can be equivalent while later sampling diverges. Compare policy decisions, invariants, outcomes, and traces rather than assuming identical tokens.
Reference
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* 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.
Async-to-Lloyal Rosetta Stone
| JavaScript instinct | Lloyal/Effection form | 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 |
| fire-and-forget Promise | yield* spawn(...) |
Start a child that cannot outlive this scope |
Promise.race(...) |
yield* race(...) |
Race owned alternatives and halt losers |
| 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 |
| temporary workspace | withSpine(...) |
Borrow live inference state and reclaim it |
Further language-level references:
- https://frontside.com/effection/guides/v4/thinking-in-effection/
- https://frontside.com/effection/guides/v4/async-rosetta-stone/
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);
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.
- 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.
Continue into adaptive execution
This guide establishes the ownership model and shows the advanced procedures it can express. The next guide goes deeper on the lifecycle boundary that production harnesses use most often:
While work is still owned and running, what should an Agent do next?
AgentPolicy can decide to explore or exploit, accept or reject a Tool call, nudge an Agent towards reporting, retry a transient failure, stop one Agent, or recover findings before pruning. It operates inside the ownership model rather than replacing it.
Continue with How Agents Know When to Stop.