present

package
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Sep 4, 2026 License: MIT Imports: 7 Imported by: 0

Documentation

Overview

Package present records the presentation event stream — the rich, view-only view of a turn (tool dispatches with readOnly/profile/parentId, tool progress chunks, results with durationMs/truncated/attachments, notice/phase/compaction cards, expert-collab cards) — so a frontend can rebuild the exact transcript a user saw even after a tab switch, reload, or crash.

It exists because the durable session store (internal/agent Session.Save) only persists provider.Message — the model's view of the conversation. Everything that makes the UI informative (how long a tool took, whether its output was truncated, what notices fired, what a sub-agent did) is carried by event.Event and, before this package, lived only in the frontend's memory. Reloading from the durable store rebuilt a degraded transcript with those fields blank.

The sidecar is <session>.present.jsonl: one Record per line, rewritten in full on each save (mirroring Session.Save's rewrite strategy) and TRUNCATED on compaction to stay consistent with the post-compact provider.Message array — a user sees exactly what the model still remembers, nothing the model forgot. It never feeds the LLM; provider.Request is built solely from session.Messages.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func PresentPath

func PresentPath(sessionPath string) string

PresentPath returns the sidecar path for a session path: "<session>.jsonl" → "<session>.present.jsonl". For a session path that does not end in .jsonl it appends ".present.jsonl" anyway, so the sidecar stays a sibling.

Types

type Attachment

type Attachment struct {
	Path string `json:"path,omitempty"`
	Kind string `json:"kind,omitempty"`
}

Attachment mirrors event.Attachment (a tool-produced file) for JSON serialization.

type Collab

type Collab = json.RawMessage

Collab mirrors event.Collab (expert-team collaboration) for the collab cards. Stored as raw JSON so the package need not depend on the experts shape; the frontend already deserializes the same structure from the live stream.

type Compaction

type Compaction struct {
	Trigger  string `json:"trigger,omitempty"`
	Messages int    `json:"messages,omitempty"`
	Summary  string `json:"summary,omitempty"`
	Archive  string `json:"archive,omitempty"`
}

Compaction mirrors event.Compaction for the compaction cards.

type FileDiff

type FileDiff struct {
	Diff    string `json:"diff"`
	Added   int    `json:"added"`
	Removed int    `json:"removed"`
}

FileDiff mirrors event.FileDiff on a Tool record.

type Kind

type Kind string

Kind is the string tag of a Record. It mirrors event.Kind's meaningful subset — the kinds a frontend renders — as a stable string so the JSONL is self- describing and survives event.Kind reordering. Kinds that only drive live interaction (ApprovalRequest, AskRequest, TurnStarted/Done) are either omitted or recorded as plain markers (a turn boundary marker) since replaying them has no interactive effect.

const (
	KindTurnStarted       Kind = "turn_started"
	KindReasoning         Kind = "reasoning"
	KindText              Kind = "text"
	KindMessage           Kind = "message"
	KindToolDispatch      Kind = "tool_dispatch"
	KindToolResult        Kind = "tool_result"
	KindToolProgress      Kind = "tool_progress"
	KindUsage             Kind = "usage"
	KindNotice            Kind = "notice"
	KindPhase             Kind = "phase"
	KindCompactionStarted Kind = "compaction_started"
	KindCompactionDone    Kind = "compaction_done"
	KindRetrying          Kind = "retrying"
	KindSteer             Kind = "steer"
	KindPaused            Kind = "paused"
	KindResumed           Kind = "resumed"
	KindExpertCollab      Kind = "expert_collab"
)

type Profile

type Profile struct {
	Model  string `json:"model,omitempty"`
	Effort string `json:"effort,omitempty"`
}

Profile mirrors event.Profile (subagent model/effort).

type Record

type Record struct {
	Kind       Kind        `json:"kind"`
	Text       string      `json:"text,omitempty"`
	Reasoning  string      `json:"reasoning,omitempty"`
	Level      string      `json:"level,omitempty"` // "info"|"warn" for notice
	Tool       *Tool       `json:"tool,omitempty"`
	Usage      *Usage      `json:"usage,omitempty"`
	Compaction *Compaction `json:"compaction,omitempty"`
	Collab     Collab      `json:"collab,omitempty"`
	Retry      *Retry      `json:"retry,omitempty"`
}

Record is one presentation event, persisted as one JSONL line in the sidecar. Only the fields meaningful for Kind are populated; the rest are zero/omitted.

func FromEvent

func FromEvent(e event.Event) (Record, bool)

FromEvent projects the presentation-relevant fields of an event.Event onto a Record. Returns ok=false for kinds that have no presentation value (ApprovalRequest, AskRequest, TurnDone) — these drive live interaction and are not worth persisting. TurnStarted is recorded as a turn-boundary marker so the recorder can truncate by turn on compaction.

func Load

func Load(path string) ([]Record, int, error)

Load reads a sidecar produced by Save, returning the records and the rewriteVersion they were aligned to. A missing file returns (nil, 0, nil) — callers treat that as "no sidecar, fall back to degraded history".

type Recorder

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

Recorder accumulates the presentation records for one session and writes them as a sidecar. It is goroutine-safe. The zero value is a no-op recorder (all methods are safe to call); wire it via NewRecorder when a session path is known.

Consistency model: the recorder keeps the full record list in memory and rewrites the sidecar on Save (mirroring Session.Save's strategy). On compaction the controller calls TruncateToTurn, which drops records from completed turns the model has forgotten — so the sidecar and the post-compact provider.Message array describe the same turns. RewriteVersion is captured on each Save so a stale sidecar (from before a compaction that didn't yet flush) can be detected and discarded on load.

func NewRecorder

func NewRecorder() *Recorder

NewRecorder returns a ready recorder.

func (*Recorder) Append

func (r *Recorder) Append(e event.Event)

Append records one event if it has presentation value. Safe to call on a nil recorder (no-op). TurnStarted marks a new turn boundary the retention cap and any future turn-aligned truncation can cut against.

func (*Recorder) Records

func (r *Recorder) Records() []Record

lastCompactionEvent is held by the controller (it sees the live stream) and passed to SyncBeforeSave; this package does not retain events.// Records returns a snapshot of the current records (for in-process consumers like PresentForTab that can read from memory without hitting disk).

func (*Recorder) Reset

func (r *Recorder) Reset()

Reset drops all in-memory records and version state so the recorder can be re-seeded for a different session — the controller calls it when it rebinds to another session file (new session, clear, fork/branch switch, resume) and the accumulated records describe the OLD conversation. Without this, those records would leak into the new session's sidecar on the next Save, and an in-process reader (PresentRecords) would serve the old conversation's cards for the new session.

func (*Recorder) RewriteVersion

func (r *Recorder) RewriteVersion() int

RewriteVersion returns the rewrite version the in-memory records are aligned to (0 = never saved or seeded). Pair with Records for in-process readers that need to detect a stale stream after a compaction, mirroring the sidecar's header line.

func (*Recorder) Save

func (r *Recorder) Save(path string) error

Save writes the current records to path as JSONL (one Record per line), using the tmp-file-then-rename pattern from Session.Save so a crash mid-write can't leave a partial sidecar. An empty path is a no-op. The rewriteVersion is written as a header line (Kind="_header") so Load can detect a stale sidecar.

func (*Recorder) SeedFromPath

func (r *Recorder) SeedFromPath(path string) error

SeedFromPath loads an existing sidecar into the recorder so a freshly-built controller (e.g. after close+reopen) extends the persisted history instead of overwriting it. It is meant to be called once, before the first Save, when the session path is known. A missing file is a no-op (first-ever turn). If the recorder already has records or a rewriteVersion, SeedFromPath is a no-op (idempotent against accidental double-seed). Best-effort: a read failure is logged via the returned error but does not block recording.

Without this, the new controller's empty recorder would, on the first post- reopen turn's Save, full-rewrite the sidecar with only that turn's records — silently erasing every prior turn's rich fields and notice/phase/compaction cards. The session.jsonl skeleton survives (so text/tools are fine), but the presentation layer degrades to "no durations / no cards" for old turns.

func (*Recorder) SetRewriteVersion

func (r *Recorder) SetRewriteVersion(v int)

SetRewriteVersion records the session's current RewriteVersion, captured at Save so a stale sidecar can be detected on load. Called by the controller alongside Save.

func (*Recorder) SyncBeforeSave

func (r *Recorder) SyncBeforeSave(currentVersion int, lastDone event.Event)

SyncBeforeSave reconciles the recorder with the session right before a Save. If the session's RewriteVersion advanced since the last sync, the recorder is reset: a rewrite means compaction/prune/softTrim/summarize physically replaced Messages, so the sidecar must not keep records describing what the model has forgotten. On a compaction specifically, the CompactionDone card is re-seeded (if observed) so the user still sees "N messages compacted"; a prune/softTrim (which only emits Notice, no CompactionDone) clears the records outright — acceptable, since those rewrites are silent in the live stream too.

This must trigger on ANY RewriteVersion change, not just CompactionDone, because prune.go's SoftTrim/Prune bump the version and rewrite Messages without emitting a Compaction event — gating on lastDone.Kind would let those rewrites leave stale records in the sidecar.

type Retry

type Retry struct {
	Attempt int `json:"attempt"`
	Max     int `json:"max,omitempty"`
}

Retry carries Retrying event fields.

type Tool

type Tool struct {
	ID          string       `json:"id,omitempty"`
	Name        string       `json:"name,omitempty"`
	Args        string       `json:"args,omitempty"`
	Output      string       `json:"output,omitempty"`
	Err         string       `json:"err,omitempty"`
	ReadOnly    bool         `json:"readOnly,omitempty"`
	Truncated   bool         `json:"truncated,omitempty"`
	DurationMs  int64        `json:"durationMs,omitempty"`
	ParentID    string       `json:"parentId,omitempty"`
	Attachments []Attachment `json:"attachments,omitempty"`
	Profile     *Profile     `json:"profile,omitempty"`
	// FileDiff preserves a writer dispatch's server-side preview so a replayed
	// session shows the same diff cards the live one did.
	FileDiff *FileDiff `json:"fileDiff,omitempty"`
}

Tool captures the presentation fields of an event.Tool. It is wider than what survives in provider.Message: ReadOnly, ParentID, DurationMs, Truncated, Attachments, Profile, and the streamed Progress chunks are all here. On dispatch only ID/Name/Args/ReadOnly/ParentID/Profile are set; on result Output/Err/ Truncated/DurationMs/Attachments are filled; on progress only ID/Output(chunk).

type Usage

type Usage struct {
	InputTokens  int `json:"inputTokens,omitempty"`
	OutputTokens int `json:"outputTokens,omitempty"`
	TotalTokens  int `json:"totalTokens,omitempty"`
	CacheRead    int `json:"cacheRead,omitempty"`
}

Usage carries the per-turn token telemetry, mirroring the subset of provider.Usage a frontend renders.

Jump to

Keyboard shortcuts

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