Documentation
¶
Overview ¶
Package eventsource provides a REFERENCE implementation of the event-sourced rehydration path for a host whose system of record is an append-only EVENT LOG (rather than mecatl's own snapshot store): Fold reconstructs a *session.Session by folding a port.EventLog stream back into the aggregate.
WHY THIS EXISTS. mecatl persists a session via SNAPSHOTS (engine/adapter/sessnap, the JSON DTO the store adapters round-trip), and its OWN resume always reloads from that snapshot. But the durable EventLog (ADR 0027 Phase 3a) records the FULL chronological timeline — the same events a relay emits — and a host that already keeps an append-only event log as its system of record (a downstream consumer) would rather implement port.SessionStore.Load by folding its own event stream into a Session than maintain a parallel snapshot. ADR 0027 shipped the durable RECORDING of that stream (List-2 row 11) but left the RECONSTRUCTION direction as snapshot-only; this package is the documented, reference-implemented shape that closes that deferral (ADR 0038).
WHAT FOLD RECONSTRUCTS — AND WHAT IT CANNOT. A pure event fold rebuilds the STRUCTURAL conversation faithfully: the assistant/tool message sequence, every assistant text + tool-call, every tool result, paired so the history is provider-replayable (ValidateToolPairing passes). It derives cumulative Usage, the lifecycle State/stop, the Counters, and a trailing pending ask.
It is byte-identical-REPLAY-faithful, however, ONLY for providers that do not use the opaque assistant-message replay fields — because those fields NEVER cross the event stream:
- Message.Reasoning (the provider reasoning REPLAY blob)
- Message.ProviderPhase (the OpenAI Responses phase marker)
- Message.ReasoningItemID (the provider reasoning-item id)
- ToolCall.ItemID (the provider item id)
reach the conversation ONLY via Session.RecordAssistant in the loop, never via an emitted Event. (EvReasoningDelta carries a human-readable reasoning SUMMARY, which the loop deliberately never places on Message.Reasoning — so a fold MUST NOT either.) A reconstructed Session is therefore a faithful structural conversation, and a byte-identical replay only for providers that leave those four fields empty (e.g. mockllm, a plain chat model). For OpenAI/Anthropic reasoning models the snapshot (which carries them) is the byte-identical path, which is why mecatl's own resume uses the snapshot; the fold is for event-log-SoR hosts that accept (or themselves carry, in a richer event schema) this contract boundary. This is a DOCUMENTED CONTRACT LIMITATION (engine/COMPATIBILITY.md, ADR 0038), not a bug.
CREATION METADATA is supplied via SessionMeta: the id, mode, limits, workspace, profile, provider/model selector, reasoning effort, authoritative title/provenance, kind/relationship, adoption source/request digest, and createdAt are facts that NO event carries, so the caller (who created or discovered the session and thus knows them) provides them alongside the stream. A legacy empty title falls back to the first genuine EvUserPrompt. There is deliberately no EvSessionCreated event (ADR 0038 records that as a possible future).
USER MESSAGES: the loop emits a log-only EvUserPrompt at every site it records a user-role message — the genuine client prompt AND the harness-authored synthetic continuations (no-progress nudge, background-pending nudge, background-completion notice) — so the fold reconstructs user turns in stream order, and the pre-compaction span recovered from EvCompactionArchive carries its user messages verbatim. The reconstructed conversation is therefore COMPLETE except the provider-private replay fields above. (Project-instruction messages discovered at turn 0 — AGENTS.md/CLAUDE.md — are NOT event-carried; they are derivable from the workspace and are out of the conversation the fold rebuilds.)
This is an EXCLUDED reference adapter (engine/COMPATIBILITY.md): it carries no public-API stability promise and is not part of the guarded core surface.
Index ¶
Constants ¶
This section is empty.
Variables ¶
var ( // ErrStream is returned when the event iterator yields an error; it wraps the // underlying per-item error. ErrStream = errors.New("eventsource: event stream error") // ErrReconstruct is returned when the folded events cannot be reconstructed into // a valid Session (an unpairable conversation, an inconsistent terminal state). ErrReconstruct = errors.New("eventsource: cannot reconstruct session") )
Errors returned by Fold.
Functions ¶
func Fold ¶
Fold reconstructs a *session.Session by folding the durable event stream back into the aggregate, supplemented by the creation metadata in meta. It is the reference implementation of an event-sourced port.SessionStore.Load.
The returned Session has its Conversation, Counters, cumulative Usage, lifecycle State, recorded stop reason, and trailing pending ask reconstructed from the stream. See the package doc for the replay-fidelity limitation (Reasoning / ProviderPhase / ReasoningItemID / ItemID are not event-carried).
It returns ErrStream (wrapping the per-item error) if the iterator yields an error, and ErrReconstruct if the reconstructed history is not provider-replayable or the derived state is inconsistent. It never panics.
Types ¶
type SessionMeta ¶
type SessionMeta struct {
// ID is the session id (session.New's first argument).
ID session.SessionID
// Mode is the permission posture.
Mode session.PermissionMode
// Limits are the configured stop conditions.
Limits session.Limits
// Workspace is the root directory tools operate against.
Workspace string
// Profile is the opaque tool-surface profile label ("" = default).
Profile string
// ProviderID and ModelID are the opaque neutral provider+model selector pair
// ("" / "" = server default).
ProviderID string
ModelID string
// ReasoningEffort is the opaque neutral reasoning-effort token (ADR 0055), ""
// when unset. Opaque to the domain; carried so the rehydrated session re-mints
// the same-effort per-session engine via the factory.
ReasoningEffort string
// DebugMCPServers and DebugMCPTools are the durable selected global MCP names
// and exact direct-tool ceiling for a debug session.
DebugMCPServers []string
DebugMCPTools []string
DebugTargetFingerprint string
// Title and TitleProvenance are authoritative creation/discovery metadata when
// supplied. A legacy empty title is derived from the first genuine user event.
Title string
TitleProvenance session.TitleProvenance
// Kind and Relationship are the trusted producer taxonomy supplied alongside
// the event stream. An empty kind is legacy and folds to unknown.
Kind session.SessionKind
Relationship session.SessionRelationship
// Incarnation and Owner are immutable creation identity. Empty Incarnation is
// legacy metadata and folds to the deterministic prefix-disjoint legacy token.
Incarnation session.IncarnationID
Owner *session.Principal
// Authority is the plain derived-capability payload supplied with creation
// metadata. Nil is a documented pre-feature legacy record; a present payload
// is validated and bound before reconstruction proceeds.
Authority *session.Authority
// AdoptionSourceID and AdoptionRequestDigest are the immutable legacy-session
// adoption proof. They mirror sessnap's flat AdoptionMetadata fields because
// events do not carry creation metadata.
AdoptionSourceID session.SessionID
AdoptionRequestDigest string
// CreatedAt is the creation timestamp.
CreatedAt time.Time
}
SessionMeta carries the creation facts that NO event in the stream records, so a fold can construct the aggregate. The caller — the event-log-SoR host, which created the session and therefore knows these — supplies them alongside the stream. They mirror the inert creation labels sessnap restores by direct assignment (Profile / ProviderID / ModelID are opaque to the domain).