session

package
v0.27.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 19, 2026 License: Apache-2.0 Imports: 11 Imported by: 0

README

pkg/session

pkg/session exposes the live data-plane and control-plane contracts for one running rig. It is the surface a consumer (TUI, CLI, HTTP, test) holds and drives; session construction and restoration are owned exclusively by pkg/rig.

Operation hooks are also installed at the rig boundary, not on a live session. Pass a hook.Set with rig.WithHooks when defining the rig. Every new or restored session then uses that immutable compiled policy for new native-loop work and checked journal appends; sessions expose no mutable hook registry.

It is a contracts package — it defines interfaces, not implementations. The concrete Session lives in internal/sessionruntime and is returned to callers as session.SessionController.

What is session?

Three contracts live here, layered by trust:

  • Session — the ordinary data plane: identity, the active loop, the loop registry, submit, compact, subscribe, respond to a gate, interrupt.
  • SessionController — the trusted policy and lifecycle view. It embeds Session and adds SetActiveLoop, LoopController, CheckpointWorkspace, RestoreWorkspace, Shutdown.
  • GateHost — the capability to raise a host-owned gate (a form or an out-of-band URL) and wait on its answer. A separate contract from SessionController, because opening a gate is not part of running a session and the two roles have different holders (an MCP binding servicing an elicitation is a GateHost; the client that answers is a Session).

How to use

// From pkg/rig:
session, err := r.NewSession(ctx)
if err != nil { return err }
defer session.Shutdown(ctx)

// Submit user input to the primary loop.
inputID, err := session.Submit(ctx, []content.Block{
    &content.TextBlock{Text: "Summarize README.md."},
})
if err != nil { return err }

// Or target a specific loop (e.g. a delegate).
_, err = session.SubmitToLoop(ctx, reviewerLoopID, blocks)

// Subscribe to the event stream before submitting so no event is missed.
sub, err := session.SubscribeEvents(nil)
go func() {
    for delivery := range sub.Events() {
        handleEvent(delivery.Event)
        if delivery.Event.EndsTurn() { /* ... */ }
    }
}()

// Answer a permission gate raised by a tool.
session.RespondGate(ctx, gate.GateResponse{
    GateID: openGateID,
    Action: gate.ApproveActionApprove,
})

// Trust a host-owned gate (a form an MCP integration raised, e.g.):
host, ok := session.(session.GateHost)
if ok {
    id, _ := host.OpenHostGate(ctx, loopID, gate.KindForm, payload)
    answer, _ := host.AwaitGateAnswer(ctx, id)
    _ = answer
}

Sibling packages

  • pkg/rig — the composition root that returns a SessionController.
  • pkg/loop — the loop.Handle / loop.Controller values the session exposes.
  • pkg/eventevent.EventFilter and event.Subscription for SubscribeEvents.
  • pkg/gategate.GateResponse for RespondGate and gate.Gate/gate.Payload for OpenHostGate.
  • pkg/hook — rig-installed guards and around observers for native runtime operations and checked journal appends.
  • pkg/workspacestoreworkspacestore.Ref for CheckpointWorkspace / RestoreWorkspace.

How it is designed

The session is a coordinator with no goroutine. It owns a *hub.Hub, a loop registry, the journal, and the workspace placement; methods serialize access with normal RWMutexes. The loops are the actors; the session dispatches commands to them and fans their events back in through the hub.

                       Consumer (TUI / CLI / HTTP / test)
                                  │
                                  │  Submit / SubmitToLoop / SubscribeEvents
                                  │  RespondGate / Interrupt / Shutdown
                                  ▼
                ┌─────────────────────────────────────────────────┐
                │ SessionController (this package — contracts)     │
                │  • Submit → command.UserInput → active loop       │
                │  • SubmitToLoop → command.UserInput → target loop │
                │  • RespondGate → command.ApproveToolCall / Deny…  │
                │  • Interrupt → priority Interrupt to every loop   │
                │  • Shutdown → priority Shutdown, drain, close     │
                └──────┬────────────────────────┬───────────────────┘
                       │                        │
            publish    │                        │  dispatch
                       ▼                        ▼
                ┌──────────────┐         ┌────────────────────┐
                │  pkg/hub     │ ◀────  │ Loop actor          │
                │  (fan-in)    │  publish │ (internal/         │
                │              │         │  loopruntime)       │
                └──────┬───────┘         └────────────────────┘
                       │
                       ▼
                Subscribers (TUI / SSE / journal / tests)
