history

package
v0.21.0 Latest Latest
Warning

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

Go to latest
Published: Sep 12, 2026 License: Apache-2.0 Imports: 12 Imported by: 0

Documentation

Overview

Package history defines provider-neutral conversation history contracts and model middleware.

Reader, Writer, Clearer, and Store use core/chat protocol values directly. WindowStore is an explicit read-side retention decorator. It merges system messages and retains only complete user-led turns, so assistant Tool calls, Tool results, reasoning, and final text cannot be split at the read boundary. Optional cross-conversation and replacement capabilities remain separate interfaces. The zero-value-ready reference implementation lives in core/history/inmemory.

Conversation IDs are runtime scope carried with WithConversationID, not serialized request metadata. Middleware binds that scope to model calls.

Writes preserve message order within one call. Conversation listing is an optional capability and returns unique IDs in lexical order. Concurrent writes and writes through distinct Store instances have no common ordering guarantee unless a backend documents one. Writer returns WriteOutcome even on failure; errors never silently mean that no messages were stored. A CommitError identifies history failure after model completion and preserves the attempted messages and write outcome.

The Host owns conversation turn ordering. Concurrent-safe store methods do not isolate the whole Read/model/Write sequence; multi-instance hosts must coordinate that sequence across instances if they require serial turns.

Persistent backends live in independent leaf modules so database drivers do not enter Core:

historystores/postgres/  — PostgreSQL (pgx + JSONB)
historystores/redis/     — Redis (RPUSH / LRANGE lists)
historystores/mongodb/   — MongoDB (document per message)
historystores/cassandra/ — Cassandra (TIMEUUID clustering key)
historystores/neo4j/     — Neo4j (node per message)
historystores/cosmosdb/  — Azure Cosmos DB (NoSQL API)

Every backend reads and writes only the current core/chat tagged JSON wire.

Example
package main

import (
	"fmt"

	"github.com/Tangerg/scope/core/history"
)

func main() {
	conversationID, err := history.NewConversationID("customer-42")
	if err != nil {
		panic(err)
	}
	fmt.Println(conversationID)
}
Output:
customer-42

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	ErrInvalidWindow  = errors.New("history: invalid message window")
	ErrWindowTooSmall = errors.New("history: message window too small")
)
View Source
var ErrInvalidConversationID = errors.New("history: invalid conversation ID")
View Source
var ErrInvalidWriteOutcome = errors.New("history: invalid write outcome")

ErrInvalidWriteOutcome identifies a Writer that returned contradictory facts.

View Source
var ErrNilStore = errors.New("history: nil store")
View Source
var ErrNilStream = errors.New("history: middleware: nil stream sequence")

ErrNilStream reports a Streamer contract violation before any delta is consumed.

Functions

func WithConversationID

func WithConversationID(ctx context.Context, conversationID ConversationID) context.Context

WithConversationID returns a child context carrying the history partition key for one model call. An empty ID deliberately shadows and disables an ID inherited from a parent context. As with context.WithValue, ctx must not be nil.

Types

type Clearer

type Clearer interface {
	// Clear removes the complete conversation and is idempotent when it is
	// already absent. Implementations must honor ctx.
	Clear(ctx context.Context, conversationID ConversationID) error
}

Clearer removes every message for one conversation.

type CommitError added in v0.20.0

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

CommitError means model generation completed but history persistence failed. It owns the exact batch submitted to Write, so recovery need not call the model again. Outcome governs whether a suffix is safe to submit: uncertain writes require reconciliation, and Accepted messages must not be appended again. During streaming this error can follow the terminal model delta.

func (*CommitError) ConversationID added in v0.20.0

func (c *CommitError) ConversationID() ConversationID

func (*CommitError) Error added in v0.20.0

func (c *CommitError) Error() string

func (*CommitError) Messages added in v0.20.0

func (c *CommitError) Messages() []chat.Message

Messages returns an independently owned copy of the complete attempted batch.

func (*CommitError) Outcome added in v0.20.0

func (c *CommitError) Outcome() WriteOutcome

func (*CommitError) Unwrap added in v0.20.0

func (c *CommitError) Unwrap() error

type ConversationID

type ConversationID string

ConversationID identifies one history partition. Its zero value is invalid. Use NewConversationID when converting runtime input; string constants may be converted directly when the value is known at compile time.

func ConversationIDFromContext

func ConversationIDFromContext(ctx context.Context) (ConversationID, bool)

ConversationIDFromContext returns the ID carried by ctx. Empty values behave as absent so middleware can transparently skip history for unbound calls.

func NewConversationID

func NewConversationID(value string) (ConversationID, error)

NewConversationID makes the identifier a validated type so a raw, unchecked string cannot reach a store boundary. Product identity stays with the host; this type only guarantees the value is usable as a key.

func (ConversationID) String

func (c ConversationID) String() string

func (ConversationID) Validate

func (c ConversationID) Validate() error

type Lister

type Lister interface {
	// Conversations returns detached, unique identifiers in lexical order. An
	// empty store yields a non-nil empty slice; concurrent writes may appear or
	// not according to the backend's snapshot boundary.
	Conversations(ctx context.Context) ([]ConversationID, error)
}

Lister enumerates unique conversation IDs in lexical order. Implementations return a non-nil empty slice when no conversations exist. Concurrent mutations may affect the result.

type Middleware

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

Middleware replays and persists history around synchronous and streaming chat capabilities. It is immutable after construction and safe for concurrent use when its Store is safe for concurrent use. This does not serialize complete Read/model/Write turns for the same conversation. The Host must serialize those turns across all instances if ordering is required; different conversations may proceed concurrently.

