session

package
v0.2.0-alpha.8 Latest Latest
Warning

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

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

Documentation

Overview

Package session provides the core session model: identity, the linked history journal, the reducer that replays it, recovery analysis, and the persistence seam.

A session's durable form is the bundle described in docs/design/local-session-storage.md: metadata in meta.json, the conversation in an append-only history.jsonl, traces and artifacts alongside them. This package owns what those records mean and how a branch reduces to the state a resumed turn starts from. Making them durable belongs to internal/infra/sessionstore; deciding when state commits belongs to internal/agentapp.

Index

Constants

View Source
const (
	ItemTurnStarted          = "turn_started"
	ItemMessage              = "message"
	ItemToolExecutionStarted = "tool_execution_started"
	ItemToolResult           = "tool_result"
	ItemCompaction           = "compaction"
	ItemNotesReplaced        = "notes_replaced"
	ItemTodosReplaced        = "todos_replaced"
	ItemAdditionalPromptSet  = "additional_prompt_set"
	ItemHeadSelected         = "head_selected"
	ItemTurnFinished         = "turn_finished"
	ItemTurnRecovered        = "turn_recovered"
)

Item types. The set is closed for the semantics defined here; a reader that meets a type outside it decides what to do from Item.Required, not from this list. See docs/design/local-session-storage.md §6.3 and §6.4.

View Source
const (
	// The first three are aliases rather than fresh literals: the agent loop
	// classifies a finished call and this package writes that classification
	// down, so one definition keeps the file format and the loop from drifting.
	ToolStatusCompleted = agent.ToolStatusCompleted
	ToolStatusFailed    = agent.ToolStatusFailed
	ToolStatusDenied    = agent.ToolStatusDenied
	// ToolStatusUnknown has no counterpart in the loop because the loop never
	// produces it: it is written by recovery, for a call the loop did not live
	// long enough to classify.
	ToolStatusUnknown = "unknown"
)

Tool outcome statuses carried by a ToolResult.

ToolStatusUnknown is not an outcome the tool reported. It is what BuildMax writes for a call that crossed the execution boundary without returning, whether the turn was cancelled, interrupted, or lost with its process: the call may already have changed the world, and saying so is the only honest answer available.

View Source
const (
	TurnCompleted   = "completed"
	TurnFailed      = "failed"
	TurnCanceled    = "canceled"
	TurnInterrupted = "interrupted"
)

Terminal turn statuses. Canceled and interrupted are separate because they are separate events — a person stopping the turn against the process being shut down under it — and they must not read the same. The run layer draws the same line; see docs/design/graceful-shutdown.md.

View Source
const (
	// OutcomeNotStarted: the model asked for the call and BuildMax never
	// recorded entering the tool, so nothing outside BuildMax happened.
	OutcomeNotStarted = "not_started"
	// OutcomeUnknown: BuildMax recorded entering the tool and never recorded a
	// result. The call may have changed the world. It is never retried
	// automatically.
	OutcomeUnknown = "outcome_unknown"
	// OutcomeKnown: a result was recorded, whatever it says.
	OutcomeKnown = "known"
)

Tool outcome classifications for a call found on an interrupted branch. See docs/design/local-session-storage.md §7.3.

View Source
const HistoryVersion = 1

HistoryVersion is the journal format this build writes and is the only one it reads. See docs/design/local-session-storage.md §6.1.

View Source
const MetaVersion = 1

MetaVersion is the meta.json format this build writes and is the only one it reads. See docs/design/local-session-storage.md §5.

Variables

View Source
var (
	ErrHistoryVersion  = errors.New("unsupported history version")
	ErrHistoryCorrupt  = errors.New("history corrupt")
	ErrUnknownRequired = errors.New("history contains an unknown required record")
	ErrHeadNotFound    = errors.New("history head not found")
)

History errors. They are distinct because callers act on them differently: an unsupported version is a build that is too old, corruption is a file that cannot be trusted, and an unknown required type is a record this build would mis-reduce if it guessed.

View Source
var ErrAlreadyHead = errors.New("this point is already the head")

ErrAlreadyHead reports that a point is where the branch already ends, so nothing follows it. Rewinding there is a caller mistake; forking there is the ordinary "branch off from here", so a fork surface reads this as an empty span rather than a failure.

View Source
var ErrLocked = errors.New("session is open in another process")