Why GateHost is separate from SessionController

Two independent reasons, both from the interface-segregation rule:

  1. Coupling. Almost every SessionController consumer — TUI, CLI, test — submits work, watches events, and answers gates. Almost none raise one. Widening SessionController would force every implementation to grow three methods only an integration host calls.
  2. Holder. A SessionController is the session's operator; a GateHost is whatever opened a particular gate and is blocked on its answer — an MCP binding servicing an elicitation, say. The two ends of the same gate should not collapse into one god-interface.

A live session implements GateHost, so a host obtains one by asserting: host, ok := controller.(session.GateHost). The contract is host-owned gates only (gate.KindForm, gate.KindOpenURL with gate.ResolverSession); there is deliberately no way to mint a permission or ask-user gate through it, because a host that could mint one could park — or forge an approval against — a loop that is not its own.

Documentation

Overview

Package session exposes the live session data-plane and control-plane contracts. Session construction and restoration are owned exclusively by package rig.

Index

Constants

View Source
const (
	RestoreRuntimeMissing            = "missing_runtime"
	RestoreRuntimeUnavailable        = "runtime_unavailable"
	RestoreRuntimeTargetMismatch     = "target_mismatch"
	RestoreRuntimeCredentialMismatch = "credential_mismatch" // #nosec G101 -- closed error-category label, not a credential
	RestoreRuntimeEffortMismatch     = "effort_mismatch"
)

Variables

This section is empty.

Functions

This section is empty.

Types

type AcceptAllDecider

type AcceptAllDecider struct{}

AcceptAllDecider accepts every assessment. It backs the deprecated WithAllowConfigMismatch shim (wired in a later task).

func (AcceptAllDecider) DecideRestore

type AgentNameMismatchError

type AgentNameMismatchError struct{ Persisted, Configured identity.AgentName }

func (*AgentNameMismatchError) Error

func (e *AgentNameMismatchError) Error() string

type ConfigMismatchError

type ConfigMismatchError struct{ Persisted, Live event.ConfigFingerprint }

ConfigMismatchError is the legacy config-drift restore error. For a manifest-carrying session it is superseded by RestoreRejectedError, which carries a typed drift assessment; the legacy fingerprint path (a session with no ConfigManifest configured) still returns it during the deprecation window. Its formal deprecation and removal path is open question 9 in docs/plans/2026-07-16-session-versioning-migration-design.md; it is not marked Deprecated here because internal code still depends on it.

func (*ConfigMismatchError) Error

func (e *ConfigMismatchError) Error() string

type DefaultPolicyDecider

type DefaultPolicyDecider struct{}

DefaultPolicyDecider is the fail-secure default: accept when every change is Info, reject when any change is Warn.

func (DefaultPolicyDecider) DecideRestore

type GateError

type GateError struct {
	GateID gate.ID
	Kind   GateErrorKind
	Cause  error
}

func (*GateError) Error

func (e *GateError) Error() string

func (*GateError) GateErrorKind

func (e *GateError) GateErrorKind() string

func (*GateError) Unwrap

func (e *GateError) Unwrap() error

type GateErrorKind

type GateErrorKind string
const (
	GateNotFound      GateErrorKind = "not_found"
	GateNotReady      GateErrorKind = "not_ready"
	GateKindMismatch  GateErrorKind = "kind_mismatch"
	GateActionInvalid GateErrorKind = "action_invalid"
	GateCapacity      GateErrorKind = "capacity"
	GateAppendFailed  GateErrorKind = "append_failed"
)

