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 ¶
- func GenerateTurnID() string
- func WithThreadClock(c Clock) func(*Thread)
- type AddTurnPayload
- type Clock
- type ClockFunc
- type FileRepository
- func (r *FileRepository) HydrateThread(_ context.Context, threadID string) (map[string]*Turn, string, error)
- func (r *FileRepository) SaveTurn(_ context.Context, threadID string, turn *Turn) error
- func (r *FileRepository) UpdateThreadTip(_ context.Context, threadID, turnID string) error
- func (r *FileRepository) UpdateTurnControl(_ context.Context, threadID, turnID string, control TraversalControl) error
- func (r *FileRepository) UpdateTurnParent(_ context.Context, threadID, turnID, parentID string) error
- type JournalEntry
- type MemoryRepository
- func (r *MemoryRepository) HydrateThread(_ context.Context, threadID string) (map[string]*Turn, string, error)
- func (r *MemoryRepository) Journal(threadID string) []JournalEntry
- func (r *MemoryRepository) SaveTurn(_ context.Context, threadID string, turn *Turn) error
- func (r *MemoryRepository) UpdateThreadTip(_ context.Context, threadID, turnID string) error
- func (r *MemoryRepository) UpdateTurnControl(_ context.Context, threadID, turnID string, control TraversalControl) error
- func (r *MemoryRepository) UpdateTurnParent(_ context.Context, threadID, turnID, parentID string) error
- type Meta
- type Metadata
- type Repository
- type Role
- type State
- type Thread
- func (t *Thread) AllTurns() []Turn
- func (t *Thread) Append(role Role, artifacts ...artifact.Artifact)
- func (t *Thread) Meta() Meta
- func (t *Thread) ResolveActivePath() []Turn
- func (t *Thread) SaveTurn(turn *Turn)
- func (t *Thread) SetControl(turnID string, control TraversalControl)
- func (t *Thread) SetCurrentTip(turnID string)
- func (t *Thread) SetParent(turnID, parentID string)
- func (t *Thread) Turns() []Turn
- type TraversalControl
- type Turn
- type TxType
- type UpdateControlPayload
- type UpdateParentPayload
- type UpdateTipPayload
- type View
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
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
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 ¶
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
ClockFunc adapts a plain function to the Clock interface. Useful in tests that need to stamp turns with a fixed or deterministic time.
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
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
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 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 ¶
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.
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
NewThread constructs an empty Thread with the default clock. Pass WithThreadClock to substitute.
func (*Thread) AllTurns ¶ added in v1.0.0
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
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
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
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
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
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
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
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 ¶
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
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.
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.