Sessions and human approval
Riverbeam records every invocation in a small hierarchy. The same records let an agent pause for a person to approve an action, then resume.
The execution hierarchy
| Record | Created when | Status values |
|---|---|---|
| Session | First request with a new session_id | none; last_active_at is updated after each run |
| Interaction | Each request; reused when you pass its interaction_id | in_progress, completed, failed |
| Run | Each pass through the pipeline | running, completed, failed, interrupted |
| Message | Input turn, agent reply, resume result | role human, ai or tool; type message or action_result |
| Action | The agent asks for approval | pending, resolved (cancelled exists but is never set) |
Messages carry a per-session sequence number. Agents are identified by agent_id_for(name), a
UUIDv5 derived from the agent name, so the same name always maps to the same id.
A session is created by the first request that uses its session_id, and any later request with
that id continues it, whoever sends it. The user id stored on the session is not checked. If session
ids can leak or be guessed, check in your API that the session belongs to the caller, for example
with get_session(session_id) and its user_id.
Stores
The BeamStore trait in beam_store persists the hierarchy. Two implementations ship:
| Store | Use |
|---|---|
InMemoryStore | The default in RiverBeam; tests and single-process development |
PgBeamStore | Postgres, schema ref_beam |
use std::sync::Arc;
use beam_store::PgBeamStore;
let store = PgBeamStore::connect(&database_url).await?; // creates the execution tables if missing
beam.set_store(Arc::new(store));
PgBeamStore::connect builds a connection pool and creates the tables it uses. Use
PgBeamStore::new(pool) when you already have a pool and manage the schema yourself. The table
layout is in Database schema.
You can read the records back through the trait: get_session, get_interaction, get_run,
get_action and list_messages(session_id).
Human approval and resume
An agent pauses by returning an AgentResult with interrupted: true and a list of
action_required entries. The built-in RigAgentRunner does
this before running a tool marked confirm_required, such as git_push.
1. The agent asks for approval
The final done frame carries the request:
{
"agent_name": "author",
"interrupted": true,
"action_required": [
{
"action_id": "9c1b2d3e-4f50-4a61-8273-849506172839",
"kind": "confirmation",
"tool": "git_push",
"args": {"ref": "docs/update"}
}
],
"content": [{"type": "text", "text": "…"}]
}
Riverbeam saves one pending action per entry, closes the run as interrupted, and leaves the
interaction open. Keep the interaction_id from the result.
2. The client resumes
Send the resume command with the interaction and one
ActionResponseEnvelope per action:
{
"agent_name": "author",
"session_id": "5f8915eb-31eb-4f8a-9517-e2c15ce7bc41",
"interaction_id": "40744652-2720-4b10-86ee-7333c1be8165",
"content": [],
"actions": [
{
"action_id": "9c1b2d3e-4f50-4a61-8273-849506172839",
"kind": "confirmation",
"request_id": "2b7c4d5e-6f70-4a81-9293-a4b5c6d7e8f9",
"status": "submitted",
"payload": {"tool": "git_push", "args": {"ref": "docs/update"}}
}
],
"auth_context": {"user_id": "u1"}
}
resume always streams: it returns a StreamAck and the frames arrive on the stream channel.
3. Riverbeam resolves the actions
For each action, the pipeline checks it and marks it resolved, then saves an action_result
message and logs agent_resumed:
| Check | Error |
|---|---|
action_id is a UUID | BEM-063 (400) |
| The action exists | BEM-027 (404) |
The action is still pending | BEM-066 (400) |
The agent then receives the actions in AgentInput.actions. RigAgentRunner runs the tool named in
each response's payload.tool with payload.args, and returns the tool output as the result.
Actions are marked resolved before the agent runs. If the tool then fails, for example a push
rejected with BEM-330, the action cannot be resumed again (BEM-066); ask the agent again in a
new message.
RigAgentRunner runs the tool and arguments from the response payload, not the ones stored with
the pending action, and it does not look at the response status. A cancelled response still
runs the tool, and the action is stored as resolved. Until this is enforced in the library, check
in your API layer that each response is submitted, and replace its payload with the stored tool
and args. Tutorial step 5 shows a resume route that
does this.
Current limitations
- Resume does not check that an action belongs to the interaction being resumed.
BEM-064andBEM-065are reserved for these checks but not raised yet.BEM-062(missinginteraction_id) cannot occur throughinvoke, which fills in a missing id. - Checking and resolving an action are separate steps, so two resumes of one action at the same time can both run its tool. Allow one resume per action in your API.
PgBeamStoreallocates message sequence numbers with a count query, so two concurrent writes to one session can get the same number. Its writes are not wrapped in transactions.PgBeamStorestoresuser_idandprofile_idonly when they are UUIDs; other values are dropped.