defineTool, rebunoFetch, step(), and
resume.
The execution context
On each dispatch the agent builds anExecutionContext and sets it as an
ambient value (an AsyncLocalStorage from node:async_hooks) for the life of
the handler. Every recording primitive finds it there, so you thread no object
through your code:
defineToolandwrapToolcallctx.invokeTool(...)step()callsctx.invokeTool(..., { kind: "local" })rebunoFetchcallsctx.beginLlm(...), thenctx.recordLlm(...)once the provider response is in hand
Error. Read the context
yourself with the exported execution() accessor, which returns it or throws if
there is none.
Determinism and step ids
The kernel derives each effect’s step id from its content. The SDK sends the kind, target, and args with the dispatch id from the webhook, and the id comes back in the decision. The SDK computes nothing and keeps no step state between calls. The id is built from five fields:executionIdscopes the id to one execution.kindistool_call,llm_call, orlocal.targetis the tool id, or the model id for an LLM call.argsHashis a hash of the arguments, canonicalized by the kernel.occurrencedisambiguates identical calls. The kernel counts how many times each(kind, target, argsHash)triple has appeared in this dispatch. Calling the same tool with the same args twice produces two distinct steps, occurrence 0 then 1. The counter is cleared when a dispatch is claimed, so every delivery attempt counts from zero.
step() instead.
Step arguments have to be JSON-serializable. Anything else throws a TypeError
when the SDK encodes the submission, before the body runs.
Submitting and deciding a step
For each effect the context asks the kernel what to do before running the body.submitStep returns a StepDecision:
On
proceed the body runs. Success calls completeStep with the result.
Failure calls failStep, and for tools throws ToolError.
Blocked and Terminated are also recorded on the context, so the agent
re-throws one even if your handler swallowed it. See
Dispatch and resume.
Heartbeats and leases
A dispatch holds a lease so the kernel won’t reclaim it and re-deliver while the handler is still running. Handlers routinely outlive a fixed lease, so the context wraps the whole handler in asetInterval that calls heartbeat every
30 seconds.
The lease is the (dispatch_id, dispatch_attempt) pair the webhook arrived
with, and every mutation sends it back. A handler whose dispatch was reclaimed
and re-delivered is therefore refused rather than writing alongside the attempt
that replaced it. Its next heartbeat throws LeaseSuperseded, which aborts the
handler’s kernel client.
The heartbeat only fires if the handler yields to the event loop. Everything
naturally long in a handler (provider calls, MCP tools, kernel round-trips) is
I/O-bound and async, so this holds. A fully synchronous, blocking body starves
it, so offload CPU-bound work to a worker thread. That is the reason for the
guidance in Tools.
A superseded run stops renewing, since the lease belongs to the newer dispatch.
Signing
Both directions of the agent-kernel channel are authenticated with the shared agent secret, using HMAC-SHA256 via Web Crypto.- Kernel to agent (the webhook): the kernel signs the body, and the agent
verifies
Rebuno-Signature: sha256=...with a constant-time compare before doing anything. A bad or missing signature gets a401. - Agent to kernel: the kernel client signs every request body the same way and
sends
Rebuno-SignatureplusRebuno-Agent-Id. Step submissions also sendRebuno-Dispatch-Id, which scopes the kernel’s occurrence counter.
Client uses Bearer auth (REBUNO_API_KEY) instead, since it is a
client caller rather than the agent.