type GateHost

type GateHost interface {
	// OpenHostGate opens g and returns its id. The gate is public and answerable
	// when it returns. The caller MUST then either AwaitGateAnswer or CloseGate;
	// abandoning it without either leaks the answer slot for the session's life.
	OpenHostGate(context.Context, uuid.UUID, gate.Gate, gate.Payload) (gate.ID, error)
	// AwaitGateAnswer blocks until the gate is answered and returns the validated
	// answer, including the form values that are absent from every durable record.
	// An answer is delivered exactly once. Cancelling the context abandons the
	// wait and frees the slot but does NOT close the gate — the gate is durable
	// state and the context is the caller's, so an opener that gives up must
	// CloseGate.
	AwaitGateAnswer(context.Context, gate.ID) (gate.Answer, error)
	// CloseGate withdraws a gate without answering it, waking any awaiter with a
	// *GateError{GateNotFound}. It is how an opener cleans up after a cancelled or
	// timed-out request.
	CloseGate(context.Context, gate.ID, gate.CloseReason) error
}

GateHost is the capability to raise a HOST-OWNED gate: to put a structured question or an out-of-band action to a human and receive the answer directly.

It is a SEPARATE contract rather than three more methods on SessionController, for two independent reasons.

The first is segregation. Opening a gate is not part of running a session: almost every consumer of a SessionController — the TUI, the CLI, a test — submits work, watches events, and answers gates, and none of them raise one. Widening SessionController would force every implementation to grow three methods that only an integration host calls, which is the exact coupling the interface rules forbid.

The second is that the two contracts have different holders. A SessionController is the session's operator. A GateHost is whatever opened a particular gate and is blocked on its answer — an MCP binding servicing an elicitation, say. Those are the two ends of the same gate, and RespondGate (on Session) is the other end: a client answers, the host receives. Keeping them separate keeps that asymmetry visible instead of collapsing both roles into one god-interface.

A live session implements it, so a host obtains one by asserting on the controller rig returns:

host, ok := controller.(session.GateHost)

The contract is host-owned gates ONLY (gate.KindForm and gate.KindOpenURL with gate.ResolverSession). There is deliberately no way to open a permission or ask-user gate through it: those are answered by resuming a parked loop, and a host that could mint one could park — or forge an approval against — a loop that is not its own. An implementation MUST refuse anything else at open time rather than at answer time, so a caller learns its request was invalid before a human is shown a prompt that can never be delivered.

type RestoreDecider

type RestoreDecider interface {
	DecideRestore(ctx context.Context, assessment event.DriftAssessment) (RestoreDecision, error)
}

RestoreDecider answers a restore drift assessment. It runs while the restore lease is held; ctx carries the restore deadline, and a timeout is a rejection.

An ACCEPTING RestoreDecision must honor the RestoreDecision contract above: a valid Source (empty defaults to policy) and bounded Actor/Message (truncated, not rejected). A decider therefore cannot brick a restore with a malformed decision — the constructor normalizes an accepting decision before it becomes a durable ConfigurationAdopted.

type RestoreDecision

type RestoreDecision struct {
	Accept  bool
	Source  event.DecisionSource // user | policy | operator (migration reserved for Harness)
	Actor   string
	Message string
}

RestoreDecision is an application's answer to a drift assessment. Source, Actor, and Message are recorded durably on the resulting ConfigurationAdopted.

Contract for an ACCEPTING decision (Accept == true): the valid decider Sources are user | policy | operator. `migration` is RESERVED for Harness itself (a Phase-2 migration) and must never be stamped by a decider. An empty OR migration Source on accept is normalized to policy by the restore constructor, so a decider can neither omit a source nor forge a migration adoption. Actor and Message are BOUNDED audit fields: the restore constructor truncates them (to MaxConfigActorLen / MaxConfigMessageLen bytes) before writing the durable adoption, so an over-long value is silently shortened rather than bricking the restore — never rely on their full length surviving.

