sessionrecorder

package
v1.1.2 Latest Latest
Warning

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

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

Documentation

Overview

Package sessionrecorder is an opt-in host utility that records typed agent-adaptor events under a host-owned session key and serves them back by a cursor that stays monotonic across runs. EventRecorder, NewMemoryEventBackend, and NewJSONLEventBackend form the typed Event API.

Placement

This package lives under hosttools because:

  • the core SDK (github.com/agent-dance/agent-adaptor) does not import it, preserving the boundary between execution and host persistence;
  • it is one concrete answer to UI session-history recovery while leaving storage policy under host control;
  • it keeps the SDK stateless by default while still saving every host from writing the same JSONL-plus-cursor plumbing.

HostSeq vs provider/run sequence numbers

Event ordering is scoped to one run. Two runs that share the same host-side session key — for example a browser's stable thread id that survives a page refresh — restart their run-local sequence, so that sequence alone cannot serve as a cross-run recovery cursor. A naive filter over a provider or run-local sequence can fold old-run events into the new window and reorder the stream.

HostSeq is the cursor this package assigns. It is strictly monotonic within one session key regardless of how many runs contribute events to that session, so the standard increment-and-resume protocol

afterHostSeq := lastKnownHostSeq
records, _ := recorder.Since(ctx, sessionKey, afterHostSeq)
lastKnownHostSeq = records[len(records)-1].HostSeq

works across arbitrary run boundaries.

Choosing a sessionKey

sessionKey is intentionally a neutral aggregation key. There are two canonical patterns; pick one and keep it stable per logical thread you want to read back:

  1. Audit style (recommended starting point): sessionKey = RunID - one record stream per run, immutable after the run ends - downside: cannot accumulate cross-run history of the same logical conversation; the host correlates RunIDs externally

  2. Conversation style: sessionKey = ThreadID (or any host-stable "logical thread" identifier) - history accumulates across resumed/forked runs under the same UI conversation

EventRecorder assigns HostSeq in process. All access for one sessionKey must therefore route to one process, regardless of which key style is chosen. Multi-process hosts need sticky routing or a coordinator-aware EventBackend that owns sequence allocation transactionally.

Scope

The package handles "append an Event, read Events back by cursor".

The package does NOT own:

  • persistent HITL pending state — derive it on demand from the existing history. The recorder remains the single source of truth; introducing a separate pending dimension creates double-write inconsistency risk.
  • HTTP / SSE transport — that's bridges/* and the host's HTTP router's job.
  • routing / sticky-by-thread dispatch across pods — the package is single-process by design. Multi-pod hosts should plug a shared EventBackend (e.g. Redis, Postgres) below the EventRecorder.
  • fan-out (Stream → SSE + EventRecorder + metrics) — that's a few lines of host-owned for loop; see examples/web-chat/copilotkit for the canonical pattern.

Index

Constants

This section is empty.

Variables

