ledger

package
v1.2.3 Latest Latest
Warning

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

Go to latest
Published: Jul 15, 2026 License: AGPL-3.0 Imports: 15 Imported by: 0

Documentation

Overview

Package ledger defines the State interface and supporting types for ore's conversation history model.

State is a mutable interface: Append() mutates in place. Turns() returns the active path through the conversation tree (see Thread.ResolveActivePath) as a defensive copy so providers can safely iterate without synchronization. The in-memory implementation (Thread) is intentionally not goroutine-safe; concurrency control is a future middleware concern.

Package ledger defines the State interface and supporting types for ore's conversation history model.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func GenerateTurnID added in v1.0.0

func GenerateTurnID() string

GenerateTurnID returns a 16-character hex string drawn from a cryptographically random source. Sufficient for uniqueness within a single thread without depending on an external UUID library.

Exported so packages that build ephemeral Thread instances from raw Turn inputs (e.g. compaction) can assign IDs to turns that arrive without one, preserving the order-preserving semantics of loading a turn slice into a state implementation.

func WithThreadClock added in v1.0.0

func WithThreadClock(c Clock) func(*Thread)

WithThreadClock configures Thread to use a custom Clock for turn timestamps. If not used, Thread defaults to the real wall-clock time.

Types

type AddTurnPayload added in v1.0.0

type AddTurnPayload struct {
	Timestamp time.Time `json:"timestamp"`
	Turn      Turn      `json:"turn"`
}

