Documentation / guides
Session controller
Use the live control surface returned by a Rig.
The value returned by Rig.NewSession and Rig.RestoreSession is a
session.SessionController. The interface intentionally separates ordinary
data-plane work from trusted lifecycle mutations. The concrete runtime also
implements session.GateHost, but that capability is a separate assertion.
Data-plane contract
This is the exact published session.Session interface:
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 returned command or input UUID is a correlation identity, not a promise that a turn has started. Observe the resulting Reply or terminal event on the subscription.
Control-plane contract
SessionController embeds the data plane and adds the trusted mutations:
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
}
loop.Controller is the exact loop-scoped mutation view:
type Controller interface {
Handle
SetMode(context.Context, ModeName) error
Change(context.Context, ...Change) error
Interrupt(context.Context) error
}
Handle itself is read-only:
type Handle interface {
ID() uuid.UUID
Mode() ModeName
Model() model.Model
}
Callers should retain the controller only for the session lifetime and close it once. Loop handles remain registered when an agent turn becomes idle; they are not disposable worker handles.
Gate-host contract
An integration that opens a form or open-URL prompt asserts the separate host-owned capability:
type GateHost interface {
OpenHostGate(context.Context, uuid.UUID, gate.Gate, gate.Payload) (gate.ID, error)
AwaitGateAnswer(context.Context, gate.ID) (gate.Answer, error)
CloseGate(context.Context, gate.ID, gate.CloseReason) error
}
OpenHostGate accepts only gate.KindForm and gate.KindOpenURL with
gate.ResolverSession. It derives the public schema or origin from the
validated private payload. Permission and ask-user loop gates are deliberately
not reachable through this interface. The opener must either await the answer
or close the gate, including after a cancelled wait.
func hostCapability(c session.SessionController) (session.GateHost, error) {
host, ok := c.(session.GateHost)
if !ok {
return nil, fmt.Errorf("session does not expose host gates")
}
return host, nil
}
Errors are typed
Session refusals use *session.SessionError with a closed
SessionErrorKind. A caller can distinguish a missing loop, an exited actor,
closing, a durable persistence fault, or an unsupported native compaction
without parsing text. Gate operations use *session.GateError, and loop
configuration uses *loop.ChangeError.
var se *session.SessionError
if errors.As(err, &se) {
switch se.Kind {
case session.SessionLoopNotFound, session.SessionLoopExited:
// Refresh the loop view.
case session.SessionClosing:
// Stop admitting work.
case session.SessionFaulted:
// Surface the durable fault and require operator recovery.
}
}