memory

package
v0.0.0-...-2769864 Latest Latest
Warning

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

Go to latest
Published: Oct 6, 2026 License: Apache-2.0 Imports: 18 Imported by: 0

README

pkg/memory — storage, search, knowledge graph, and the append-only audit kinds

The agent-memory framework. Three layers stack here, each with its own pluggable interface:

Layer Interface Backends today
Storage memory.Backend inmem/, sqlite/, postgres/, shadow/ (dual-write)
Ranked retrieval memory.SearchProvider search/inmem/, search/postgres/, search/sqlite/, search/graphiti/
Knowledge graph memory.KGProvider kg/graphiti/

All three are chosen by dependency injection in internal/cmd/operator, not by a registry — there is exactly one concrete set per process, picked from env (POSTGRES_URI, EMBEDDING_ENDPOINT, GRAPHITI_ENDPOINT, MEMORY_READ_SOURCE).

Local (facade.go) is the in-process Memory implementation: it wraps a Backend, owns the per-(Scope, Kind) ScopeHooks materialization, and fans out signal dispatch. httpclient speaks the same Memory interface over HTTP, so the runner, oap and channelsd get the facade's semantics without an in-process backend.

Query and Search coexist deliberately

  • Query is boolean filtering: tags, indexed content fields, time ranges, links. It returns what matches, unranked.
  • Search is scoring. It delegates to search.CompositeSearcher, which fans out to every registered SearchProvider concurrently, merges with Reciprocal Rank Fusion, and post-filters through SpiceDB.

Neither subsumes the other. Do not "unify" them.

Append-only kinds are Ed25519-signed into a hash chain

This is the invariant a newcomer is most likely to break.

The transcript (turn), authz_decision, tool_session and the *_audit kinds declare Retention{AppendOnly: true}. The Local facade enforces it:

  • A Put that changes existing content returns ErrAppendOnlyConflict. A byte-identical re-put is idempotent (compared on canonical form, so a postgres round trip does not count as a change).
  • Per-entry Delete is refused. Scope-level deletion (retention, session GC) is still allowed.
  • Every such entry carries a Provenance envelope — publisher, keyID, a per-(scope, publisher) monotonic seq, prevHash, and a signature over a canonical digest. Seq + prevHash form a hash chain per (scope, publisher), making modification, fabrication, gaps, reordering and (with the tail anchor) truncation detectable.
  • The facade verifies on write. Unsigned or forged append-only writes are rejected from token callers and in-process writers.

Operational consequence: oap memory put / oap memory delete of an append-only kind is rejected. Only the runner, the components and the operator may write those kinds, and only through the signing path. oap audit verify <session> walks every chain offline and exits non-zero on hard findings.

See provenance/ and publisherkeys/.

Subpackages

Package What it holds
assetlimits/ One shared size ceiling for inbound attachments. Zero dependencies, deliberately
httpclient/ memory.Memory over the operator's HTTP API
httpsrv/ The bearer-authed HTTP API serving a memory.Memory
inmem/ In-memory Backend — structured filters only
kg/ Knowledge-graph provider implementations
kinds/ The ~31 registered memory Kinds
memcopy/ Turn-anchored memory-copy primitive used by the restart/fork reconciler
memtest/ Backend test doubles, incl. the postgres-reshaping RoundTripBackend
postgres/ Postgres Backend
provenance/ Ed25519 signing + verification + chain heads for append-only entries
publisherkeys/ Publisher → keyID → Ed25519 pubkey store for component publishers
search/ CompositeSearcher, the RRF ranker, and the search providers
sentinel/ Single source of truth for how a memory sentinel error crosses the HTTP wire
shadow/ Dual-write Backend (inmem primary + postgres secondary) with switchable reads
spicedbauthorizer/ memory.Authorizer over SpiceDB's memory_entry type
sqlite/ SQLite Backend — a strict peer of postgres, not a degraded inmem
tokens/ Per-AgentSession bearer-token registry used by the HTTP handler

Note: the KGProvider interface itself lives at kg.go in this package, not under kg/ — kg/ holds only the implementation.

Documentation

Overview

Package memory is the agent-memory framework: the Memory interface (Put/Query/Search/SendSignal), the Kind registry, the Local facade over a pluggable Backend, generic Query and scope semantics, signal dispatch, and the provider-neutral transcript types that flow over the HTTP API. A session's transcript is the turn Kind (kinds/turn) over that same Backend; the Backend implementations live in inmem/, sqlite/, postgres/ and shadow/.

Index

Constants

View Source
const PermissionViewMemory = "view_memory"

PermissionViewMemory is the permission that names a resource pool's AUDIENCE: whoever holds it on the resource may see what the pool holds.

It is what a read of pool entries is expanded under — the per-datum tag for a pool's entries is minted against `<type>:<id>#view_memory` — and it is deliberately NOT the permission the session reached the pool through. A session may hold a slot grant of `(customer, read)`; that says the agent may work with the resource, and says nothing about who may see the resource's memory. Minting under the slot's permission would give the datum whatever audience `read` happens to expand to, which nothing downstream would notice.

Named here, beside ScopeKindResource, because the pool scope and the pool audience are one design: a scope of this Kind is exactly the set of entries this permission governs.

View Source
const ScopeKindResource = "resource"

ScopeKindResource is the Scope.Kind for memory that lives on a RESOURCE rather than a session. Its ID is a SpiceDB object reference, "<type>:<id>".

Type-qualified rather than a bare id for two reasons. Audience derivation needs the type to know which definition's view_memory to expand; and a bare id collides across types the moment two resource types share one.

View Source
const ScopeKindUser = "user"