AddTurnPayload is the payload of a TxAddTurn transaction. It captures a single Turn and the timestamp the transaction was recorded (which may differ from the turn's Timestamp if the clock is changed at runtime).

type Clock

type Clock interface {
	Now() time.Time
}

Clock provides the current time for a Thread (or any State implementation) to set turn timestamps. Implementations can be swapped for deterministic testing.

type ClockFunc added in v1.2.0

type ClockFunc func() time.Time

ClockFunc adapts a plain function to the Clock interface. Useful in tests that need to stamp turns with a fixed or deterministic time.

func (ClockFunc) Now added in v1.2.0

func (f ClockFunc) Now() time.Time

Now implements Clock.

type FileRepository added in v1.0.0

type FileRepository struct {
	// contains filtered or unexported fields
}

FileRepository is the default Repository implementation. Each thread is persisted as a single `.jsonl` file in a directory: `<dir>/<thread-id>.jsonl`. Each line is one JournalEntry.

FileRepository uses O_APPEND for atomic appends within a single process (POSIX guarantees that small writes to an append-only file are atomic on local filesystems). It is single-writer per thread: concurrent Save/Update calls from multiple goroutines on the same thread are serialized by a per-thread mutex.

FileRepository is not safe across multiple processes. Cross-process safety would require file locking (flock), which is out of scope for v1.

func NewFileRepository added in v1.0.0

func NewFileRepository(dir string) (*FileRepository, error)

NewFileRepository constructs a FileRepository backed by the given directory. The directory is created if it does not exist. Thread files are stored as `<dir>/<thread-id>.jsonl`.

func (*FileRepository) HydrateThread added in v1.0.0

func (r *FileRepository) HydrateThread(_ context.Context, threadID string) (map[string]*Turn, string, error)

HydrateThread reads the thread's journal file line-by-line and replays it to reconstruct the in-memory state. A malformed line (truncated write, garbage) stops replay; the thread's state reflects the last well-formed entry.

If the journal file does not exist, the result is an empty turns map and an empty current tip (no error).

func (*FileRepository) SaveTurn added in v1.0.0

func (r *FileRepository) SaveTurn(_ context.Context, threadID string, turn *Turn) error

SaveTurn appends a TxAddTurn entry to the thread's journal.

func (*FileRepository) UpdateThreadTip added in v1.0.0

func (r *FileRepository) UpdateThreadTip(_ context.Context, threadID, turnID string) error

UpdateThreadTip appends a TxUpdateTip entry.

func (*FileRepository) UpdateTurnControl added in v1.0.0

func (r *FileRepository) UpdateTurnControl(_ context.Context, threadID, turnID string, control TraversalControl) error

UpdateTurnControl appends a TxUpdateControl entry.

func (*FileRepository) UpdateTurnParent added in v1.0.0

func (r *FileRepository) UpdateTurnParent(_ context.Context, threadID, turnID, parentID string) error

UpdateTurnParent appends a TxUpdateParent entry.

type JournalEntry added in v1.0.0

type JournalEntry struct {
	TxType    TxType          `json:"tx_type"`
	Timestamp time.Time       `json:"timestamp"`
	Payload   json.RawMessage `json:"payload"`
}

JournalEntry is the on-disk shape of a single transaction.

Each line in a thread's .jsonl file is one JournalEntry. The Payload is a type-specific struct encoded as encoding/json.RawMessage; the concrete shape is selected by TxType.

type MemoryRepository added in v1.0.0

type MemoryRepository struct {
	// contains filtered or unexported fields
}

MemoryRepository is an in-memory implementation of Repository. It holds each thread's journal as a slice of JournalEntry in append order and replays the slice to reconstruct state.

MemoryRepository is safe for concurrent use across goroutines; the underlying append is guarded by a per-thread mutex. This is the only Repository implementation that supports concurrent writers.

MemoryRepository is intended primarily for tests and ephemeral use; production code should use FileRepository.

func NewMemoryRepository added in v1.0.0

func NewMemoryRepository() *MemoryRepository

NewMemoryRepository constructs an empty MemoryRepository.

func (*MemoryRepository) HydrateThread added in v1.0.0

func (r *MemoryRepository) HydrateThread(_ context.Context, threadID string) (map[string]*Turn, string, error)

HydrateThread replays the thread's journal from top to bottom to reconstruct its state. A malformed entry stops the replay; the thread's state reflects the last well-formed entry.

If the thread has no journal entries, the result is an empty turns map and an empty current tip.

func (*MemoryRepository) Journal added in v1.0.0

func (r *MemoryRepository) Journal(threadID string) []JournalEntry

Journal returns a copy of the journal entries for the given thread. Useful for tests; not part of the Repository interface.

func (*MemoryRepository) SaveTurn added in v1.0.0

func (r *MemoryRepository) SaveTurn(_ context.Context, threadID string, turn *Turn) error

SaveTurn appends a TxAddTurn entry to the thread's journal.

func (*MemoryRepository) UpdateThreadTip added in v1.0.0

func (r *MemoryRepository) UpdateThreadTip(_ context.Context, threadID, turnID string) error

UpdateThreadTip appends a TxUpdateTip entry to the thread's journal.

func (*MemoryRepository) UpdateTurnControl added in v1.0.0

func (r *MemoryRepository) UpdateTurnControl(_ context.Context, threadID, turnID string, control TraversalControl) error

UpdateTurnControl appends a TxUpdateControl entry to the thread's journal.

func (*MemoryRepository) UpdateTurnParent added in v1.0.0

func (r *MemoryRepository) UpdateTurnParent(_ context.Context, threadID, turnID, parentID string) error

UpdateTurnParent appends a TxUpdateParent entry to the thread's journal.

type Meta

type Meta interface {
	// Get returns the value stored for key and a boolean indicating
	// whether the key was present.
	Get(key string) (string, bool)

	// Set stores value under key, replacing any prior value.
	Set(key, value string)

	// All returns a defensive copy of the metadata map. Mutating
	// the returned map does not affect the underlying ledger.
	All() map[string]string
}

Meta is the metadata context carried by a State. It mirrors the shape of context.Context but is keyed on string identifiers and is mutable: a conversation's metadata grows over time as turns are appended and processors add facts about the conversation as a whole (e.g. future checkpoint markers).

Meta values are produced by State.Meta; they are not constructed directly. Each call to State.Meta returns a handle backed by the same underlying storage, so writes made through one handle are visible through any other handle for the same State.

Concurrency: like State itself (and its in-memory implementation Thread), Meta is not safe for concurrent use. The framework's serial pipeline — the loop worker goroutine, the Transform chain, the session's worker — is the only contract; concurrent access from outside that pipeline is a future middleware concern.

type Metadata added in v1.0.0

type Metadata struct {
	// Control is a traversal directive interpreted by
	// [Thread.ResolveActivePath]. The empty string is treated as
	// [ControlContinue].
	Control TraversalControl `json:"control,omitempty"`
}

Metadata carries per-turn control state. Future turn-level facts (provenance, attribution, retention) extend this struct additively.

type Repository added in v1.0.0

type Repository interface {
	// SaveTurn appends a TxAddTurn transaction to the thread's journal.
	// The turn's ID must be unique within the thread.
	SaveTurn(ctx context.Context, threadID string, turn *Turn) error

	// UpdateThreadTip appends a TxUpdateTip transaction that sets
	// the thread's CurrentTip to the given turn ID.
	UpdateThreadTip(ctx context.Context, threadID, turnID string) error

	// UpdateTurnControl appends a TxUpdateControl transaction that
	// sets the turn's Metadata.Control to the given value.
	UpdateTurnControl(ctx context.Context, threadID, turnID string, control TraversalControl) error

	// UpdateTurnParent appends a TxUpdateParent transaction that
	// re-parents the turn. The new parent ID is stored verbatim.
	UpdateTurnParent(ctx context.Context, threadID, turnID, parentID string) error

	// HydrateThread reconstructs the thread's in-memory state by
	// replaying its journal from top to bottom. Returns the turns
	// map and the latest current tip pointer.
	//
	// A malformed line (truncated last write, unrecognized payload)
	// is tolerated by stopping replay at that point: the thread's
	// state reflects the last well-formed entry. The error return is
	// reserved for hard failures (file not readable, permission
	// denied, etc.).
	HydrateThread(ctx context.Context, threadID string) (turns map[string]*Turn, currentTip string, err error)
}

Repository persists thread state as an append-only journal of JournalEntry records. Implementations are single-writer: concurrent calls to Save* / Update* from multiple goroutines are not safe. The framework's serial pipeline is the contract.

Implementations decide on the physical storage layout (e.g. one file per thread, one global file with thread_id in each entry, an in-memory store for tests). The interface makes no commitment to the wire format; only the journal entry shape is specified.

type Role

type Role string

Role represents the role of a participant in a conversation turn.

const (
	RoleSystem    Role = "system"
	RoleUser      Role = "user"
	RoleAssistant Role = "assistant"
	RoleTool      Role = "tool"
)

type State

type State interface {
	// Turns returns the active path through the conversation history
	// (in chronological order). For [Thread] this is the result of
	// [Thread.ResolveActivePath].
	Turns() []Turn

	// Append adds a new turn to the ledger. It mutates in place.
	Append(role Role, artifacts ...artifact.Artifact)

	// Meta returns the metadata context for this ledger. The handle
	// is live — writes propagate to the underlying state and are
	// visible to subsequent reads. See the [Meta] contract for
	// the serialization format and concurrency expectations.
	Meta() Meta
}

State is a mutable conversation state that the core loop appends to.

Thread is the canonical implementation; a tree-backed type whose active path is resolved by Thread.ResolveActivePath.

func NewView

func NewView(base State, turns []Turn) State

NewView creates a state wrapper that returns the given turns from Turns() and delegates Append() to the base ledger. If turns is empty or nil, it returns the base state directly as an identity optimization.

func Prepend

func Prepend(base State, virtual []Turn) State

Prepend returns a State view that projects virtual turns before the base state's current turns on every call to Turns(). Append() delegates to the base ledger. If virtual is empty, it returns the base state directly as an identity optimization.

type Thread added in v1.0.0

type Thread struct {

	// CurrentTip selects the active branch. Walking back from this
	// turn's [Turn.ParentID] chain produces [Thread.Turns].
	CurrentTip string
	// contains filtered or unexported fields
}

Thread is the tree-backed implementation of State.

A Thread owns a set of turns (the tree) plus a current-tip pointer identifying which branch is "active". The active path is computed on demand by Thread.ResolveActivePath, which walks back from the current tip via Turn.ParentID, applying each turn's Metadata.Control directive.

Thread identity (the address by which a conversation is found) is not stored on the Thread. It is held by the Session that owns the Thread and used as a key into the journal repository. See Repository for the persistence contract.

Thread is not safe for concurrent use. The framework's serial pipeline — the loop worker goroutine, the Transform chain, the session's worker — is the only contract.

func NewThread added in v1.0.0

func NewThread(opts ...func(*Thread)) *Thread

NewThread constructs an empty Thread with the default clock. Pass WithThreadClock to substitute.

func (*Thread) AllTurns added in v1.0.0

func (t *Thread) AllTurns() []Turn

AllTurns returns a snapshot of every turn in the thread (not just the active path). The slice order is unspecified. Use this for serialization paths that need to preserve every branch; for the LLM-facing view, use Thread.Turns (or Thread.ResolveActivePath).

func (*Thread) Append added in v1.0.0

func (t *Thread) Append(role Role, artifacts ...artifact.Artifact)

Append adds a new turn to the thread, advancing the current tip. The new turn's ParentID is set to the previous current tip (or "" if the thread was empty), and the current tip advances to the new turn's ID. The new turn's ID is generated from a cryptographically random source.

func (*Thread) Meta added in v1.0.0

func (t *Thread) Meta() Meta

Meta returns the metadata handle for this thread. The handle is live: reads and writes operate on the thread's internal map. Multiple calls return handles that share the same backing storage, so writes through one are visible through any other.

As with the rest of Thread's API, the Meta handle is not safe for concurrent use; the same serial-call contract applies.

func (*Thread) ResolveActivePath added in v1.0.0

func (t *Thread) ResolveActivePath() []Turn

ResolveActivePath walks the tree from Thread.CurrentTip backwards via Turn.ParentID and returns the turns in chronological order, honoring each turn's Metadata.Control directive:

  • ControlContinue (or empty): include the turn, continue the walk.
  • ControlStop: include the turn, terminate the walk.
  • ControlSkip: exclude the turn, continue the walk.

If CurrentTip is "" (empty thread), the result is empty.

If a turn's ParentID references a non-existent turn (broken tree), the walk terminates at that gap. If a turn in the chain is missing from the thread, the walk terminates at the gap.

func (*Thread) SaveTurn added in v1.0.0

func (t *Thread) SaveTurn(turn *Turn)

SaveTurn inserts (or replaces) a turn in the thread without advancing the current tip. Use this when hydrating from a journal or restoring from persisted state. The caller is responsible for ensuring the turn's ID is unique within the thread.

func (*Thread) SetControl added in v1.0.0

func (t *Thread) SetControl(turnID string, control TraversalControl)

SetControl updates a turn's Metadata.Control directive. This is the in-memory equivalent of Repository.UpdateTurnControl.

If turnID does not exist in the thread, SetControl is a no-op.

func (*Thread) SetCurrentTip added in v1.0.0

func (t *Thread) SetCurrentTip(turnID string)

SetCurrentTip sets the active branch pointer. Use this after hydrating, when forking to switch branches, or when re-anchoring the walk after a re-parent.

func (*Thread) SetParent added in v1.0.0

func (t *Thread) SetParent(turnID, parentID string)

SetParent re-parents a turn, updating the in-memory tree structure. This is the in-memory equivalent of Repository.UpdateTurnParent; the repository persists it as a journal event.

If turnID does not exist in the thread, SetParent is a no-op. The new parent ID is stored verbatim; the caller is responsible for ensuring the target exists if chain consistency matters.

func (*Thread) Turns added in v1.0.0

func (t *Thread) Turns() []Turn

Turns returns the active-path projection of the tree: the turns reachable by walking back from Thread.CurrentTip via Turn.ParentID, applying each turn's Metadata.Control directive.

The result is in chronological order (root-to-tip). A defensive copy is returned so callers can iterate without affecting the underlying tree.

type TraversalControl added in v1.0.0

type TraversalControl string

TraversalControl directs how a turn participates in the active-path resolution walk. It is part of Turn.Metadata and is interpreted by Thread.ResolveActivePath.

const (
	// ControlContinue (the zero value) includes the turn in the active
	// path and continues tracing backward through the parent's ancestry.
	ControlContinue TraversalControl = "continue"

	// ControlStop includes the turn in the active path but immediately
	// terminates backward traversal. Use it for compaction summary turns
	// that absorb everything that came before them.
	ControlStop TraversalControl = "stop"

	// ControlSkip excludes the turn from the active path while continuing
	// to trace its ancestry. Use it to hide heavy tool results or
	// to prune intermediate logs without breaking chain connectivity.
	ControlSkip TraversalControl = "skip"
)

type Turn

type Turn struct {
	// ID is the unique identifier of this turn. Generated on Append
	// using a cryptographically random source. Stable across the
	// lifetime of the turn.
	ID string `json:"id"`

	// ParentID is the ID of the turn immediately preceding this one
	// on the chain. Empty for root turns.
	ParentID string `json:"parent_id,omitempty"`

	// Role is the speaker for this turn.
	Role Role `json:"role"`

	// Artifacts is the polymorphic list of content blocks produced
	// by the speaker. Wire adapters serialize per provider schema.
	Artifacts []artifact.Artifact `json:"-"` // overridden by custom UnmarshalJSON/MarshalJSON

	// Timestamp is when the turn was appended. Set by the [Thread]
	// from a configured clock; serializable but omitted
	// from JSON when zero.
	Timestamp time.Time `json:"timestamp,omitempty"`

	// Metadata carries per-turn control state (see [TraversalControl]).
	Metadata Metadata `json:"metadata,omitempty"`
}

Turn represents a single coordinate in the conversation tree.

A turn carries both tree mechanics (ID, ParentID) and consumer-facing content (Role, Artifacts, Timestamp). The persistent tree unit IS the consumer-facing projection; there is no separate "Node" type.

All turns except the root carry exactly one parent ID. The parent's ID is empty for root turns.

func (Turn) MarshalJSON

func (t Turn) MarshalJSON() ([]byte, error)

MarshalJSON implements json.Marshaler, omitting the zero timestamp and empty metadata. ID is always emitted because every persisted turn must be addressable. Artifacts are wrapped with their Kind tag for round-trip via the artifact registry.

func (*Turn) UnmarshalJSON added in v1.0.0

func (t *Turn) UnmarshalJSON(data []byte) error

UnmarshalJSON implements json.Unmarshaler. Artifacts are unwrapped from their kind-tagged envelope via the artifact registry; the rest of the fields follow the struct tags.

type TxType added in v1.0.0

type TxType string

TxType identifies the kind of mutation recorded in a JournalEntry. Four transaction types are supported:

  • TxAddTurn: a new turn is created in the thread.
  • TxUpdateTip: the active tip pointer is moved.
  • TxUpdateControl: a turn's Metadata.Control directive is changed.
  • TxUpdateParent: a turn's Turn.ParentID link is changed.

All transactions are append-only. Once written to the journal, the entry is never modified or reordered.

const (
	TxAddTurn       TxType = "add_turn"
	TxUpdateTip     TxType = "update_tip"
	TxUpdateControl TxType = "update_control"
	TxUpdateParent  TxType = "update_parent"
)

type UpdateControlPayload added in v1.0.0

type UpdateControlPayload struct {
	Timestamp time.Time        `json:"timestamp"`
	TurnID    string           `json:"turn_id"`
	Control   TraversalControl `json:"control"`
}

UpdateControlPayload is the payload of a TxUpdateControl transaction.

type UpdateParentPayload added in v1.0.0

type UpdateParentPayload struct {
	Timestamp time.Time `json:"timestamp"`
	TurnID    string    `json:"turn_id"`
	ParentID  string    `json:"parent_id"`
}

UpdateParentPayload is the payload of a TxUpdateParent transaction.

type UpdateTipPayload added in v1.0.0

type UpdateTipPayload struct {
	Timestamp  time.Time `json:"timestamp"`
	CurrentTip string    `json:"current_tip"`
}

UpdateTipPayload is the payload of a TxUpdateTip transaction.

type View

type View struct {
	// contains filtered or unexported fields
}

View is a general State wrapper that returns a caller-provided slice of turns from Turns() while delegating Append() to a base State. This enables arbitrary state projections (compaction, filtering, reordering) without mutating the persistent conversation buffer.

func (*View) Append

func (v *View) Append(role Role, artifacts ...artifact.Artifact)

Append delegates to the underlying base ledger.

func (*View) Meta

func (v *View) Meta() Meta

Meta delegates to the base ledger. Views do not own metadata; they are projections of turns. Writes through a View's Meta are visible on the base ledger.

func (*View) Turns

func (v *View) Turns() []Turn

Turns returns a defensive copy of the projected turns.

Jump to

Keyboard shortcuts

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