@tool, http_client(), step(), and
resume.
The execution context
On each dispatch the agent builds anExecutionContext and sets it as an
ambient value (a contextvars.ContextVar) for the life of the handler. Every
recording primitive finds it there, so you thread no object through your code:
@toolandwrap_toolcallctx.invoke_tool(...)step()callsctx.invoke_tool(..., kind="local")http_client()callsctx.begin_llm(...), thenctx.record_llm(...)once the provider response is in hand
RuntimeError. Read the
context yourself with rebuno.execution(), which returns it or raises 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:execution_idscopes the id to one execution.kindistool_call,llm_call, orlocal.targetis the tool id, or the model id for an LLM call.args_hashis a hash of the arguments, canonicalized by the kernel.occurrencedisambiguates identical calls. The kernel counts how many times each(kind, target, args_hash)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.
rebuno.step() instead.
Step arguments have to be JSON-serializable. Anything else raises 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.submit_step returns a StepDecision:
On
proceed the body runs. Success calls complete_step with the result.
Failure calls fail_step, and for tools raises ToolError.
Blocked and Terminated are also recorded on the context, so the agent
re-raises 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 a background task that callsheartbeat
every min(lease_timeout_seconds / 3, 30) seconds: three renewals per lease
period, capped.
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 raises LeaseSuperseded, which cancels the
handler.
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 blocking synchronous body starves
it, so wrap CPU-bound work in a thread with asyncio.to_thread(...). That is
the reason for the guidance in Tools.
Signing
Both directions of the agent-kernel channel are authenticated with the shared agent secret.- Kernel to agent (the webhook): the kernel signs the body with HMAC-SHA256, and
the agent verifies
Rebuno-Signature: sha256=...withhmac.compare_digestbefore doing anything. A bad or missing signature gets a401. - Agent to kernel:
KernelClientsigns every request body the same way and sendsRebuno-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.