func NewMiddleware

func NewMiddleware(store ReadWriter) (Middleware, error)

NewMiddleware requires a ReadWriter because history is only useful when both halves cross the same boundary: it must read prior turns into the request and write the new ones back. Splitting them would let a caller wire a reader against a different store than the writer.

func (Middleware) Call

func (m Middleware) Call(next chat.Model) chat.Model

Call is a chat.CallMiddleware. The response result is the canonical assistant message persisted to history.

func (Middleware) Stream

func (m Middleware) Stream(next chat.Streamer) chat.Streamer

Stream is a chat.StreamMiddleware. History I/O remains lazy: no read occurs until the returned sequence is iterated. Fresh input and the accumulated assistant response are persisted only after natural, error-free completion.

type ReadWriter

type ReadWriter interface {
	Reader
	Writer
}

ReadWriter combines the capabilities required by components that replay and append history without owning retention or deletion policy.

type Reader

type Reader interface {
	// Read returns a detached snapshot in stored order. Unknown conversations
	// yield a non-nil empty slice; implementations honor ctx and never expose
	// mutable backing storage.
	Read(ctx context.Context, conversationID ConversationID) ([]chat.Message, error)
}

Reader returns the messages to replay for one conversation. Implementations return a non-nil empty slice for an unknown conversation and transfer ownership of returned protocol values to the caller.

type Sequence added in v0.16.0

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

Sequence assigns the positions a store writes alongside its messages.

Writer promises that one Write reads back in argument order, and a store that derives positions from the wall clock cannot keep that promise by itself: a clock that steps backward — an NTP correction, a suspended host — would place a later batch before an earlier one, and messages inside a single batch can land on the same instant however fine the clock's resolution. A Sequence hands out a contiguous, strictly increasing run per call and never reissues a position it has already given out, which is what makes the ordering the contract advertises hold.

A Sequence orders the writes made through one value. Ordering across separate Store instances stays implementation-defined, as Writer says.

func NewSequence added in v0.16.0

func NewSequence(stride time.Duration) (*Sequence, error)

NewSequence spaces consecutive positions stride apart.

Pass one nanosecond unless the store's position type is coarser than the clock: a Cassandra TIMEUUID advances in 100-nanosecond ticks, so reserving at nanosecond spacing there would let distinct positions collapse onto one identifier.

func (*Sequence) Reserve added in v0.16.0

func (s *Sequence) Reserve(count int) int64

Reserve returns the first position of a contiguous run of count positions, each one stride after the last. Successive calls strictly increase even when the wall clock does not.

A count below one is a caller bug rather than a runtime condition — every store reserves the length of a batch it has already refused to build empty — so it panics instead of widening every call site with an unreachable branch.

type Store

type Store interface {
	ReadWriter
	Clearer
}

Store is the ordinary per-conversation read/write/clear contract. Optional cross-conversation or retention capabilities remain separate interfaces.

type WindowStore

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

WindowStore projects reads to at most limit messages while preserving a merged system message followed by a suffix of complete conversation turns. A user message starts a turn; every following assistant and tool message remains in that turn until the next user message. Writes and clears pass through to the authoritative Store. Read returns ErrWindowTooSmall rather than splitting the newest complete turn, and the merged system message counts toward the configured limit.

func NewWindowStore

func NewWindowStore(store Store, limit int) (WindowStore, error)

NewWindowStore projects reads to a bounded suffix of complete turns rather than a message count, because cutting mid-turn strands a tool result from the call that produced it and breaks their protocol relationship. Writes still reach the authoritative store, so windowing narrows what a model sees without discarding history.

func (WindowStore) Clear

func (w WindowStore) Clear(ctx context.Context, conversationID ConversationID) error

func (WindowStore) Read

func (w WindowStore) Read(ctx context.Context, conversationID ConversationID) ([]chat.Message, error)

func (WindowStore) Write

func (w WindowStore) Write(ctx context.Context, conversationID ConversationID, messages ...chat.Message) (WriteOutcome, error)

type WriteOutcome added in v0.20.0

type WriteOutcome struct {
	Accepted  int
	Uncertain bool
}

WriteOutcome describes the acknowledged facts of one Write attempt. It never implies idempotency or rollback. The zero value confirms that nothing was written. Accepted counts the confirmed prefix in argument order. Uncertain means additional messages after that prefix may have been stored; the caller must reconcile them before retrying. Otherwise all remaining messages are confirmed unwritten. All messages may be accepted even if ancillary work fails.

func (WriteOutcome) Validate added in v0.20.0

func (w WriteOutcome) Validate(count int, err error) error

Validate checks the outcome against the attempted batch size and error. Successful calls must acknowledge the full batch without uncertainty.

type Writer

type Writer interface {
	// Write validates and snapshots the full argument batch before appending it
	// in argument order. The outcome must account for every acknowledged or
	// uncertain effect even on error; validation failures return the zero outcome.
	// A nil error acknowledges the complete batch.
	Write(ctx context.Context, conversationID ConversationID, messages ...chat.Message) (WriteOutcome, error)
}

Writer appends messages to one conversation. Implementations preserve the order of messages within each call, validate and snapshot them before returning, and prevent later caller mutation from altering stored history. The relative order of concurrent calls and writes issued through distinct Store instances is implementation-defined.

Directories

Path Synopsis
Package inmemory provides a zero-value-ready in-process history store.
Package inmemory provides a zero-value-ready in-process history store.
Package storetest provides reusable conformance checks for history stores.
Package storetest provides reusable conformance checks for history stores.

Jump to

Keyboard shortcuts

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