ErrLocked reports that something else already holds a session's writer lock.

It lives here rather than in the file backend because Store.Open's contract names it: a caller deciding whether to report "busy" or to fail programs against the interface, and would otherwise have to import an implementation to ask a question the interface already answers.

View Source
var ErrNoLanding = errors.New("no record precedes this message")

ErrNoLanding reports that a message cannot be rewound because nothing on the branch precedes it. A picker filters those out; a surface handed one anyway needs to say why rather than fail as "not found".

View Source
var ErrSessionNotFound = errors.New("session not found")

ErrSessionNotFound is returned when a session does not exist.

Functions

func CtxWithSessionID

func CtxWithSessionID(ctx context.Context, id string) context.Context

CtxWithSessionID returns a context that carries the given session ID.

func Head(items []Item) (string, error)

Head returns the id of the item the next append extends.

It is the last physical record, with no special case for rewind. Every record chains to its physical predecessor except HeadSelected, which chains to the item being returned to, so redirecting the branch is already expressed in the parent links and the head never has to be stored or searched for.

func NewID

func NewID() string

NewID returns a new session ID.

func RewindLanding

func RewindLanding(items []Item, head, messageID string) (string, error)

RewindLanding returns the record a rewind removing messageID lands on.

Rewind is exclusive: the message a person picks is the one they want back in the input box, so it leaves the branch along with everything after it, and the head has to name the record before it rather than the message itself.

That record is the physical predecessor on the branch, not the previous message: notes, todos, a compaction, and the `turn_finished` of the turn before all sit between two messages, and they belong to work that is being kept. The one record stepped over is `turn_started`, because §7.1 opens a turn before the prompt that starts it — landing there would leave the branch inside the turn being dropped.

A message with nothing before it has no landing. That is the first prompt of a session, and rewinding it would ask for a branch with no records at all; starting a new session says the same thing honestly.

func SessionIDFromContext

func SessionIDFromContext(ctx context.Context) (string, bool)

SessionIDFromContext returns the session ID from ctx, or ("", false) if not set.

func Validate

func Validate(items []Item) error

Validate checks a whole journal at once. It is Validator over a slice, for callers that already hold every item.

Types

type AbandonedEffect

type AbandonedEffect struct {
	ToolCallID string
	ToolName   string
	// Returned reports whether the tool produced a result. A call that crossed
	// the execution boundary without returning is listed too, and is the more
	// dangerous of the two: it may have changed as much as one that finished,
	// and nothing recorded what.
	Returned bool
}

AbandonedEffect is one tool call the conversation is about to move past.

type AbandonedWork

type AbandonedWork struct {
	// Messages is how many model-visible messages leave the branch.
	Messages int
	// Effects are the tool calls that reached their tools, in the order they
	// ran. Empty means the abandoned span was conversation only — the one case
	// where a rewind really does undo everything it moved past.
	Effects []AbandonedEffect
}

AbandonedWork is what a rewind moves the conversation past and does not undo.

It exists because rewind is honest about a hazard rather than hiding it (§8.1 of docs/design/local-session-storage.md). The model's history returns to an earlier point; files, processes and network calls do not. A surface that offers rewind without showing this leaves the user believing the opposite, and leaves the model reasoning from a workspace picture that is no longer true.

func Abandoned

func Abandoned(items []Item, head, target string) (AbandonedWork, error)

Abandoned reports what rewinding from head to target would leave in place.

target must be on the branch ending at head, and must not be head itself: asking about where the branch already ends is answered with ErrAlreadyHead rather than an empty span, because a surface that computed the wrong target would otherwise report "nothing abandoned" and look correct.

func (AbandonedWork) Undoable

func (a AbandonedWork) Undoable() bool

Undoable reports whether the rewind moves past nothing that touched the world, so a surface can say so plainly instead of warning about nothing.

type AdditionalPromptSet

type AdditionalPromptSet struct {
	Text string `json:"text"`
}

AdditionalPromptSet replaces the durable additional system prompt.

type Compaction

type Compaction struct {
	CoveredHeadID string `json:"covered_head_id"`
	Summary       string `json:"summary"`
}

Compaction replaces the model-visible prefix of this branch.

CoveredHeadID names the last item the summary accounts for, which is what keeps compaction branch-scoped: a summary produced after a fork point cannot be reused by a branch that does not contain the items it summarised.

