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.