ScopeKindUser is the per-user scope kind (see the Scope doc in entry.go — reserved there since the framework's inception, first used by the user_preference kind).

Variables

View Source
var (
	// ErrAppendOnlyConflict: Put attempted to change an existing entry
	// of an append-only Kind. Byte-identical re-puts are not conflicts.
	ErrAppendOnlyConflict = errors.New("append-only kind: entry exists with different content")
	// ErrAppendOnlyKind: per-entry Delete attempted on an append-only
	// Kind (scope-level retention deletion is unaffected).
	ErrAppendOnlyKind = errors.New("append-only kind: entries cannot be deleted")
	// ErrProvenanceRequired: a write to an append-only Kind lacked a Provenance
	// envelope. Raised by the operator's verify-on-write; httpsrv answers 403.
	ErrProvenanceRequired = errors.New("append-only kind: signed provenance required")
	// ErrBadProvenance: the Provenance envelope failed verification.
	ErrBadProvenance = errors.New("append-only kind: provenance verification failed")
	// ErrInvalidEntry: Put refused the Entry on its own merits — an unregistered
	// Kind, or an ID / link ID not carrying its Kind's prefix. It separates a
	// CALLER's bug, which no retry can fix, from a store or authorization fault,
	// which a retry usually does.
	//
	// Load-bearing across the wire: httpsrv maps it to 400 and everything
	// unrecognized to 500, and httpclient retries 5xx while treating 4xx as
	// permanent — so the sentinel is what keeps a transient store fault from
	// reaching the caller as a permanent bad request nobody retries. Wrap every
	// new validation refusal in Put with it; never wrap a fault.
	ErrInvalidEntry = errors.New("memory: invalid entry")
	// ErrInvalidQuery is ErrInvalidEntry's read-side twin: the REQUEST is wrong
	// on its own merits, so no retry can make it succeed — a Query whose
	// FieldEquals.Path names no content key of any Kind it queries (see
	// validateFieldPaths), or a knowledge-graph read whose entity UUID is not
	// UUID-shaped.
	//
	// Same wire weight as ErrInvalidEntry: httpsrv answers 400 and httpclient
	// treats 4xx as permanent. Without it the agent's query_memory tool — whose
	// Kinds and FieldEquals come from LLM-supplied arguments — spent eight
	// retries and ~17s of a turn on a guessed field name before the "did you
	// mean" surfaced. Wrap every read-side refusal that blames the caller;
	// never wrap a store fault.
	ErrInvalidQuery = errors.New("memory: invalid query")
	// ErrKGScopeMismatch: a scoped knowledge-graph read named an entity that
	// belongs to another session's graph. A policy refusal, not a fault —
	// permanent, so httpsrv answers 403 rather than letting the caller retry
	// it eight times. The refusal text deliberately says nothing about which
	// group the entity IS in; see graphiti.GetEntity.
	ErrKGScopeMismatch = errors.New("memory: entity is not in this session's graph")
	// ErrKGUnsupported: a knowledge-graph read this deployment's provider
	// cannot answer at all — Graphiti's self-hosted REST server (graph_service)
	// has no endpoint that returns entities or communities, only facts, and one
	// read (GetEntity) cannot even be scope-checked because its response never
	// carries a group id. Not a fault and not the caller's error — a retry
	// cannot install an endpoint that does not exist, so httpsrv answers 404
	// like ErrNoSearchProviders rather than a retry loop. A richer KGProvider
	// (Graphiti's separate MCP server, or a future backend) might answer the
	// same call; this deployment's cannot.
	ErrKGUnsupported = errors.New("memory: knowledge-graph capability not supported by this deployment")
	// ErrKindNotSessionWritable: a per-session credential tried to author (Put
	// or Delete) a Kind whose WriteAuthority is ComponentWritten. A policy
	// refusal, not a fault — permanent, so httpsrv answers 403 and httpclient
	// does not retry it.
	//
	// The Kind-level half of the write door. The scope-level half
	// (ErrMissingApproval) answers only "may this credential write into this
	// session?", which for a runner's own session is always yes — so without
	// this half a session bearer could author every registered Kind in its
	// scope, including the component-owned records authzd and the operator read
	// to make authorization decisions.
	ErrKindNotSessionWritable = errors.New("memory: kind is not session-writable")
	// ErrKindNotSessionReadable: a per-session credential NAMED a Kind it may
	// not read. The mirror of ErrKindNotSessionWritable, and equally a policy
	// refusal rather than a fault — permanent, so httpsrv answers 403 and
	// httpclient does not retry it.
	//
	// It exists because some Kinds hold data the platform keeps ON BEHALF of a
	// session that the session's own agent must never see — redacted content
	// being the first. Returning an empty result instead would be worse than
	// refusing: the caller would read absence as "there is nothing there" and
	// carry on believing it had looked.
	ErrKindNotSessionReadable = errors.New("memory: kind is not session-readable")
)
View Source
var ErrIndexConflict = errors.New("memory: index conflict")

ErrIndexConflict signals that a turn Append's (index, role) collides with a previously-stored turn whose payload differs. Surfaced by turn.Appender.

View Source
var ErrMissingApproval = errors.New("memory: missing capability approval")

ErrMissingApproval is returned by EnsureApproval when the context carries no approval authorizing the requested (permission, resource). It is a sentinel so callers can map it to the right surface (HTTP 403, a denied CLI message).

View Source
var ErrNoSearchProviders = errors.New("memory: no search providers configured")

Functions

func CallerFrom

func CallerFrom(ctx context.Context) (string, bool)

CallerFrom reports the user id and whether one is set. Unlike TokenSessionFrom, the bool folds emptiness in: an empty caller is genuinely "no user", the normal state for a runner or component write.

func CanonicalContent

func CanonicalContent(raw json.RawMessage) json.RawMessage

CanonicalContent normalizes JSON content so equality is invariant under a JSONB round trip: the trip through interface{} sorts object keys and normalizes numbers, and json.Marshal re-emits without whitespace. Applied to BOTH sides of a comparison, all it asks of the storage layer is semantic JSON equality — exactly what JSONB preserves.

Invalid JSON falls back to the raw bytes, so a malformed payload still compares byte-for-byte rather than collapsing to equal-with-everything.

func CanonicalTime

func CanonicalTime(t time.Time) time.Time

CanonicalTime truncates t to the finest precision every backend can store. Postgres TIMESTAMPTZ holds microseconds and pgx truncates on the way in, so the nanoseconds a time.Now() CreatedAt carries do not survive the trip and must not participate in a sameness comparison. Truncation (not rounding) and UTC, to agree exactly with what the encoder did to the stored value.

func ContentKeys

func ContentKeys(t reflect.Type) map[string]struct{}

ContentKeys returns the top-level JSON object keys a Kind's Content struct serializes to — the vocabulary FieldFilter.Path draws from. `json:"-"` fields are omitted; an untagged exported field contributes its Go name, matching what encoding/json emits. An embedded struct with no JSON name is flattened as encoding/json flattens it; one with a name contributes only that name, since it serializes as a nested object.

nil for a nil or non-struct type, meaning "no vocabulary is knowable" — callers must NOT read it as "this Kind has no keys".

func EnsureApproval

func EnsureApproval(ctx context.Context, perm Permission, resource string) error

EnsureApproval returns nil iff ctx carries an approval authorizing (perm, resource); otherwise an error wrapping ErrMissingApproval that names the missing perm and resource. Rules differ by tier:

  • INTERNAL-tier perms (internal: prefix, e.g. AppendAudit) are satisfied ONLY by an internal-source approval matching (perm, resource) exactly. The unexported mintInternal is the sole producer of one, so a bearer or system approval — even one naming an internal: perm via the exported ForBearerToken/ForSpiceDBCheck — can never satisfy it. That is what makes "only pkg/memory can authorize an append-only write" unforgeable rather than conventional.
  • NON-internal perms are satisfied by a system approval (any resource), or by an exact (perm, resource) match from any source.

func InEntryAppendedDispatch

func InEntryAppendedDispatch(ctx context.Context) bool

InEntryAppendedDispatch reports whether ctx already carries an in-flight SignalEntryAppended dispatch. Exported (the mark itself stays unforgeable — only withEntryAppendedDispatch, unexported, can set it) because pkg/memory/provenance's SigningMemory needs to observe it too: SigningMemory holds a per-scope mutex across its own call into Put (Sign and the durable write must commit as one unit), and a ScopeHooks reaction reachable from inside that same call — via this dispatch — that Puts into the SAME scope through the SAME SigningMemory would re-enter that mutex on the same goroutine and deadlock, not merely recurse. Local.Put's own append-only write still happens once for a direct (unwrapped) caller — this flag only suppresses the SIGNAL a Put would otherwise emit, and lets a SigningMemory-wrapped caller suppress the WRITE itself, one layer up, where the deadlock would otherwise occur.

func IsResourceScope

func IsResourceScope(s Scope) bool

IsResourceScope reports whether s addresses a resource pool.

func KindAppendOnly

func KindAppendOnly(name string) bool

KindAppendOnly reports whether the named registered Kind is append-only. Unknown kinds read as mutable: append-only enforcement only ever tightens behavior for kinds that explicitly asked for it.

func NewID

func NewID(k Kind) string

NewID generates a unique ID for an Entry of Kind k: k.IDPrefix() plus 8 random bytes hex-encoded, so it satisfies Put's prefix validation by construction.

func ProbeLimit

func ProbeLimit(limit int) int

ProbeLimit returns the row count to request from a store so a truncated answer stays distinguishable from a complete one: one more row than the caller asked for. The extra row is never rendered — only the fact that it existed is (see TrimProbe).

An unlimited read (limit <= 0) needs no probe: nothing is discarded, so nothing can be hidden. A limit already at MaxInt is returned unchanged rather than wrapped negative, which a store would read as "unlimited" and answer with the whole scope.

func RegisterKind

func RegisterKind(k Kind)

RegisterKind adds k to the global Kind registry, called from each Kind package's init() so collisions panic at startup rather than at first Put. Panics on:

  • empty Name() or empty IDPrefix();
  • a Name() starting with "_" (reserved for HTTP scope routes like "_query");
  • duplicate Name();
  • exact IDPrefix() collision with another Kind;
  • any substring-prefix overlap with another Kind's prefix, so "op-" cannot coexist with "ops-" — an ID would otherwise validate under both.

func ResetRegistryForTest

func ResetRegistryForTest()

ResetRegistryForTest clears the registry. Tests only.

func ResourceRef

func ResourceRef(s Scope) (objType, objID string, ok bool)

ResourceRef splits a resource scope back into its SpiceDB object ref. ok is true only for an ID that ResourceScope could have produced — a resource Kind whose ID is anything else (empty halves, a second ':', an embedded '/') is refused rather than half-parsed, since a caller that hand-built a Scope must not be able to smuggle a '/' into objType past the collision guard ResourceScope enforces.

func SafeFieldPath

func SafeFieldPath(path string) bool

SafeFieldPath reports whether path is safe to interpolate into a SQL expression as a JSON field accessor. A FieldFilter.Path reaches the SQL backends as an identifier, not a bind parameter, so this is the injection guard — never interpolate a path that fails it.

func SessionMayRead

func SessionMayRead(kind string) bool

SessionMayRead reports whether a session credential may read this Kind.

Fail-closed on the two cases that are not a plain "yes": an unregistered Kind is unreadable, because "declared nothing" must not read as "anyone may read it" — the same reasoning authorizeKindWrite applies to writes.

func SystemContext

func SystemContext(ctx context.Context, component string) context.Context

SystemContext returns a child of ctx that acts as the named platform component: any per-session token identity and caller are cleared (via WithoutTokenSession / WithoutCaller) and a system approval is attached (via WithSystemApproval). For operator-internal reads/writes performed on behalf of the platform rather than the calling session — the user_preference kind's commit path is the first caller: it must author ComponentWritten entries in a user's scope without inheriting whatever session or caller mark the triggering request carried.

func TrimProbe

func TrimProbe[T any](rows []T, limit int) ([]T, bool)

TrimProbe drops the extra row ProbeLimit asked for and reports whether it was there — that is, whether limit cut the answer short. Rows at or under the limit come back untouched with truncated false, so a listing that merely filled its page is never reported as one that was cut.

It trims to limit even when a store over-answers the probe, so a caller can rely on the result honoring the limit it asked for whatever the store did.

func WithApproval

func WithApproval(ctx context.Context, a ...Approval) context.Context

WithApproval returns a context carrying the given approvals appended to any already present. A nil/empty call is a no-op.

func WithCaller

func WithCaller(ctx context.Context, canonicalID string) context.Context

WithCaller records the USER on whose behalf the request runs, as a canonical subject id. Absent for component and per-session credentials — see TokenSessionFrom for the credential's own identity, which is a different question.

func WithKGScope

func WithKGScope(ctx context.Context, s Scope) context.Context

WithKGScope threads the session scope a knowledge-graph read must be filtered to. The operator's per-session /memory/_kg handler sets it; the platform-admin (admind) cross-session browser deliberately does NOT, so its reads stay unscoped (it is gated on platform#view_audit instead).

func WithKindReadDoor

func WithKindReadDoor(ctx context.Context, who string) context.Context

WithKindReadDoor marks ctx as a credential the per-kind read door governs, naming who for the refusal log.

It exists because the door was deciding "may this credential see this Kind" by asking a DIFFERENT question: whether the context carried a token SESSION. That mark is provenance — it binds an append-only entry's publisher to its writer — and the webd token, which acts on ANY session and belongs to none, never gets one. So the door skipped the one browser-facing component token entirely, and it could read pt_tag_content, and every other platform-only Kind, for any session in the cluster.

A caller is now TOLD it is behind the door rather than inferred to be. That is the property worth having: a component token added later inherits webd's exemption by default under the old rule, simply by not carrying an unrelated mark, and under this one it inherits nothing — someone has to decide.

TokenSessionFrom remains a SECOND trigger (see kindReadDoorSubject). Every per-session bearer is behind the door, and replacing the old trigger rather than adding to it would have reopened the door for every runner at once.

func WithSystemApproval

func WithSystemApproval(ctx context.Context, component string) context.Context

WithSystemApproval is sugar for WithApproval(ctx, ForSystemCaller(component)).

func WithTokenSession

func WithTokenSession(ctx context.Context, sess NamespacedName) context.Context

WithTokenSession marks ctx as originating from a PER-SESSION bearer token issued for sess. It answers a different question from WithCaller and the two must not be conflated:

  • CallerFrom is the USER on whose behalf the request runs. It is what spicedbauthorizer writes as memory_entry#creator, and it is deliberately absent for non-user callers — the AgentSession reconciler registers every per-session memory token with an empty callerID, because a runner's write has no user creator to record.
  • TokenSessionFrom is the CREDENTIAL's own identity: which session's token authenticated this request. It is present for exactly the per-session bearers and absent for everything else, independent of whether a user is attached.

Provenance verification needs the second: an append-only entry is attributed to a publisher (session:<ns>/<name>), and the question at the door is "is the writer the publisher it claims to be", not "which user asked". Reusing CallerID would force it non-empty for runners, silently writing memory_entry#creator tuples for a subject that is not a user.

func WithoutCaller

func WithoutCaller(ctx context.Context) context.Context

WithoutCaller returns ctx with the caller mark cleared, so CallerFrom reports absent again. Mirrors WithoutTokenSession: because CallerFrom folds emptiness into its bool (see above), storing "" under the same key is sufficient — no change to CallerFrom's storage or contract is needed.

For the same authorship hand-offs WithoutTokenSession exists for: a path that stops acting on a user's behalf and starts writing as the platform itself (SystemContext) must drop the mark, or the write would still be attributed to that user.

func WithoutTokenSession

func WithoutTokenSession(ctx context.Context) context.Context

WithoutTokenSession returns ctx with the token-session mark removed, so TokenSessionFrom reports absent again.

For authorship HAND-OFFS only: a path that stops acting on the token holder's behalf and starts writing as the process itself (Local's ScopeHooks fan-out) must drop the mark, or the provenance writer-binding demands the process's own entries be attributed to the token holder. NOT a way to opt out of the binding for a write the token holder still authored.

Types

type Approval

type Approval struct {
	// contains filtered or unexported fields
}

Approval is an opaque witness that authorization for (perm, resource) was established. Its fields are unexported, so a constructor in this package is the ONLY way to obtain one: an outside package can neither forge a value nor write it into a context, since the context key is unexported too.

func ForBearerToken

func ForBearerToken(perm Permission, resource, tokenID string) Approval

ForBearerToken mints an approval for a validated bearer token already confirmed to authorize `resource`. CONTRACT: call only after the token-scope check has passed.

func ForSpiceDBCheck

func ForSpiceDBCheck(perm Permission, resource, subject string) Approval

ForSpiceDBCheck mints an approval established by a successful SpiceDB check. CONTRACT: call ONLY inside a Check == HAS_PERMISSION branch.

func ForSystemCaller

func ForSystemCaller(component string) Approval

ForSystemCaller mints a wildcard approval for a trusted in-process/system caller (operator controllers, ingestion hooks, the system bearer tokens). It satisfies any NON-internal permission for any resource, and can NEVER satisfy an internal-tier one — EnsureApproval enforces that.

type AttachmentBlock

