agentproto

package
v0.10.0 Latest Latest
Warning

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

Go to latest
Published: Aug 25, 2026 License: MIT Imports: 23 Imported by: 0

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

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

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

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

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

func CleanSnippetOneline(s string) string

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

func MoreWindow(level int) int

MoreWindow maps a --more level (0 = none) to a window radius.

func NormalizeSessionArg added in v0.9.0

func NormalizeSessionArg(s string) string

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

func Outline(session8 string, scope []view.Scope, includeTools bool) (*OutlineResult, error)

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

func Read(ref string, scope []view.Scope, opts ReadOpts) (*ReadResult, error)

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

type ScopeFn func() []view.Scope

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(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.

Jump to

Keyboard shortcuts

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