type ConversationStats

type ConversationStats struct {
	// UserMessages counts messages the user actually wrote. A background
	// event travels as a user-role message because no provider has a portable
	// role for one, and counting those as things the user said would inflate
	// the number that reads most like effort.
	UserMessages int
	// BackgroundMessages counts the user-role messages that carry a Source:
	// command results, subagent results, monitor events.
	BackgroundMessages int
	// AssistantTurns counts assistant messages, including the ones whose whole
	// content was a tool call.
	AssistantTurns int
	// ToolCalls counts calls the assistant issued. ToolResults counts the
	// results that came back; the two differ when a run was cut short between
	// the call and its result.
	ToolCalls   int
	ToolResults int
	// Tools is the per-tool breakdown, heaviest by result bytes first. A tool
	// the assistant called but whose result never arrived still appears, with
	// zero bytes.
	Tools []ToolStats
	// TextBytes is what the conversation's own text weighs — user prompts and
	// assistant replies. ToolResultBytes is what came back from tools.
	//
	// Kept apart because they are spent differently: the first is the
	// conversation, the second is what the run pulled into it, and on a long
	// agent session the second is usually the larger by an order of magnitude.
	TextBytes       int
	ToolResultBytes int
	// CompactedMessages is how many messages sit before the compaction
	// boundary — summarized away, still stored.
	CompactedMessages int
	// Notes and Todos are the durable state the session carries.
	Notes int
	Todos int
}

ConversationStats is the shape of a session's history: who said how much, which tools ran, and how many bytes each of them put back into the context.

It is derived from the stored messages alone and needs no run to be live, so it answers for a session whose traces have been removed. What it cannot answer is anything time-shaped — durations, run boundaries, which model ran — because the history carries no timestamps. That half comes from the traces.

func Stats

func Stats(st State) ConversationStats

Stats folds a session's stored history into its shape.

A tool result names the call it answers rather than the tool it came from, so results are attributed by walking the assistant tool calls first and looking each result's call id up. A result whose call is not in the history — the assistant message was compacted out from under it — is counted in the totals under an empty name rather than dropped: the bytes are in the context either way.

type ForkedFrom

type ForkedFrom struct {
	SessionID    string `json:"session_id"`
	CheckpointID string `json:"checkpoint_id"`
	HeadID       string `json:"head_id"`
}

ForkedFrom is immutable provenance on a session created by forking another. It is written once at fork time and never updated afterward.

type HeadSelected

type HeadSelected struct {
	Reason string `json:"reason,omitempty"`
}

HeadSelected records a branch choice, which is why rewind is one append and not an append plus a metadata write that could disagree with it.

The item being returned to is ParentID. It is not repeated in the payload: every other record's parent is its physical predecessor, and this is the one record that deliberately points somewhere else, so the parent link already says everything a target field would. Storing it twice would only create a pair that could disagree.

type Header struct {
	Type      string    `json:"type"`
	Version   int       `json:"version"`
	SessionID string    `json:"session_id"`
	CreatedAt time.Time `json:"created_at"`
}

Header is the journal's immutable first record.

func NewHeader

func NewHeader(sessionID string, createdAt time.Time) Header

NewHeader returns the header for a new session journal.

func (Header) Validate

func (h Header) Validate() error

Validate rejects a header this build cannot interpret. A session ID that disagrees with the directory is corruption rather than an alternate spelling, but only the caller knows the directory, so that check lives with it.

type Item

type Item struct {
	Seq      uint64
	ID       string
	ParentID string
	TS       time.Time
	Required bool
	TurnID   string
	Payload  Payload
}

Item is one journal record. Seq is its physical position and ID/ParentID its logical one; the two are separate because rewind leaves abandoned branches in place rather than truncating them.

func Branch

func Branch(items []Item, head string) ([]Item, error)

Branch returns the items from the root to head in logical order.

Items on abandoned branches are skipped, which is the whole point of keeping them: rewind leaves them in the file, and only the parent chain decides what the model sees.

func ForkPrefix

func ForkPrefix(items []Item, throughID string) ([]Item, error)

ForkPrefix returns the branch ending at throughID as a journal of its own.