type AttachmentBlock struct {
	// Filename is the sanitized, user-supplied name.
	Filename string `json:"filename"`
	// MIME is the kind-reported content type — a hint, not a verified fact.
	MIME string `json:"mime"`
	// SizeBytes is the kind-reported size. May be 0 when unreported.
	SizeBytes int64 `json:"sizeBytes,omitempty"`
	// Ref is the artifactstore handle for the original bytes.
	Ref string `json:"ref"`
	// TextRef is the artifactstore handle for the extracted text; EMPTY when no
	// extractor exists for this MIME (images, today). Its presence is
	// load-bearing: with TextRef empty the runner keeps a native block attached
	// for the whole session, since nothing else represents the file; with it set
	// the block is kept only on the arrival turn, because the text carries it
	// after.
	TextRef string `json:"textRef,omitempty"`
	// ArchiveRef is the handle of the ARCHIVE this member came out of; empty
	// for a file the user attached directly.
	//
	// Load-bearing at hydration: an archive member is excluded from the
	// persistent native-block window unless pinned, because its archive's
	// index already named it and stated its readability. Without this field a
	// bundle of 187 log files would fill that window with four arbitrary
	// members and name them in an unreadable-attachments note the agent was
	// never told about.
	ArchiveRef string `json:"archiveRef,omitempty"`

	// Pages is the extractor-reported page/slide count; 0 when the format has no
	// real notion of pages.
	Pages int `json:"pages,omitempty"`
}

AttachmentBlock records a file a user attached to an inbound message that the pipeline actually fetched and, when a backend claimed its MIME, extracted. Ref/TextRef are artifactstore handles, not bytes — the same by-reference shape respond_to_user's `attached` field and fetch_artifact use. Filename is untrusted, listener-sanitized input: never treat it as more than a label.

type Authorizer

type Authorizer interface {
	// AuthorizePut runs AFTER the Local.Put door has already authorized the
	// write, so it is NOT itself the write-permission gate. Its job is to WRITE
	// the entry-level relationships (memory_entry#session, #creator, and
	// caller-independent ones like artifact#parent) so future reads/deletes can
	// be checked. A non-nil error — e.g. a failed relationship write — denies
	// the write and reaches the caller as-is.
	//
	// With CallerFrom(ctx) absent (in-process controller calls),
	// implementations SHOULD allow the write unconditionally.
	AuthorizePut(ctx context.Context, e Entry) error

	// AuthorizeQuery post-filters a backend result once the EnsureApproval door
	// has authorized the scope as a whole, returning only the entries the
	// caller (CallerFrom(ctx)) may read. Invoked only when a caller is present,
	// so it is a finer per-entry layer on the door, not the read gate itself.
	// Implementations bulk-check to minimize round-trips.
	AuthorizeQuery(ctx context.Context, entries []Entry) ([]Entry, error)

	// AuthorizeDelete runs before backend.Delete; a non-nil error denies the
	// deletion. With CallerFrom(ctx) absent, implementations SHOULD allow.
	AuthorizeDelete(ctx context.Context, scope Scope, kind, id string) error

	// CleanupScope idempotently removes all entry-level authorization
	// relationships for the scope.
	CleanupScope(ctx context.Context, scope Scope) error
}

Authorizer gates memory operations behind an external authorization system. When set on the Local facade, every Put/Query/Delete passes through it.

It is NOT the front door: EnsureApproval at the top of each facade method is what enforces coarse read/write authorization, with or without an Authorizer configured. The Authorizer is the tuple-writer (AuthorizePut records entry-level relationships for future checks) plus an optional finer per-entry filter, reached only once the door has already let the call through.

Implementations MUST be safe for concurrent use.

type Backend

type Backend interface {
	Capabilities() Capabilities

	Put(ctx context.Context, e Entry) error
	Get(ctx context.Context, scope Scope, kind, id string) (Entry, bool, error)
	Delete(ctx context.Context, scope Scope, kind, id string) error
	// DeleteScope removes every entry under scope. Idempotent — an
	// empty scope is not an error.
	DeleteScope(ctx context.Context, scope Scope) error

	Query(ctx context.Context, q Query) (QueryResult, error)
	// QueryAllScopes is Query without the single-scope restriction; see
	// CrossScopeQuery. Backends must apply ordering before Offset/Limit.
	QueryAllScopes(ctx context.Context, q CrossScopeQuery) (QueryResult, error)
	Status(ctx context.Context, scope Scope) (ScopeStatus, error)
}

Backend is the storage primitive every backend implementation satisfies. Signal dispatch is framework concern (Memory.SendSignal), not backend concern — backends never receive Signals.

type Capabilities

type Capabilities struct {
	// ContentSchemas: honors Kind.ContentSchema() for FieldEquals predicates.
	ContentSchemas bool
	// FieldRange: honors FieldFilter.Op values other than Eq (Lt, Lte, Gt, Gte,
	// IsNil, NotNil). Backends reporting false silently move non-Eq FieldFilter
	// predicates into DroppedPredicates.
	FieldRange bool
	// LinkTraversal: max hop depth. 0 = no link queries, 1 = one-hop,
	// -1 = unbounded.
	LinkTraversal int
	// ReverseLinks: can answer "what links TO this entry?".
	ReverseLinks bool
	// TagFilters: honors Query.Tags.
	TagFilters bool
	// TimeRange: honors Query.Since / Query.Until.
	TimeRange bool
}

Capabilities declares what a Backend can answer. The facade never lifts behavior beyond what the backend declares — predicates outside the declared capabilities surface as QueryResult.DroppedPredicates entries.

type ContentBlock

type ContentBlock struct {
	// Type selects which of the fields below is populated: "text", "tool_use",
	// "tool_result" or "attachment".
	Type string `json:"type"`
	// Text is the block's prose; set only when Type == "text".
	Text string `json:"text,omitempty"`
	// ToolUse is the model's tool invocation; set only when Type == "tool_use".
	ToolUse *ToolUseBlock `json:"toolUse,omitempty"`
	// ToolResult is the tool's answer; set only when Type == "tool_result".
	ToolResult *ToolResultBlock `json:"toolResult,omitempty"`
	// Attachment carries a stored inbound file's handles and metadata, set only
	// when Type == "attachment" — one block per inbound attachment whose bytes
	// reached the store, whether or not extraction also succeeded (see
	// AttachmentBlock.TextRef).
	Attachment *AttachmentBlock `json:"attachment,omitempty"`
}

ContentBlock mirrors the LLM provider-neutral block shape so the runner can persist its native shape and reload it without translation.

type CrossScopeQuery

type CrossScopeQuery struct {
	ScopeKind string     // required, e.g. "session"
	Kinds     []string   // required, non-empty
	Since     *time.Time // inclusive
	Until     *time.Time // exclusive
	OrderDesc bool       // true = newest first
	Limit     int        // 0 = no limit
	Offset    int        // rows to skip after ordering
}

CrossScopeQuery matches entries of the given kinds across ALL scopes of ScopeKind, time-filtered and time-ordered. Unlike Query there is NO per-entry authorization post-filter — its only caller is the admind admin API, which gates on the SpiceDB platform permissions first. Do not expose it through any per-user surface.

type EmbeddingProvider

type EmbeddingProvider interface {
	Embed(ctx context.Context, text string) ([]float32, error)
}

EmbeddingProvider turns text into a vector for similarity search.

type Entry

type Entry struct {
	// Scope is the sub-universe the entry belongs to; queries never cross it.
	Scope Scope `json:"scope"`
	// Kind names the registered Kind; an unregistered one is refused at Put.
	Kind string `json:"kind"`
	// ID MUST start with the Kind's registered IDPrefix — enforced at Put.
	ID string `json:"id"`
	// CreatedAt is authorship time; canonicalized to microseconds before any
	// sameness comparison, since that is all a TIMESTAMPTZ column keeps.
	CreatedAt time.Time `json:"createdAt"`
	// Links are typed references out of this entry; nil and empty are the same
	// entry to the signed digest.
	Links []Link `json:"links,omitempty"`
	// Tags are an unordered SET — the digest sorts them, so two orderings are
	// one entry; nil and empty are likewise the same.
	Tags []string `json:"tags,omitempty"`
	// Content is the Kind's ContentSchema payload, opaque when it declares none.
	Content json.RawMessage `json:"content,omitempty"`
	// Provenance is nil on mutable kinds and required on append-only ones
	// wherever a ProvenanceVerifier is configured.
	Provenance *Provenance `json:"provenance,omitempty"`
}

Entry is the unit of storage; (Scope, Kind, ID) is the unique key.

type FieldFilter

type FieldFilter struct {
	// Path is the key the value is STORED under — the Content struct's `json`
	// tag, NOT its Go field name. Backends render the predicate as
	// content->>'<Path>', so "TurnIndex" for a field tagged `json:"turnIndex"`
	// extracts NULL and can never be true. Local.Query rejects a Path naming no
	// content key of the Kinds queried, rather than answering empty forever.
	//
	// Path is a TOP-LEVEL key, not a nesting expression: both SQL backends read
	// a dot as part of the key name (postgres's jsonb ->> takes a literal key,
	// sqlite's ->> quotes the operand into $."a.b"), so "a.b" selects a key
	// literally named "a.b" and never reaches a nested {"a":{"b":…}}. Nested
	// content is not queryable. The Kind should also list the key in
	// IndexedFields().
	Path string `json:"path"`
	// Op is the comparison; empty means equality.
	Op FieldOp `json:"op,omitempty"`
	// Value is the right-hand side; ignored by the IsNil/NotNil ops.
	Value any `json:"value,omitempty"`
}

FieldFilter selects entries by a content field predicate. A backend without Capabilities.ContentSchemas drops it into QueryResult.DroppedPredicates at runtime; one without Capabilities.FieldRange drops non-Eq predicates the same way.

type FieldOp

type FieldOp string

FieldOp is the comparison operator for a FieldFilter predicate. The zero value ("") means FieldOpEq, so an op-less filter is an equality filter.

const (
	FieldOpEq     FieldOp = "eq"
	FieldOpLt     FieldOp = "lt"
	FieldOpLte    FieldOp = "lte"
	FieldOpGt     FieldOp = "gt"
	FieldOpGte    FieldOp = "gte"
	FieldOpIsNil  FieldOp = "is_nil"
	FieldOpNotNil FieldOp = "not_nil"
)

func NormalizeFieldOp

func NormalizeFieldOp(op FieldOp) FieldOp

NormalizeFieldOp resolves the zero value to FieldOpEq, leaving any other op unchanged.

type KGCommunity

type KGCommunity struct {
	// UUID is the graph's identifier for the cluster.
	UUID string `json:"uuid"`
	// Name is the cluster's generated label.
	Name string `json:"name"`
	// Summary describes what its members have in common.
	Summary string `json:"summary"`
	// Members are the entity UUIDs in the cluster.
	Members []string `json:"members"`
}

KGCommunity is a cluster of related entities the graph derived.

type KGEntity

type KGEntity struct {
	// UUID is the graph's identifier, stable across re-ingestion of the same entity.
	UUID string `json:"uuid"`
	// Name is the entity as the extractor named it.
	Name string `json:"name"`
	// Summary is the graph's rolling description, rewritten as facts accumulate.
	Summary string `json:"summary"`
	// Attributes are extractor-supplied properties; absent when none were found.
	Attributes map[string]string `json:"attributes,omitempty"`
}

KGEntity is a node the graph extracted and deduplicated.

type KGFact

type KGFact struct {
	// UUID is the graph's identifier for this edge.
	UUID string `json:"uuid"`
	// Name is the relationship type.
	Name string `json:"name"`
	// Fact is the natural-language assertion.
	Fact string `json:"fact"`
	// FromEntity is the source entity's UUID.
	FromEntity string `json:"fromEntity"`
	// ToEntity is the target entity's UUID.
	ToEntity string `json:"toEntity"`
	// ValidAt is when the fact began holding; nil when the graph could not date it.
	ValidAt *time.Time `json:"validAt,omitempty"`
	// InvalidAt is when a later fact contradicted this one; nil means still current.
	InvalidAt *time.Time `json:"invalidAt,omitempty"`
}