type RestoreDiscoveryError

type RestoreDiscoveryError struct {
	Kind      RestoreDiscoveryErrorKind
	SessionID uuid.UUID
}

func (*RestoreDiscoveryError) Error

func (e *RestoreDiscoveryError) Error() string

type RestoreDiscoveryErrorKind

type RestoreDiscoveryErrorKind string
const (
	RestoreNoSessionStarted RestoreDiscoveryErrorKind = "no_session_started"
	RestoreNoPrimerLoop     RestoreDiscoveryErrorKind = "no_primer_loop"
)

type RestoreError

type RestoreError struct {
	Kind  RestoreErrorKind
	Cause error
}

func (*RestoreError) Error

func (e *RestoreError) Error() string

func (*RestoreError) Unwrap

func (e *RestoreError) Unwrap() error

type RestoreErrorKind

type RestoreErrorKind string
const (
	RestoreLeaseFailed   RestoreErrorKind = "lease_failed"
	RestoreJournalFailed RestoreErrorKind = "journal_failed"
	RestoreReplayFailed  RestoreErrorKind = "replay_failed"
	RestoreAppendFailed  RestoreErrorKind = "append_failed"
	// RestoreAdoptionInvalid names the specific failure of building/validating the
	// durable ConfigurationAdopted (event.ValidateEvent rejected it), distinct from
	// RestoreAppendFailed (an actual journal Append failure — lost lease, storage
	// error). It lets a caller tell "the decision produced a malformed adoption"
	// apart from "the journal write failed".
	RestoreAdoptionInvalid       RestoreErrorKind = "adoption_invalid"
	RestoreLoopFailed            RestoreErrorKind = "loop_failed"
	RestoreContextDone           RestoreErrorKind = "context_done"
	RestoreIDGenerationFailed    RestoreErrorKind = "id_generation_failed"
	RestoreForeignSIDMissing     RestoreErrorKind = "foreign_sid_missing"
	RestoreForeignBuilderMissing RestoreErrorKind = "foreign_builder_missing"
	RestoreMaterializeFailed     RestoreErrorKind = "materialize_failed"
)

type RestoreRejectedError

type RestoreRejectedError struct {
	Assessment event.DriftAssessment
	Source     event.DecisionSource
	Cause      error
}

RestoreRejectedError reports a restore refused by the configured RestoreDecider (or by default policy). It carries the full typed assessment so callers and operators see exactly which fields drifted and how severely. Cause is set only when the rejection was caused by the decider ITSELF failing (a returned error or a timeout — a timeout is a rejection): it stays inspectable via Unwrap so callers can errors.As/errors.Is through to the underlying cause (e.g. context.DeadlineExceeded). A plain policy rejection leaves Cause nil.

func (*RestoreRejectedError) Error

func (e *RestoreRejectedError) Error() string

func (*RestoreRejectedError) Unwrap

func (e *RestoreRejectedError) Unwrap() error

Unwrap exposes the underlying decider failure (nil for a plain policy rejection) so errors.As/errors.Is reach it.

type RestoreRuntimeMismatchError

type RestoreRuntimeMismatchError struct {
	Kind    string
	Harness loop.AgentHarnessName
	Cause   error
}

RestoreRuntimeMismatchError reports a fail-closed adapter restore decision. Its public text is deliberately category-only: journal selectors, model keys, credentials, and catalog/provider details never reach model-facing errors.

func (*RestoreRuntimeMismatchError) Error

func (*RestoreRuntimeMismatchError) Unwrap

func (e *RestoreRuntimeMismatchError) Unwrap() error

type RuntimeRestoreRequest added in v0.27.0

type RuntimeRestoreRequest struct {
	AgentName       identity.AgentName
	Harness         loop.AgentHarnessName
	Profile         loop.RuntimeProfileName
	Source          loop.RuntimeSourceName
	Credential      loop.CredentialMode
	Target          model.ModelKey
	Effort          model.Effort
	SelectionKind   loop.RuntimeSelectionKind
	SmallModelAlias loop.ModelAlias
	Mismatch        string
	Catalog         loop.RuntimeCatalog
}

