Documentation
¶
Overview ¶
Package agentstore reads the conversation transcripts that coding agents — Claude Code, Codex, Pi — keep under the user's home directory, and serves them back as one normalized shape: a summary per session, pages of messages, and search hits.
The trust model is read-only, and read-only over stores this package does not own. Every file here belongs to another program, is written in a format that program has never promised to keep, and may be mid-append at the moment it is read. So nothing in this package writes into a store, nothing treats a line it cannot parse as an error, and nothing treats the end of a file as the end of a session: an unknown line type is a future version of the tool, a malformed line is a crash mid-write, and both are skipped rather than failed on, because failing would make this viewer's availability depend on the stability of three formats nobody versioned. A file that cannot be read at all simply is not listed.
The one thing this package does write is its own index, a cache of the summaries it has computed, kept under the flue config directory. Summaries name working directories, first prompts and session titles — the same order of sensitivity as the scrollback in a session snapshot — so the index inherits the config directory's discipline: 0700 directory, 0600 file, written to a fresh inode and renamed into place. A corrupt or missing index is rebuilt from the transcripts, never fatal; the transcripts are the truth and the index only ever a summary of them.
Index ¶
- Variables
- type Hit
- type Index
- func (x *Index) ReadPage(tool Tool, id string, offset int64, dir string, limit int) (Page, error)
- func (x *Index) Resolve(tool Tool, id string) (string, Summary, bool)
- func (x *Index) Search(ctx context.Context, query string, tools []string, cwd string, limit int) (hits []Hit, building, truncated bool, err error)
- func (x *Index) Snapshot(tools []string, cwd string) ([]Summary, bool)
- type Message
- type Page
- type Resume
- type Summary
- type TokenUsage
- type Tool
Constants ¶
This section is empty.
Variables ¶
var ( // ErrUnknownTool is a tool name outside the three this package reads. ErrUnknownTool = errors.New("agentstore: unknown tool") // ErrNotFound is a (tool, id) the index does not hold, or one whose file // vanished between the index sweep and the read. ErrNotFound = errors.New("agentstore: no such session") // ErrBadQuery is a search with nothing to search for. ErrBadQuery = errors.New("agentstore: empty query") )
Errors the callers translate onto the wire. Two rather than several, because the client has two moves: a name outside the protocol is the client's bug (bad_message), and a session that is not there is the ordinary answer for a store another program prunes (not_found).
Functions ¶
This section is empty.
Types ¶
type Hit ¶
type Hit struct {
Tool Tool `json:"tool"`
ID string `json:"id"`
Cwd string `json:"cwd"`
Title string `json:"title,omitempty"`
Ts string `json:"ts,omitempty"`
Role string `json:"role"`
// Snippet is the match with ~80 characters of context each side, folded
// onto one line, no markup.
Snippet string `json:"snippet"`
Offset int64 `json:"offset"`
}
Hit is one search match: enough of the session to label the result, and enough of the message to open it — Offset pages straight to the line.
type Index ¶
type Index struct {
// contains filtered or unexported fields
}
Index is the in-memory map of every transcript the sweep has met, keyed by path, persisted as one JSON file so a daemon restart does not re-parse a hundred sessions to answer its first list.
func New ¶
New returns an index over home's stores, persisting under persistDir. The persisted file is loaded if it is there and readable; a corrupt or missing one costs a rebuild on the first sweep and nothing else — the transcripts are the truth and the file only a cache of reading them.
func (*Index) ReadPage ¶
ReadPage answers one agentRead. dir is "forward" or "backward" (the caller's to validate, forward being the default); limit is clamped to the package bounds, with zero meaning the default. A tool outside the protocol is ErrUnknownTool — the caller's bug — and an id the index does not hold, or one whose file has vanished since the sweep, is ErrNotFound — the ordinary kind of miss over a store another program prunes.
func (*Index) Resolve ¶
Resolve is the only way a (tool, id) becomes a path: reads and searches go through it, so a path never has to cross the wire to name a transcript.
func (*Index) Search ¶
func (x *Index) Search(ctx context.Context, query string, tools []string, cwd string, limit int) (hits []Hit, building, truncated bool, err error)
Search answers one agentSearch. tools and cwd filter the way the agents verb filters; limit is clamped, zero meaning the default. building reports whether a sweep is still running — a client that wants the stragglers re-asks, the way it re-asks the list. An empty query is ErrBadQuery, the caller's bug: matching everything is an answer nobody is asking for.
ctx is the caller's interest in the answer, checked between files and on every line: the budget is sixty-four megabytes, and a connection that closes mid-scan should stop costing this machine file reads the moment nobody is left to read the answer. A cancellation caught between files is ctx.Err(); one caught mid-file surfaces as an ordinary truncated result, which is fine — the one caller that cancels has already stopped listening.
func (*Index) Snapshot ¶
Snapshot answers the agents verb: the sessions the index holds right now — filtered, newest first — and whether a sweep is still running. It also pokes the sweep, so looking at the list is what keeps the list fresh. tools filters by tool name when non-empty; a name that matches no adapter matches no session, which is what a filter means. cwd filters by exact working directory.
type Message ¶
type Message struct {
// Role is "user", "assistant" or "system". System is where each adapter
// puts the lines a harness injected — hook context, command echoes,
// environment preambles — so a viewer can fold them away and the message
// count does not inflate with text no human wrote.
Role string `json:"role"`
// Kind is "text", "thinking", "tool_call" or "tool_result".
Kind string `json:"kind"`
// Ts is RFC 3339, or absent when the line carries no timestamp.
Ts string `json:"ts,omitempty"`
Model string `json:"model,omitempty"`
// Text is the message body: prose for text and thinking, the input JSON
// for a tool_call, the result body for a tool_result.
Text string `json:"text"`
// ToolName is set on tool_call always, and on tool_result when the
// transcript names the tool there — Claude and Pi only record the call id
// on results, so those arrive without one.
ToolName string `json:"toolName,omitempty"`
// Sidechain marks a subagent's message, which Claude keeps in the same
// file as the conversation that spawned it.
Sidechain bool `json:"sidechain,omitempty"`
// Truncated says Text was cut to the per-block cap on its way into a
// page. The transcript still holds the whole thing.
Truncated bool `json:"truncated,omitempty"`
Offset int64 `json:"offset"`
}
Message is one normalized entry of a transcript page.
Offset is the byte offset of the line the message came from, and it is the stable anchor: a client keys on it, and hands it back as agentRead.offset to page from here. One line can normalize to several messages — an assistant line holding a thinking block and a tool call — and they all carry the line's offset, so the anchor names the line, not the message.
type Page ¶
type Page struct {
Messages []Message
// Start is the offset of the first returned message's line; Next is the
// offset parsing would continue from. Forward pages advance with Next,
// backward pages load earlier with Start.
Start int64
Next int64
Eof bool
FileSize int64
}
Page is one agentRead answer: a window of messages and where the window sits in the file. Eof says the parse reached the end of the file as it was at read time — not that the session is over, since the file may still be growing; the same doctrine as the read verb's eof.
type Resume ¶
Resume is the command that picks a session back up, with the directory to run it in. It is a hint printed for a human, never something this package executes; a session whose working directory is unknown carries no Resume at all, because a resume command run in the wrong directory is worse than none.
type Summary ¶
type Summary struct {
ID string `json:"id"`
Tool Tool `json:"tool"`
Cwd string `json:"cwd"`
// Title is the best name the transcript offers, by each tool's own
// precedence — a human-set title beats a generated one beats the first
// prompt. FirstPrompt is kept beside it rather than folded in, so a client
// can show both the name and the opening ask.
Title string `json:"title,omitempty"`
FirstPrompt string `json:"firstPrompt,omitempty"`
// StartedAt and EndedAt are the first and last message timestamps —
// messages, not bookkeeping lines, so a title rewritten hours later does
// not stretch the session.
StartedAt time.Time `json:"startedAt"`
EndedAt time.Time `json:"endedAt"`
MessageCount int `json:"messageCount"`
ToolCallCount int `json:"toolCallCount"`
Models []string `json:"models"`
Tokens TokenUsage `json:"tokens"`
// CostUsd is present only when the transcript itself records dollar cost,
// which today is Pi alone. Absent is "not recorded", not "free": this
// package reports what the store says and estimates nothing.
CostUsd float64 `json:"costUsd,omitempty"`
FileSize int64 `json:"fileSize"`
Resume *Resume `json:"resume,omitempty"`
}
Summary is one session as the list shows it: identity, where it ran, what it was about, and what it cost. Everything here is computed from the transcript alone.
type TokenUsage ¶
type TokenUsage struct {
Input int64 `json:"input"`
Output int64 `json:"output"`
CacheRead int64 `json:"cacheRead"`
CacheWrite int64 `json:"cacheWrite"`
}
TokenUsage is one session's token consumption, in the four buckets every tool's accounting maps onto. The mapping is per-adapter — Claude sums lines deduped by requestId, Codex reports a cumulative total this copies, Pi sums per-message — and this struct is where the three spellings converge.