It is the branch, not the physical prefix: a parent that was rewound holds abandoned records too, and a child that copied those would carry history its own parent chain never reaches. Branch already answers this — every item it returns has its parent in the same slice — so the result is self-contained and needs no repair.

Item ids are preserved, because they are the stable identity §8.3 keeps, and preserving them is what lets a child's records be recognised as the same work the parent did. Sequence numbers are not: seq is a record's physical position in the journal holding it, and carrying a parent's numbering across would make it describe a file this journal is not. The child is renumbered from one, which is also why a gap in the parent leaves no trace here.

func NewItem

func NewItem(seq uint64, id, parentID string, ts time.Time, turnID string, payload Payload) Item

NewItem builds an item with Required derived from the payload, which is the only place the two are allowed to be decided together.

func (Item) MarshalJSON

func (it Item) MarshalJSON() ([]byte, error)

MarshalJSON writes the record shape documented in §6.2. Required is always emitted, including when false, so an older reader meeting an unknown type always finds an explicit answer rather than having to assume one.

func (Item) Type

func (it Item) Type() string

Type returns the item's discriminator, or "" when it carries no payload.

func (*Item) UnmarshalJSON

func (it *Item) UnmarshalJSON(b []byte) error

UnmarshalJSON decodes a record, keeping an unrecognised type as an UnknownPayload instead of failing. Refusing here would make every forward extension a load error even when the record only adds information.

type ItemSummary

type ItemSummary struct {
	ID string `json:"id"`
	// ProjectID is what the picker and --continue filter by, so it is in the
	// projection: scoping a list to the current Project must not cost a read of
	// every session's meta.json.
	ProjectID string `json:"project_id,omitempty"`
	Kind      Kind   `json:"kind"`
	// CreatedAt is carried as well as UpdatedAt because the picker orders by
	// it: a list that reordered itself every time a session was touched would
	// move entries under the cursor.
	CreatedAt  time.Time   `json:"created_at"`
	UpdatedAt  time.Time   `json:"updated_at"`
	Title      string      `json:"title,omitempty"`
	Workspace  string      `json:"workspace,omitempty"`
	Pinned     bool        `json:"pinned,omitempty"`
	ForkedFrom *ForkedFrom `json:"forked_from,omitempty"`
}

ItemSummary is one session's row in the picker projection (§12): enough to list and group forks without reading a session's journal.

type Kind

type Kind string

Kind distinguishes an ordinary user session from a subagent's private one. See docs/design/local-session-storage.md §9.

const (
	KindUser     Kind = "user"
	KindSubagent Kind = "subagent"
)

type LoadMode

type LoadMode int

LoadMode controls how much of a session Store.Load reads.

const (
	// LoadMetaOnly reads meta.json alone, for listing and presentation paths
	// that do not need the journal.
	LoadMetaOnly LoadMode = iota
	// LoadFull replays the branch ending at the current head and validates it.
	LoadFull
)

type Loaded

type Loaded struct {
	Meta Meta
	// Head is the current head's id, or "" for a session with no items yet.
	Head string
	// Items is the branch from the root to Head, in logical order. Populated
	// only for LoadFull.
	Items []Item
	// State is Reduce(Items, Head). Populated only for LoadFull.
	State State
	// Recovery classifies an interrupted turn on this branch, per §7.3.
	// Populated only for LoadFull.
	Recovery Recovery
}

Loaded is what Store.Load or Store.Open returns.

type MessageItem

type MessageItem struct {
	Message llm.Message `json:"message"`
}

MessageItem carries one complete portable message, not only its text: an assistant record keeps its tool calls and its opaque provider state, and a user record keeps background provenance and non-text parts.

type Meta

type Meta struct {
	Version   int       `json:"version"`
	ID        string    `json:"id"`
	Kind      Kind      `json:"kind"`
	CreatedAt time.Time `json:"created_at"`
	UpdatedAt time.Time `json:"updated_at"`

	// ProjectID is the local Project this session belongs to. It is the
	// relationship key --continue, the picker, and project memory all select
	// by, and it is immutable: a session may move among the Workspace roots of
	// its Project, but it never silently moves to another Project. MetaUpdate
	// has no field for it.
	//
	// It is optional at this boundary rather than required because task-run and
	// other non-local sessions have no local Project, and giving them a
	// fabricated one to satisfy the shape would put fake rows in the catalog.
	// CLI and Desktop enforce the stronger local invariant where they create
	// sessions. See docs/design/local-project-memory.md §6.3.
	ProjectID string `json:"project_id,omitempty"`

	Title     string `json:"title,omitempty"`
	Workspace string `json:"workspace,omitempty"`
	Pinned    bool   `json:"pinned,omitempty"`

	// SelectedModel is what the next turn should use. A completed or
	// interrupted turn's own TurnStarted record is what it actually used;
	// see §5.
	SelectedModel string `json:"selected_model,omitempty"`

	// Usage and cost are local aggregate reporting, not resume input, and are
	// carried forward by MetaUpdate rather than recomputed from history: the
	// rates that applied to an earlier turn are not necessarily the ones
	// configured now.
	PromptTokens     int       `json:"prompt_tokens,omitempty"`
	CompletionTokens int       `json:"completion_tokens,omitempty"`
	CacheReadTokens  int       `json:"cache_read_tokens,omitempty"`
	CacheWriteTokens int       `json:"cache_write_tokens,omitempty"`
	Cost             *llm.Cost `json:"cost,omitempty"`
	CostIncomplete   bool      `json:"cost_incomplete,omitempty"`

	// Lineage fields are set only when Kind is KindSubagent, and are immutable
	// once written: they describe how this session came to exist, not
	// anything it is currently doing.
	ParentSessionID  string `json:"parent_session_id,omitempty"`
	ParentRunID      string `json:"parent_run_id,omitempty"`
	ParentToolCallID string `json:"parent_tool_call_id,omitempty"`
	AgentType        string `json:"agent_type,omitempty"`
	DelegationDepth  int    `json:"delegation_depth,omitempty"`
	// Hidden excludes the session from the ordinary picker and --continue. It
	// is set by kind, not toggled independently.
	Hidden bool `json:"hidden,omitempty"`

	// ForkedFrom is set only on a user session created by forking another.
	ForkedFrom *ForkedFrom `json:"forked_from,omitempty"`
}

Meta is a session's current metadata record: presentation, the current selections a new turn would use, and running aggregates. It holds nothing history also determines — see §4 — so it carries no head and no sequence counter; both are derived from the journal by Head (§6.2).

func ApplyMetaUpdate

func ApplyMetaUpdate(m Meta, update MetaUpdate, now time.Time) Meta

ApplyMetaUpdate returns m with update applied and UpdatedAt advanced to now. It does not mutate m.

func NewMeta

func NewMeta(id string, kind Kind, createdAt time.Time) Meta

NewMeta returns a fresh Meta for a new session. CreatedAt and UpdatedAt start equal, since nothing has changed since creation.

func (Meta) CacheReadShare

func (m Meta) CacheReadShare() (share float64, ok bool)

CacheReadShare is the fraction of the session's prompt that was served from a provider's cache, and ok=false when there is nothing to divide or the provider reported no cache usage at all. A zero share and an unreported one are different facts: only the first says the cache missed.

func (Meta) Usage

func (m Meta) Usage() llm.Usage

Usage is the session's accumulated token usage.

func (Meta) Validate

func (m Meta) Validate() error

Validate rejects a Meta this build cannot act on.

type MetaUpdate

type MetaUpdate struct {
	Title         *string
	Workspace     *string
	Pinned        *bool
	SelectedModel *string

	AddPromptTokens     int
	AddCompletionTokens int
	AddCacheReadTokens  int
	AddCacheWriteTokens int
	// AddCost is added to the running total. A currency mismatch against the
	// existing total is not an error here: it marks the total incomplete
	// instead, because BuildMax holds no exchange rate and inventing one would
	// produce a figure that is wrong in both currencies.
	AddCost            *llm.Cost
	MarkCostIncomplete bool
}

MetaUpdate describes a change to a session's current selections or running aggregates. It cannot express a change to Kind, lineage, or ForkedFrom — those are immutable — and it has no field for anything resumable, because a resumable change must go through history instead (§4). A nil field leaves that value unchanged; token and cost fields are deltas, added to what is already recorded, because usage accumulates across turns.

type NotesReplaced

type NotesReplaced struct {
	Notes []agent.Note `json:"notes"`
}

NotesReplaced carries the complete stamped list, not a delta. Durable state is small and rewritten wholesale, so a full list costs little and removes any question about how two partial writes combine.

type Payload

type Payload interface {
	// contains filtered or unexported methods
}

Payload is one item type's body. Implementations are the types below; an unrecognised type decodes to UnknownPayload rather than failing, so that Item.Required rather than the decoder decides whether the journal is usable.

type Recovery

type Recovery struct {
	// TurnID is the interrupted turn. Empty when nothing needs repair.
	TurnID string
	// Uncertain are calls that crossed the execution boundary without
	// returning. Each needs a durable unknown result before the model sees the
	// branch again, so it is told to verify rather than left to assume.
	Uncertain []ToolCallOutcome
	// NotStarted are calls the model requested that never reached a tool. They
	// need no repair record; they are reported because a caller deciding what
	// to say to the user wants the distinction.
	NotStarted []ToolCallOutcome
}

Recovery is what an interrupted branch needs before it accepts new work.

func Analyze

func Analyze(items []Item, head string) (Recovery, error)

Analyze classifies the branch ending at head for interruption repair.

A branch whose last turn was closed needs nothing: a turn that ended as completed, failed, canceled, or interrupted was ended by a process that knew what it had done, and re-deriving that judgement would only risk contradicting it. Only a turn with no terminal record is repaired.

func (Recovery) Needed

func (r Recovery) Needed() bool

Needed reports whether this branch has an interrupted turn to close.

type State

type State struct {
	Messages          []llm.Message
	CompactionIdx     int
	CompactionSummary string
	Notes             []agent.Note
	Todos             []agent.Todo
	AdditionalPrompt  string
	// LastTurn is the turn id of the most recent TurnStarted on this branch,
	// and Open reports whether it lacks a matching TurnFinished. Together they
	// are what tells a caller whether it is resuming or repairing.
	LastTurn string
	Open     bool
}

State is what a branch of history reduces to: everything a resumed turn needs and nothing a turn produced only as evidence. Timing, usage, and sandbox details are the trace's business, not this.

func Reduce

func Reduce(items []Item, head string) (State, error)

Reduce replays the branch ending at head and returns the state it produces.

It is deterministic and total over a validated journal: the same items and head always give the same State, which is what lets an incremental reducer be checked against a full replay.

func (State) HistoryMessages

func (s State) HistoryMessages() []llm.Message

HistoryMessages returns the model-visible messages: everything after the compaction boundary, since what precedes it is represented by the summary.

type Store

type Store interface {
	// Create makes a new session directory with its metadata and an empty,
	// headered journal. It fails if the session already exists.
	Create(ctx context.Context, meta Meta) error

	// Open acquires the writer lock and returns a Writer for id, along with
	// what was already on this session's journal — repairing a torn tail and
	// classifying an interrupted turn for recovery in the process. It fails
	// with ErrLocked if another process already holds the lock.
	Open(ctx context.Context, id string) (Writer, error)

	// Load reads a session without acquiring the writer lock or repairing
	// anything. A writer may be active concurrently; Load only ever sees a
	// stable prefix.
	Load(ctx context.Context, id string, mode LoadMode) (Loaded, error)

	// UpdateMeta changes current selections or running aggregates. It cannot
	// change anything resumable — MetaUpdate has no field for that — so it
	// never touches the journal.
	UpdateMeta(ctx context.Context, id string, update MetaUpdate) error

	// List returns the picker projection. includeHidden controls whether
	// subagent sessions (§9) are included; the ordinary picker passes false.
	List(ctx context.Context, includeHidden bool) ([]ItemSummary, error)
}

Store is the persistence seam between AgentApp and physical storage. It expresses session semantics, not paths: core owns what these operations mean, infra owns making them durable. See docs/design/local-session-storage.md §14.

Store itself never appends to a journal. Appending requires the writer lock, which Open acquires and Writer.Close releases — a capability the interface makes explicit so a caller cannot append without holding it, and so the lock is held for as long as a caller is actively committing a turn rather than re-acquired call by call, which is what keeps two turns from interleaving into one open span. See §12.

type TodosReplaced

type TodosReplaced struct {
	Todos []agent.Todo `json:"todos"`
}

TodosReplaced carries the complete stamped list. See NotesReplaced.

type ToolCallOutcome

type ToolCallOutcome struct {
	ToolCallID string
	ToolName   string
	Class      string
}

ToolCallOutcome is one call's classification after an interruption.