KGFact is an edge: one relationship the graph asserts between two entities.

type KGInput

type KGInput struct {
	// GroupID partitions the graph per session; it is the isolation boundary a
	// scoped read is checked against.
	GroupID string `json:"groupId"`
	// Content is the raw text to extract entities and facts from.
	Content string `json:"content"`
	// Role attributes the content ("user", "assistant", …).
	Role string `json:"role"`
}

KGInput is one episode handed to the graph for entity extraction.

type KGProvider

type KGProvider interface {
	Ingest(ctx context.Context, input KGInput) error
	SearchFacts(ctx context.Context, query string, limit int) ([]KGFact, error)
	GetEntity(ctx context.Context, uuid string) (*KGEntity, error)
	EntityFacts(ctx context.Context, entityUUID string) ([]KGFact, error)
	RelatedEntities(ctx context.Context, entityUUID string, limit int) ([]KGEntity, error)
	Communities(ctx context.Context, groupID string) ([]KGCommunity, error)
}

KGProvider is the graph-native read/write surface. Extraction, dedup and contradiction detection happen inside the provider, asynchronously, so an Ingest that returns nil has been accepted, not yet reflected in reads.

type Kind

type Kind interface {
	// Name is the entry's Kind field and the registry's primary key
	// (e.g., "authz_decision"). Must be non-empty and unique across
	// the registry.
	Name() string
	// IDPrefix is the literal byte prefix every Entry of this Kind's
	// ID must start with (e.g., "authzd-"). Required, non-empty, and
	// must not be a prefix-of nor be prefixed-by any other registered
	// Kind's prefix.
	IDPrefix() string
	// Retention advertises this Kind's preferences to the backend.
	Retention() Retention
	// ContentSchema optionally declares the Kind's Content struct so backends
	// advertising Capabilities.ContentSchemas can index declared fields.
	// nil means opaque content.
	ContentSchema() reflect.Type
	// IndexedFields names the fields the Kind wants indexed, as the keys they
	// are STORED under — the Content struct's `json` tags, NOT the Go field
	// names. FieldFilter.Path draws from the same vocabulary and accessor
	// authors read this list before writing a query, so a Go name here is
	// misinformation that reappears as a predicate matching nothing. Only
	// meaningful when ContentSchema is non-nil.
	IndexedFields() []string
	// WriteAuthority declares WHICH CREDENTIAL CLASS may author entries of this
	// Kind. An interface method rather than a table in a consumer, so a new Kind
	// cannot be registered without answering the question.
	WriteAuthority() WriteAuthority
	// NewScopeHooks materializes the per-(Scope, Kind) hook signals are
	// dispatched to, lazily on the first SendSignal touching the scope. Keep it
	// cheap — long-running work belongs in OnSignal.
	NewScopeHooks(scope Scope) ScopeHooks
}

Kind declares one variety of entry that the framework can store. Kinds register at init() time via RegisterKind. Implementations typically live under pkg/memory/kinds/<name>/.

func LookupKind

func LookupKind(name string) (Kind, bool)

LookupKind returns the Kind registered under name, for Put validation and for callers holding a Kind name as a string.

func RegisteredKinds

func RegisteredKinds() []Kind

RegisteredKinds snapshots every registered Kind, sorted by Name so ScopeHooks materialization and signal dispatch are deterministic.

type Link struct {
	// Relation is a free-form label from a small conventional vocabulary
	// ("of_operation", "for_resource", "by_subject", "approved_by", …).
	Relation string `json:"relation"`
	// Scope of the target; zero means the containing entry's own scope.
	Scope Scope `json:"scope"`
	// Kind of the target. An unregistered Kind skips ID-prefix validation.
	Kind string `json:"kind"`
	// ID of the target; validated against Kind's IDPrefix when Kind is registered.
	ID string `json:"id"`
}

Link is a typed reference to another entry.

type LinkFilter

type LinkFilter struct {
	// Relation to match; empty matches any relation.
	Relation string
	// Kind of the linked entry.
	Kind string
	// ID of the linked entry.
	ID string
}

LinkFilter selects entries by one of their links.

type Local

type Local struct {
	// contains filtered or unexported fields
}

Local is the in-process Memory implementation. It wraps a Backend, owns the Kind registry's per-(Scope, Kind) ScopeHooks materialization, and fans out Signal dispatch.

func NewLocal

func NewLocal(b Backend, opts ...LocalOption) *Local

NewLocal wraps b in an in-process facade. A nil Backend panics: every facade method dereferences it, so it is a programmer error, not a runtime condition.

func (*Local) Capabilities

func (m *Local) Capabilities() Capabilities

Capabilities returns the backend's declared capabilities, for `oap doctor`-style tooling; correctness-side callers never branch on it.

func (*Local) Delete

func (m *Local) Delete(ctx context.Context, scope Scope, kind, id string) error

Delete is idempotent — deleting a non-existent entry is not an error.

The door: a MUTABLE Kind requires an approval for (DeleteMemory, scope.ID) (bearer, SpiceDB, or system) or Delete returns ErrMissingApproval before touching the backend. An append-only Kind is refused outright — FIRST and unconditionally, regardless of approval. A per-session credential is additionally held to the Kind's WriteAuthority: removal is authorship, and a runner that could delete `cold_start_task` could replay the cold-start review that entry's existence is what closes.

func (*Local) DeleteScope

func (m *Local) DeleteScope(ctx context.Context, scope Scope) error

DeleteScope removes the MUTABLE (working) memory of a scope. It STRUCTURALLY CANNOT remove append-only kinds — the tamper-evident audit log (transcript, audit, authz-decision, tool-session, …) is PERMANENT and survives every scope deletion by design: a session "disappearing" must never take its logs with it, so no code path can erase them. Operator-only, in-process; not on the Memory interface, not over HTTP.

The door: every call MUST carry an approval for (DeleteMemory, scope.ID). Only system (in-process operator/controller) callers reach this.

authz.CleanupScope is deliberately NOT called: the scope's read grants must survive so the retained audit stays queryable after the session is gone.

func (*Local) Get

func (m *Local) Get(ctx context.Context, scope Scope, kind, id string) (Entry, bool, error)

Get delegates to the backend. Returns (zero, false, nil) when the entry does not exist.

func (*Local) Put

func (m *Local) Put(ctx context.Context, e Entry) (Entry, error)

Put validates the Entry against the registered Kind's prefix rules, then forwards to the backend.

The door: a MUTABLE Kind requires an approval for (WriteMemory, e.Scope.ID) (bearer, SpiceDB, or system) or Put returns ErrMissingApproval before touching the backend. For an APPEND-ONLY Kind authorization IS provenance verification — a genuinely-new entry must pass the configured ProvenanceVerifier (with none configured the write is logged and allowed, for local/dev), after which the facade self-mints the internal AppendAudit approval proving the gate was satisfied. A WriteMemory or system approval can never substitute for either.

func (*Local) Query

func (m *Local) Query(ctx context.Context, q Query) (QueryResult, error)

Query forwards to the backend. Callers wanting to observe degradation should check QueryResult.Partial / DroppedPredicates; correctness-wise every caller has a fallback path for empty results regardless of cause. A caller treating the answer as the complete match set must check QueryResult.Truncated, which says whether q.Limit ended the read before the matches did.

The door: every call MUST carry an approval for (ReadMemory, q.Scope.ID) (bearer, SpiceDB, or system) or Query returns ErrMissingApproval before touching the backend — checked unconditionally, independent of whether an Authorizer is configured or a caller is present.

func (*Local) QueryAllScopes

func (m *Local) QueryAllScopes(ctx context.Context, q CrossScopeQuery) (QueryResult, error)

QueryAllScopes runs a cross-scope query against the backend. ADMIN-ONLY: there is deliberately no Authorizer post-filter here (the per-entry authorizer models per-session interact, which platform admins bypass by design) — callers MUST gate on the SpiceDB platform permissions first.

func (*Local) Reindex

func (m *Local) Reindex(ctx context.Context, scope Scope) (int, error)

Reindex re-Indexes every entry in scope on each registered SearchProvider, for adding a provider to an existing deployment or recovering from index corruption. Per-entry failures are logged and skipped; the count returned is the number of entries successfully submitted to ALL providers.

The door: every call MUST carry an approval for (ReadMemory, scope.ID) (bearer, SpiceDB, or system) or Reindex returns ErrMissingApproval before touching the backend.

func (*Local) Search

func (m *Local) Search(ctx context.Context, req SearchRequest) (MergedSearchResult, error)

Search delegates to the configured Searcher. Returns ErrNoSearchProviders when no Searcher has been set.

The per-kind read door applies here exactly as it does on Query, and for the same reason: a SearchProvider answers from the BACKEND rather than through this facade (the in-memory one calls Backend.Query directly; the SQL ones index every Kind that is Put), so nothing below this line re-applies it. Gating only Query would hide a Kind from the tool nobody calls while leaving it readable through search_memory, which is the tool the model holds.

func (*Local) SendSignal

func (m *Local) SendSignal(ctx context.Context, sig Signal) error

SendSignal dispatches sig to every registered Kind's ScopeHooks for sig.Scope, materializing hooks on first dispatch per (Scope, Kind) and reusing them. Per-hook errors are collected via errors.Join, so a failing hook on one Kind never prevents other Kinds from reacting.

The door: every call MUST carry an approval for (WriteMemory, sig.Scope.ID) (bearer, SpiceDB, or system) or SendSignal returns ErrMissingApproval before any hook is materialized or dispatched.

func (*Local) Status

func (m *Local) Status(ctx context.Context, scope Scope) (ScopeStatus, error)

Status returns the backend's view of the scope's lifecycle state. No backend produces Archived today; runner-on-resume treats Live and Unknown identically.

type LocalOption

type LocalOption func(*Local)

LocalOption is a functional option for NewLocal.

func WithAuthorizer

func WithAuthorizer(a Authorizer) LocalOption

WithAuthorizer sets the Authorizer consulted on every Put/Query/Delete. Unset (the default) means no authorization checks.

func WithLogger

func WithLogger(l logr.Logger) LocalOption

func WithProvenanceVerifier

func WithProvenanceVerifier(v ProvenanceVerifier) LocalOption

WithProvenanceVerifier enforces signed provenance on append-only writes: every such Put must carry an envelope verifying under a registered publisher key.

func WithSearchProviders

func WithSearchProviders(ps ...SearchProvider) LocalOption

WithSearchProviders registers search providers that receive index updates on Put/Delete/DeleteScope. These are kept separate from the Searcher so the facade can index into providers without importing the composite layer.

func WithSearcher

func WithSearcher(s Searcher) LocalOption

WithSearcher sets the Searcher used by Local.Search. When nil (the default), Search returns ErrNoSearchProviders.

type Memory

type Memory interface {
	Put(ctx context.Context, e Entry) (Entry, error)
	Query(ctx context.Context, q Query) (QueryResult, error)
	Search(ctx context.Context, req SearchRequest) (MergedSearchResult, error)
	SendSignal(ctx context.Context, sig Signal) error
}

Memory is the caller-facing memory interface. Local (operator) and httpclient.Client (runner/ap/channelsd) implement it.

type MergedSearchResult