RuntimeRestoreRequest is the bounded, secret-free runtime selection context a composition may use when exact durable runtime reconstruction fails.

type RuntimeRestoreResolver added in v0.27.0

type RuntimeRestoreResolver interface {
	ResolveRuntimeRestore(context.Context, RuntimeRestoreRequest) (loop.Resolved, error)
}

RuntimeRestoreResolver lets the composition layer authorize a current runtime selection for a durable loop. Omitting it keeps exact, fail-closed reconstruction.

type Session

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

Session is the ordinary data-plane view of one live rig execution.

type SessionController

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
}

SessionController is the trusted policy and lifecycle view of a Session.

type SessionError

type SessionError struct {
	Kind  SessionErrorKind
	Cause error
}

func (*SessionError) Error

func (e *SessionError) Error() string

func (*SessionError) Unwrap

func (e *SessionError) Unwrap() error

type SessionErrorKind

type SessionErrorKind string
const (
	SessionIDGenerationFailed            SessionErrorKind = "id_generation_failed"
	SessionLoopIDGenerationFailed        SessionErrorKind = "loop_id_generation_failed"
	SessionLoopExited                    SessionErrorKind = "loop_exited"
	SessionLoopNotFound                  SessionErrorKind = "loop_not_found"
	SessionEventChannelClosed            SessionErrorKind = "event_channel_closed"
	SessionContextDone                   SessionErrorKind = "context_done"
	SessionClosing                       SessionErrorKind = "session_closing"
	SessionFaulted                       SessionErrorKind = "session_faulted"
	SessionLoopDepthExceeded             SessionErrorKind = "loop_depth_exceeded"
	SessionLoopQuotaExceeded             SessionErrorKind = "loop_quota_exceeded"
	SessionForeignBuilderMissing         SessionErrorKind = "foreign_builder_missing"
	SessionCompactionUnsupported         SessionErrorKind = "compaction_unsupported"
	SessionDelegateIntentAppendFailed    SessionErrorKind = "delegate_intent_append_failed"
	SessionDelegateAdmissionCommitFailed SessionErrorKind = "delegate_admission_commit_failed"
)

type TurnRejectedError

type TurnRejectedError struct{ Reason event.RejectReason }

func (*TurnRejectedError) Error

func (e *TurnRejectedError) Error() string

type WorkspaceNotConfiguredError

type WorkspaceNotConfiguredError struct{}

func (*WorkspaceNotConfiguredError) Error

type WorkspaceRecoveryError

type WorkspaceRecoveryError struct {
	Path   string
	Reason string
	Cause  error
}

WorkspaceRecoveryError reports that a per-session workspace destination could not be established or recovered safely. Path identifies the refused filesystem object, Reason is a stable diagnostic, and Cause preserves any syscall failure.

func (*WorkspaceRecoveryError) Error

func (e *WorkspaceRecoveryError) Error() string

func (*WorkspaceRecoveryError) Unwrap

func (e *WorkspaceRecoveryError) Unwrap() error

type WorkspaceRootBusyError

type WorkspaceRootBusyError struct {
	Root        string
	HolderEpoch uint64
	Cause       error
}

WorkspaceRootBusyError reports that an exclusive workspace root is already leased by another session. HolderEpoch is copied from the storage refusal so callers never need an internal runtime type to diagnose contention.

func (*WorkspaceRootBusyError) Error

func (e *WorkspaceRootBusyError) Error() string

func (*WorkspaceRootBusyError) Unwrap

func (e *WorkspaceRootBusyError) Unwrap() error

type WorkspaceRootLeaseLostError

type WorkspaceRootLeaseLostError struct{}

WorkspaceRootLeaseLostError reports that an exclusive workspace lease ended while its session was live. It is the public leaf chained by SessionFaulted.

func (*WorkspaceRootLeaseLostError) Error

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL