agentstore

package
v0.8.0 Latest Latest
Warning

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

Go to latest
Published: Aug 18, 2026 License: MIT Imports: 13 Imported by: 0

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

Constants

This section is empty.

Variables

View Source
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

func New(persistDir, home string) *Index

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

func (x *Index) ReadPage(tool Tool, id string, offset int64, dir string, limit int) (Page, error)

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

func (x *Index) Resolve(tool Tool, id string) (string, Summary, bool)

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

func (x *Index) Snapshot(tools []string, cwd string) ([]Summary, bool)

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

type Resume struct {
	Cmd []string `json:"cmd"`
	Cwd string   `json:"cwd"`
}

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.

type Tool

type Tool string

Tool names one of the transcript stores this package reads. The values are wire strings — a client sends them in agentRead and reads them off every summary — so they are spelled the way the wire spells them.

const (
	ToolClaude Tool = "claude"
	ToolCodex  Tool = "codex"
	ToolPi     Tool = "pi"
)

Jump to

Keyboard shortcuts

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