Skip to documentation
Documentation navigation

Documentation navigation

Documentation / guides

Session controller

Use the live control surface returned by a Rig.

developer

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.
	}
}

Source and proof

← back to documentation