the manual / sec 05
Session: run and control a Rig
A Session is one live execution created by a Rig. It owns the runtime state that should not be shared between executions:
- live Loop actors;
- active turns and tool calls;
- durable event history;
- permission gates;
- the resolved workspace and its checkpoints.
The Rig is reusable. The Session is the running thing.
Create or restore
session, err := assembly.NewSession(ctx)
NewSession returns a session.SessionController with a newly generated session
identifier.
session, err := assembly.RestoreSession(ctx, sessionID)
RestoreSession rebuilds live runtime state from the durable journal. It does
not create a second history. It resumes the existing session identity after
checking that the current Rig is compatible with the stored configuration.
The caller owns the returned controller and must shut it down:
defer func() {
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
_ = session.Shutdown(shutdownCtx)
}()
Subscribe before submitting
Attach an event consumer before starting work so you do not miss live streaming events:
stream, err := session.SubscribeEvents(event.EventFilter{
Ephemeral: event.LoopScope{All: true},
Enduring: event.LoopScope{All: true},
})
if err != nil {
return err
}
defer stream.Close()
An event filter has separate scopes for two classes:
- Ephemeral events report live progress such as token deltas and tool-call lifecycle. They are not journaled.
- Enduring events report accepted input, turns, steps, gates, configuration changes, workspace transitions, and terminal outcomes. They survive restore.
event.LoopScope{All: true} selects every Loop. To follow only specific Loops,
populate the Loops map with their identifiers.
Consume the channel continuously. A subscription is bounded so one stalled
consumer cannot block the Session. After the channel closes, call stream.Err()
to distinguish an intentional close from a forced loss.
Submit input
Send human-authored input to the active primer:
inputID, err := session.Submit(ctx, []content.Block{
&content.TextBlock{Text: "Plan my week around these three goals."},
})
Submit is fire-and-forget. A nil error means the input was sent to the Loop,
not that the turn succeeded. The returned inputID is the correlation ID carried
by the resulting reply events.
The event stream reports whether the input was queued, started, folded into other input, rejected, cancelled, completed, failed, or interrupted.
To address a specific live Loop instead of the active primer:
inputID, err := session.SubmitToLoop(ctx, loopID, blocks)
This is useful for interfaces that focus different agents within one Session. It does not change which primer is active.
Content blocks
Input is an ordered slice of content.Block values. The shared core/content
package defines:
TextBlock;ImageBlock;AudioBlock;DocumentBlock;ThinkingBlock;ToolUseBlock;ToolResultBlock.
Ordinary user input usually contains text, images, audio, or documents. Declare
matching model capabilities with inference.WithImages and other model options
where appropriate. Provider codecs reject unsupported shapes rather than
silently dropping them.
Read events
for delivery := range stream.Events() {
switch ev := delivery.Event.(type) {
case event.TokenDelta:
if text, ok := ev.Chunk.(*content.TextChunk); ok {
fmt.Print(text.Text)
}
case event.GateOpened:
// Present ev.Gate to a person or a policy resolver.
case event.TurnDone:
// ev.Message is the complete assistant response.
case event.TurnFailed:
// ev.Err is the typed live failure cause when available.
case event.TurnInterrupted:
// The turn ended through cancellation.
}
}
if err := stream.Err(); err != nil {
return err
}
Each event.Delivery contains:
Event, a member of the sealed event union;JournalSeq, which is zero for ephemeral events and the durable sequence for enduring events.
Use concrete event types rather than parsing event names or error strings.
Answer permission gates
The access gate may decide that a tool call needs human approval. The Session
emits event.GateOpened and pauses the affected work without blocking the rest
of the process. A permission gate carries one combined request listing the
command and every capability that still needs approval.
A permission gate is answered with exactly one of three actions. Approve the call once:
err := session.RespondGate(ctx, gate.GateResponse{
GateID: opened.Gate.ID,
Action: string(gate.ApprovalApprove),
Source: gate.ResponseSource{Kind: gate.ResponseFromUser},
})
Approve it and persist the displayed reusable allow rules for this workspace:
err := session.RespondGate(ctx, gate.GateResponse{
GateID: opened.Gate.ID,
Action: string(gate.ApprovalApproveAlwaysWorkspace),
Source: gate.ResponseSource{Kind: gate.ResponseFromUser},
})
Deny it:
err := session.RespondGate(ctx, gate.GateResponse{
GateID: opened.Gate.ID,
Action: string(gate.ApprovalDeny),
Source: gate.ResponseSource{Kind: gate.ResponseFromUser},
})
The three exact actions are gate.ApprovalApprove,
gate.ApprovalApproveAlwaysWorkspace, and gate.ApprovalDeny. There is no
session or user-global approval scope: Approve writes nothing, and
Approve always for this workspace atomically appends the reusable rules to the
single workspace permission file. A saved deny always beats a saved allow.
Gates are durable. A restorable gate can remain open across process restart and
be answered after RestoreSession reconstructs it.
Interrupt work
interrupted, err := session.Interrupt(ctx)
This requests cancellation of every in-flight turn in the Session and reports whether any running work was actually interrupted. The durable event stream reports the resulting terminal outcomes.
For one Loop subtree, obtain its trusted controller and call Interrupt:
controller, ok := session.LoopController(loopID)
if ok {
err = controller.Interrupt(ctx)
}
Select and change Loops
Inspect the current primer:
active := session.ActiveLoop()
fmt.Println(active.ID(), active.Mode(), active.Model().Name)
Switch the active primer by live Loop identifier:
err := session.SetActiveLoop(ctx, anotherPrimerID)
Change one Loop’s declared mode:
controller, ok := session.LoopController(loopID)
if !ok {
return errors.New("loop not found")
}
err := controller.SetMode(ctx, "deep-review")
Or change its model and effort atomically:
err := controller.Change(
ctx,
loop.ChangeModel(newModel),
loop.ChangeEffort(model.EffortHigh),
)
Changes apply at a turn boundary and produce enduring events.
Compact context
Request manual compaction on the active Loop:
compactID, err := session.Compact(ctx)
Or target one Loop:
compactID, err := session.CompactToLoop(ctx, loopID)
The returned ID correlates durable compaction outcomes. Compaction is supported only for native Loops configured with an explicit compaction policy and a compatible Hustle registered on the Rig.
Checkpoint and restore the workspace
Create an explicit content-addressed snapshot:
ref, err := session.CheckpointWorkspace(ctx)
Restore the current Session workspace to an earlier reference:
err := session.RestoreWorkspace(ctx, ref)
Both operations require a configured workspace placement. Workspace restoration is a control-plane action and should be exposed only to trusted callers.
Public interfaces
The ordinary data plane is:
type Session interface {
SessionID() uuid.UUID
ActiveLoop() loop.Handle
Loop(uuid.UUID) (loop.Handle, bool)
Submit(context.Context, []content.Block) (uuid.UUID, error)
SubmitToLoop(context.Context, uuid.UUID, []content.Block) (uuid.UUID, error)
Compact(context.Context) (uuid.UUID, error)
CompactToLoop(context.Context, uuid.UUID) (uuid.UUID, error)
SubscribeEvents(event.EventFilter) (event.Subscription, error)
RespondGate(context.Context, gate.GateResponse) error
Interrupt(context.Context) (bool, error)
}
The trusted control plane adds:
type SessionController interface {
Session
SetActiveLoop(context.Context, uuid.UUID) error
LoopController(uuid.UUID) (loop.Controller, bool)
CheckpointWorkspace(context.Context) (workspacestore.Ref, error)
RestoreWorkspace(context.Context, workspacestore.Ref) error
Shutdown(context.Context) error
}
Prefer the narrower session.Session interface in code that does not need
trusted policy or lifecycle operations.
Next, add persistence, tools, multiple agents, and deployment surfaces.