Documentation
¶
Overview ¶
Package agentproto is the search→narrow→bounded-read protocol for LLM agents: an agent recalls prior conversations WITHOUT pasting whole transcripts — it gets ranked conversation refs, then reads BOUNDED excerpts on demand.
Three verbs: Search (ranked refs), Read (bounded excerpt around a ref), Outline (a session's goal→resolution arc).
Index ¶
- Constants
- func CleanSnippetOneline(s string) string
- func LocateSession(session8 string, scope []view.Scope, more ScopeFn) (dbPath, fullSID string, err error)
- func MoreWindow(level int) int
- func NormalizeSessionArg(s string) string
- func OutlineAndRender(w io.Writer, session8 string, scope []view.Scope, more ScopeFn, ...) error
- func ReadAndRender(w io.Writer, ref string, scope []view.Scope, more ScopeFn, opts ReadOpts, ...) error
- func RenderSearchOneline(w io.Writer, env SearchEnvelope)
- func SearchAndRender(w io.Writer, query string, scope []view.Scope, opts SearchOpts, ...) error
- func TopicsAndRender(w io.Writer, query string, scope []view.Scope, opts TopicsOpts, wantJSON bool) error
- type ErrAmbiguousSession
- type ErrAmbiguousUUID
- type ErrMsgNotFound
- type ErrSessionNotFound
- type OutlineOpts
- type OutlineResult
- type ReadOpts
- type ReadResult
- type ScopeFn
- type ScopeReport
- type SearchEnvelope
- type SearchOpts
- type SearchRef
- type SubagentInfo
- type TopicHit
- type TopicsOpts
- type TopicsResult
- type VectorCoverage
- type Warning
Constants ¶
const ( DefaultSearchLimit = 8 // top-N conversations to surface (matches the human --limit default) DefaultReadBudget = 4000 // chars; the ceiling a bare --budget uses (no longer a default cap — reads are whole by default, #3) OutlineBookend = 4 // messages each end for the arc summary ReadWindow = 8 // ±messages around the anchor for Read )
Protocol constants.
const ( ScopeSearched = "searched" // indexed fresh + searched ScopeEmpty = "empty" // searched fresh, zero rows ScopeSkippedError = "skipped_error" // index/open failed — not searched ScopeStaleFallback = "stale_fallback" // busy-lock → searched a possibly-stale cached index // ScopeNotConsolidated marks a project whose own database exists but has // never been folded into the one store. A one-store read cannot see it, so it // is named rather than left out — the same rule the other statuses follow: a // corpus may shrink, but never silently. ScopeNotConsolidated = "not_consolidated" )
Scope status values for incompleteness-as-data (#6).
const ( StoreConsolidated = "consolidated" // one database, one query StorePerProject = "per-project" // the fan-out: one database per project )
Store values for SearchEnvelope.Store — which index actually answered.
const ( // WarnRecencySkew: relevance put an older hit on top while a much newer match // exists. Facts: newest (date of the freshest match). WarnRecencySkew = "recency_skew" // WarnBroadQuery: the terms are corpus-common, so the hit that matters is // buried under incidental mentions. Facts: matches, matches_is_lower_bound. WarnBroadQuery = "broad_query" // WarnCurrentTurnExcluded: candidates from the caller's own live turn were // withheld. Facts: excluded (how many). WarnCurrentTurnExcluded = "current_turn_excluded" // WarnScopeIncomplete: at least one project was not searched, or was served // from a possibly-stale cached index. Facts: scopes, incomplete, errored, stale. WarnScopeIncomplete = "scope_incomplete" // WarnNotConsolidated: one or more project databases have never been folded // into the one store, so their history was outside this answer. A corpus gap // with a known fix, which is why it carries the command. Facts: databases. WarnNotConsolidated = "not_consolidated" // WarnStoreFallback: the one store did not answer, so the per-project fan-out // did. The two rank differently, so a caller comparing answers has to know // which reader produced this one. Facts: store (which reader answered). WarnStoreFallback = "store_fallback" // WarnProjectSpread: the hits span several projects, so the set is wider than // one context. Facts: projects, sample. WarnProjectSpread = "project_spread" // WarnVectorGap: the semantic tier ran with partial vector coverage over the // searched corpus. Facts: candidate_msgs, vectored_msgs, missing_msgs. WarnVectorGap = "vector_gap" // WarnRawHistory: the standing reminder that these are raw transcripts, not // current truth. No facts — it is a property of the corpus, not of this query. WarnRawHistory = "raw_history" )
Warning codes. A code is the stable identity of an advisory — the string an agent branches on — so the human sentence beside it can be reworded without breaking a caller.
Variables ¶
This section is empty.
Functions ¶
func CleanSnippetOneline ¶ added in v0.9.0
CleanSnippetOneline strips ANSI escapes, newlines, tabs, and all control characters from s, collapsing consecutive whitespace so the snippet is strictly single-line plain text suitable for oneline output.
func LocateSession ¶
func LocateSession(session8 string, scope []view.Scope, more ScopeFn) (dbPath, fullSID string, err error)
LocateSession resolves a session8 prefix to its (db path, full session id) across scope (nil = all projects, enumerated through more only if the sweep is reached — see ScopeFn), the exported door onto the private locateSession. The `tag` verb uses it to find the db it must open read-write and the full session id whose messages it tags. Returns *ErrSessionNotFound / *ErrAmbiguousSession unchanged, so callers can render the same hints as Read and Outline.
func MoreWindow ¶ added in v0.9.0
MoreWindow maps a --more level (0 = none) to a window radius.
func NormalizeSessionArg ¶ added in v0.9.0
NormalizeSessionArg exposes the shared pasted-ref normalization to other session-taking command helpers.
func OutlineAndRender ¶
func OutlineAndRender(w io.Writer, session8 string, scope []view.Scope, more ScopeFn, opts OutlineOpts, wantJSON bool) error
OutlineAndRender resolves session8 within scope and writes its goal→resolution arc to w (JSON when wantJSON). The exported entry the top-level `outline` subcommand calls.
func ReadAndRender ¶
func ReadAndRender( w io.Writer, ref string, scope []view.Scope, more ScopeFn, opts ReadOpts, wantJSON bool, ) error
ReadAndRender resolves ref within scope and writes the bounded excerpt to w (JSON when wantJSON). The exported entry the top-level `read` subcommand calls, so reading is a top-level verb (`rawclaw read <ref>`). scope nil = every project, enumerated through more only if the one store cannot answer the ref (see ScopeFn).
func RenderSearchOneline ¶ added in v0.9.0
func RenderSearchOneline(w io.Writer, env SearchEnvelope)
RenderSearchOneline formats each search hit as exactly one tab-separated line: <sess8>:<uuid8>\t<started_iso>\t<project>\t<snippet_prose> It emits no conversational headers or footers and suppresses all ANSI styling.
func SearchAndRender ¶
func SearchAndRender( w io.Writer, query string, scope []view.Scope, opts SearchOpts, embedder embed.Embedder, scopeLabel string, wantJSON bool, ) error
SearchAndRender runs Search and writes the result to w: the agent envelope as text, or as JSON when wantJSON. This is the exported entry the default CLI path calls so a bare `rawclaw "query"` IS the search — search is the default verb. scopeLabel is the human-facing "across all projects" / "on <project>" suffix in the text header.
func TopicsAndRender ¶
func TopicsAndRender(w io.Writer, query string, scope []view.Scope, opts TopicsOpts, wantJSON bool) error
TopicsAndRender runs Topics and writes the result to w (JSON when wantJSON, else text). The exported entry the top-level `topics` subcommand calls.
Types ¶
type ErrAmbiguousSession ¶
type ErrAmbiguousSession struct {
Prefix string
Candidates []sessionCand
}
ErrAmbiguousSession is returned when a session8 prefix matches more than one session across scope. It mirrors the resume path (cli.runResume): list the candidates, resolve none.
func (*ErrAmbiguousSession) Error ¶
func (e *ErrAmbiguousSession) Error() string
type ErrAmbiguousUUID ¶
type ErrAmbiguousUUID struct{ UUID8 string }
ErrAmbiguousUUID is returned when a uuid8 prefix matches more than one message in the session (a 32-bit prefix collision) — resolving none, git-style.
func (*ErrAmbiguousUUID) Error ¶
func (e *ErrAmbiguousUUID) Error() string
type ErrMsgNotFound ¶
type ErrMsgNotFound struct{ UUID8 string }
ErrMsgNotFound is returned when no message in the located session carries the uuid8 prefix.
func (*ErrMsgNotFound) Error ¶
func (e *ErrMsgNotFound) Error() string
type ErrSessionNotFound ¶
type ErrSessionNotFound struct{ Prefix string }
ErrSessionNotFound is returned when a session8 prefix matches nothing in scope.
func (*ErrSessionNotFound) Error ¶
func (e *ErrSessionNotFound) Error() string
type OutlineOpts ¶ added in v0.9.0
type OutlineOpts struct {
IncludeTools bool
IncludeThinking bool
IncludeSubagents bool
ScopeFallback ScopeFn
}
Outline returns a session's bookend arc (first/last N user+assistant messages). OutlineOpts groups optional inclusion and fallback options for Outline.
type OutlineResult ¶
type OutlineResult struct {
Project string `json:"project"`
SessionID string `json:"session_id"`
ISO string `json:"iso"`
MessageCount int `json:"message_count"`
Start []view.ViewMsg `json:"start"`
End []view.ViewMsg `json:"end"`
MidCount int `json:"mid_count"`
Topics []string `json:"topics,omitempty"` // topic-layer segment labels for this session, in order
Subagents []SubagentInfo `json:"subagents,omitempty"` // child subagent threads for this session
}
OutlineResult is a session's bookend arc.
func Outline ¶
Outline returns a session's bookend arc (first/last N user+assistant messages). Returns an error if the session is not found. A pasted read-ref token ("ref=<session8>:<uuid8>" or "<session8>:<uuid8>") resolves via its session half.
func OutlineWith ¶ added in v0.9.0
func OutlineWith(session8 string, scope []view.Scope, opts OutlineOpts) (*OutlineResult, error)
OutlineWith returns a session's outline with granular inclusion options.
type ReadOpts ¶
type ReadOpts struct {
Focus string
Budget *int // nil = no cap (the default since #3)
IncludeTools bool
IncludeThinking bool
IncludeSubagents bool
Window int
Around int
// ScopeFallback supplies the project list for a nil scope, called ONLY if
// the one store cannot answer the id (see ScopeFn). Nil = every project
// under a background context.
ScopeFallback ScopeFn
}
ReadOpts groups the read-verb options (keeps Read's signature small, like view.AnchoredViewOpts). Window/Around express the expand-in-place ladder (#4):
- Window == 0 → the default ±ReadWindow context (rung 2).
- Window > 0 → an explicit window radius (rung 3 --more / rung 4).
- Around > 0 → re-center the window `Around` messages after the anchor (scroll within the session on the SAME stable ref).
type ReadResult ¶
type ReadResult struct {
Project string `json:"project"`
SessionID string `json:"session_id"`
AnchorID int `json:"anchor_id"`
FocusSnippet string `json:"focus_snippet"`
CharBudget *int `json:"char_budget"` // nil = no cap
ReadRef string `json:"read_ref"` // the stable "<session8>:<uuid8>" ref this read resolved
Truncated bool `json:"truncated"`
// Never-silent trim (#5): when Truncated, these carry the machine counts AND
// the literal command an agent re-issues to recover the hidden content. Empty
// when nothing was trimmed.
TrimmedChars int `json:"trimmed_chars,omitempty"`
TrimmedMsgs int `json:"trimmed_msgs,omitempty"`
NextCommand string `json:"next_command,omitempty"`
Subagents []SubagentInfo `json:"subagents,omitempty"`
*view.AnchoredView
}
ReadResult is a bounded excerpt around a ref. Embeds the AnchoredView shape plus protocol metadata.
func Read ¶
Read returns a bounded excerpt around the message identified by ref ("<session8>:<uuid8>"). opts.Budget of nil = no cap. Returns an error on a bad ref or a session/message not found. Expansion (--more/--around) is a cheap follow-up on the SAME resolved ref — it never re-runs search (#4).
type ScopeFn ¶ added in v0.9.0
ScopeFn builds the per-project scope list on demand. Building it is not a listing: it opens every per-project index — and after a schema change, migrates every one of them — which on a real corpus costs minutes, more than the run's watchdog allows. An id the one store can answer needs no list at all, so the verbs take the list as a function and call it only if the store comes up empty. SearchOpts.ScopeFallback is the same seam for search; a nil ScopeFn means "every project", resolved under a background context.
type ScopeReport ¶
type ScopeReport struct {
Project string `json:"project"`
Dir string `json:"dir"`
Status string `json:"status"`
Detail string `json:"detail,omitempty"`
}
ScopeReport records how one project scope fared during a search, so an agent reads a partial result AS partial instead of mistaking it for complete (#6).
type SearchEnvelope ¶
type SearchEnvelope struct {
Results []SearchRef `json:"results"`
Scopes []ScopeReport `json:"scopes_report"`
Complete bool `json:"complete"`
Count int `json:"count"`
TotalMatches int `json:"total_matches"`
TotalIsLowerBound bool `json:"total_is_lower_bound,omitempty"`
HasMore bool `json:"has_more"`
NextCommand string `json:"next_command,omitempty"`
// VectorCoverage reports candidate embedding coverage across the searched
// corpus, and whether the semantic tier ran.
VectorCoverage VectorCoverage `json:"vector_coverage"`
// Warnings carries every advisory the search wants to raise, as data. Each
// entry states a code, the fact that triggered it, and the human line — so an
// agent can branch on Code instead of pattern-matching English, and the text
// renderer has nothing to decide beyond printing what is present. Empty means
// the search had nothing to warn about, which is the common case for a narrow
// query with clean hits.
Warnings []Warning `json:"warnings,omitempty"`
// ExcludedCurrentTurn counts the candidates withheld as the caller's own live
// turn (SearchOpts.CurrentSession). Reported rather than dropped quietly: an
// agent that knows a record was withheld can ask for it; one that doesn't
// just sees a hole. 0 = nothing was withheld, which is the case whenever the
// caller didn't say where it was.
ExcludedCurrentTurn int `json:"excluded_current_turn,omitempty"`
// Store names which index answered — the one consolidated store, or the
// per-project fan-out. The two rank differently (one corpus vs one corpus per
// project), so a reader comparing two answers has to be able to tell them
// apart. StoreNote says why the fan-out was used, and is empty otherwise.
Store string `json:"store,omitempty"`
StoreNote string `json:"store_note,omitempty"`
}
SearchEnvelope wraps the ranked results with the per-scope completeness report. Complete is false if any scope was skipped, served from a stale fallback, or had matches the limit/fetch window hid.
The truncation block is the never-silent counterpart of ReadResult's trim fields: an agent must never receive N results without learning the set is larger. Count is len(Results). TotalMatches is the DISTINCT candidates found within the fetch window; TotalIsLowerBound is true when a scope hit the fetch ceiling, so the true total is >= TotalMatches (it's a floor, never claimed as exact). HasMore is set whenever anything was hidden, and NextCommand is the literal command an agent re-issues to widen.
func Search ¶
func Search(rawQuery string, scope []view.Scope, opts SearchOpts, embedder embed.Embedder) SearchEnvelope
Search returns rank-ordered conversation refs matching query within scope, wrapped in an envelope that reports completeness (#6) so a partial result is never mistaken for complete. With a non-nil embedder in relevance mode (no --sort), keyword anchors are RRF-fused with vector-KNN anchors — parity with the default discovery path.
The scope argument selects which reader answers, and the two rank differently:
- nil scope means "no project list decided this call" — the one consolidated store answers it, one database and one query, with Project / Source / IncludePath narrowing pushed down as WHERE clauses on the row. Every hit is then scored against ONE corpus, which is what makes the ranking comparable across projects.
- a non-nil scope is an explicit list of projects to fan out over, one database each. Scores from separate databases are NOT comparable (bm25 is computed per database), so this ordering is a merge of per-project rankings, and it stays only for the cases the one store cannot serve.
The nil-scope path falls back to the fan-out — via opts.ScopeFallback — when the store is absent or empty, or when the requested narrowing names a project the store has never heard of. Falling back is always announced in StoreNote.
type SearchOpts ¶
type SearchOpts struct {
Limit int
Role string
Sort string
IncludeTools bool
IncludeSubagents bool
Since string // "" = no bound; else YYYY-MM-DD inclusive
Before string // "" = no bound; else YYYY-MM-DD inclusive
MinMessages int // 0 = no minimum
IncludePath string // "" = no filter; else a regex over the project working dir
ExcludePath string // "" = no filter; else a regex over the project working dir
Oneline bool // emit results in oneline format (<read_ref>\t<started_iso>\t<project>\t<snippet>)
// CurrentSession is the session the caller is live in ("" = unknown, the
// pre-existing behavior). Its CURRENT TURN — and only that — is withheld from
// the results; see dropCurrentTurn for what the turn is and why the rest of
// the session stays searchable.
CurrentSession string
// Project and Source narrow the ONE store by column instead of by choosing
// which databases to open: Project is the exact project label (the same label
// paths.ProjectLabel stamps on the row), Source is the source tool. Both empty
// = the whole corpus. They are read only on the one-store path; the fan-out
// narrows by the scope list it is given.
Project string
Source string
// ScopeFallback supplies the per-project scope list, called ONLY if the
// one-store path cannot answer. It is a function because enumerating every
// project costs seconds of directory and git probing on a real corpus, and
// the whole point of the one store is not to pay that on a search that never
// touches it. Nil falls back to every project under a background context.
ScopeFallback func() []view.Scope
}
SearchOpts groups the optional search filters (keeps the signature small). The scope filters (Role/Since/Before/MinMessages/IncludePath/ExcludePath) mirror the default-discovery flags so the default search honors the SAME scoping the human path does, instead of leaking their values into the FTS5 query.
type SearchRef ¶
type SearchRef struct {
Project string `json:"project"`
SessionID string `json:"session_id"`
ISO string `json:"iso"`
Snippet string `json:"snippet"`
ReadRef string `json:"read_ref"`
// Topic is the topic-layer label covering the matched message, attached
// after ranking as a display-only annotation (see attachTopics). It never
// participates in matching or ordering. Empty for an untagged session;
// omitempty then keeps the JSON identical to an untagged corpus.
Topic string `json:"topic,omitempty"`
// Last is where the hit's session ENDED UP: its most recent real activity,
// attached after ranking (see attachLastActivity). A hit is a point in the
// middle of a conversation — in a 1500-message session the match can predate
// the session's actual conclusion by hours, and nothing on the hit line said
// so. Like Topic this is display-only and cannot influence ranking. Empty
// when the session's whole scanned tail is machinery, which renders as no
// line at all rather than a hit captioned with a tool result.
Last string `json:"last,omitempty"`
// Missing is true when this conversation's backing source file is gone but its
// content was retained in the index (durable retention, D1). Surfaced so a
// retained-but-missing hit is not read as current state (D7). omitempty keeps
// the JSON byte-identical for the common present case.
Missing bool `json:"missing,omitempty"`
// Routine is true when this conversation carries an effective routine verdict.
// omitempty keeps the JSON byte-identical for normal sessions.
Routine bool `json:"routine,omitempty"`
}
SearchRef is one ranked conversation ref. The ReadRef token ("<session8>:<uuid8>") is what an agent passes to Read.
type SubagentInfo ¶ added in v0.9.0
type SubagentInfo struct {
SessionID string `json:"session_id"`
MessageCount int `json:"message_count"`
Title string `json:"title,omitempty"`
}
SubagentInfo is one child subagent session of a parent session.
type TopicHit ¶
type TopicHit struct {
Topic string `json:"topic"`
Project string `json:"project"`
ReadRef string `json:"read_ref"`
Routine bool `json:"routine,omitempty"`
}
TopicHit is one on-demand topic-finder result: a tagged topic label, its project, and a read-ref (<session8>:<uuid8>) pointing at where that topic BEGINS (the segment's start message). Topics are deliberately OUT of the default search ranking — this is the separate tool an agent reaches for only when a normal search is ambiguous.
type TopicsOpts ¶ added in v0.9.0
type TopicsOpts struct {
Limit int
Project string // "" = every project; else the one project to search
IncludePath string // "" = no filter; else a regex over the project working dir
// ScopeFallback supplies the project list for a nil scope, called ONLY if
// the one store cannot answer (see ScopeFn). Nil = every project under a
// background context.
ScopeFallback ScopeFn
}
TopicsOpts groups the topic-finder's scope narrowing. Both narrowing fields resolve to project labels against the one store: Project is already a label, IncludePath is a pattern matched in Go against the (project, working directory) pairs the store knows about.
type TopicsResult ¶
type TopicsResult struct {
Query string `json:"query"`
Hits []TopicHit `json:"hits"`
Note string `json:"note,omitempty"`
}
TopicsResult wraps the topic-finder hits with the query and a note. Note is the helpful empty-state hint when no topics are tagged anywhere in scope.
func Topics ¶
func Topics(query string, scope []view.Scope, opts TopicsOpts) (TopicsResult, error)
Topics searches ONLY the topic layer, returning hits ordered by FTS rank and capped at Limit. Each hit resolves the segment's START message id (what MatchTopics returns) to its uuid for a read-ref. It never touches the keyword/vector ranking — this is the separate, on-demand finder. The empty-state note distinguishes "no match" from "nothing tagged yet".
Ordering is GLOBAL, across every project at once, because the whole corpus is one FTS index: bm25 folds in corpus statistics, and those are only comparable when every candidate was scored against the same corpus. Limit is therefore a cap on the combined list, not a per-project quota — the old per-project cap existed only because a merge across independent databases could not be ordered, and it let a weak hit from a small project sit alongside a strong one from a large project as though they ranked equally.
scope is the fallback path's project enumeration, used only when the one store cannot answer.
type VectorCoverage ¶ added in v0.10.0
type VectorCoverage struct {
Ran bool `json:"ran"`
CandidateMsgs int `json:"candidate_msgs"`
VectoredMsgs int `json:"vectored_msgs"`
MissingMsgs int `json:"missing_msgs"`
}
VectorCoverage records candidate embedding coverage across the searched corpus, and whether the semantic tier ran.
type Warning ¶ added in v0.9.0
type Warning struct {
Code string `json:"code"`
Message string `json:"message"`
Facts map[string]any `json:"facts,omitempty"`
}
Warning is one advisory carried as data rather than prose (the "warnings are data, not prose" doctrine recorded in the prior-art survey under docs/design, borrowed from robot-mode output in other agent-facing tools).
Code is what an agent branches on. Facts carries the measurement that made the warning fire, so a caller can apply its own threshold instead of inheriting ours. Message is the same statement in English, and exists so the text renderer holds no copy of its own — the two surfaces cannot drift because there is only one string.