type ToolExecutionStarted

type ToolExecutionStarted struct {
	ToolCallID string `json:"tool_call_id"`
	ToolName   string `json:"tool_name"`
}

ToolExecutionStarted marks that an approved call is about to cross into the tool. It is the record that makes an interrupted call distinguishable from one that never ran, and it is worthless unless it reaches stable storage before the tool may change anything.

type ToolResult

type ToolResult struct {
	ToolCallID string            `json:"tool_call_id"`
	Status     string            `json:"status"`
	Content    string            `json:"content,omitempty"`
	Parts      []llm.ContentPart `json:"parts,omitempty"`
}

ToolResult closes one call. It is stored as its own item rather than as a second generic message; the reducer projects it to the tool-role message that provider adapters require.

type ToolStats

type ToolStats struct {
	Name string
	// Calls is how many times the assistant asked for it.
	Calls int
	// ResultBytes is what its results put back into the context. This is the
	// number that answers which tool is filling the context window, and it is
	// not derivable from the call count: one search can outweigh fifty reads.
	ResultBytes int
	// MaxResultBytes is the largest single result, so one outlier is not
	// hidden inside an average.
	MaxResultBytes int
}

ToolStats is one tool's share of a session.

type TurnFinished

type TurnFinished struct {
	Status     string `json:"status"`
	ErrorClass string `json:"error_class,omitempty"`
}

TurnFinished closes a turn. Its presence is what tells the next open that there is nothing to recover.

type TurnRecovered

type TurnRecovered struct {
	TurnID               string   `json:"turn_id"`
	UncertainToolCallIDs []string `json:"uncertain_tool_call_ids,omitempty"`
}

TurnRecovered makes a cold recovery explicit before new work is accepted, so the repair appears in the journal once rather than being re-derived on every open.

type TurnStarted

type TurnStarted struct {
	RunID         string `json:"run_id"`
	Model         string `json:"model,omitempty"`
	WorkspaceRoot string `json:"workspace_root,omitempty"`
	ContextWindow int    `json:"context_window,omitempty"`
	InputKind     string `json:"input_kind,omitempty"`
}

TurnStarted opens a turn and fixes the runtime identity it ran under.

Model and WorkspaceRoot are what this turn actually used. The session's current selections live in metadata and can differ, which is the point: resuming after switching either must not restate earlier turns.

type UnknownPayload

type UnknownPayload struct {
	Kind string
	Raw  json.RawMessage
}

UnknownPayload is an item this build does not define. It keeps the bytes so a reader that only passes the journal through does not destroy them, and it carries no opinion of its own: whether the session is usable is decided by Item.Required.

type Validator

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

Validator checks a journal's items as a graph, one at a time in physical order.

It is incremental rather than whole-slice so a caller reading a damaged file knows exactly which record failed, and can therefore offer the prefix that was still good instead of only reporting that something somewhere is wrong.

func NewValidator

func NewValidator() *Validator

NewValidator returns a Validator for one journal.

func (*Validator) Add

func (v *Validator) Add(it Item) error

Add checks one item against everything already accepted.

It rejects what would make a reduction wrong: a duplicate identity, a parent that has not appeared, more than one root, and a record this build cannot interpret but must. It does not check that seq numbers are contiguous — see docs/design/local-session-storage.md §7.2 for why a gap is not a corruption signal.

func (*Validator) Done

func (v *Validator) Done() error

Done reports problems only a finished journal can show.

type Writer

type Writer interface {
	// Loaded is what Open found when the writer lock was acquired.
	Loaded() Loaded

	// Append writes items and returns only once they are durable. items must
	// continue the branch this Writer opened: each item's ParentID must chain
	// from Loaded().Head (or from a preceding item in the same call), and
	// their Seq values must be contiguous starting after the highest Seq this
	// Writer has appended so far. A caller that gets this wrong is a bug, not
	// a race — the lock already rules out a second writer — so Append reports
	// it as an ordinary error rather than needing an optimistic-concurrency
	// retry.
	Append(ctx context.Context, items ...Item) error

	// Close releases the writer lock. It is always safe to call, including
	// after Append has failed.
	Close() error
}

Writer is one session held open for append. It owns the writer lock for its whole lifetime: nothing else may append to, rewind, or fork from this session until Close releases it.

Jump to

Keyboard shortcuts

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