Documentation
¶
Overview ¶
Package spanembed embeds the span projection for semantic search.
Embeddings are a derived-side concern: the derive/worker family is the single writer (the ingest hot path never embeds), and every embedding is keyed by deterministic span identity (org_id, trace_id, span_id) so the pass is idempotent — re-deriving or re-running embeds each span exactly once, skipping identities whose content, model, and dimensions are already current.
Only call_kind="main" llm spans embed. Shadow calls (permission checks, title generation) are the plurality of llm calls in a harness session and poison search relevance; tool and event spans carry payloads better served by structured queries. The embedded text is the span's delta-only content — the fresh input blocks plus the response blocks rendered to text — never the re-sent conversation history, which is what keeps selective embedding cheap.
Index ¶
- Constants
- Variables
- func ContentHash(text string) string
- func RenderSpanText(input, output json.RawMessage) string
- type Candidate
- type ChunkRecord
- type FailureRecord
- type Hit
- type Key
- type Pass
- type PassConfig
- type Report
- type Sink
- type Source
- type Store
- func (s *Store) EnsureSchema(ctx context.Context) error
- func (s *Store) ListCandidates(ctx context.Context, after Key, limit int) ([]Candidate, error)
- func (s *Store) PruneOrphans(ctx context.Context) (int64, error)
- func (s *Store) RecordFailure(ctx context.Context, rec FailureRecord) error
- func (s *Store) Search(ctx context.Context, orgID string, embedding []float32, topK int) ([]Hit, error)
- func (s *Store) UpsertSpanChunks(ctx context.Context, rec ChunkRecord) error
- type StoreConfig
Constants ¶
const DefaultBatchSize = 100
DefaultBatchSize bounds one candidate page.
const DefaultMaxTextBytes = 1 << 20
DefaultMaxTextBytes caps a single span's rendered text. Chunking splits an oversized span into ceil(tokens/budget) pieces, so an unbounded span would fan out into hundreds of embed calls and rows and hold several copies of its text in memory at once. Past this ceiling the span is recorded as a deterministic "too_large" failure instead — bounding per-span memory, embed cost, and chunk count regardless of batch size or pod limits. ~1 MiB is ~33 chunks at the model's per-chunk budget. 0 disables the guard.
const ( // DefaultTableName is the span-embedding table. It lives in the // same database as the versioned spans projection — candidate selection // and search both join against the current physical table family. DefaultTableName = "span_embeddings" )
Variables ¶
var ErrNotInitialized = errors.New("span embeddings not initialized: run the embed pass (tapes serve embed-worker or tapes dev embed-spans)")
ErrNotInitialized is returned by reads when the embedding table does not exist yet — i.e. no embed pass has ever run against this store.
Functions ¶
func ContentHash ¶
ContentHash returns the hex sha256 of the rendered span text — the change detector that makes re-derives cheap: unchanged content under an unchanged model never re-embeds.
func RenderSpanText ¶
func RenderSpanText(input, output json.RawMessage) string
RenderSpanText renders a span's stored delta-only content to the text that gets embedded: the text blocks of the fresh input followed by the text blocks of the output. Tool payloads, thinking, and images are deliberately excluded — they are structured data, not prose, and they drown the signal the search exists for.
Types ¶
type Candidate ¶
type Candidate struct {
OrgID string
TraceID string
SpanID string
SessionID string // "" when the span derived without attribution
// Input and Output are the span's stored delta-only content-block
// arrays (JSONB), nil when empty.
Input json.RawMessage
Output json.RawMessage
// ExistingHash and ExistingModel describe the current embedding
// row; both empty when the span has never been embedded.
ExistingHash string
ExistingModel string
// ExistingFailHash and ExistingFailModel describe a recorded
// deterministic embed failure; both empty when the span has no
// failure marker. A span whose current content and model match a
// recorded failure is skipped instead of re-attempted.
ExistingFailHash string
ExistingFailModel string
}
Candidate is one main llm span considered for embedding, joined with its existing embedding row (zero values when not yet embedded).
type ChunkRecord ¶ added in v0.19.0
type ChunkRecord struct {
OrgID string
TraceID string
SpanID string
SessionID string
Model string
ContentHash string
Embeddings [][]float32
}
ChunkRecord is one span's embedding write. A span that fit the model's context window has a single embedding; an oversized span was split into pieces and carries one embedding per piece, stored under chunk_idx 0..N-1 in slice order.
type FailureRecord ¶ added in v0.19.0
type FailureRecord struct {
OrgID string
TraceID string
SpanID string
SessionID string
Model string
ContentHash string
Reason string
ErrorDetail string
TokenCount int
}
FailureRecord captures a deterministic embed failure so the pass can stop retrying the span while keeping the loss observable: why it failed, how big the input was, and (via the store) how many times it has failed.
type Hit ¶
type Hit struct {
TraceID string
SpanID string
SessionID string
Score float32
UserPrompt string
Snippet string
Model string
StartedAt time.Time
}
Hit is one similarity-search result with its trace/turn context.
type Pass ¶
type Pass struct {
// contains filtered or unexported fields
}
Pass walks every eligible span and embeds the ones whose embedding is missing or stale. Idempotent by construction: span identity keys the writes and a content hash gates them, so running the pass twice (or concurrently with a re-derive) embeds each span at most once per content+model.
func NewPass ¶
func NewPass(src Source, sink Sink, embedder embeddings.Embedder, cfg PassConfig, log *slog.Logger) (*Pass, error)
NewPass creates an embed pass.
func (*Pass) Run ¶
Run executes one full pass: prune orphaned embeddings, then page through every main llm span and embed the missing/stale ones.
Error discipline: a per-span embed or write failure is counted and logged but never aborts the pass — the span stays un-embedded and the next run retries it. Only infrastructure failures (candidate listing) abort, since they would starve every remaining page.
type PassConfig ¶
type PassConfig struct {
// Model names the embedding model; stored per row so a model
// switch re-embeds existing spans.
Model string
// Dimensions is the expected embedding dimensionality. The first
// vector the model returns is checked against it, so a
// model/dims misconfiguration aborts the pass with one clear
// error instead of failing every row's insert against the sized
// vector column.
Dimensions uint
// BatchSize bounds one candidate page (default DefaultBatchSize).
BatchSize int
// MaxTextBytes caps a span's rendered text; a larger span is recorded
// as a "too_large" failure rather than embedded, bounding per-span
// memory, chunk count, and embed cost. Zero takes DefaultMaxTextBytes;
// negative disables the guard.
MaxTextBytes int
}
PassConfig configures one embed pass.
type Report ¶
type Report struct {
// Scanned counts every candidate span considered.
Scanned int `json:"scanned"`
// Embedded counts spans embedded this pass (new or re-embedded
// after a content/model change).
Embedded int `json:"embedded"`
// UpToDate counts spans skipped because their embedding already
// matches the current content and model.
UpToDate int `json:"up_to_date"`
// Empty counts spans skipped because their delta content renders
// to no text at all (e.g. a pure tool-call response).
Empty int `json:"empty"`
// Poisoned counts spans skipped because they already failed
// deterministically under this content and model; they are not
// re-attempted until their content or model changes.
Poisoned int `json:"poisoned"`
// Chunked counts spans whose text exceeded the model's context
// window and was split into multiple embedded pieces.
Chunked int `json:"chunked"`
// ChunkRows counts total embedding rows written this pass (one per
// piece), so chunked spans contribute more than one.
ChunkRows int `json:"chunk_rows"`
// Oversized counts spans the model rejected as too large (whether the
// split then succeeded or exhausted the depth cap).
Oversized int `json:"oversized"`
// Failed counts spans whose embed or write failed this pass — both
// transient faults (retried next pass) and deterministic ones (also
// recorded and skipped thereafter).
Failed int `json:"failed"`
// Pruned counts orphaned embedding rows removed (their span was
// pruned or reclassified by a re-derive).
Pruned int64 `json:"pruned"`
// OversizeTokens holds the reported/estimated token count of each
// oversized span this pass — the per-observation source for the embed
// worker's oversize-tokens histogram. Excluded from JSON to keep the
// logged summary scalar.
OversizeTokens []int `json:"-"`
// FailuresByReason counts deterministic failures recorded this pass,
// keyed by reason (e.g. "oversize", "api_400"). Excluded from JSON.
FailuresByReason map[string]int `json:"-"`
}
Report summarizes one embed pass.
type Sink ¶
type Sink interface {
UpsertSpanChunks(ctx context.Context, rec ChunkRecord) error
RecordFailure(ctx context.Context, rec FailureRecord) error
PruneOrphans(ctx context.Context) (int64, error)
}
Sink persists embeddings. *Store implements it; tests substitute a fake.
type Source ¶
type Source interface {
ListCandidates(ctx context.Context, after Key, limit int) ([]Candidate, error)
}
Source lists embed candidates. *Store implements it; tests substitute a fake.
type Store ¶
type Store struct {
// contains filtered or unexported fields
}
Store reads and writes span embeddings in the tapes Postgres database. The write path (EnsureSchema, Upsert, PruneOrphans) belongs to the derive-side embed pass; the read path (Search) backs the API's span search.
func NewStore ¶
NewStore wraps an existing connection pool. It performs no IO: writers must call EnsureSchema before upserting, readers may query immediately and receive ErrNotInitialized until a writer has run.
func (*Store) EnsureSchema ¶
EnsureSchema creates the embedding table and its HNSW index sized to the configured dimensions, or fail-fasts when an existing table was created with different dimensions. pgvector cannot resize a column in place, so a dims change requires re-embedding into a new (or dropped) table — the error says so instead of letting every subsequent insert fail.
func (*Store) ListCandidates ¶
ListCandidates pages every main llm span (keyset-ordered) joined with its current embedding state and any recorded failure. All of a span's chunk rows share one content hash and model, so the embedding join collapses them to a single row; the failure table is already one row per span. The caller decides per candidate whether an embed is due — content hashing happens in Go, so the query stays a cheap indexable join.
func (*Store) PruneOrphans ¶
PruneOrphans removes embedding and failure rows whose span no longer exists as a main llm span — pruned by a re-derive, or reclassified out of the embeddable set. Returns the total number of rows removed across both tables.
func (*Store) RecordFailure ¶ added in v0.19.0
func (s *Store) RecordFailure(ctx context.Context, rec FailureRecord) error
RecordFailure marks a span as deterministically un-embeddable under its current content and model. The candidate gate skips such spans until their content (hash) or model changes; attempts accrue across passes so the table shows how persistent the failure is.
func (*Store) Search ¶
func (s *Store) Search(ctx context.Context, orgID string, embedding []float32, topK int) ([]Hit, error)
Search returns the topK spans most similar to the query embedding, joined with their trace context (turn prompt and span payloads for the snippet). A span may have several chunk embeddings; results collapse to one hit per span, scored by the span's best-matching chunk. Scoped to one org — search is a tenant-facing read.
func (*Store) UpsertSpanChunks ¶ added in v0.19.0
func (s *Store) UpsertSpanChunks(ctx context.Context, rec ChunkRecord) error
UpsertSpanChunks writes a span's embeddings as chunk rows 0..N-1 and removes any higher-indexed rows left over from a previous, longer chunking of the same span — all in one transaction so search never sees a partially-rewritten span. A successful write also clears any failure marker for the span: the content that once failed now embeds. Re-derives keep span identity stable, so re-embedding overwrites in place.
type StoreConfig ¶
type StoreConfig struct {
// TableName defaults to DefaultTableName.
TableName string
// Dimensions is the embedding dimensionality. The table's vector
// column is created with exactly this size and EnsureSchema
// fail-fasts when an existing table disagrees — model and dims are
// a deliberate, explicit pairing (e.g. text-embedding-3-large@1024
// in cloud, embeddinggemma@768 on a local/dev deployment).
Dimensions uint
// OrgID optionally scopes candidate listing and orphan pruning to
// one tenant. Empty embeds every org.
OrgID string
}
StoreConfig configures a span-embedding store.