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 ¶
- func PresentPath(sessionPath string) string
- type Attachment
- type Collab
- type Compaction
- type FileDiff
- type Kind
- type Profile
- type Record
- type Recorder
- func (r *Recorder) Append(e event.Event)
- func (r *Recorder) Records() []Record
- func (r *Recorder) Reset()
- func (r *Recorder) RewriteVersion() int
- func (r *Recorder) Save(path string) error
- func (r *Recorder) SeedFromPath(path string) error
- func (r *Recorder) SetRewriteVersion(v int)
- func (r *Recorder) SyncBeforeSave(currentVersion int, lastDone event.Event)
- type Retry
- type Tool
- type Usage
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func PresentPath ¶
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 ¶
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 ¶
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.
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 (*Recorder) Append ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 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.