type MergedSearchResult struct {
	// Entries are the ranked, authorization-filtered hits across all providers.
	Entries []ScoredEntry `json:"entries"`
	// PerProvider keeps each provider's raw answer for diagnosis; a provider
	// that errored is absent rather than present-and-empty. Each answer may
	// hold one hit more than SearchRequest.Limit — the probe row the searcher
	// asks for to decide Truncated, kept here because this is the raw answer.
	PerProvider map[string]SearchResult `json:"perProvider,omitempty"`
	// Truncated reports that SearchRequest.Limit ended this page before the
	// matches did: at least one further hit ranked below the cut. False means
	// every match is in Entries.
	//
	// Like QueryResult.Truncated it describes the READ: an Authorizer
	// post-filter may leave fewer than Limit hits in Entries with Truncated
	// still true.
	Truncated bool `json:"truncated"`
}

MergedSearchResult is what the composite searcher returns to a caller.

type NamespacedName

type NamespacedName struct {
	Namespace string `json:"namespace"`
	Name      string `json:"name"`
}

NamespacedName identifies an AgentSession. Mirrors k8s types.NamespacedName but kept import-free so this package can be used from non-k8s code.

func TokenSessionFrom

func TokenSessionFrom(ctx context.Context) (NamespacedName, bool)

TokenSessionFrom reports the session whose bearer token authenticated this request, and whether the request arrived on such a token at all.

The bool is PRESENCE, not non-emptiness, and that distinction is the whole point: a security check written as `if caller != "" { … }` degrades silently to "no check" for the one caller class that never carries a caller. Treat a present-but-empty NamespacedName as a wiring bug and fail CLOSED, never as "not token-originated".

type OrderBy

type OrderBy struct {
	// Field is the column to order by; "createdAt" is the only value any
	// backend honors, and anything else falls back to the backend's default.
	Field string
	// Desc: true for newest-first.
	Desc bool
}

OrderBy controls Query result ordering.

type Permission

type Permission string

Permission names a gated capability on the memory data plane. An "internal:" prefix marks the dangerous tier (see approval_internal.go) — those can only be authorized by pkg/memory's own trusted path, never by a bearer or system approval.

const (
	ReadMemory   Permission = "memory:read"
	WriteMemory  Permission = "memory:write"
	DeleteMemory Permission = "memory:delete"
	ReadKG       Permission = "kg:read"

	// AppendAudit gates append-only (tamper-evident) writes. Internal tier:
	// mintable only inside pkg/memory, by the provenance verify-on-write path.
	AppendAudit Permission = "internal:append-audit"
)

type PoolReader

type PoolReader interface {
	QueryPool(ctx context.Context, sessionScope, poolScope Scope, q Query) (QueryResult, error)
}

PoolReader is the READ half of the same addressing: query a resource pool's entries rather than the caller's own scope.

It exists for the same reason PoolWriter does, and the reason is the same shape: over an HTTP transport a scope is not a value the caller passes, it is the URL, and that URL names a SESSION (which is what the bearer token authorizes). Query builds it from Scope.ID split on "/", so a resource scope ID — "dossier:d-1", which has no "/" — produced "/memory/observation/dossier:d-1/" and a 404 from the real server. An in-process facade has no such problem; a client does, and it has to say so at the call site.

sessionScope is therefore not redundant with poolScope: it is what the request is authenticated AS, while poolScope is what it addresses. The server proves the session holds a grant on the pool before serving it, exactly as it does for a write (httpsrv.destinationFor runs on every method).

The signature takes a whole Query rather than just the pool so a caller can filter — the provenance chain-head scan reads one Kind at a time.

type PoolWriter

type PoolWriter interface {
	PutToPool(ctx context.Context, sessionScope, poolScope Scope, e Entry) (Entry, error)
}

PoolWriter is the capability of writing an entry into a resource's memory pool rather than the caller's own scope — httpclient.Client.PutToPool's exact signature, named here so every caller that needs to ASSERT for the capability (rather than call a concrete *httpclient.Client directly) shares one definition instead of each redeclaring an identical anonymous interface.

Deliberately NOT part of Memory (see httpclient.Client.PutToPool's own doc): Memory.Put routes by e.Scope, and a pool write cannot, since the server refuses to read a destination out of the body at all — a caller that wants a pool has to say so at the call site, via this interface, rather than through the ambient Memory contract every store implements.

A wrapper that decorates a Memory (provenance.SigningMemory) and wants to forward a pool write to whatever concrete store it holds type-asserts its inner value against this interface — and must fail LOUDLY when that assertion fails, naming the concrete inner type, rather than silently answering "no" to a capability the inner may genuinely have further down a chain of wrappers. A silent "unsupported" answer here is indistinguishable from a genuinely pool-incapable backend, which is exactly the shape of bug that left record_observation refusing every call in a live runner while every test in the tree stayed green: SigningMemory embeds Memory as an INTERFACE field, so Go's method promotion could never forward the concrete *httpclient.Client's PutToPool.

type Provenance

type Provenance struct {
	// Publisher identifies the signer; equals the memory layer's caller ID for
	// the writer (per-session IDs for runners/CLI; "system:channelsd",
	// "system:authzd", "system:operator").
	Publisher string `json:"publisher"`
	// KeyID is the hex SHA-256 (first 16 bytes) of the Ed25519 public key, used
	// to look the key up in the publisher-key registry.
	KeyID string `json:"keyId"`
	// Seq is the publisher's per-scope 1-based monotonic sequence, spanning all
	// append-only kinds the publisher writes in the scope. A gap means loss.
	Seq uint64 `json:"seq"`
	// PrevHash is the EntryDigest of the publisher's previous entry in this
	// scope; empty only for Seq 1.
	PrevHash string `json:"prevHash,omitempty"`
	// Sig is the Ed25519 signature over the canonical entry digest.
	Sig []byte `json:"sig"`
}

Provenance carries the publisher attestation for append-only kinds: a per-publisher Ed25519 signature over a canonical entry digest, hash-chained per (scope, publisher) so gaps, reordering and truncation are detectable. nil on mutable kinds.

type ProvenanceVerifier

type ProvenanceVerifier interface {
	VerifyEntry(ctx context.Context, caller string, e Entry) error
}

ProvenanceVerifier validates the provenance envelope on an append-only write. The operator injects one so the facade enforces signing without importing crypto. caller is CallerFrom(ctx) — the USER, and "" for both in-process operator writes and per-session bearers, so binding an entry's publisher to its author must read TokenSessionFrom(ctx), not caller.

type PtTagContent

type PtTagContent struct {
	TagID   string `json:"tagID"`
	Content string `json:"content"`
	MIME    string `json:"mime,omitempty"`
}

PtTagContent is one resolved (id, content) datum.

type PtTagMintRequest

type PtTagMintRequest struct {
	// ToolUseID is the tool_use block whose result carried the datum into
	// context, matching the link infoleakage_taint records under the same name.
	ToolUseID string `json:"toolUseID,omitempty"`

	// Resources are the objects the call read, as SpiceDB type/id pairs. The
	// minter expands each to its authorized subjects. Required for a LEAF mint.
	Resources []PtTagResourceRef `json:"resources,omitempty"`

	// DerivedFrom names existing tags this datum was assembled from. When set,
	// the mint is DERIVED: it takes no resources and no audience of its own,
	// because `reader` resolves the intersection of these through SpiceDB.
	DerivedFrom []string `json:"derivedFrom,omitempty"`

	// UntrustedOrigin marks a leaf whose source may carry injected content.
	// Meaningful only on a leaf mint: integrity is declared where a datum
	// enters and inherited below it through carries_untrusted.
	UntrustedOrigin bool `json:"untrustedOrigin,omitempty"`

	// Content is the datum itself, stored behind the tag so the PLATFORM can
	// resolve it later — to place it in a child's context when a data slot is
	// bound, or to answer an audit. Optional: a tag with no content is still a
	// complete provenance record, it just cannot be handed onward.
	//
	// Note this does NOT put the content anywhere the agent can reach. It
	// lands in pt_tag_content, whose SessionReadable is false precisely so
	// that query_memory — which takes arbitrary Kind names — cannot ask for it
	// straight back.
	//
	// Written in the same call as the tag, never separately: content stored
	// under a tag that failed to mint would be bytes nobody's audience governs,
	// and a tag whose content arrived later would have a window in which a
	// disclosure decision was made about something not yet there.
	Content string `json:"content,omitempty"`

	// MIME describes Content, so a platform forwarding the datum can label it.
	MIME string `json:"mime,omitempty"`
}

PtTagMintRequest is the body of POST /memory/_pttag_mint/{ns}/{name}.

Note what is absent: any way to state an audience. The caller says what its tool call TOUCHED; the minter decides who may see it. That split is the security property of the whole route — pt_tag is ComponentWritten precisely so a session cannot author its own reader set, and a request field carrying one would hand the pen straight back.

type PtTagMintResponse

type PtTagMintResponse struct {
	TagID string `json:"tagID"`
}

PtTagMintResponse returns the id of the tag that now governs the datum.

type PtTagRegion

type PtTagRegion struct {
	ID      string `json:"id"`
	Content string `json:"content"`
}

PtTagRegion is one claimed (id, content) pair from an outbound payload.

type PtTagResolveRequest

type PtTagResolveRequest struct {
	// TagIDs are the tags whose content the caller asks for. The scope in the
	// URL is where the bytes live (the caller's own scope for a derive, the
	// PARENT scope for a bound slot); entitlement is per-tag, not per-scope.
	TagIDs []string `json:"tagIDs"`
}

PtTagResolveRequest is the body of POST /memory/_pttag_resolve/{ns}/{name}.

Unlike _pttag_verify (which confirms ids and returns no bytes), this route RETURNS content — the bytes a caller is ENTITLED to receive. It exists for the two platform paths that legitimately need a datum's bytes back: derive_tag's validator (it must read the source tags it derives from) and a subagent's bound data slot (a child needs the parent's bound datum to place in its context). Both read pt_tag_content, which is component-read (SessionReadable false), so neither can read it over the memory API for itself; the operator holds the credential and gates each tag on the caller's pt_tag:<id>#access — session + ancestor + granted_to — before returning it.

type PtTagResolveResponse

type PtTagResolveResponse struct {
	Contents []PtTagContent `json:"contents,omitempty"`
}

PtTagResolveResponse returns the content for the entitled tags only. A tag the caller lacks pt_tag:<id>#access to, or whose bytes are not in the named scope, is simply ABSENT — the route never says which of the two it was, and never returns bytes for a tag the caller may not receive.

type PtTagResourceRef

type PtTagResourceRef struct {
	Type string `json:"type"`
	ID   string `json:"id"`
	// Permission is the permission the read was authorized under, so the
	// minter expands the right subject set rather than guessing one.
	Permission string `json:"permission"`
}

PtTagResourceRef is one object a call read.

type PtTagVerifyRequest

type PtTagVerifyRequest struct {
	// Regions are the nonce-matched pt-untrusted regions the runner parsed out
	// of the outbound payload — each an id the payload claims plus the bytes it
	// carried under that id.
	Regions []PtTagRegion `json:"regions"`
}

PtTagVerifyRequest is the body of POST /memory/_pttag_verify/{ns}/{name}.

It exists because the CONTENT-BINDING half of the per-datum egress check — "do these bytes actually belong to the tag the payload claims?" — must run where the stored bytes live. pt_tag_content is component-written AND component-READ (SessionReadable is false), so the runner, holding a session bearer, cannot read it over the memory HTTP API to compare for itself; the read door refuses a token-originated request by design. Worse, if the runner COULD read it, letting the runner do the comparison would defeat the check — a compromised runner could pair a witnessed wide id with fabricated bytes and simply declare a match. So the runner PARSES the payload's regions (a parse, not a privilege) and asks the operator to bind them; the operator holds the component credential and returns only WHICH ids bound, never the bytes.

type PtTagVerifyResponse

type PtTagVerifyResponse struct {
	// Verified are the ids whose supplied content matched what the platform
	// stored for them.
	Verified []string `json:"verified,omitempty"`
	// AllBound is true only when EVERY requested region bound. A partially
	// forged payload — one real region beside a fabricated one — is not one the
	// caller can reason about per-datum, so the caller drops the whole payload
	// to the coarse floor rather than honour the regions that happened to match.
	AllBound bool `json:"allBound"`
}

PtTagVerifyResponse reports which regions bound to the bytes the platform stored for their claimed id.

Verified carries only ids, never content: this route confirms provenance, it does not hand a datum back — the caller already has the bytes it sent, and returning them would make _pttag_verify a read-back channel for the very pt_tag_content the design keeps out of a session's reach.

type Query

type Query struct {
	Scope        Scope         // required; the framework rejects an empty one
	Kinds        []string      // empty means all kinds in scope
	IDs          []string      // exact-id filter
	LinkedTo     []LinkFilter  // entries whose Links contain {Relation?, Kind, ID}
	LinkedFrom   []LinkFilter  // reverse: entries that ARE linked-to {Kind, ID}
	Tags         []string      // entries carrying ALL the listed tags
	FieldEquals  []FieldFilter // content predicates; dropped by backends lacking ContentSchemas
	Since, Until *time.Time    // half-open CreatedAt window; nil means unbounded
	OrderBy      OrderBy
	Limit        int // 0 means the backend's own cap
}

Query is the rich superset of recall predicates. Backends drop what they can't handle into QueryResult.DroppedPredicates; callers always have a fallback path for empty / partial results.

type QueryResult

type QueryResult struct {
	// Entries matched the honored predicates; empty is a valid answer, not an error.
	Entries []Entry
	// DroppedPredicates names each Query predicate the backend could not honor
	// ("FieldEquals", "LinkedFrom", …). A capability signal for operators;
	// callers must not branch on it for correctness.
	DroppedPredicates []string
	// Partial is true iff len(DroppedPredicates) > 0.
	Partial bool
	// Truncated reports that Query.Limit ended this read before the matches
	// did: at least one more entry matched and is not in Entries. False means
	// Entries is every match, so a caller may treat it as the whole answer.
	//
	// It is set by Local.Query, which probes one row past the limit (see
	// ProbeLimit) rather than guessing from len(Entries) — a set holding
	// exactly Limit entries is otherwise indistinguishable from one the limit
	// cut short, and that ambiguity is what lets a reader act on part of a
	// result set believing it was all of it.
	//
	// It describes the READ, not the caller's view of it: an Authorizer
	// post-filter may drop entries afterwards, leaving fewer than Limit in
	// Entries with Truncated still true. That is correct — the store held more
	// than the limit allowed to be read, whatever was then filtered out.
	//
	// Backend implementations leave it false; the facade owns it.
	Truncated bool
}

QueryResult is what Backend.Query returns.

type Ranker

type Ranker interface {
	Rank(results map[string]SearchResult, limit int) []ScoredEntry
}

Ranker merges per-provider results, whose scores are not comparable across providers, into one ordering.

type Retention

type Retention struct {
	// EssentialWhileLive: never evict while ScopeStatus == Live.
	EssentialWhileLive bool
	// ArchiveOn names signal kinds that flip this Kind's entries
	// from "kept" to "eligible for eviction".
	ArchiveOn []SignalKind
	// TTLAfterArchive: how long after an ArchiveOn signal the
	// backend may drop the entry. Zero = backend default; negative
	// = never auto-drop after archive.
	TTLAfterArchive time.Duration
	// SoftCapPerScope is a per-scope FIFO cap for cache-shaped
	// Kinds (label). Zero = no cap.
	SoftCapPerScope int
	// AppendOnly: entries of this Kind are immutable once written. Put
	// may create (or idempotently re-put byte-identical content) but
	// never change an existing entry, and per-entry Delete is refused.
	// Scope-level deletion (retention, session GC) is unaffected — that
	// is the backend's retention policy, not entry mutation.
	AppendOnly bool

	// NeverForkCopy: a fork must NOT copy this Kind's entries into the child
	// scope. The forking code derives whatever the child needs instead.
	//
	// This exists for Kinds whose entries are a CHAIN rather than a set —
	// where meaning comes from order and from the (scope, publisher) provenance
	// sequence, not from each entry alone. Copying those into a new scope
	// produces a chain whose seq and prevHash refer to a history the child does
	// not have, so it verifies as gapped at best and replays in the wrong order
	// at worst.
	//
	// The plan gate is the motivating case: its rules ("a denial survives a
	// later supersede", "the last write wins for this ceiling") are all
	// order-dependent, so a mis-ordered replay in a child could resurrect
	// authority a human revoked.
	NeverForkCopy bool
}

Retention is a Kind's advice to the backend about lifecycle and immutability of entries. Fields are hints — backends honor what their policy supports.

type Scope

type Scope struct {
	// Kind is the sub-universe's type — "session" or ScopeKindResource today.
	Kind string `json:"kind"`
	// ID identifies the scope within its Kind — "<namespace>/<name>" for
	// sessions, "<type>:<id>" for a resource pool. The two shapes are
	// deliberately disjoint; ResourceScope enforces it.
	ID string `json:"id"`
}

Scope identifies a sub-universe of entries. Two Kinds are in use: "session", which most callers emit, and ScopeKindResource, a memory pool living on a resource (see resourcescope.go). The framework permits others ("agentclass", "user") for cross-session use, and nothing branches on the value.

func KGScopeFrom

func KGScopeFrom(ctx context.Context) (Scope, bool)

KGScopeFrom returns the KG read scope and true when one is set with a non-empty ID; ok is false when unset or the ID is empty (the unscoped admin path).

func ParseResourceRef

func ParseResourceRef(ref string) (Scope, error)

ParseResourceRef parses ref as "<type>:<id>" and returns the Scope ResourceScope would build from the two halves.

The one parser every reader of a `resource`/`?pool=`/write-destination argument should call, rather than each hand-rolling its own strings.Cut — a review of this feature found three call sites doing exactly that (record_observation's Execute, httpsrv's destinationFor, the info-leakage audience gate's evalPoolWrite) at three different strictnesses: two delegated to ResourceScope's validation and one did not, so a ref like "dossier:a:b" was a resolvable destination to the gate and a refusal to the tool that actually writes. The direction was safe (nothing was written), but it is a crack in the exact agreement pkg/agent/runner's cross-package pin exists to hold, and it let the gate hand a malformed object ref through to a permission lookup. One parser closes it structurally: the three sites cannot drift on strictness again because there is only one strictness to drift from.

func ResourceScope

func ResourceScope(objType, objID string) (Scope, error)

ResourceScope builds the Scope for one resource instance's memory pool.

Refuses a '/' in either half. The memory capability door keys on scope.ID ALONE — httpsrv mints ForBearerToken(perm, id, …) and CompositeSearcher checks EnsureApproval(ctx, ReadMemory, sc.ID) — and a session's ID is "<ns>/<name>". Allowing a slash here would let an approval minted for a resource authorize a session, or the reverse. The two ID shapes staying disjoint is a security property, not a formatting preference.

func UserScope

func UserScope(canonicalID string) (Scope, error)

UserScope returns the scope holding one user's cross-session data, keyed by the bare canonical user id (base64url). Refuses ids that could collide with session ("<ns>/<name>") or resource ("<type>:<id>") scope IDs — the capability doors key on scope.ID alone, so disjointness is load-bearing (see resourcescope.go's ResourceScope for the same rule on the other non-session scope kind).

type ScopeHooks

type ScopeHooks interface {
	OnSignal(ctx context.Context, sig Signal) error
}

ScopeHooks reacts to signals dispatched into the (Scope, Kind) pair. Implementations switch on Signal.Kind for the signals they care about and ignore the rest. Errors are collected (not aborted-on) by the framework's dispatch loop.

type ScopeStatus

type ScopeStatus struct {
	// State is the backend's verdict on the scope.
	State StatusState `json:"state"`
	// ArchivedAt is when the entries were shed; nil unless State is archived.
	ArchivedAt *time.Time `json:"archivedAt,omitempty"`
}

ScopeStatus is the result of Backend.Status(scope).

type ScoredEntry

type ScoredEntry struct {
	// Entry is the stored entry, unmodified.
	Entry Entry `json:"entry"`
	// Score is comparable only within one Source; providers use different
	// scales, which is why merging goes through the ranker rather than a sort.
	Score float64 `json:"score"`
	// Source is the SearchProvider.Name that produced the hit.
	Source string `json:"source"`
}

ScoredEntry is one hit with the rank the provider or ranker gave it.

type SearchCapabilities

type SearchCapabilities struct {
	// TextSearch: honors SearchRequest.Text lexically.
	TextSearch bool `json:"textSearch"`
	// VectorSearch: honors SearchRequest.Text by embedding similarity.
	VectorSearch bool `json:"vectorSearch"`
	// FieldFilters: honors SearchRequest.Fields.
	FieldFilters bool `json:"fieldFilters"`
	// TagFilters: honors SearchRequest.Tags.
	TagFilters bool `json:"tagFilters"`
	// TimeRange: honors SearchRequest.Since / Until.
	TimeRange bool `json:"timeRange"`
	// LinkTraversal: can follow entry links while searching.
	LinkTraversal bool `json:"linkTraversal"`
}

SearchCapabilities is a provider's declaration of what it can honor; the composite searcher never lifts a provider beyond what it claims here.

type SearchProvider

type SearchProvider interface {
	Name() string
	SearchCapabilities() SearchCapabilities
	Search(ctx context.Context, req SearchRequest) (SearchResult, error)
	Index(ctx context.Context, scope Scope, kind, id string, content []byte) error
	DeleteIndex(ctx context.Context, scope Scope, kind, id string) error
	DeleteScopeIndex(ctx context.Context, scope Scope) error
}

SearchProvider is one ranked-retrieval backend. The Local facade fans index updates out to every registered provider on Put/Delete/DeleteScope, logging and continuing past a failure rather than failing the write.

type SearchRequest

type SearchRequest struct {
	// Scopes to search; every one must be separately authorized.
	Scopes []Scope `json:"scopes"`
	// Text is the free-text / vector query; empty makes this structured-only.
	Text string `json:"text,omitempty"`
	// Kinds to restrict to; empty means all kinds.
	Kinds []string `json:"kinds,omitempty"`
	// Tags an entry must ALL carry to match.
	Tags []string `json:"tags,omitempty"`
	// Fields are content predicates; providers lacking FieldFilters drop them.
	Fields []FieldFilter `json:"fields,omitempty"`
	// Since bounds CreatedAt below; nil means unbounded.
	Since *time.Time `json:"since,omitempty"`
	// Until bounds CreatedAt above; nil means unbounded.
	Until *time.Time `json:"until,omitempty"`
	// Limit caps the MERGED result, not each provider's; 0 means the default.
	Limit int `json:"limit,omitempty"`
}

SearchRequest is one ranked-retrieval query, fanned out to every provider.

type SearchResult

type SearchResult struct {
	// Entries are this provider's hits, its own best-first.
	Entries []ScoredEntry `json:"entries"`
	// DroppedFilters names request filters this provider could not honor, so a
	// narrower question silently answered as a wider one is visible.
	DroppedFilters []string `json:"droppedFilters,omitempty"`
}

SearchResult is one provider's answer.

type Searcher

type Searcher interface {
	Search(ctx context.Context, req SearchRequest) (MergedSearchResult, error)
}

Searcher is the search executor; the Local facade optionally holds one, and Search returns ErrNoSearchProviders when it does not.

type SessionReadabilityDecider

type SessionReadabilityDecider interface {
	// SessionReadable reports whether a SESSION credential may read entries of
	// this Kind. False means platform components only.
	SessionReadable() bool
}

SessionReadabilityDecider is the READ half of the per-kind door, and it is optional: a Kind that does not implement it is session-readable, which is what every Kind was before this existed. Only a Kind holding data the agent must never see implements it.

Optional rather than a required Kind method because the two directions are not symmetric in cost or in risk. Every Kind must answer WriteAuthority — getting it wrong grants a session the pen for records a component trusts. Almost no Kind needs to be unreadable, and making all thirty-odd declare "yes, readable" would be thirty lines of noise around one real answer.

The risk that comes with optional — a future sensitive Kind forgetting to implement it — is covered by a reviewed pin over the registry, the same device that guards the session-writable set.

type Signal

type Signal struct {
	// Kind selects which hooks react and what Payload's shape is.
	Kind SignalKind `json:"kind"`
	// Scope selects which (Scope, Kind) hooks receive the signal.
	Scope Scope `json:"scope"`
	// At is the event time as the sender observed it, not receipt time.
	At time.Time `json:"at"`
	// Payload is opaque to the framework; the Kind owning this SignalKind
	// defines its shape. Empty when the signal carries no data.
	Payload json.RawMessage `json:"payload,omitempty"`
}

Signal is a lifecycle event fanned out to EVERY registered Kind's ScopeHooks for the scope — a hook that does not recognize the Kind ignores it.

type SignalKind

type SignalKind string

SignalKind is a free-form, namespaced signal identifier owned by some Kind's package (convention: "<owning-kind>/<event>"). Deliberately not an enum, so a Kind can introduce a signal without a framework change.

const (
	// SignalEntryAppended fires after a successful append-only Put. It carries
	// the entry's Scope so a hook can fold that scope's log; it deliberately
	// carries no entry payload, so a hook reads the durable record rather than
	// trusting what the writer said it wrote.
	SignalEntryAppended SignalKind = "memory/entry.appended"
)

Framework-level signal kinds: fired by the facade itself on every registered Kind's ScopeHooks, rather than by one Kind's own package.

type StatusState

type StatusState string

StatusState is the lifecycle state of a Scope as observed by the backend.

const (
	// StatusLive: the backend has (or expects) entries for this scope and
	// intends to serve them.
	StatusLive StatusState = "live"
	// StatusArchived: the backend once held this scope's entries and has since
	// shed them; runner-on-resume surfaces it as "session expired". No shipped
	// backend produces it — time-based archival is not implemented.
	StatusArchived StatusState = "archived"
	// StatusUnknown: no entries the backend can vouch for — brand new or really
	// gone. Callers treat it the same as a fresh scope.
	StatusUnknown StatusState = "unknown"
)

type ToolResultBlock

type ToolResultBlock struct {
	// ToolUseID is the ToolUseBlock.ID this result answers.
	ToolUseID string `json:"toolUseId"`
	// Content is the tool's output as the model will see it.
	Content string `json:"content"`
	// IsError marks a failed call; the content is then the error text.
	IsError bool `json:"isError,omitempty"`
	// RenderedLive records that the live surfaces already showed Content to the
	// user, so a resumed transcript must show it too — the live == reload
	// invariant. Set by the runner from tool.Result.RenderedLive, which only a
	// streaming/interactive toolkit's composed result carries; ordinary tool
	// output (a memory query's rows, a kubectl dump) leaves it false and stays
	// hidden. Read by turn.VisibleTimeline. Content is still the ENVELOPED form
	// the model saw — a renderer unwraps it (pkg/agent/toolenvelope).
	//
	// omitempty is load-bearing for the same reason as Turn.Via: the provenance
	// digest omits empty fields, so an unset flag must not perturb it, and
	// entries written before this existed stay verifiable byte-for-byte.
	RenderedLive bool `json:"renderedLive,omitempty"`
}

type ToolUseBlock

type ToolUseBlock struct {
	// ID correlates this call with the ToolResultBlock answering it.
	ID string `json:"id"`
	// Name is the tool as the agent loop knows it, not the upstream MCP name.
	Name string `json:"name"`
	// Input is raw JSON arguments, opaque to the store.
	Input []byte `json:"input"`
}

type Turn

type Turn struct {
	// Index is the turn's 0-based position in the transcript; (Index, Role) is
	// the identity turn.Appender guards against conflicting re-writes.
	Index int `json:"index"`
	// Role is "user" | "assistant" | "system_note".
	Role string `json:"role"`
	// Content holds the provider-neutral blocks, persisted verbatim.
	Content []ContentBlock `json:"content"`
	// CreatedAt is when the turn was recorded.
	CreatedAt time.Time `json:"createdAt"`
	// Usage is token accounting; nil on anything but an assistant turn.
	Usage *Usage `json:"usage,omitempty"`
	// Author is the canonical subject (e.g. "user:YWxpY2U") of the human who
	// authored this turn, including the index-0 initial prompt, which carries
	// the session starter's subject. Empty for agent/runner-authored turns
	// (tool results), system notes, and kubectl/bento-driven sessions with no
	// human starter. In multiplayer sessions it is the actual sender, not the
	// session initiator.
	Author identity.Subject `json:"author,omitempty"`
	// Via names the browser/TUI surface that injected this turn, as a view URN
	// (urn:ap:view:artifact:artifact-3f2a1b8c/artrev-9d4e0117). Empty for turns
	// arriving through the Channel's own listener. Server-minted, never
	// client-supplied. omitempty is load-bearing: the provenance digest omits
	// empty fields, so an unset Via must not perturb it.
	Via string `json:"via,omitempty"`
	// Refused marks an assistant turn whose provider response carried
	// stop_reason=refusal. The turn stays in the append-only transcript for
	// audit, but replay excludes it from the reconstructed message history so a
	// retry regenerates it cleanly. omitempty for the same digest reason as Via.
	Refused bool `json:"refused,omitempty"`
	// Model is the uniform display id of the model that actually served the
	// turn, "<provider>/<served-model>". Empty on user turns.
	Model string `json:"model,omitempty"`
}

Turn is one entry in a session's transcript. user/assistant roles feed back into the LLM messages array on resume; system_note is operator/runner metadata, filtered out at replay time.

type Usage

type Usage struct {
	// InputTokens billed for the prompt, cache reads excluded.
	InputTokens int64 `json:"inputTokens"`
	// OutputTokens billed for the completion.
	OutputTokens int64 `json:"outputTokens"`
	// CacheCreationTokens written into the prompt cache this turn.
	CacheCreationTokens int64 `json:"cacheCreationTokens,omitempty"`
	// CacheReadTokens served from the prompt cache instead of being billed as input.
	CacheReadTokens int64 `json:"cacheReadTokens,omitempty"`
}

Usage is per-turn token accounting as the provider reported it.

type WriteAuthority

type WriteAuthority uint8

WriteAuthority answers "may an agent session author entries of this Kind?".

The memory HTTP plane authenticates three credential classes: the per-session bearer a runner pod holds, the system component tokens (channelsd/authzd/webd), and — not a credential at all — the operator's own in-process calls. Without this classification a valid per-session token could Put ANY registered Kind into its scope, so a compromised runner could author component-owned state: `cold_start_task` (which authzd reads as "this session's scope review is decided" and the runner BLOCKS on), every append-only audit kind, channelsd's `parked_prompt`, authzd's extraction records.

It lives on the Kind because that is the only place a new variant is guaranteed to be looked at. A denylist in the HTTP handler, or an `if kind == "cold_start_task"` in a consumer, protects exactly the kinds somebody remembered that day and silently admits every kind added after.

const (
	// ComponentWritten: only a platform component may author this Kind — the
	// operator in-process, or a system component bearer (channelsd, authzd). A
	// per-session credential is refused.
	//
	// The ZERO VALUE, deliberately: the failure directions are asymmetric. A
	// Kind wrongly left ComponentWritten refuses a legitimate write LOUDLY on
	// its first attempt, naming the Kind and the rule; a Kind wrongly left
	// session-writable is silent, indefinite, and re-opens exactly the hole this
	// type closes. Fail toward the noisy one.
	ComponentWritten WriteAuthority = iota

	// SessionWritten: the agent session itself authors this Kind, through the
	// per-session memory bearer its runner pod holds (and, on the same
	// credential, `oap memory put`) — transcript turns, artifacts, labels, the
	// per-turn audit the runner produces about its own tool calls.
	//
	// Choosing it is a claim about the THREAT model, not convenience: it says
	// the session's agent loop — driven by model output and by whatever content
	// its tools pulled in — is an acceptable author of this record. If a
	// component reads the Kind to make an authorization decision, it is not.
	SessionWritten
)

func (WriteAuthority) String

func (w WriteAuthority) String() string

String renders a WriteAuthority for logs and test failures. An out-of-range value renders as such rather than panicking: enforcement already treats anything that is not exactly SessionWritten as component-only, so an unknown value is a fail-closed outcome that should be readable, not fatal.

Directories