View Source
var DefaultKeyPattern = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9_\-]{0,127}$`)

DefaultKeyPattern matches portable single-component session keys: an alphanumeric leader followed by at most 127 alphanumerics, dashes, or underscores.

View Source
var ErrInvalidSessionKey = errors.New("sessionrecorder: invalid session key")

ErrInvalidSessionKey identifies a key rejected by the recorder or its backend. The validator's diagnostic is included in the returned error.

View Source
var ErrJSONLEventBackendClosed = errors.New("sessionrecorder: jsonl event backend closed")

ErrJSONLEventBackendClosed is returned when an operation is attempted after a JSONLEventBackend has been closed.

View Source
var ErrJSONLEventLogCorrupt = errors.New("sessionrecorder: corrupt jsonl event log")

ErrJSONLEventLogCorrupt identifies a malformed, truncated, or internally inconsistent event log. Load never silently skips these records: an audit history that cannot be replayed faithfully is an error, not a shorter successful history.

Functions

This section is empty.

Types

type EventBackend

type EventBackend interface {
	// Load returns all known records for sessionKey in HostSeq order.
	Load(ctx context.Context, sessionKey string) ([]EventRecord, error)

	// Append persists exactly one record for sessionKey. It must not return
	// nil after an encoding failure, a short/partial write, or a storage
	// error. Backend-specific documentation defines whether nil means the
	// record reached the operating system or stable storage.
	Append(ctx context.Context, sessionKey string, r EventRecord) error

	// Sessions enumerates keys the backend has any records for.
	Sessions(ctx context.Context) ([]SessionInfo, error)

	// Close releases backend resources. Implementations must be safe for
	// concurrent use and repeated calls; repeated callers observe the same
	// close result.
	io.Closer
}

EventBackend is the low-level storage interface behind an EventRecorder — Backend's contract verbatim, typed on EventRecord.

func NewMemoryEventBackend

func NewMemoryEventBackend() EventBackend

NewMemoryEventBackend returns an EventBackend that keeps records in process memory. It is intended for tests and explicitly ephemeral hosts.

type EventOption

type EventOption func(*eventRecorder)

EventOption configures an EventRecorder constructed via NewEventRecorder.

func WithEventClock

func WithEventClock(fn func() time.Time) EventOption

WithEventClock overrides the time source used to stamp RecordedAt.

func WithEventKeyValidator

func WithEventKeyValidator(v KeyValidator) EventOption

WithEventKeyValidator overrides the session-key validator. The default is DefaultKeyValidator.

type EventRecord

type EventRecord struct {
	HostSeq    HostSeq
	RecordedAt time.Time
	Event      adaptor.Event
}

EventRecord is one persisted unified event together with the HostSeq assigned to it. It marshals to a stable JSON envelope (kind + authoritative event metadata + event payload) and unmarshals back to the typed event.

func (EventRecord) MarshalJSON

func (r EventRecord) MarshalJSON() ([]byte, error)

MarshalJSON encodes the record as {host_seq, recorded_at, kind, meta, event}.

func (*EventRecord) UnmarshalJSON

func (r *EventRecord) UnmarshalJSON(data []byte) error

UnmarshalJSON restores the typed event from the envelope.

type EventRecorder

type EventRecorder interface {
	// Record appends an event under sessionKey and returns the record
	// with the HostSeq it was assigned. HostSeq values strictly increase
	// within one sessionKey; a rejected backend write rolls the number
	// back so a retry gets the same one.
	Record(ctx context.Context, sessionKey string, ev adaptor.Event) (EventRecord, error)

	// Since returns records whose HostSeq is strictly greater than
	// afterHostSeq, in ascending order. afterHostSeq == 0 fetches the
	// whole known history.
	Since(ctx context.Context, sessionKey string, afterHostSeq HostSeq) ([]EventRecord, error)

	// Sessions enumerates known session keys, most recent first.
	Sessions(ctx context.Context) ([]SessionInfo, error)

	io.Closer
}

EventRecorder is the host-facing typed Event recording API. Implementations MUST be safe for concurrent use.

func NewEventRecorder

func NewEventRecorder(backend EventBackend, opts ...EventOption) EventRecorder

NewEventRecorder wraps an EventBackend into an EventRecorder. Same single-process HostSeq contract as New: route all access for a given sessionKey through one process, or plug a coordinator-aware backend.

Panics only if backend is nil.

type HostSeq

type HostSeq = uint64

HostSeq is the host-scoped cursor assigned to each recorded Event. It is strictly monotonic within one session key and remains stable across SDK run boundaries.

type JSONLEventBackend

type JSONLEventBackend interface {
	EventBackend
	Flush() error
}

JSONLEventBackend is the durable, typed-Event implementation of EventBackend. Flush synchronizes every dirty session file with its storage device. Close is idempotent and flushes before closing files.

Append synchronizes its record before returning by default, so a successful EventRecorder.Record is durable without a separate Flush call. Hosts that deliberately choose buffered durability can opt out with WithoutJSONLEventSyncOnAppend and establish their own Flush boundaries.

func NewJSONLEventBackend

func NewJSONLEventBackend(dir string, opts ...JSONLEventOption) (JSONLEventBackend, error)

NewJSONLEventBackend creates a typed Event JSONL backend rooted at dir. Each session is stored as <dir>/<sessionKey>.jsonl with one stable EventRecord envelope per line.

The constructor rejects an empty path, resolves dir to an absolute path, and creates dir plus missing parents. Creation errors are returned; it never substitutes an in-memory backend. Missing parents use the configured directory mode, while existing directory permissions are left untouched. Opening or writing an individual session file happens in Append and any resulting error is returned to the caller.

The backend coordinates concurrent callers within one process. Concurrent writers in different processes are intentionally unsupported; use a coordinator-aware EventBackend for that deployment model.

type JSONLEventOption

type JSONLEventOption func(*jsonlEventBackend)

JSONLEventOption configures NewJSONLEventBackend.

func WithJSONLEventDirMode

func WithJSONLEventDirMode(mode os.FileMode) JSONLEventOption

WithJSONLEventDirMode changes the creation mode for the storage directory and any missing parents. The default is 0o700. The process umask still applies; existing directories are not chmod'ed.

func WithJSONLEventFileMode

func WithJSONLEventFileMode(mode os.FileMode) JSONLEventOption

WithJSONLEventFileMode changes the creation mode for new log files. The default is 0o600 because events may contain prompts, tool arguments, and process output. The process umask still applies; existing files are not chmod'ed.

func WithJSONLEventKeyValidator

func WithJSONLEventKeyValidator(v KeyValidator) JSONLEventOption

WithJSONLEventKeyValidator replaces the business key validator. The backend's cross-platform single-file-component check remains mandatory, so a custom validator can tighten accepted keys but cannot enable path traversal or nested paths.

func WithoutJSONLEventSyncOnAppend

func WithoutJSONLEventSyncOnAppend() JSONLEventOption

WithoutJSONLEventSyncOnAppend chooses buffered durability. Append still performs one complete JSONL write and reports encoding/write errors, but the caller must use Flush (or check Close's error) to observe storage sync failures. The default synchronizes every append.

type KeyValidator

type KeyValidator func(sessionKey string) error

KeyValidator returns an error when a session key is not accepted. A validator must be deterministic and side-effect-free.

var DefaultKeyValidator KeyValidator = func(key string) error {
	if !DefaultKeyPattern.MatchString(key) {
		return fmt.Errorf("sessionrecorder: refused session key %q: must match %s", key, DefaultKeyPattern)
	}
	return nil
}

DefaultKeyValidator applies DefaultKeyPattern. JSONLEventBackend performs an additional non-replaceable filesystem-containment check.

type SessionInfo

type SessionInfo struct {
	Key        string    `json:"key"`
	LastSeq    HostSeq   `json:"last_seq"`
	RecordedAt time.Time `json:"recorded_at"`
}

SessionInfo summarizes one recorded session for recent-session listings.

Jump to

Keyboard shortcuts

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