Path Synopsis
Package assetlimits holds size ceilings shared by both sides of the inbound-attachment upload path: pkg/memory/httpsrv (the operator's POST /inbound-asset route, which enforces the ceiling) and pkg/channels/channelsd/pipeline (which clamps its own pre-fetch size check to the same number, so a file the operator will reject is never downloaded in full first).
Package assetlimits holds size ceilings shared by both sides of the inbound-attachment upload path: pkg/memory/httpsrv (the operator's POST /inbound-asset route, which enforces the ceiling) and pkg/channels/channelsd/pipeline (which clamps its own pre-fetch size check to the same number, so a file the operator will reject is never downloaded in full first).
Package httpclient is the HTTP client for the operator's memory API.
Package httpclient is the HTTP client for the operator's memory API.
Package httpsrv exposes a memory.Memory over a bearer-authed HTTP API.
Package httpsrv exposes a memory.Memory over a bearer-authed HTTP API.
kg
kinds
all
Package all is a convenience: blank-importing it from a binary triggers the init() registration of every memory Kind.
Package all is a convenience: blank-importing it from a binary triggers the init() registration of every memory Kind.
approval
Package approval is the memory Kind for approval-flow events (request + outcome).
Package approval is the memory Kind for approval-flow events (request + outcome).
artifact
Package artifact registers the logical-artifact head Kind.
Package artifact registers the logical-artifact head Kind.
artifactrevision
Package artifactrevision registers the immutable per-revision Kind of the artifact-versioning system.
Package artifactrevision registers the immutable per-revision Kind of the artifact-versioning system.
auditkey
Package auditkey implements the "audit_key" memory Kind: the operator's durable witness of which Ed25519 key an AgentSession instance signs its append-only records with.
Package auditkey implements the "audit_key" memory Kind: the operator's durable witness of which Ed25519 key an AgentSession instance signs its append-only records with.
authz_session_config
Package authz_session_config is the memory Kind for the per-session authz config snapshot: the entity-extraction config plus the two fields authzd derives this session's cold-start authorization policy from.
Package authz_session_config is the memory Kind for the per-session authz config snapshot: the entity-extraction config plus the two fields authzd derives this session's cold-start authorization policy from.
authzdecision
Package authzdecision is the memory Kind for authz.Check outcomes.
Package authzdecision is the memory Kind for authz.Check outcomes.
channel_msg_ref
Package channel_msg_ref implements the "channel_msg_ref" memory kind: a sparse index that maps a channel-kind-scoped message reference (Slack: channel_id:thread_ts:message_ts; future kinds: their own) to the turn index where the inbound message was recorded.
Package channel_msg_ref implements the "channel_msg_ref" memory kind: a sparse index that maps a channel-kind-scoped message reference (Slack: channel_id:thread_ts:message_ts; future kinds: their own) to the turn index where the inbound message was recorded.
coldstarttask
Package coldstarttask is the memory Kind recording the approver's decision for a new session's cold-start scope proposal: which task (cleaned / original / none) the runner should run as the first turn, and whether scope was applied.
Package coldstarttask is the memory Kind recording the approver's decision for a new session's cold-start scope proposal: which task (cleaned / original / none) the runner should run as the first turn, and whether scope was applied.
contentguardaudit
Package contentguardaudit is the memory Kind for the audit log of content-guard enforcement: per-call content-inspection findings (pass/block/approve), one entry per firing.
Package contentguardaudit is the memory Kind for the audit log of content-guard enforcement: per-call content-inspection findings (pass/block/approve), one entry per firing.
envelopefact
Package envelopefact is the memory Kind for a fact derived by platform code from a VERIFIED inbound delivery — the signed webhook body that opened or advanced a session.
Package envelopefact is the memory Kind for a fact derived by platform code from a VERIFIED inbound delivery — the signed webhook body that opened or advanced a session.
extracted_entity
Package extracted_entity is the memory Kind for raw LLM-extracted entity candidates produced by internal/cmd/authzd.
Package extracted_entity is the memory Kind for raw LLM-extracted entity candidates produced by internal/cmd/authzd.
extraction_state
Package extraction_state is the memory Kind tracking authzd's per-turn extraction lifecycle: pending → complete | failed.
Package extraction_state is the memory Kind tracking authzd's per-turn extraction lifecycle: pending → complete | failed.
factcontent
Package factcontent holds the content shape the two fact Kinds share.
Package factcontent holds the content shape the two fact Kinds share.
infoleakageaudit
Package infoleakageaudit is the memory Kind for information-leakage audit events.
Package infoleakageaudit is the memory Kind for information-leakage audit events.
infoleakagedecision
Package infoleakagedecision is the memory Kind for per-session information- leakage approval decisions.
Package infoleakagedecision is the memory Kind for per-session information- leakage approval decisions.
infoleakagetaint
Package infoleakagetaint is the memory Kind for per-session taint records.
Package infoleakagetaint is the memory Kind for per-session taint records.
internal/auditaccessor
Package auditaccessor holds the shared append-one / list-all accessor boilerplate for the simple per-scope audit Kinds.
Package auditaccessor holds the shared append-one / list-all accessor boilerplate for the simple per-scope audit Kinds.
internal/undecodable
Package undecodable holds the one operator-facing report every Kind accessor owes when a stored Entry will not decode.
Package undecodable holds the one operator-facing report every Kind accessor owes when a stored Entry will not decode.
label
Package label is the memory Kind for (resourceType, id) → display label tuples extracted from MCP tool responses.
Package label is the memory Kind for (resourceType, id) → display label tuples extracted from MCP tool responses.
lifecycle
Package lifecycle is the canonical Kind for session/scope-lifecycle signals.
Package lifecycle is the canonical Kind for session/scope-lifecycle signals.
lineage
Package lineage implements the "lineage" memory kind: bidirectional fork edges that record parent ↔ child relationships between AgentSessions when a user triggers Restart-from-here.
Package lineage implements the "lineage" memory kind: bidirectional fork edges that record parent ↔ child relationships between AgentSessions when a user triggers Restart-from-here.
metaagentaudit
Package metaagentaudit is the memory Kind for the per-invocation audit log of metaagent decisions.
Package metaagentaudit is the memory Kind for the per-invocation audit log of metaagent decisions.
metaagentthread
Package metaagentthread is the memory Kind for messages in the metaagent's sub-thread (user mentions + metaagent responses).
Package metaagentthread is the memory Kind for messages in the metaagent's sub-thread (user mentions + metaagent responses).
observation
Package observation is the memory Kind for a note an agent session writes into a resource-scoped memory pool: free text one session concluded about a resource (a customer, a repo, whatever the resource type is), durable for a later session holding a slot on the same pool to read.
Package observation is the memory Kind for a note an agent session writes into a resource-scoped memory pool: free text one session concluded about a resource (a customer, a repo, whatever the resource type is), durable for a later session holding a slot on the same pool to read.
observedfact
Package observedfact is the memory Kind for a fact derived from a tool RESULT by a declared `observes` block.
Package observedfact is the memory Kind for a fact derived from a tool RESULT by a declared `observes` block.
parkedprompt
Package parkedprompt implements the "parked_prompt" memory kind: the durable record of a render-ready prompt a session is parked on, so it can be re-surfaced to a surface that attaches later, or after a restart.
Package parkedprompt implements the "parked_prompt" memory kind: the durable record of a render-ready prompt a session is parked on, so it can be re-surfaced to a surface that attaches later, or after a restart.
plangateaudit
Package plangateaudit is the memory Kind for the plan gate's authorization record: which plan was approved, which phase was active, and what the gate decided for each permissioned call.
Package plangateaudit is the memory Kind for the plan gate's authorization record: which plan was approved, which phase was active, and what the gate decided for each permissioned call.
preferenceaccess
Package preferenceaccess is the memory Kind for the audit log of subject-named preference reads: every GET /memory/_preferences?user-ref= request, resolved or not.
Package preferenceaccess is the memory Kind for the audit log of subject-named preference reads: every GET /memory/_preferences?user-ref= request, resolved or not.
preferencewrite
Package preferencewrite is the memory Kind for the audit log of first-party human writes to user preferences: every direct edit via the Slack App Home (or other UI surfaces) into the acting user's own user_preference scope.
Package preferencewrite is the memory Kind for the audit log of first-party human writes to user preferences: every direct edit via the Slack App Home (or other UI surfaces) into the acting user's own user_preference scope.
pttag
Package pttag is the memory Kind for per-datum provenance tags.
Package pttag is the memory Kind for per-datum provenance tags.
pttagcontent
Package pttagcontent is the memory Kind for redacted content held behind a provenance tag.
Package pttagcontent is the memory Kind for redacted content held behind a provenance tag.
relwritesaudit
Package relwritesaudit is the memory Kind for tuples written by pkg/authz/relwrites after a successful tool call.
Package relwritesaudit is the memory Kind for tuples written by pkg/authz/relwrites after a successful tool call.
scopeaudit
Package scopeaudit is the memory Kind for the audit log of applied ScopeDeltas — one entry per applied delta.
Package scopeaudit is the memory Kind for the audit log of applied ScopeDeltas — one entry per applied delta.
sessionscope
Package sessionscope is the memory Kind for the dynamic session scope (Layer 2).
Package sessionscope is the memory Kind for the dynamic session scope (Layer 2).
systemprompt
Package systemprompt is the memory Kind for the append-only record of the system prompt an agent actually ran under.
Package systemprompt is the memory Kind for the append-only record of the system prompt an agent actually ran under.
tool_dispatch_snapshot
Package tool_dispatch_snapshot implements the "tool_dispatch_snapshot" memory kind: a record that the runner requested — and the controller committed — a workspace snapshot immediately before a specific tool dispatch with stateImpact ∈ {readwrite, external}.
Package tool_dispatch_snapshot implements the "tool_dispatch_snapshot" memory kind: a record that the runner requested — and the controller committed — a workspace snapshot immediately before a specific tool dispatch with stateImpact ∈ {readwrite, external}.
toolcatalog
Package toolcatalog is the memory Kind for the append-only record of which tools an agent was OFFERED, and from which turn.
Package toolcatalog is the memory Kind for the append-only record of which tools an agent was OFFERED, and from which turn.
toolchainaudit
Package toolchainaudit is the memory Kind for the append-only record of toolchain selection.
Package toolchainaudit is the memory Kind for the append-only record of toolchain selection.
toolguardaudit
Package toolguardaudit is the memory Kind for the audit log of tool-guard enforcement events: breaker trips/recoveries, rate-limit hits, denials and halts.
Package toolguardaudit is the memory Kind for the audit log of tool-guard enforcement events: breaker trips/recoveries, rate-limit hits, denials and halts.
toolsession
Package toolsession registers the tool_session Kind — one Entry per parsed event from an interactive tool's stream (e.g.
Package toolsession registers the tool_session Kind — one Entry per parsed event from an interactive tool's stream (e.g.
triggerdelivery
Package triggerdelivery is the memory Kind for the append-only record of the signed webhook delivery that STARTED a session.
Package triggerdelivery is the memory Kind for the append-only record of the signed webhook delivery that STARTED a session.
turn
Package turn registers the LLM-transcript Kind.
Package turn registers the LLM-transcript Kind.
uiaction
Package uiaction is the memory Kind for an agent-UI action's lifecycle record: one entry per request ID, rewritten in place as the call progresses (submitted -> awaiting_approval -> running -> settled).
Package uiaction is the memory Kind for an agent-UI action's lifecycle record: one entry per request ID, rewritten in place as the call progresses (submitted -> awaiting_approval -> running -> settled).
uiviewmodel
Package uiviewmodel is the memory Kind for an agent-UI Tier-1 view-model: one record per (session, ui, hook), rewritten in place each time the agent calls update_view for that hook.
Package uiviewmodel is the memory Kind for an agent-UI Tier-1 view-model: one record per (session, ui, hook), rewritten in place each time the agent calls update_view for that hook.
uiviewparams
Package uiviewparams is the memory Kind for the agent's own binding-parameter choices on an agent-defined UI: one record per (session, ui), rewritten in place each time the agent calls set_view_params.
Package uiviewparams is the memory Kind for the agent's own binding-parameter choices on an agent-defined UI: one record per (session, ui), rewritten in place each time the agent calls set_view_params.
userpreference
Package userpreference is the memory Kind for one user's saved preference values, keyed per (classNamespace, className, key) in the user's own scope (memory.UserScope).
Package userpreference is the memory Kind for one user's saved preference values, keyed per (classNamespace, className, key) in the user's own scope (memory.UserScope).
Package memcopy provides the turn-anchored memory-copy primitive the AgentSession controller's restart reconciler uses to fork a new session from an existing one.
Package memcopy provides the turn-anchored memory-copy primitive the AgentSession controller's restart reconciler uses to fork a new session from an existing one.
Package memtest provides Backend test doubles for the memory framework.
Package memtest provides Backend test doubles for the memory framework.
Package pools answers one question: which resource memory pools may this session touch, and in which direction?
Package pools answers one question: which resource memory pools may this session touch, and in which direction?
Package provenance signs and verifies append-only memory entries: per-publisher Ed25519 signatures over a canonical entry digest, hash-chained per (scope, publisher) for gap/truncation detection.
Package provenance signs and verifies append-only memory entries: per-publisher Ed25519 signatures over a canonical entry digest, hash-chained per (scope, publisher) for gap/truncation detection.
Package pttagmint derives a datum's audience and records its provenance tag.
Package pttagmint derives a datum's audience and records its provenance tag.
Package publisherkeys is an in-memory publisher→keyID→Ed25519 key store for component publishers (channelsd, authzd, operator).
Package publisherkeys is an in-memory publisher→keyID→Ed25519 key store for component publishers (channelsd, authzd, operator).
Package sentinel is the single source of truth for how a memory sentinel error crosses the HTTP wire, in both directions: httpsrv reads Table to pick the response status and stamp Header; httpclient reads it to rebuild the sentinel from Header.
Package sentinel is the single source of truth for how a memory sentinel error crosses the HTTP wire, in both directions: httpsrv reads Table to pick the response status and stamp Header; httpclient reads it to rebuild the sentinel from Header.
Package tokens maintains a per-AgentSession bearer-token registry.
Package tokens maintains a per-AgentSession bearer-token registry.

Jump to

Keyboard shortcuts

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