// Copyright 2024 Statelet Contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
syntax = "proto3";
package statelet.v1;
option java_outer_classname = "StateletProto";
// Statelet key-value store RPC service.
//
// All keys and values are raw bytes. Column families (cf) are identified by
// a uint32 id. Use USER_COLUMN_FAMILY_ID = 0 for the default user CF.
service Statelet {
// Liveness check.
rpc Ping(PingRequest) returns (PingResponse);
// Write a single key-value pair.
rpc Put(PutRequest) returns (PutResponse);
// Read the value for a key. Returns found=false when the key does not exist.
rpc Get(GetRequest) returns (GetResponse);
// Delete a key.
rpc Delete(DeleteRequest) returns (DeleteResponse);
// Merge an operand into the existing value (requires a MergeOperator on the CF).
rpc Merge(MergeRequest) returns (MergeResponse);
// Atomically apply a batch of Put/Delete/Merge operations.
rpc BatchWrite(BatchWriteRequest) returns (BatchWriteResponse);
// Atomically apply one conditional gate mutation plus plain trailing
// Put/Delete/Merge operations. The whole batch commits only when the gate
// predicate holds at the shard leader.
rpc ConditionalBatchWrite(ConditionalBatchWriteRequest) returns (ConditionalBatchWriteResponse);
// Atomic set-if-(not-)exists (compare-and-swap on key presence) for a single
// key. The existence check and the write commit as one indivisible step at
// the owning shard leader, backing Redis `SET … NX/XX`, `SETNX` and `GETSET`
// through the gateway.
rpc ConditionalSet(ConditionalSetRequest) returns (ConditionalSetResponse);
// Read multiple keys in a single round-trip.
rpc BatchGet(BatchGetRequest) returns (BatchGetResponse);
// ── Text embedding + search (gateway-side embedding) ────────────────────
// Insert text: gateway embeds text, then stores KV metadata + vector.
rpc TextPut(TextPutRequest) returns (TextPutResponse);
// Search by text: gateway embeds query, runs vector search, hydrates KV metadata.
rpc TextSearch(TextSearchRequest) returns (TextSearchResponse);
// ── Text + Graph (gateway-side embedding + graph storage) ──────────────
// Embed text → GraphAddNode (with vector + properties) + optional GraphAddEdge.
rpc TextGraphPut(TextGraphPutRequest) returns (TextGraphPutResponse);
// Embed query → GraphSearch → hydrate node properties.
rpc TextGraphSearch(TextGraphSearchRequest) returns (TextGraphSearchResponse);
// Query edges for a graph node (delegates to GraphQueryEdges).
rpc TextGraphQueryEdges(TextGraphQueryEdgesRequest) returns (TextGraphQueryEdgesResponse);
// Get a graph node's properties by ID.
rpc TextGraphGetNode(TextGraphGetNodeRequest) returns (TextGraphGetNodeResponse);
// ── Embedding primitive (gateway-only) ─────────────────────────────────
//
// Pure text→vector: embed each text with the gateway's resident dense model
// and return the raw vectors, storing NOTHING. Lets a caller that owns its
// own storage pipeline (its own node ids, properties, graph structure) get
// statelet-local vectors and then write them via VectorPut / GraphAddNode —
// instead of depending on an external embedding API or ceding node
// construction to TextGraphPut. The model runs once, in the gateway.
rpc Embed(EmbedRequest) returns (EmbedResponse);
// ── Triple store (epic #1432) ──────────────────────────────────────────
//
// Write one (s, p, o, valid_from, valid_to, props?) triple into the
// per-graph triple CF `t:{graph}` (provisioned as an ordinary `CfType::User`
// CF). Terms are interned through the on-CF dictionary (T2ID/ID2T/META) and
// the three permutations (SPO authoritative + POS/OSP index-only), the LIT
// row (for a literal object), the dictionary rows and the bumped META
// high-water are all written in ONE atomic `WriteBatch` (one Raft entry), so
// the permutations and dictionary never diverge across a crash. Returns the
// interned subject/predicate/object ids. Gateway-only (Phase 1 of #1432).
rpc TriplePut(TriplePutRequest) returns (TriplePutResponse);
// Query one bound/unbound triple pattern (s?, p?, o?, as_of?) against the
// per-graph triple CF `t:{graph}`. The handler selects the SPO/POS/OSP index
// whose leading columns are bound (the 6-pattern BGP table), `seek(prefix)`s
// and iterates-while-prefix over the merged memtable+`.sst` view, applies the
// inverted-`valid_from` newest-wins ordering plus the optional `as_of`
// temporal filter, hides tombstoned triples, and resolves each TermId back to
// its string through the on-CF ID2T dictionary. Gateway-only (Phase 2 of
// #1432).
rpc TripleQuery(TripleQueryRequest) returns (TripleQueryResponse);
// Evaluate a basic graph pattern (BGP): a list of triple patterns sharing
// variables. The handler runs a left-deep, selectivity-ordered
// index-nested-loop join (INLJ) over the Phase-2 single-pattern primitive —
// it orders patterns most-bound-first, binds variables left-to-right, and
// evaluates each pattern as a Phase-2 prefix scan parameterized by the current
// binding. Returns one row of variable→value bindings per solution. No
// cost-based planner in v1 (documented limitation; structural selectivity
// only). Gateway-only (Phase 3 of #1432).
rpc TripleBgp(TripleBgpRequest) returns (TripleBgpResponse);
// Cross-modal linkage between the vector index (HNSW/SpFresh) and the triple
// store, exploiting the shared id space (term-id == node-id, no remap). Three
// modes generalize the existing temporal_join pattern:
// 1. VECTOR_TO_TRIPLE: HNSW.search(q,k) → node ids → SPO prefix-scan
// (id, P?, ?) to filter/re-rank by symbolic structure.
// 2. TRIPLE_TO_VECTOR: BGP/single-pattern → ids → HNSW.search for
// similar-but-unconnected entities.
// 3. VECTOR_GUIDED_KHOP: rank a triple frontier (k-hop expansion) by vector
// similarity, returning the top-N most semantically relevant neighbors.
// Gateway-only (Phase 3 of #1432).
rpc TripleLink(TripleLinkRequest) returns (TripleLinkResponse);
// Resolve the conflict set containing a node: expand contradicts/gedge_rev
// edges, then return the authoritative claim, the dissenting set, and the
// policy rationale. Gateway-only; auditable "show consensus" endpoint.
rpc ResolveConflict(ResolveConflictRequest) returns (ResolveConflictResponse);
// (#828 LongMemEval Phase 5b) LLM-free entity-resolution candidate generation:
// given a graph (+ optional query terms) run the blocking-then-scoring resolver
// (alias rule + entity-mention ANN nearest-neighbor + lexical) and return the
// candidate clusters (canonical → surfaces, with method+score). Gateway-only;
// candidate primitive — transitive persistence is Phase 5c.
rpc ResolveEntities(ResolveEntitiesRequest) returns (ResolveEntitiesResponse);
// ── Vector index operations ──────────────────────────────────────────────
// Create or reconfigure an HNSW vector index.
rpc CreateVectorIndex(CreateVectorIndexRequest) returns (CreateVectorIndexResponse);
// Drop an HNSW vector index.
rpc DropVectorIndex(DropVectorIndexRequest) returns (DropVectorIndexResponse);
// Insert or update a vector in the index.
rpc VectorPut(VectorPutRequest) returns (VectorPutResponse);
// Remove a vector from the index.
rpc VectorDelete(VectorDeleteRequest) returns (VectorDeleteResponse);
// Approximate nearest neighbor search.
rpc VectorSearch(VectorSearchRequest) returns (VectorSearchResponse);
// Retrieve a stored vector by id.
rpc VectorGet(VectorGetRequest) returns (VectorGetResponse);
// Batch insert vectors into the index.
rpc VectorBatchPut(VectorBatchPutRequest) returns (VectorBatchPutResponse);
// Batch delete vectors from the index.
rpc VectorBatchDelete(VectorBatchDeleteRequest) returns (VectorBatchDeleteResponse);
// Train quantization parameters (PQ codebooks, IVF centroids) for an index.
rpc VectorTrain(VectorTrainRequest) returns (VectorTrainResponse);
// Sample random vectors from a named index on this node (used for global training).
rpc VectorSample(VectorSampleRequest) returns (VectorSampleResponse);
// Export live (id, vector) pairs from a LEGACY independent-family index,
// paginated by id. Retirement migration (P7) only; graph-backed and exempt
// index types return FailedPrecondition.
rpc VectorExport(VectorExportRequest) returns (VectorExportResponse);
// Ingest sparse (term->weight) documents into a per-index inverted posting store.
rpc SparseIngest(SparseIngestRequest) returns (SparseIngestResponse);
// Hybrid dense + sparse retrieval with RRF or weighted fusion.
rpc HybridSearch(HybridSearchRequest) returns (HybridSearchResponse);
// Scan keys with an optional prefix filter. Returns a page of key-value pairs.
rpc Scan(ScanRequest) returns (ScanResponse);
// Delete all keys matching a prefix. Returns the number of keys deleted.
rpc DeleteByPrefix(DeleteByPrefixRequest) returns (DeleteByPrefixResponse);
// Collect per-shard / per-CF statistics from this data node.
rpc GetNodeStats(GetNodeStatsRequest) returns (GetNodeStatsResponse);
// Admin: checkpoint this data node — force-flush every hosted shard's state
// machine, advance idle shards' WAL TRUNCATE floors, and GC reclaimable WAL
// segments. Called after bulk ingest so a subsequent restart replays a small
// WAL instead of the whole ingest burst (issue #754). Data-node only; the
// gateway exposes it as POST /api/v1/admin/checkpoint fanning out per node.
rpc Checkpoint(CheckpointRequest) returns (CheckpointResponse);
// Admin: read this node's Hybrid Logical Clock (epic #1478, Phase 1). Returns
// the current HLC reading plus whether cross-shard transactions are enabled,
// so the clock that orders cross-shard commits is observable. Sampling the
// clock advances it (it is a `now()`), so this is a lightweight admin probe,
// not a hot-path RPC.
rpc GetClusterClock(GetClusterClockRequest) returns (GetClusterClockResponse);
// ── Agent State: data-node leaf operations ─────────────────────────────
// Add a causal step (write props + content atomically).
rpc AgentAddStep(AgentAddStepRequest) returns (AgentAddStepResponse);
// Add a causal edge (forward + reverse).
rpc AgentAddEdge(AgentAddEdgeRequest) returns (AgentAddEdgeResponse);
// Get a causal step's metadata.
rpc AgentGetStep(AgentGetStepRequest) returns (AgentGetStepResponse);
// Get a causal step's content.
rpc AgentGetContent(AgentGetContentRequest) returns (AgentGetContentResponse);
// Get edges for a step (incoming or outgoing).
rpc AgentGetEdges(AgentGetEdgesRequest) returns (AgentGetEdgesResponse);
// Single-shard BFS traversal returning steps + edges from GraphSST.
rpc AgentLocalTraverse(AgentLocalTraverseRequest) returns (AgentLocalTraverseResponse);
// Compare-and-swap put.
rpc AgentCasPut(AgentCasPutRequest) returns (AgentCasPutResponse);
// Single-DBImpl optimistic transaction commit with snapshot-isolation
// conflict detection. The client buffers reads (cf, key, observed_seq) and
// writes (puts/deletes), then submits them in one call; the server takes
// sharded key locks, re-validates the read-set against the latest seqs, and
// atomically applies the writes (or aborts on conflict).
rpc AgentTxnCommit(AgentTxnCommitRequest) returns (AgentTxnCommitResponse);
// Coordination primitives. A claim is an atomic SetIfNotExists(claim_key,
// agent_id); a lease adds a TTL so an un-renewed holder auto-expires; renew
// and release are fenced so only the live holder can act. Issue #691.
rpc AgentClaim(AgentClaimRequest) returns (AgentClaimResponse);
rpc AgentLease(AgentLeaseRequest) returns (AgentLeaseResponse);
rpc AgentRenew(AgentRenewRequest) returns (AgentRenewResponse);
rpc AgentRelease(AgentReleaseRequest) returns (AgentReleaseResponse);
// Cross-shard ACID transactions — Phase 2 (epic #1478): the internal
// Percolator-style *prewrite*. Conditionally places a LockRecord intent on
// lock/<cf>/<user_key> in the coordination CF and stages the provisional
// value, aborting on a conflicting lock or a newer committed version. Gated
// behind STATELET_CROSS_SHARD_TXN, DEFAULT OFF (returns FailedPrecondition
// when the flag is unset); commit/roll-forward arrive in Phase 3.
rpc AgentPrewrite(AgentPrewriteRequest) returns (AgentPrewriteResponse);
// Cross-shard ACID transactions — Phase 3 (epic #1478): the internal commit
// point + resolution drivers, used by the gateway coordinator. AgentCommitPrimary
// is the single fence-gated CAS that flips the primary TxnStatus
// Prewritten->Committed (or Aborted); AgentRollForward replaces a secondary's
// LockRecord with a WriteRecord and materializes the staged value;
// AgentRollback drops a secondary's intent + staged value. All gated behind
// STATELET_CROSS_SHARD_TXN, DEFAULT OFF.
rpc AgentCommitPrimary(AgentCommitPrimaryRequest) returns (AgentCommitPrimaryResponse);
rpc AgentRollForward(AgentRollForwardRequest) returns (AgentRollForwardResponse);
rpc AgentRollback(AgentRollbackRequest) returns (AgentRollbackResponse);
// Cross-shard ACID transactions — Phase 4 (epic #1478): the read-path lock
// resolver. Given a (cf, key) and a snapshot read_ts, resolves any blocking
// prewrite lock via the primary TxnStatus — rolling a committed secondary
// forward, cleaning an aborted/stale (TTL-expired) intent, or reporting the
// commit decision is still pending. Idempotent and callable by any reader. The
// gateway runs this as a pre-step before its causal/vector reads. Gated behind
// STATELET_CROSS_SHARD_TXN, DEFAULT OFF.
rpc AgentResolveLock(AgentResolveLockRequest) returns (AgentResolveLockResponse);
// Cross-shard ACID transactions (epic #1478, issue #1598): read the primary
// TxnStatus for a primary key from THIS node's coordination shard. The primary
// commit record is written through the coordination Raft group, so a secondary
// OWNER shard whose node is not in the coordination shard's replica set cannot
// read it locally (RF < node_count, disjoint replica sets). The owner-shard
// resolver routes the primary-status read here (to the coordination-shard
// leader) instead of its node-local store, mirroring the write side. Internal
// /admin-only and gated behind STATELET_CROSS_SHARD_TXN, DEFAULT OFF.
rpc AgentReadTxnStatus(AgentReadTxnStatusRequest) returns (AgentReadTxnStatusResponse);
// Cross-shard ACID transactions (epic #1478, issue #1598): enumerate every
// DECIDED primary TxnStatus (Committed/Aborted) on the receiving node's
// coordination shard, with its participant (cf,key) list. The gateway's
// recovery sweep calls this on the coordination-shard leader (the primary
// statuses live there) and then fans a roll-forward / roll-back to each
// participant's OWNER shard — the secondary locks the coordination shard cannot
// itself reach under RF < node_count. Internal/admin-only, gated behind
// STATELET_CROSS_SHARD_TXN (default OFF).
rpc AgentListDecidedPrimaries(AgentListDecidedPrimariesRequest) returns (AgentListDecidedPrimariesResponse);
// Cross-shard ACID transactions — Phase 3 (epic #1478): the user-facing
// gateway coordinator RPC. Drives prewrite->commit->roll-forward for a
// write_set spanning any set of shards/Raft groups, returning an all-or-nothing
// commit decision. A single-shard write_set takes the optimistic fast path and
// bypasses 2PC. Gated behind STATELET_CROSS_SHARD_TXN, DEFAULT OFF (returns
// FailedPrecondition when the flag is unset).
rpc CrossShardTxnCommit(CrossShardCommitRequest) returns (CrossShardCommitResponse);
// Cross-shard ACID transactions — Phase 5 (epic #1478): recovery & liveness.
// ResolveStaleTxn is the user-facing admin RPC that drives the idempotent
// stale-lock resolver — it consults each lock's primary TxnStatus (Committed
// -> roll forward; absent/Prewritten + lock TTL expired -> roll back) and
// reclaims coordinator-crash / partition-orphaned intents. AgentResolveStaleTxn
// is the internal per-coordination-shard driver the gateway fans to. Both gated
// behind STATELET_CROSS_SHARD_TXN, DEFAULT OFF (FailedPrecondition when unset).
rpc ResolveStaleTxn(ResolveStaleTxnRequest) returns (ResolveStaleTxnResponse);
rpc AgentResolveStaleTxn(AgentResolveStaleTxnRequest) returns (AgentResolveStaleTxnResponse);
// AgentGcExpiredLocks is the internal per-OWNER-shard sweep the gateway fans to
// (issue #1795). The decided-primaries scan (AgentListDecidedPrimaries) only
// enumerates COMMITTED/ABORTED primaries from the coordination shard, so a
// coordinator that crashed after prewriting secondaries but BEFORE the commit
// point leaves its primary forever Prewritten — never enumerated, so its
// orphaned owner-shard intents are only reclaimed if a read happens to hit the
// exact key. This RPC scans the TTL-expired prewrite locks on one owner shard
// and resolves each against the coordination-shard primary status (rolling back
// the never-committed ones), so the gateway sweep reclaims still-Prewritten
// orphans across every participant shard without a read. Mirrors TiKV's
// background ResolveLocks sweep over participant Regions. Gated behind
// STATELET_CROSS_SHARD_TXN, DEFAULT OFF.
rpc AgentGcExpiredLocks(AgentGcExpiredLocksRequest) returns (AgentGcExpiredLocksResponse);
// Cross-shard ACID transactions — Phase 6 (epic #1478): buffered
// BEGIN/COMMIT/ROLLBACK (TiKV-style optimistic 2PC). `TxnBegin` allocates a
// transaction handle (the primary key every prewrite will fence on); the
// client buffers its write_set locally and submits it at `TxnCommit`, which
// drives the same prewrite->commit->roll-forward as CrossShardTxnCommit but
// pinned to the begun primary. `TxnRollback` discards the handle (optimistic
// 2PC prewrites nothing before COMMIT, so it is a buffer-drop ack). All gated
// behind STATELET_CROSS_SHARD_TXN, DEFAULT OFF (FailedPrecondition when unset).
rpc TxnBegin(TxnBeginRequest) returns (TxnBeginResponse);
rpc TxnCommit(TxnCommitRequest) returns (TxnCommitResponse);
rpc TxnRollback(TxnRollbackRequest) returns (TxnRollbackResponse);
// Expire an existing edge (set valid_to).
rpc AgentExpireEdge(AgentExpireEdgeRequest) returns (AgentExpireEdgeResponse);
// Cascade-expire (#693): retract a fact and recursively close every fact
// transitively derived from it — across agent boundaries — bitemporally,
// diamond/cycle-safe, with retraction provenance + change-feed emission.
rpc AgentCascadeExpire(AgentCascadeExpireRequest) returns (AgentCascadeExpireResponse);
// Supersede a fact with a replacement (#693 phase 4). Closes old_fact, records
// the Supersedes edge, and — when cascade=true — cascade-closes old_fact's
// derived dependents (stamped Superseded/Cascaded provenance).
rpc AgentSupersedeFact(AgentSupersedeFactRequest) returns (AgentSupersedeFactResponse);
// Transactional memory ingest (#780): dedup / create + provenance edges /
// supersede candidates, all committed as ONE atomic, snapshot-isolated
// WriteBatch via the optimistic transaction manager. On a snapshot-isolation
// conflict the engine retries within a bounded budget, then returns
// action=Conflict (back-pressure) without writing.
rpc AgentMemoryIngest(AgentMemoryIngestRequest) returns (AgentMemoryIngestResponse);
// Get edge version history for a specific (src, dst, type) triple.
rpc AgentEdgeHistory(AgentEdgeHistoryRequest) returns (AgentEdgeHistoryResponse);
// ── Memory-scope provenance audit (#697 phase 4) ────────────────────────
// Read back the immutable provenance log (one record per access decision:
// AddStep/GetStep/Traverse/FindSimilar/GetEdges, incl. AdminBypass). Gated by
// ManageMemoryScope. The result is materialized (not streamed) since the audit
// tool scans a bounded time window; large windows page via after_ts/after_seq.
rpc AgentQueryProvenance(AgentQueryProvenanceRequest) returns (AgentQueryProvenanceResponse);
// ── Memory-scope team-membership admin (#697 phase 2c / #794) ───────────
// Grant or revoke an agent's membership of a team. Durable through the
// metadata Raft group. Gated by ManageMemoryScope. The grant store is
// metadata-side, so on the raw Statelet leaf this is unimplemented — operators
// call it through AgentStateService, which authorizes then applies the op.
rpc AgentManageTeamGrant(AgentManageTeamGrantRequest) returns (AgentManageTeamGrantResponse);
// ── Durable agent execution (#846, epic #699 / sub-epic #792) ───────────
// Raft-backed run/step home: each write is a RAFT_TYPE_KV log entry on the
// owning shard, replicated to a quorum before ack, so a crashed multi-step
// agent resumes at the exact failed step on a new leader. RunStep/CompleteStep
// are split so the server never executes client code over the wire (Temporal/
// DBOS record-before-effect across the network).
rpc AgentStartRun(AgentStartRunRequest) returns (AgentStartRunResponse);
rpc AgentRunStep(AgentRunStepRequest) returns (AgentRunStepResponse);
rpc AgentCompleteStep(AgentCompleteStepRequest) returns (AgentCompleteStepResponse);
rpc AgentCheckpointGet(AgentCheckpointGetRequest) returns (AgentCheckpointGetResponse);
rpc AgentCheckpointLatest(AgentCheckpointLatestRequest) returns (AgentCheckpointLatestResponse);
rpc AgentProvenanceChainQuery(AgentProvenanceChainQueryRequest) returns (AgentProvenanceChainQueryResponse);
rpc AgentResumeFromStep(AgentResumeFromStepRequest) returns (AgentResumeFromStepResponse);
rpc AgentResumeSemantic(AgentResumeSemanticRequest) returns (AgentResumeSemanticResponse);
rpc AgentGetRunStatus(AgentGetRunStatusRequest) returns (AgentGetRunStatusResponse);
// Phase 5 (#797): branch/time-travel resume — fork a run from any historical
// step_seq into a NEW AgentFork branch + child run, leaving the source run
// untouched (LangGraph "time-travel"). Leaf RPC homed on the source run's shard.
rpc AgentForkRun(AgentForkRunRequest) returns (AgentForkRunResponse);
rpc AgentForkAcrossCandidates(AgentForkAcrossCandidatesRequest) returns (AgentForkAcrossCandidatesResponse);
// Phase 2 (#1699): content-addressed artifact records for large durable-run
// results. Leaf RPCs are homed on the owning run shard and authorized against
// RunRecord.agent_id.
rpc AgentArtifactPut(AgentArtifactPutRequest) returns (AgentArtifactPutResponse);
rpc AgentArtifactGet(AgentArtifactGetRequest) returns (AgentArtifactGetResponse);
rpc AgentArtifactResolve(AgentArtifactResolveRequest) returns (AgentArtifactResolveResponse);
// ── Team time-travel (#787, epic #698 Phase 3) ──────────────────────────
// Leaf, single-shard "as-of-then" team belief reconstruction. The gateway
// fans these out to every shard, pins one committed ordinal per shard (the
// FoundationDB-style read version), merges/dedupes, paginates, and tolerates
// dead shards (partial view). Calls the in-process #724 operators
// CausalGraphManager::team_snapshot / team_diff.
rpc AgentTeamSnapshotLocal(TeamSnapshotLocalRequest) returns (TeamSnapshotLocalResponse);
rpc AgentTeamDiffLocal(TeamDiffLocalRequest) returns (TeamDiffLocalResponse);
// ── Bitemporal belief queries: "who believed what, when" ────────────────
// Combined (valid-time as_of, transaction-time tx_as_of, author) query that
// reconstructs any agent's (or the team's) belief state at a past instant.
rpc AgentBeliefQuery(AgentBeliefQueryRequest) returns (AgentBeliefQueryResponse);
// Per-agent belief divergence at (as_of, tx_as_of): "A believes X, B believes ¬X".
rpc AgentBeliefDivergence(AgentBeliefDivergenceRequest) returns (AgentBeliefDivergenceResponse);
// ── Raw agent-state row access (P3) ────────────────────────────────────
// Read rows out of an agent column family BY NAME, so a coordinator outside
// the storage process can drive agent semantics itself instead of asking the
// data node to. This is what lets the agent RPCs move to the gateway: the
// gateway already links the row codecs (one codebase), it was only missing
// the bytes.
//
// Why a dedicated pair rather than the generic Get/Scan: the agent CFs live
// on the shared DB and are deliberately NOT registered in the metadata CF
// registry, so `(cf, key)` routing resolves nothing for them. Registering
// them would mint a SECOND Raft group over rows a different group already
// writes — a split-brain shape that has erased data in this system before.
// Instead these address the pinned coordination shard explicitly and are
// served ONLY by its leader, exactly like the claim/lease CAS path.
rpc AgentStateGet(AgentStateGetRequest) returns (AgentStateGetResponse);
rpc AgentStateScan(AgentStateScanRequest) returns (AgentStateScanResponse);
// Append one already-decided provenance record to the immutable audit log.
//
// The scope DECISION moves to the gateway with the rest of agent semantics;
// the audit APPEND stays here. §4 of the design already excludes the
// provenance log from the triple-plane move, and keeping the append local
// avoids turning a best-effort local write into a cross-process failure mode
// on every audited read. The gateway sends the record it built; this RPC is
// internal-token gated, same as the rest of the agent surface, because a
// caller that could reach it directly could forge audit entries.
rpc AgentAppendProvenance(AgentAppendProvenanceRequest) returns (AgentAppendProvenanceResponse);
// Raw adjacency of one anchor, with the bitemporal filters applied and NO
// scope filtering. Edges are served from an in-memory index rebuilt at open,
// not read row-by-row, so `AgentStateScan` cannot reconstruct them — and
// reimplementing bitemporal visibility on the coordinator would put the
// subtlest filtering in this system in two places. Temporal filtering stays
// with the index; the scope decision is the caller's.
//
// Leaks peer ids by construction. Internal-token gated, never client-facing.
rpc AgentStateEdges(AgentStateEdgesRequest) returns (AgentStateEdgesResponse);
// Batch sibling of AgentStateGet over one role. A coordinator filtering an
// adjacency list has to check every peer's scope; one round trip per peer is
// fine in-process and untenable across it, so the peers resolve in one call.
rpc AgentStateBatchGet(AgentStateBatchGetRequest) returns (AgentStateBatchGetResponse);
// Per-author belief resolution for one edge slot ("A believes X, B believes
// not-X"). Another reduction over the revision chain — this time grouped by
// author — so it stays with the index for the same reason the bitemporal form
// does. Returns every author's belief unfiltered; the caller applies scope.
rpc AgentStateBeliefDivergence(AgentStateBeliefDivergenceRequest)
returns (AgentStateBeliefDivergenceResponse);
// Fetch a run record together with one of its checkpoints, from the shard the
// `run_id` self-routes to (`run_id >> 40`) — NOT the coordination shard the
// causal primitives serve, because durable-execution state is homed per run
// shard.
//
// Both in one call because the caller needs the run record to authorize the
// checkpoint at all: splitting them would make every checkpoint read two
// round trips to answer one question.
rpc AgentRunCheckpointGet(AgentRunCheckpointGetRequest)
returns (AgentRunCheckpointGetResponse);
// Team belief reconstruction / diff over one shard, UNFILTERED, together with
// that shard's committed read version.
//
// The graph operators (`team_snapshot` / `team_diff`) walk the in-memory
// index and the read version is the shard's own MVCC seq, so both stay here;
// visibility filtering, the global sort and pagination are the caller's.
// Answering the read version in the SAME call is the point — it is the
// FoundationDB-style per-shard fence the coordinator maxes across shards, and
// fetching it separately would fence against a different instant than the one
// the answer was computed at.
rpc AgentStateTeamRead(AgentStateTeamReadRequest) returns (AgentStateTeamReadResponse);
// Conditional write PINNED to the coordination shard's Raft group.
//
// The generic ConditionalBatchWrite routes by `(cf, key)`, which for agent
// coordination state is the wrong group: those rows are written through the
// coordination shard, and ordering them in a different group would let two
// writers to the same claim key be serialized by two different logs. Same
// pinning the claim/lease CAS already relies on.
rpc AgentStateConditionalWrite(AgentStateConditionalWriteRequest)
returns (ConditionalBatchWriteResponse);
// Per-key engine VERSIONS (no values) for arbitrary `(cf, key)` pairs, read
// from the coordination shard together with that shard's current sequence.
//
// Versions only, deliberately. Optimistic-commit validation and conflict
// reporting need nothing else, and a versions-only surface is a far smaller
// capability than "read any CF by name" — it cannot disclose content. Serves
// the same admin-only callers `AgentTxnCommit` already restricts itself to.
rpc AgentStateVersions(AgentStateVersionsRequest) returns (AgentStateVersionsResponse);
// Allocate a contiguous block of causal step/fact ids from the ONE authority.
//
// Uniqueness comes from a single in-process atomic on the coordination
// shard's causal manager, not from anything a caller could reproduce: two
// coordinators running their own counters would hand out the same id. So a
// coordinator that needs ids asks for them, and amortizes the round trip by
// taking a block.
rpc AgentStateAllocIds(AgentStateAllocIdsRequest) returns (AgentStateAllocIdsResponse);
// Subscribe to write events on this shard (server-streaming to gateway).
// DEPRECATED (CDC Phase 5b, issue #823): superseded by SubscribeCommitted,
// which is a durable, ordered, offset-addressable, resumable superset of this
// best-effort live-only feed. Prefer SubscribeCommitted for all new consumers;
// this RPC remains for backward compatibility and will be removed in a future
// major version.
rpc AgentSubscribeWrites(AgentSubscribeWritesRequest) returns (stream AgentWriteEventProto) {
option deprecated = true;
}
// Durable, ordered, offset-addressable, resumable change-feed (CDC) keyed on
// the stable Raft log index. Catch-up from a past offset (replayed from the
// durable log) then live-tail; consumer-checkpointed resume (issue #692).
rpc SubscribeCommitted(SubscribeCommittedRequest) returns (stream CommittedFeedItem);
// ── Agent State: branch (fork) leaf operations ──────────────────────────
rpc AgentFork(AgentForkRequest) returns (AgentForkResponse);
rpc AgentMergeBranch(AgentMergeBranchRequest) returns (AgentMergeBranchResponse);
rpc AgentDiscardBranch(AgentDiscardBranchRequest) returns (AgentDiscardBranchResponse);
rpc AgentListBranches(AgentListBranchesRequest) returns (AgentListBranchesResponse);
rpc AgentBranchPut(AgentBranchPutRequest) returns (AgentBranchPutResponse);
rpc AgentBranchGet(AgentBranchGetRequest) returns (AgentBranchGetResponse);
// ── Graph index operations ────────────────────────────────────────────────
// Create a graph index (6 CFs + HNSW config).
rpc CreateGraphIndex(CreateGraphIndexRequest) returns (CreateGraphIndexResponse);
// Drop a graph index and its CFs.
rpc DropGraphIndex(DropGraphIndexRequest) returns (DropGraphIndexResponse);
// Add a node with optional vector and properties.
rpc GraphAddNode(GraphAddNodeRequest) returns (GraphAddNodeResponse);
// Batch add multiple nodes with vectors and properties.
rpc GraphBatchAddNode(GraphBatchAddNodeRequest) returns (GraphBatchAddNodeResponse);
// Remove a node from the graph: evicts its vector from the HNSW index,
// deletes its properties and every temporal edge that touches it. Used by the
// conflict-resolver DELETE path so the graph and vector indexes never diverge.
rpc GraphRemoveNode(GraphRemoveNodeRequest) returns (GraphRemoveNodeResponse);
// Add a temporal edge between two nodes.
rpc GraphAddEdge(GraphAddEdgeRequest) returns (GraphAddEdgeResponse);
// Batch add multiple temporal edges in one atomic write (mirrors
// GraphBatchAddNode for edges). Collapses N per-edge proposals to ~1.
rpc GraphBatchAddEdge(GraphBatchAddEdgeRequest) returns (GraphBatchAddEdgeResponse);
// Internal data-node RPC: apply a shard-local subset of graph writes.
// Used when a logical graph write spans multiple CF shards/leaders.
rpc GraphBatchWrite(GraphBatchWriteRequest) returns (GraphBatchWriteResponse);
// Internal data-node RPC: read the current durable value of a shard-local
// subset of graph keys. Used to capture pre-images before a multi-leader
// graph write so a partial failure can be compensated (rows restored).
rpc GraphBatchRead(GraphBatchReadRequest) returns (GraphBatchReadResponse);
// HNSW nearest neighbor search on graph vectors.
rpc GraphSearch(GraphSearchRequest) returns (GraphSearchResponse);
// Vector-anchored multi-hop expansion (GraphRAG primitive): vector search
// for anchor nodes, then BFS-expand the induced subgraph from those anchors
// with depth / edge-type / as_of filters — all in one server-side call.
rpc GraphSearchExpand(GraphSearchExpandRequest) returns (GraphSearchExpandResponse);
// Unified GraphRAG retrieval (issue #696): one server-side call that does
// vector-seed -> graph expansion -> blended rerank by similarity + recency +
// graph-distance, scoped by valid-time / transaction-time and memory scope,
// returning ranked facts with provenance and bitemporal validity. Composes
// GraphSearch (anchors) + GraphQueryEdgesBatch (bitemporal BFS) +
// GraphGetNodesBatch (hydration). Served by the gateway only.
rpc GraphRagSearch(GraphRagSearchRequest) returns (GraphRagSearchResponse);
// Get a graph node's properties by ID (reads from graph node CF directly).
rpc GraphGetNode(GraphGetNodeRequest) returns (GraphGetNodeResponse);
// Query temporal edges for a node.
rpc GraphQueryEdges(GraphQueryEdgesRequest) returns (GraphQueryEdgesResponse);
// Batched edge query: query temporal edges for many nodes at once, all
// routed to the same shard. Edge-type and as_of/time filters are applied at
// the data node. Used by the gateway's cross-shard BFS to expand a whole
// per-shard frontier in one RPC (O(hops x shards) instead of O(visited)).
rpc GraphQueryEdgesBatch(GraphQueryEdgesBatchRequest) returns (GraphQueryEdgesBatchResponse);
// Batched node-properties fetch: hydrate many nodes that route to the same
// shard in a single RPC. Used by cross-shard traversal prop hydration.
rpc GraphGetNodesBatch(GraphGetNodesBatchRequest) returns (GraphGetNodesBatchResponse);
// First-class multi-hop BFS traversal from a start node. Honors direction,
// depth, edge-type and as_of/time filters, and (on the gateway) drives
// cross-shard hops by re-dispatching frontiers to shard leaders.
rpc GraphTraverse(GraphTraverseRequest) returns (GraphTraverseResponse);
// Shard-local scan of the reverse label posting list (ROLE_LABEL_INDEX):
// resolve each label string to its interned label_id, prefix-seek the
// posting list, and return member node ids (optionally hydrated NodeProp
// JSON), capped. Multi-label = server-side conjunctive intersection
// (smallest posting list first). The gateway fans this out across the shard
// set and re-applies the global cap after merge. Used as the label+property
// MATCH anchor-resolution entry point (epic #1429).
rpc GraphNodesByLabel(GraphNodesByLabelRequest) returns (GraphNodesByLabelResponse);
// Temporal join: align graph edges with KV time-series data.
// For each edge in the time range, looks up the corresponding KV entries
// at the edge's timestamp. Used for news → price alignment.
rpc GraphTemporalJoin(GraphTemporalJoinRequest) returns (GraphTemporalJoinResponse);
// Graph analytics: run PageRank / WCC / DegreeCentrality over a graph index's
// edges. The gateway fans the computation out across every shard owning the
// graph's edge CF, merges all local edge lists into one global adjacency, and
// runs a single global PageRank/WCC/DegreeCentrality (so masses sum to 1, WCC
// components are not split across shards, and DegreeCentrality normalizes over
// the full node set). Optionally writes scores back into node properties.
rpc GraphAnalytics(GraphAnalyticsRequest) returns (GraphAnalyticsResponse);
// Internal cross-shard fan-out helper for GraphAnalytics: dump one shard's
// local analytics edge list (the same filtered/deduped (src,dst) pairs
// `build_analytics_graph` would feed the engine) as packed parallel u64
// arrays, so the gateway can merge edges from all shards into one global
// adjacency. Not intended for direct client use.
rpc GraphAnalyticsEdges(GraphAnalyticsEdgesRequest) returns (GraphAnalyticsEdgesResponse);
// Internal cross-shard fan-out helper for GraphAnalytics write-back: persist a
// batch of (node_id, score, component) rows for nodes this shard owns into
// their ROLE_NodeProp "__analytics" sub-key. The gateway routes each node to
// its owning shard so a data node only receives its own nodes.
rpc GraphAnalyticsWriteScores(GraphAnalyticsWriteScoresRequest) returns (GraphAnalyticsWriteScoresResponse);
// Weighted shortest-path / pathfinding (Dijkstra / A*, k-shortest via Yen's)
// over user graph edges. Gateway-only: expands a cost-ordered frontier across
// shard leaders, decoding edge weights from edge properties.
rpc GraphShortestPath(GraphShortestPathRequest) returns (GraphShortestPathResponse);
// Read-only declarative pattern-match graph query (an openCypher subset:
// MATCH path patterns with node/edge-type filters, WHERE on node properties
// plus an `as_of` temporal predicate, RETURN / LIMIT). Gateway-only: the
// query is parsed + planned, then compiled to existing engine traversal
// primitives (GraphTraverse / GraphShortestPath / GraphSearchExpand) and
// WHERE predicates are evaluated against hydrated ROLE_NodeProp JSON in the
// distributed-result merge stage. CREATE / MERGE are not supported.
rpc GraphQuery(GraphQueryRequest) returns (GraphQueryResponse);
}
// ─── Raft Internal Service ────────────────────────────────────────────────────
//
// Used for peer-to-peer replication between Statelet nodes.
// Clients should not call these RPCs directly.
service RaftService {
// Leader → Follower: replicate log entries and/or send heartbeat.
rpc AppendEntries(AppendEntriesRequest) returns (AppendEntriesResponse);
// Candidate → All peers: request a vote during leader election.
rpc RequestVote(RequestVoteRequest) returns (RequestVoteResponse);
// Leader → Follower: install a full state-machine snapshot.
rpc InstallSnapshot(InstallSnapshotRequest) returns (InstallSnapshotResponse);
// Leader → Follower: chunk-streamed variant of InstallSnapshot for
// shard-sized payloads (see InstallSnapshotChunk).
rpc InstallSnapshotStream(stream InstallSnapshotChunk) returns (InstallSnapshotResponse);
// Leader → Target follower: graceful leadership transfer (etcd
// MsgTimeoutNow). The leader sends this only after verifying the target's
// log is caught up; the target starts a real election immediately,
// bypassing the randomized timeout and PreVote.
rpc TimeoutNow(TimeoutNowRequest) returns (TimeoutNowResponse);
// Leader → Follower: digest handshake BEFORE an InstallSnapshot transfer.
// A follower that "needs a snapshot" often already holds byte-identical
// shard state (raft index-space divergence after the historical reopen
// resets; restart-time truncate-to-tip): the leader asks the follower for
// a digest of its materialized range state and compares it with its own —
// on a match it ships a metadata-only InstallSnapshot (state_verified)
// that fast-forwards the follower's raft position without moving any data.
rpc ShardStateDigest(ShardStateDigestRequest) returns (ShardStateDigestResponse);
}
// ─── Ping ─────────────────────────────────────────────────────────────────────
message PingRequest {}
message PingResponse {
string message = 1; // always "PONG"
}
// ─── Put ──────────────────────────────────────────────────────────────────────
message PutRequest {
uint32 cf = 1; // column family id (0 = default user CF)
bytes key = 2;
bytes value = 3;
uint64 shard_id = 4; // gateway-stamped shard id (0 = skip validation)
uint64 shard_epoch = 5; // gateway-stamped epoch (0 = skip validation)
}
message PutResponse {}
// ─── Get ──────────────────────────────────────────────────────────────────────
message GetRequest {
uint32 cf = 1;
bytes key = 2;
uint64 shard_id = 3;
uint64 shard_epoch = 4;
// Also resolve the key's engine sequence into `GetResponse.seq`. Opt-in
// because it costs an extra memtable + SST version probe on top of the read:
// only a coordinator building a read set needs it, and an ordinary Get should
// not pay for it.
bool with_seq = 5;
}
message GetResponse {
bool found = 1; // false ↔ key not found (or tombstoned)
bytes value = 2; // only meaningful when found = true
// Engine sequence this key was last written at; 0 when absent. A coordinator
// that reads a key it does not write needs this to build an
// `AgentTxnRead.observed_seq` — without it, optimistic transactions can only
// be driven from inside the storage process. NOT the same clock as the
// Percolator `read_ts` fields elsewhere in this file: that is a packed HLC,
// this is the engine's MVCC sequence.
uint64 seq = 3;
}
// ─── Delete ───────────────────────────────────────────────────────────────────
message DeleteRequest {
uint32 cf = 1;
bytes key = 2;
uint64 shard_id = 3;
uint64 shard_epoch = 4;
}
message DeleteResponse {}
// ─── Merge ────────────────────────────────────────────────────────────────────
message MergeRequest {
uint32 cf = 1;
bytes key = 2;
bytes value = 3; // merge operand
uint64 shard_id = 4;
uint64 shard_epoch = 5;
}
message MergeResponse {}
// ─── BatchWrite ───────────────────────────────────────────────────────────────
enum WriteOp {
PUT = 0;
DELETE = 1;
MERGE = 2;
}
message WriteEntry {
uint32 cf = 1;
WriteOp op = 2;
bytes key = 3;
bytes value = 4; // empty for DELETE
// Absolute expiry (ms since epoch); 0 = no TTL. Leases need per-key expiry
// at write time — a lease that has to be reaped by a separate sweep is not a
// lease. Ignored for DELETE.
uint64 expire_at = 5;
}
message BatchWriteRequest {
repeated WriteEntry entries = 1;
uint64 shard_id = 2;
uint64 shard_epoch = 3;
}
message BatchWriteResponse {}
// Predicate for a conditional batch gate. Values are deliberately prefixed
// because proto enum variants share package-level generated names in some
// languages.
enum WriteConditionKind {
WRITE_CONDITION_NONE = 0;
WRITE_CONDITION_IF_ABSENT = 1;
WRITE_CONDITION_IF_PRESENT = 2;
WRITE_CONDITION_IF_VALUE_EQUALS = 3;
// Multi-key optimistic gate: apply the WHOLE batch only if every entry in
// `read_set` still has its observed sequence. Unlike the single-key kinds
// above, the predicate spans keys the batch does not write, which is what
// makes snapshot isolation expressible on the generic KV wire instead of
// only through the agent-specific transaction RPCs.
WRITE_CONDITION_IF_READ_SET_UNCHANGED = 4;
// Single-key CAS on the engine sequence: apply only if the gate key EXISTS
// and is still at exactly `condition_seq`.
//
// Not expressible as a one-key `read_set`. That gate means "not overwritten
// since" — it rejects only a NEWER version (`latest > observed`), so an
// ABSENT key passes it and the write silently CREATES the row. This one means
// "still exactly this version, and still there", which is what a fenced
// compare-and-swap actually promises.
WRITE_CONDITION_IF_SEQ_EQUALS = 5;
}
message ConditionalBatchWriteRequest {
WriteEntry gate = 1; // PUT or DELETE; MERGE is invalid as a gate
WriteConditionKind condition = 2;
bytes condition_value = 3; // expected value for IF_VALUE_EQUALS
repeated WriteEntry entries = 4; // plain entries gated by `gate`
uint64 shard_id = 5;
uint64 shard_epoch = 6;
// Only for WRITE_CONDITION_IF_READ_SET_UNCHANGED. Each entry is a key the
// caller read at `observed_seq`; the batch applies only if none of them has
// been overwritten since. Validated at the SAME atomic point the batch is
// applied, so no concurrent write can slip in between.
repeated AgentTxnRead read_set = 7;
// Only for WRITE_CONDITION_IF_SEQ_EQUALS: the exact engine sequence the gate
// key must still hold.
uint64 condition_seq = 8;
}
message ConditionalBatchWriteResponse {
bool applied = 1; // false when the predicate failed and no entry applied
bool prev_found = 2;
bytes prev_value = 3;
// Sequence the batch committed at (0 when not applied). This is the fencing
// token a claim/lease hands to its holder, and the `commit_seq` an optimistic
// transaction reports.
uint64 new_seq = 4;
// Sequence the gate key currently holds (0 when absent). On rejection this is
// the conflicting version, which is what a CAS caller needs to retry against.
uint64 prev_seq = 5;
}
// ─── ConditionalSet ──────────────────────────────────────────────────────────
// Predicate for an atomic conditional set (`ConditionalSet`).
enum SetExpectation {
IF_ABSENT = 0; // NX: apply the write only if the key has no live value
IF_PRESENT = 1; // XX: apply the write only if the key already has a live value
IF_VALUE_EQUALS = 2; // CAS: apply only if the live value matches expected_value
}
message ConditionalSetRequest {
uint32 cf = 1;
bytes key = 2;
bytes value = 3;
SetExpectation expect = 4;
uint64 shard_id = 5;
uint64 shard_epoch = 6;
bytes expected_value = 7;
}
message ConditionalSetResponse {
bool applied = 1; // predicate held and the write committed
bool prev_found = 2; // key had a live value under the apply-time check
bytes prev_value = 3; // that prior value (empty when prev_found = false)
}
// ─── BatchGet ────────────────────────────────────────────────────────────────
message BatchGetRequest {
uint32 cf = 1;
repeated bytes keys = 2;
uint64 shard_id = 3;
uint64 shard_epoch = 4;
// Also resolve each key's engine sequence into `BatchGetEntry.seq`. Opt-in
// for a second reason beyond the per-key probe cost of `GetRequest.with_seq`:
// it also gives up the LSM-coalesced batch read (one pinned snapshot +
// grouped block fetches), which reports no per-key version. Leave it false
// for MGET-style bulk reads.
bool with_seq = 5;
}
message BatchGetEntry {
bytes key = 1;
bool found = 2;
bytes value = 3;
// Engine sequence this key was last written at; 0 when absent. Same clock and
// same purpose as `GetResponse.seq` — a coordinator batching its reads needs
// one `observed_seq` per key, or it has to fall back to N point Gets.
uint64 seq = 4;
}
message BatchGetResponse {
repeated BatchGetEntry entries = 1;
}
// ─── Scan ────────────────────────────────────────────────────────────────────
message ScanRequest {
uint32 cf = 1; // column family id
bytes prefix = 2; // key prefix filter (empty = scan all keys)
bytes cursor = 3; // resume cursor (empty = start from beginning)
uint32 limit = 4; // max entries to return (0 = default 100)
uint64 shard_id = 5;
uint64 shard_epoch = 6;
// Also report each entry's engine sequence in `ScanEntry.seq`. Unlike the
// point-read flags this is nearly free (the scan already walks internal keys
// and knows the version it settled on), but it stays opt-in so the field's
// meaning is unambiguous: 0 means "not requested", never "version zero".
bool with_seq = 7;
}
message ScanResponse {
repeated ScanEntry entries = 1;
bytes next_cursor = 2; // empty = no more data
bool has_more = 3;
bool partial_failure = 4; // true when some shards failed; client may retry
}
message ScanEntry {
bytes key = 1;
bytes value = 2;
// Engine sequence of this version — see `GetResponse.seq`.
uint64 seq = 3;
}
// ─── DeleteByPrefix ─────────────────────────────────────────────────────────
message DeleteByPrefixRequest {
uint32 cf = 1; // column family id
bytes prefix = 2; // key prefix to match (must be non-empty)
uint64 shard_id = 3;
uint64 shard_epoch = 4;
}
message DeleteByPrefixResponse {
uint32 deleted = 1; // number of keys deleted
}
// ─── Raft AppendEntries ───────────────────────────────────────────────────────
message RaftEntry {
uint64 term = 1; // term when entry was created
uint64 index = 2; // 1-based log position
bytes data = 3; // serialised WriteBatch (empty for heartbeat/no-op)
uint32 entry_type = 4; // 0 = Normal, 1 = Config
}
message AppendEntriesRequest {
uint64 term = 1; // leader's current term
uint64 leader_id = 2;
uint64 prev_log_index = 3; // index of the entry immediately before new ones
uint64 prev_log_term = 4;
repeated RaftEntry entries = 5; // empty ⇒ heartbeat
uint64 leader_commit = 6; // leader's commitIndex
uint64 shard_id = 7; // shard id for multi-shard Raft routing (0 = single-group mode)
}
message AppendEntriesResponse {
uint64 term = 1; // follower's currentTerm (for leader to update itself)
bool success = 2;
// Fast roll-back hints (§5.3 optimisation):
uint64 conflict_term = 3; // term of the conflicting entry (0 if none)
uint64 conflict_index = 4; // first index with that term (0 if none)
}
// ─── Raft RequestVote ─────────────────────────────────────────────────────────
message RequestVoteRequest {
uint64 term = 1;
uint64 candidate_id = 2;
uint64 last_log_index = 3;
uint64 last_log_term = 4;
uint64 shard_id = 5; // shard id for multi-shard Raft routing (0 = single-group mode)
bool pre_vote = 6; // Raft §9.6 PreVote: prospective ballot, must not bump receiver term
}
message RequestVoteResponse {
uint64 term = 1; // recipient's currentTerm (used to update candidate)
bool vote_granted = 2;
}
// ─── Raft TimeoutNow (graceful leadership transfer) ───────────────────────────
message TimeoutNowRequest {
uint64 term = 1; // sender's current term — the target refuses stale senders
uint64 leader_id = 2;
uint64 shard_id = 3; // shard id for multi-shard Raft routing (0 = single-group mode)
}
message TimeoutNowResponse {
uint64 term = 1; // target's currentTerm
bool accepted = 2; // false: refused (stale term / quarantined state / already leader)
}
// ─── Raft InstallSnapshot ─────────────────────────────────────────────────────
message InstallSnapshotRequest {
uint64 term = 1;
uint64 leader_id = 2;
uint64 last_included_index = 3;
uint64 last_included_term = 4;
bytes data = 5; // complete snapshot payload
uint64 shard_id = 6; // shard id for multi-shard Raft routing (0 = single-group mode)
// Digest fast-path (see ShardStateDigest): when true, `data` is empty and
// `verified_digest` echoes the digest the follower reported — the follower
// re-checks it against its cached value and, on a match, fast-forwards its
// raft position without any data transfer.
bool state_verified = 7;
bytes verified_digest = 8;
}
// Digest handshake for the InstallSnapshot fast-path. The digest covers the
// follower's MATERIALIZED (post-merge-fold) state over the shard range,
// CF-by-CF in name order — comparable across nodes despite node-local
// physical cf ids and different SST/memtable layouts.
message ShardStateDigestRequest {
uint64 shard_id = 1;
uint64 term = 2; // leader's term (context; install carries the real checks)
uint64 leader_id = 3;
bytes start_key = 4; // leader's scope — follower must hold the SAME range
bytes end_key = 5;
uint32 algo = 6; // 1 = xxh3-128 over (cf-name, key, value) length-framed stream
}
message ShardStateDigestResponse {
// False when the digest cannot be compared: scope/CF mismatch, unsupported
// algo, no snapshot scope, or the follower declined (busy). The leader
// falls back to a full transfer.
bool comparable = 1;
bytes digest = 2; // 16 bytes (xxh3-128, little-endian)
uint64 entry_count = 3;
// Responder's `last_applied` at the moment the digest was computed. Lets a
// suspect rejoiner (unclean-shutdown quarantine) turn a mismatch into a
// PROOF of state-machine divergence: two replicas at the SAME applied index
// must hold identical state, so equal positions + unequal digests can only
// mean one side durably lost applied data.
uint64 last_applied = 4;
}
// One chunk of a streamed InstallSnapshot transfer. Raft metadata rides on
// every chunk (cheap, and makes each chunk self-describing); the receiver
// takes the header fields from the FIRST chunk. Streaming exists because a
// transfer snapshot is shard-sized (1.39GB observed): as ONE unary message it
// needs a giant gRPC message cap and a 2x encode/decode memory spike on both
// ends, and tonic's bare 4MiB server default silently blocked follower
// healing for days.
//
// payload_format 0 (legacy): `data` is a raw byte slice of ONE monolithic
// msgpack payload; the receiver concatenates slices in stream order and
// decodes the whole thing at once (peak memory = full payload).
// payload_format 1 (framed): each `data` is ONE self-contained msgpack
// `SnapFrame` (Header / CfBegin / Entries / CfEnd / End); the receiver spools
// frames to disk as they arrive and the restore applies them one at a time,
// so neither side ever materializes the full payload in memory. A framed
// stream is terminated by an explicit `eof=true` marker chunk — a stream
// that ends without one was aborted by the sender (e.g. export failure) and
// must be discarded, because gRPC also ends the stream cleanly in that case.
message InstallSnapshotChunk {
uint64 term = 1;
uint64 leader_id = 2;
uint64 last_included_index = 3;
uint64 last_included_term = 4;
bytes data = 5; // payload slice (format 0) or one frame (format 1)
uint64 shard_id = 6;
uint32 payload_format = 7; // 0 = legacy monolithic slices, 1 = framed
bool eof = 8; // framed streams: terminal marker chunk (empty data)
}
message InstallSnapshotResponse {
uint64 term = 1;
// True only when the follower's state machine actually restored snapshot
// state. When false (e.g. an empty/no-op snapshot), the follower did NOT
// fast-forward last_applied/commit_index, so the leader must keep streaming
// the missing committed entries via AppendEntries instead of marking the
// follower caught up. Defaults to false for older peers.
bool installed = 2;
}
// ─── Metadata Service ─────────────────────────────────────────────────────────
//
// Exposes cluster metadata: shard map, column families, and mutating operations
// that flow through the metadata Raft group.
service MetadataService {
// List all known shards.
rpc GetShards(GetShardsRequest) returns (GetShardsResponse);
// List all registered column families.
rpc GetColumnFamilies(GetColumnFamiliesRequest) returns (GetColumnFamiliesResponse);
// Propose a metadata change through the metadata Raft group.
rpc ProposeOp(ProposeMetadataOpRequest) returns (ProposeMetadataOpResponse);
// Find the shard responsible for a (CF, key) pair.
rpc GetShardForKey(GetShardForKeyRequest) returns (GetShardForKeyResponse);
// Register a data node so the metadata service knows its addresses.
// Called by each data node on startup.
rpc RegisterNode(RegisterNodeRequest) returns (RegisterNodeResponse);
// Subscribe to shard lifecycle events (server-streaming).
// The server first streams a full state snapshot (all current shards, nodes,
// and column families as synthetic events), then streams live updates as
// they are committed through the metadata Raft group.
rpc SubscribeShardEvents(SubscribeShardEventsRequest) returns (stream ShardEventProto);
// Data nodes periodically report their per-shard/per-CF stats.
// Stats are cached in memory (not replicated via Raft).
rpc ReportNodeStats(ReportNodeStatsRequest) returns (ReportNodeStatsResponse);
// Train a vector index (orchestrated by metadata service).
// For IVF-PQ/IVF-SQ: global centroid training + distribute to nodes.
// For SPFresh: local training on each node.
rpc TrainVectorIndex(TrainVectorIndexRequest) returns (TrainVectorIndexResponse);
// Train coarse routing centroids for SPFresh (two-level routing).
rpc TrainCoarseRouting(TrainCoarseRoutingRequest) returns (TrainCoarseRoutingResponse);
}
// ─── Metadata Raft Service ────────────────────────────────────────────────────
//
// Peer-to-peer Raft replication for the metadata Raft group.
// Reuses the same message types as RaftService but is a distinct gRPC service
// so that data-plane and metadata-plane Raft traffic can be separated.
service MetadataRaftService {
rpc AppendEntries(AppendEntriesRequest) returns (AppendEntriesResponse);
rpc RequestVote(RequestVoteRequest) returns (RequestVoteResponse);
rpc InstallSnapshot(InstallSnapshotRequest) returns (InstallSnapshotResponse);
}
// ─── Shard / CF message types ─────────────────────────────────────────────────
enum ShardStatusProto {
SHARD_NORMAL = 0;
SHARD_SPLITTING = 1;
SHARD_MERGING = 2;
SHARD_MIGRATING = 3;
SHARD_RECOVERING = 4;
}
message ShardInfoProto {
uint64 shard_id = 1;
string cf = 2;
bytes start_key = 3;
bytes end_key = 4;
uint64 leader_node = 5;
repeated uint64 replicas = 6;
ShardStatusProto status = 7;
uint64 epoch = 8;
}
message CfMetadataProto {
string name = 1;
uint32 cf_id = 2;
uint32 cf_type = 3; // 0=System, 1=User, 2=Graph
string namespace = 4; // B-model authoritative owner
string database = 5;
}
// ─── GetShards ────────────────────────────────────────────────────────────────
message GetShardsRequest {}
message GetShardsResponse {
repeated ShardInfoProto shards = 1;
}
// ─── GetColumnFamilies ────────────────────────────────────────────────────────
message GetColumnFamiliesRequest {}
message GetColumnFamiliesResponse {
repeated CfMetadataProto column_families = 1;
}
// ─── ProposeOp ────────────────────────────────────────────────────────────────
message ProposeMetadataOpRequest {
bytes op_json = 1; // JSON-encoded MetadataOp
}
message ProposeMetadataOpResponse {
bool success = 1;
string error = 2;
}
// ─── GetShardForKey ───────────────────────────────────────────────────────────
message GetShardForKeyRequest {
string cf = 1;
bytes key = 2;
}
message GetShardForKeyResponse {
bool found = 1;
ShardInfoProto shard = 2;
}
// ─── RegisterNode ─────────────────────────────────────────────────────────────
message NodeInfoProto {
uint64 node_id = 1; // unique node id
string data_addr = 2; // gRPC address for client-facing data-plane requests
string raft_addr = 3; // gRPC address for Raft peer-to-peer RPCs
}
message RegisterNodeRequest {
NodeInfoProto node = 1;
}
message RegisterNodeResponse {
bool success = 1;
string error = 2;
}
// ─── SubscribeShardEvents ─────────────────────────────────────────────────────
message SubscribeShardEventsRequest {
uint64 node_id = 1; // id of the subscribing data node
}
// A single committed metadata event, encoded as a JSON MetadataOp.
// On initial connection the server first replays the full current state as
// synthetic events before streaming live updates.
message ShardEventProto {
bytes op_json = 1; // serde_json-encoded MetadataOp
}
// ─── Vector Index ────────────────────────────────────────────────────────────
enum VectorDistanceMetric {
VECTOR_L2 = 0; // Squared Euclidean distance
VECTOR_COSINE = 1; // 1 - cosine_similarity
VECTOR_INNER_PRODUCT = 2; // Negative dot product
}
// The type of vector index — determines quantization / structure.
enum VectorIndexType {
VECTOR_INDEX_HNSW = 0; // Plain HNSW (exact distances)
VECTOR_INDEX_PQ_HNSW = 1; // PQ-accelerated HNSW
VECTOR_INDEX_SQ_HNSW = 2; // SQ8-accelerated HNSW
VECTOR_INDEX_IVF_PQ = 3; // IVF with PQ fine quantization
VECTOR_INDEX_IVF_SQ = 4; // IVF with SQ8 fine quantization
VECTOR_INDEX_SPFRESH = 5; // SPFresh IVF-PQ with LIRE incremental rebalancing
VECTOR_INDEX_SPFRESH_LSM = 6; // DEPRECATED/RETIRED: route to a graph SpFresh index; create returns FailedPrecondition
VECTOR_INDEX_DISKANN = 7; // DiskANN/Vamana SSD-resident graph (PQ in RAM, vectors+graph on SSD)
VECTOR_INDEX_MULTIVECTOR = 8; // ColBERT-style late-interaction multi-vector index
}
// Configuration for a vector index.
message VectorIndexConfig {
uint32 dim = 1; // Vector dimensionality
VectorDistanceMetric metric = 2; // Distance metric
uint32 m = 3; // Max connections per layer (default 16)
uint32 m_max0 = 4; // Max connections at layer 0 (default 2*m)
uint32 ef_construction = 5; // Build-time search width (default 200)
uint32 ef_search = 6; // Default query-time search width (default 64)
VectorIndexType index_type = 7; // Index type (default: plain HNSW)
// ── IVF / SPFresh fields ────────────────────────────────────────────────────
uint32 nlist = 8; // Number of Voronoi cells (coarse clusters)
uint32 nprobe = 9; // Cells to probe at query time
uint32 pq_num_sub = 10; // PQ: number of sub-quantizers
uint32 pq_num_centroids = 11; // PQ: centroids per sub-quantizer (default 256)
uint32 pq_max_iter = 12; // PQ: k-means iterations (default 20)
uint32 ivf_max_iter = 13; // IVF: coarse k-means iterations (default 20)
uint32 split_threshold = 14; // SPFresh: posting list split threshold (0 = auto)
uint32 merge_threshold = 15; // SPFresh: posting list merge threshold (0 = auto)
float compact_delete_ratio = 16; // SPFresh: compaction trigger ratio (default 0.3)
// ── Coarse centroid routing fields ──────────────────────────────────────────
uint32 nlist_coarse = 17; // Number of coarse centroids for distributed routing (0 = disable)
uint32 nprobe_coarse = 18; // Coarse centroids to probe at search time (default: 1)
// ── DiskANN / Vamana fields ──────────────────────────────────────────────────
uint32 diskann_degree = 19; // DiskANN: max graph out-degree R (default 64)
uint32 diskann_search_list = 20; // DiskANN: build/search candidate list L (default 100)
float diskann_alpha = 21; // DiskANN: RobustPrune alpha relaxation (default 1.2)
// ── Payload secondary indexes (roaring-bitmap / field index) ──────────────────
// Fields whose payload values get a secondary index for fast pre-filtering.
// Omitted/empty => no payload index (behavior identical to today's O(n) probe).
repeated PayloadFieldIndex indexed_fields = 22;
// ── Multi-vector / late-interaction fields ─────────────────────────────────
uint32 multivector_candidates_per_token = 23; // Per query token ANN candidates (0 = default 32)
// ── Tenant-partitioned vector index (multitenancy) ──────────────────────────
// Designate a payload field as the tenant key. When non-empty, the index becomes
// a tenant router: tenant-equality filtered searches are scoped to that tenant's
// sub-structure. Empty => None => behavior identical to today. Graph-backed index
// types only (HNSW/PQ_HNSW/SQ_HNSW); a tenant_key on any other type is rejected.
string tenant_key = 24;
uint32 tenant_promote_threshold = 25; // Promote-to-dedicated-subgraph size (0 = engine default; Phase 2)
}
// A payload field to secondary-index for fast filter pre-filtering.
message PayloadFieldIndex {
string field = 1; // Payload field name
PayloadFieldKind kind = 2; // EQUALITY (bitmap) or RANGE (sorted)
}
// The kind of secondary index built for a payload field.
enum PayloadFieldKind {
PAYLOAD_FIELD_EQUALITY = 0; // keyword/bool/exact int/str -> roaring bitmap
PAYLOAD_FIELD_RANGE = 1; // numeric -> sorted (value,id)
}
// ─── CreateVectorIndex ───────────────────────────────────────────────────────
message CreateVectorIndexRequest {
string index_name = 1; // Unique name for this index
VectorIndexConfig config = 2;
}
message CreateVectorIndexResponse {}
// ─── DropVectorIndex ─────────────────────────────────────────────────────────
message DropVectorIndexRequest {
string index_name = 1;
}
message DropVectorIndexResponse {}
// ─── VectorPut ───────────────────────────────────────────────────────────────
message VectorPutRequest {
string index_name = 1;
uint64 vector_id = 2; // User-assigned vector id
repeated float vector = 3; // The embedding vector
// Optional typed metadata payload attached to this vector id. Persisted and
// replicated through the same WAL/Raft record as the vector, and queried via
// VectorSearchRequest.filter. Additive/wire-compatible: old clients omit it.
map<string, VectorFilterValue> attributes = 4;
}
message VectorPutResponse {}
// ─── VectorDelete ────────────────────────────────────────────────────────────
message VectorDeleteRequest {
string index_name = 1;
uint64 vector_id = 2;
}
message VectorDeleteResponse {}
// ─── VectorSearch ────────────────────────────────────────────────────────────
// A multi-vector (ColBERT-style) query matrix: `num_tokens * dim` floats stored
// row-major in `values`, with `dim` the per-token dimensionality. Used for
// late-interaction MaxSim search over a multi-vector index.
message MultiVectorQuery {
uint32 dim = 1; // Per-token dimensionality (stride into `values`).
repeated float values = 2; // Flat row-major matrix: num_tokens * dim floats.
}
// ─── Second-stage reranking (optional) ───────────────────────────────────────
//
// First-class, optional second-stage reranker over an over-fetched candidate
// window for plain VectorSearch / HybridSearch. Mirrors Weaviate reranker
// modules, Pinecone's rerank API, Vespa global-phase rank-profiles, and
// Cohere/Qdrant rerank: a stateless, bounded, post-merge stage run *once*
// globally after shard fan-in. Additive & wire-compatible — old clients omit
// this message ⇒ rerank off, behavior identical to today.
message RerankSpec {
bool enabled = 1; // master switch (false / message-absent ⇒ no rerank)
// over-fetch pool size; 0 ⇒ default max(k*4, 24) clamped to the mode's hard
// cap (48 for cross-encoder; 200 for the cheaper maxsim late-interaction
// path, sized for the issue's "top-K ~100-200" fused window).
uint32 rerank_k = 2;
// "cross-encoder" | "maxsim" | "learned" | "score-fusion" | "custom";
// "" ⇒ "score-fusion".
// * "maxsim" — token-level ColBERT-style MaxSim late interaction: the query
// and each hydrated passage are encoded into per-token L2-normalized
// matrices and scored Σ_q max_d q·d ÷ query-token-count. LLM-free; needs
// the embedding model loaded + query_text + passage_field, else it
// degrades to score-fusion (never errors).
// * "learned" — (#768 LongMemEval Phase 7) non-generative GBDT reranker over
// the funnel feature vector [dense, BM25, MaxSim, recency, graph-distance,
// granularity], trained on LongMemEval dev relevance labels. Applied as the
// FINAL rerank over the fused+reranked candidate set. Needs a model loaded
// (STATELET_LEARNED_RERANKER_MODEL); else degrades to score-fusion. LLM-free.
string model = 3;
// KV-key template for passage hydration (cross-encoder / maxsim). Tokens:
// {id} → candidate vector/result id (decimal)
// {index} → index_name
// e.g. "doc:{index}:{id}:text". Empty ⇒ hydration disabled ⇒ fall back to score-fusion.
string passage_field = 4;
// score-fusion only: blend = blend*norm_distance + (1-blend)*aux_signal.
// 0 ⇒ default 1.0 (pure full-precision/coarse distance re-rank). Range [0,1].
float signal_blend = 5;
// Raw query *text* for the text-based rerankers (both RPCs query by vector,
// not text). Required for model="cross-encoder" and model="maxsim"; empty ⇒
// auto-downgrade to score-fusion.
string query_text = 6;
// Dry-run pre-flight validation. When true the RPC validates the rerank spec
// (passage_field template via validate_passage_field, plus reranker
// availability for model="cross-encoder") and returns an empty successful
// response if valid, or an InvalidArgument/FailedPrecondition Status if not —
// without executing the search. SDKs expose it as rerank_validate(...).
// Additive & wire-compatible: old clients omit it ⇒ false ⇒ unchanged.
bool validate_only = 7;
}
message VectorSearchRequest {
string index_name = 1;
repeated float query = 2; // Query vector (single-vector indices).
uint32 k = 3; // Number of nearest neighbors to return
uint32 ef_search = 4; // Optional: override ef_search (0 = use default)
VectorFilter filter = 5; // Optional: metadata/attribute pre/post filter (null = no filter)
// Query payload selector. `query` (field 2) carries a single-vector query for
// the classic ANN path; `multi_vector` carries a token matrix for the
// late-interaction MaxSim path against a multi-vector index. At most one of
// the two should be populated.
oneof query_payload {
MultiVectorQuery multi_vector = 6;
}
// ── MMR diversity reranking (optional, single-vector path only) ──
// Maximal-marginal-relevance post-step: over-fetch candidates, then greedily
// select k maximizing lambda*sim(q,d) - (1-lambda)*max_{s in selected} sim(d,s).
// Additive & wire-compatible: old clients omit these ⇒ MMR off, behavior
// unchanged. The returned `distance` is always the original query distance;
// only the selected set and ordering change.
bool mmr = 7; // enable MMR post-step
float mmr_lambda = 8; // 0..1 relevance↔diversity tradeoff; 0 ⇒ default 0.5
uint32 mmr_pool = 9; // over-fetch multiplier; 0 ⇒ default 4 (fetch_k = k*mmr_pool)
// Optional second-stage rerank (cross-encoder or model-free score-fusion).
// Absent ⇒ no rerank.
RerankSpec rerank = 10;
// ── Filtered-search planner override (optional) ──
// Pin the selectivity-adaptive filtered-search strategy instead of letting
// the cardinality planner choose. Only meaningful when `filter` is set and
// the target index is graph-backed (Plain/PQ/SQ HNSW); ignored otherwise.
// 0 = auto (planner chooses from the estimated selectivity) [default]
// 1 = brute-force exact scan over the matching ids
// 2 = filtered-HNSW (traverse-through-non-matching, admit-if-match)
// 3 = ACORN two-hop neighbor expansion
// Additive & wire-compatible: old clients omit it ⇒ 0 ⇒ unchanged behavior.
uint32 planner_override = 11;
// ── Result grouping / field-collapse (optional, single-vector path only) ──
// Collapse results to at most `group_size` hits per distinct value of the
// payload attribute `group_field`, returning `groups` distinct group keys
// total ("one best chunk per document"). Pure post-search collapse over the
// post-filter candidate pool, structurally identical to the MMR over-fetch /
// re-select path. Additive & wire-compatible: old clients omit these ⇒
// grouping off ⇒ behavior byte-identical. Grouping and `mmr` are mutually
// exclusive (orthogonal selection rules) and requesting both ⇒ InvalidArgument.
string group_field = 12; // payload field to group by; empty ⇒ grouping off
uint32 group_size = 13; // max hits per group; 0 ⇒ default 1 (one-best-per-group)
uint32 groups = 14; // number of distinct group keys to return; 0 ⇒ fall back to k
uint32 group_overfetch = 15; // over-fetch multiplier; 0 ⇒ default 4, capped at MAX_POOL
// Missing-field policy. By default (false) candidates that lack `group_field`
// are dropped from grouped results (matches Qdrant/Weaviate). When true, each
// missing-field candidate is returned as its own singleton group (group_key
// empty), counting against the `groups` cap. Additive/wire-compatible: old
// clients omit it ⇒ false ⇒ drop behavior unchanged from Phase 1.
bool group_missing_as_own = 16;
}
// ─── Metadata / attribute filtering for vector search ────────────────────────
//
// A filter is a boolean tree of leaf conditions combined with AND/OR. Leaves
// compare an attribute `field` against a `value` using a comparison `op`.
// Attribute values are typed (string, int, double, bool); comparison between
// mismatched types evaluates to false rather than erroring.
enum VectorFilterOp {
VECTOR_FILTER_OP_EQ = 0; // field == value
VECTOR_FILTER_OP_NE = 1; // field != value
VECTOR_FILTER_OP_LT = 2; // field < value (numeric)
VECTOR_FILTER_OP_LTE = 3; // field <= value (numeric)
VECTOR_FILTER_OP_GT = 4; // field > value (numeric)
VECTOR_FILTER_OP_GTE = 5; // field >= value (numeric)
}
message VectorFilterValue {
oneof value {
string string_value = 1;
int64 int_value = 2;
double double_value = 3;
bool bool_value = 4;
}
}
message VectorFilterCondition {
string field = 1;
VectorFilterOp op = 2;
VectorFilterValue value = 3;
}
// A filter node is either a single leaf condition, or a boolean combination
// (AND/OR) of child filter nodes. Exactly one of the three should be set.
message VectorFilter {
message And { repeated VectorFilter filters = 1; }
message Or { repeated VectorFilter filters = 1; }
oneof node {
VectorFilterCondition condition = 1;
And and = 2;
Or or = 3;
}
}
message VectorSearchResult {
uint64 id = 1;
float distance = 2;
// Group key for field-collapse results (see VectorSearchRequest.group_field).
// Empty when grouping is off. Load-bearing for cross-shard merge: the gateway
// groups across shards on this key. Additive/wire-compatible.
string group_key = 3;
}
message VectorSearchResponse {
repeated VectorSearchResult results = 1;
}
// ─── VectorBatchPut ──────────────────────────────────────────────────────────
message VectorBatchPutEntry {
uint64 vector_id = 1;
repeated float vector = 2;
// Optional typed metadata payload for this entry (see VectorPutRequest.attributes).
map<string, VectorFilterValue> attributes = 3;
}
message VectorBatchPutRequest {
string index_name = 1;
repeated VectorBatchPutEntry vectors = 2;
}
message VectorBatchPutResponse {
uint32 inserted = 1; // Number of vectors successfully inserted
}
// ─── VectorBatchDelete ───────────────────────────────────────────────────────
message VectorBatchDeleteRequest {
string index_name = 1;
repeated uint64 vector_ids = 2;
}
message VectorBatchDeleteResponse {
uint32 deleted = 1; // Number of vectors actually removed
// Vector ids whose owning node could not be deleted in this request. Additive
// for coarse-routed distributed deletes: callers can distinguish an explicit
// partial success from a failed all-or-nothing operation.
repeated uint64 failed_vector_ids = 2;
string partial_error = 3;
}
// ─── VectorTrain ────────────────────────────────────────────────────
message VectorTrainRequest {
string index_name = 1;
repeated float centroids = 2; // Pre-trained centroids (empty = train locally)
uint32 dim = 3; // Dimensionality (needed when centroids is non-empty)
}
message VectorTrainResponse {}
// ─── VectorGet ───────────────────────────────────────────────────────────────
message VectorGetRequest {
string index_name = 1;
uint64 vector_id = 2;
}
message VectorGetResponse {
bool found = 1;
repeated float vector = 2;
}
// ─── VectorSample ─────────────────────────────────────────────────────────
message VectorSampleRequest {
string index_name = 1;
uint32 max_samples = 2;
}
message VectorSampleResponse {
repeated float vectors = 1;
uint32 dim = 2;
uint32 count = 3;
}
// ─── VectorExport (retirement migration, P7) ────────────────────────────────
message VectorExportRequest {
string index_name = 1;
uint64 after_id = 2; // exclusive lower bound; 0 starts from the beginning
uint32 limit = 3; // max entries per page (server clamps; 0 = default)
}
message VectorExportEntry {
uint64 vector_id = 1;
repeated float vector = 2;
}
message VectorExportResponse {
repeated VectorExportEntry entries = 1;
bool done = 2; // true when this page exhausted the index
// Index schema carried on every page so the migration driver can create the
// target graph without a separate metadata round trip (VectorIndexMeta does
// not record the metric).
string metric = 3; // "l2" | "cosine" | "ip"
uint32 dim = 4;
}
// ─── Sparse ingest + hybrid (dense + sparse) retrieval ────────────────────
//
// A *generic* learned-sparse retrieval path bound to a plain vector index
// (independent of agent-memory graph ingestion). `SparseIngest` populates a
// per-index inverted posting store; `HybridSearch` runs dense ANN + sparse
// top-n and fuses them with RRF or weighted (alpha) fusion. The gateway fans
// `HybridSearch` out to all shards (returning per-shard fusion *inputs* via
// `return_inputs`) and performs the global fusion.
// A single sparse document: doc id + term->weight map. If `text` is set and
// `weights` is empty, the server tokenizes `text` (shared BM25 tokenizer) and
// uses term frequencies as weights.
message SparseDocProto {
uint64 doc_id = 1;
map<string, float> weights = 2;
string text = 3;
}
message SparseIngestRequest {
string index_name = 1;
repeated SparseDocProto docs = 2;
}
message SparseIngestResponse {
uint32 ingested = 1; // Number of documents indexed
}
enum FusionMode {
FUSION_MODE_RRF = 0; // Reciprocal Rank Fusion (rank-based)
FUSION_MODE_WEIGHTED = 1; // Score-normalized weighted (alpha) fusion
}
message FusionSpecProto {
FusionMode mode = 1;
float rrf_k = 2; // RRF k constant (default 60 when 0)
float dense_weight = 3; // RRF: dense list weight (default 1)
float sparse_weight = 4; // RRF: sparse list weight (default 1)
float alpha = 5; // Weighted: alpha*dense + (1-alpha)*sparse
}
message HybridSearchRequest {
string index_name = 1;
repeated float dense_query = 2; // Dense query vector
map<string, float> sparse_query = 3; // Sparse query: term->weight
string sparse_text = 4; // If sparse_query empty, tokenize this
uint32 k = 5; // Top-k results to return
uint32 ef_search = 6; // Optional dense ef override (0 = default)
FusionSpecProto fusion = 7;
// When true (gateway → data node), the node returns its raw dense + sparse
// ranked lists as fusion inputs instead of pre-fused results, so the gateway
// can fuse globally across shards.
bool return_inputs = 8;
// Optional second-stage rerank applied after global fusion. Absent ⇒ no rerank.
RerankSpec rerank = 9;
}
// One ranked (id, score) entry. For dense inputs `score` is the distance
// (lower better); for sparse inputs and fused results it is a score (higher
// better).
message RankedEntry {
uint64 id = 1;
float score = 2;
}
// Per-shard raw BM25 sparse inputs, returned alongside `sparse_inputs` when
// return_inputs=true so the gateway can rescore every candidate against
// CORPUS-WIDE statistics (global N / df / avgdl) instead of fusing scores that
// were each computed with shard-local statistics and so are not comparable.
//
// `sparse_local_n` / `sparse_local_sum_dl` are this shard's contribution to the
// global document count and document-length sum (the gateway sums them to get
// global N and avgdl). `sparse_term_df` is this shard's per-query-term document
// frequency (summed to global df). `sparse_doc_inputs` carries, per candidate
// doc, its length and its per-term term frequencies so the gateway can apply
// the exact BM25 formula with the aggregated stats.
message SparseTermDf {
string term = 1;
uint64 df = 2; // posting-list length on this shard
}
message SparseTermTf {
string term = 1;
float tf = 2; // term frequency of `term` in this doc on this shard
}
message SparseDocInput {
uint64 id = 1;
uint32 doc_len = 2; // 0 ⇒ length unknown (gateway uses avgdl)
repeated SparseTermTf term_tfs = 3;
}
message HybridSearchResponse {
// Populated when return_inputs=false: globally fused / locally fused results.
repeated RankedEntry results = 1;
// Populated when return_inputs=true: raw per-shard fusion inputs. The dense
// distances are globally comparable; `sparse_inputs` carries the shard-local
// BM25 score and is kept for backward compatibility / fallback only — prefer
// the corpus-wide rescore built from the raw fields below.
repeated RankedEntry dense_inputs = 2;
repeated RankedEntry sparse_inputs = 3;
// Raw BM25 inputs for corpus-wide sparse rescoring (return_inputs=true).
uint64 sparse_local_n = 4;
uint64 sparse_local_sum_dl = 5;
repeated SparseTermDf sparse_term_df = 6;
repeated SparseDocInput sparse_doc_inputs = 7;
}
// ─── TrainVectorIndex (metadata service orchestrated) ─────────────────────
message TrainVectorIndexRequest {
string index_name = 1;
}
message TrainVectorIndexResponse {}
// ─── TrainCoarseRouting (metadata service orchestrated, SPFresh two-level) ──
message TrainCoarseRoutingRequest {
string index_name = 1;
uint32 nlist_coarse = 2; // Number of coarse centroids to train
uint32 nprobe_coarse = 3; // Coarse centroids to probe at search time (default: 1)
uint32 max_iter = 4; // k-means iterations (default: 20)
uint32 samples_per_node = 5; // Vectors to sample per node (default: 1000)
}
message TrainCoarseRoutingResponse {
uint32 num_centroids = 1;
}
// ─── GetNodeStats ──────────────────────────────────────────────────────────
message CheckpointRequest {}
message CheckpointResponse {
// Shards whose state machine flush was triggered.
uint64 shards_flushed = 1;
// Idle/live shards whose WAL TRUNCATE floor was force-advanced.
uint64 floors_released = 2;
// On-disk WAL segment count (incl. active) before / after the checkpoint.
uint64 segments_before = 3;
uint64 segments_after = 4;
// Total WAL bytes before / after.
uint64 wal_bytes_before = 5;
uint64 wal_bytes_after = 6;
}
message GetNodeStatsRequest {}
message CfStatsProto {
uint32 cf_id = 1;
string cf_name = 2;
uint64 memtable_memory_bytes = 3;
uint64 memtable_entry_count = 4;
uint64 sst_file_count = 5;
uint64 sst_total_bytes = 6;
uint64 sst_entry_count = 7;
// Column family type: 0=System, 1=User, 2=Graph
uint32 cf_type = 8;
}
message VectorIndexStatsProto {
string index_name = 1;
string index_type = 2; // "HNSW", "PQ_HNSW", "SQ_HNSW", "IVF_PQ", "IVF_SQ", "SPFresh", "TENANTED"
uint32 dim = 3;
uint64 num_vectors = 4;
// Tenant tiering (epic #1428, Phase 2). Only meaningful for "TENANTED"
// indices; 0 for every other index type. `promoted_tenants` is the number of
// tenants that have graduated to a dedicated O(tenant) sub-graph, and
// `fallback_vectors` is how many vectors remain in the shared small-tenant
// fallback graph — so a client can observe the O(tenant) tiering via stats.
uint32 promoted_tenants = 5;
uint64 fallback_vectors = 6;
}
message ShardStatsProto {
uint64 shard_id = 1;
repeated CfStatsProto cf_stats = 2;
// Approximate on-disk bytes for THIS shard's key range on THIS node, from
// the SST index boundaries — the same estimate auto-split/auto-merge decide
// on. `cf_stats` cannot answer this: all shards on a node share one DB, so
// SST bytes are tallied per CF per node and attributed wholesale to the
// lowest shard id. An estimate, and per replica.
uint64 approx_bytes = 3;
}
message GetNodeStatsResponse {
uint64 node_id = 1;
repeated ShardStatsProto shard_stats = 2;
// System resource usage
double cpu_usage_percent = 3;
uint64 memory_used_bytes = 4;
uint64 memory_total_bytes = 5;
uint64 disk_used_bytes = 6;
uint64 disk_total_bytes = 7;
// Storage-engine metrics (aggregated from local Prometheus counters)
double wal_bytes_written_total = 8;
double block_cache_hits_total = 9;
double block_cache_misses_total = 10;
double block_cache_evictions_total = 11;
double memtable_freeze_total = 12;
double memtable_flush_total = 13;
// WAL file size on disk (bytes) — sum across all shards on this node.
uint64 wal_file_size_bytes = 14;
// Number of shared raft-wal segment files on disk (GC health signal).
uint64 wal_file_count = 19;
// Per-index vector stats on this node.
repeated VectorIndexStatsProto vector_index_stats = 15;
// Cross-shard ACID transactions (epic #1478, Phase 1). `cross_shard_txn_enabled`
// reflects the effective gate (env flag OR cluster option); default false.
// `hlc_physical_ms` / `hlc_logical` are the node's current Hybrid Logical
// Clock reading (physical Unix-ms + logical tie-break), 0 when the feature is
// OFF (the clock is not sampled). Surfaces the flag + clock for observability.
bool cross_shard_txn_enabled = 16;
uint64 hlc_physical_ms = 17;
uint32 hlc_logical = 18;
}
// ─── GetClusterClock (epic #1478, Phase 1) ─────────────────────────────────
message GetClusterClockRequest {}
message GetClusterClockResponse {
// This node's identity.
uint64 node_id = 1;
// Current Hybrid Logical Clock reading: physical component (Unix ms) and the
// 16-bit logical tie-break. Packed form is `physical_ms << 16 | logical`.
uint64 hlc_physical_ms = 2;
uint32 hlc_logical = 3;
// The packed single-integer HLC (`physical_ms << 16 | logical`) — the form
// carried inside replicated records and on the wire.
uint64 hlc_packed = 4;
// Whether cross-shard transactions are enabled on this node (env flag OR
// cluster option). When false the returned HLC fields are 0 (not sampled).
bool cross_shard_txn_enabled = 5;
}
// ─── ReportNodeStats ──────────────────────────────────────────────────────
message ReportNodeStatsRequest {
// Reuses the same payload as GetNodeStatsResponse.
uint64 node_id = 1;
repeated ShardStatsProto shard_stats = 2;
}
message ReportNodeStatsResponse {}
// ═══════════════════════════════════════════════════════════════════════════
// Agent State DB
// ═══════════════════════════════════════════════════════════════════════════
// Client-facing gateway service for agent state operations.
// The gateway generates execution plans locally and routes to data nodes.
service AgentStateService {
// ── Branch (fork) operations ─────────────────────────────────────────
rpc Fork(AgentForkRequest) returns (AgentForkResponse);
rpc MergeBranch(AgentMergeBranchRequest) returns (AgentMergeBranchResponse);
rpc DiscardBranch(AgentDiscardBranchRequest) returns (AgentDiscardBranchResponse);
rpc ListBranches(AgentListBranchesRequest) returns (AgentListBranchesResponse);
rpc BranchPut(AgentBranchPutRequest) returns (AgentBranchPutResponse);
rpc BranchGet(AgentBranchGetRequest) returns (AgentBranchGetResponse);
// ── Causal graph operations ──────────────────────────────────────────
rpc AddStep(AgentAddStepRequest) returns (AgentAddStepResponse);
rpc AddEdge(AgentAddEdgeRequest) returns (AgentAddEdgeResponse);
rpc GetStep(AgentGetStepRequest) returns (AgentGetStepResponse);
rpc GetContent(AgentGetContentRequest) returns (AgentGetContentResponse);
rpc GetEdges(AgentGetEdgesRequest) returns (AgentGetEdgesResponse);
rpc Traverse(AgentTraverseRequest) returns (AgentTraverseResponse);
rpc FindSimilarChains(AgentFindSimilarChainsRequest) returns (AgentFindSimilarChainsResponse);
// ── Reactive state operations ────────────────────────────────────────
rpc CasPut(AgentCasPutRequest) returns (AgentCasPutResponse);
rpc TxnCommit(AgentTxnCommitRequest) returns (AgentTxnCommitResponse);
// ── Coordination primitives (claim / lease / renew / release) ─────────
// Native etcd/Consul/Zookeeper-style coordination: a claim is an atomic
// SetIfNotExists(claim_key, agent_id); a lease is the same with a TTL so an
// un-renewed holder auto-expires; renew/release are fenced so only the live
// holder can extend/drop the key. See issue #691.
rpc Claim(AgentClaimRequest) returns (AgentClaimResponse);
rpc Lease(AgentLeaseRequest) returns (AgentLeaseResponse);
rpc Renew(AgentRenewRequest) returns (AgentRenewResponse);
rpc Release(AgentReleaseRequest) returns (AgentReleaseResponse);
// ── Temporal graph operations ────────────────────────────────────────
rpc ExpireEdge(AgentExpireEdgeRequest) returns (AgentExpireEdgeResponse);
rpc EdgeHistory(AgentEdgeHistoryRequest) returns (AgentEdgeHistoryResponse);
// ── Memory-scope provenance audit (#697 phase 4) ──────────────────────
// Gated by ManageMemoryScope; the gateway authorizes then forwards to the
// data node that owns the `_agent_provenance` CF.
rpc QueryProvenance(AgentQueryProvenanceRequest) returns (AgentQueryProvenanceResponse);
// ── Memory-scope team-membership admin (#697 phase 2c / #794) ─────────
// Grant/revoke an agent's team membership; gated by ManageMemoryScope. The
// gateway authorizes then applies a durable metadata-Raft op so the change
// immediately affects Team-scope reads after the next internal-token refresh.
rpc AgentManageTeamGrant(AgentManageTeamGrantRequest) returns (AgentManageTeamGrantResponse);
// ── Team time-travel (#787) — gateway facade fan-out/fence/merge ──────
rpc AgentTeamSnapshotLocal(TeamSnapshotLocalRequest) returns (TeamSnapshotLocalResponse);
rpc AgentTeamDiffLocal(TeamDiffLocalRequest) returns (TeamDiffLocalResponse);
// Cascade-expire a fact and its derived dependents (#693).
rpc CascadeExpire(AgentCascadeExpireRequest) returns (AgentCascadeExpireResponse);
// Supersede a fact with a replacement, optionally cascading (#693 phase 4).
rpc SupersedeFact(AgentSupersedeFactRequest) returns (AgentSupersedeFactResponse);
// Transactional memory ingest (#780): forwarded to the owning data node's
// AgentMemoryIngest (atomic, snapshot-isolated dedup/create/supersede).
rpc MemoryIngest(AgentMemoryIngestRequest) returns (AgentMemoryIngestResponse);
// ── Bitemporal belief queries ("who believed what, when") ─────────────
rpc BeliefQuery(AgentBeliefQueryRequest) returns (AgentBeliefQueryResponse);
rpc BeliefDivergence(AgentBeliefDivergenceRequest) returns (AgentBeliefDivergenceResponse);
// ── Durable agent execution (#846, epic #699 / sub-epic #792) ─────────
// Run-id-pinned routing: StartRun picks a shard by hashing run_key/agent_id
// through the ShardRouter and returns run_id = (shard_id << 40) | local_seq;
// every subsequent call extracts the owning shard as run_id >> 40 — no
// metadata lookup needed to route. The gateway resolves that shard's leader
// and retries on NotLeader.
rpc StartRun(AgentStartRunRequest) returns (AgentStartRunResponse);
rpc RunStep(AgentRunStepRequest) returns (AgentRunStepResponse);
rpc CompleteStep(AgentCompleteStepRequest) returns (AgentCompleteStepResponse);
rpc CheckpointGet(AgentCheckpointGetRequest) returns (AgentCheckpointGetResponse);
rpc CheckpointLatest(AgentCheckpointLatestRequest) returns (AgentCheckpointLatestResponse);
rpc ProvenanceChainQuery(AgentProvenanceChainQueryRequest) returns (AgentProvenanceChainQueryResponse);
rpc ResumeFromStep(AgentResumeFromStepRequest) returns (AgentResumeFromStepResponse);
rpc ResumeSemantic(AgentResumeSemanticRequest) returns (AgentResumeSemanticResponse);
rpc GetRunStatus(AgentGetRunStatusRequest) returns (AgentGetRunStatusResponse);
// Phase 5 (#797): branch/time-travel resume. Routed by source_run_id >> 40 to
// the shard owning the source run; the child run is allocated on that same
// shard so its self-routing run_id stays addressable.
rpc ForkRun(AgentForkRunRequest) returns (AgentForkRunResponse);
rpc ForkAcrossCandidates(AgentForkAcrossCandidatesRequest) returns (AgentForkAcrossCandidatesResponse);
rpc ArtifactPut(AgentArtifactPutRequest) returns (AgentArtifactPutResponse);
rpc ArtifactGet(AgentArtifactGetRequest) returns (AgentArtifactGetResponse);
rpc ArtifactResolve(AgentArtifactResolveRequest) returns (AgentArtifactResolveResponse);
// ── Streaming ops ────────────────────────────────────────────────────
rpc WatchPrefix(AgentWatchPrefixRequest) returns (stream AgentWatchEventProto);
}
// ─── Agent: Branch messages ─────────────────────────────────────────────────
message AgentForkRequest {
string label = 1;
uint64 parent_branch_id = 2; // 0 = fork from main timeline
}
message AgentForkResponse {
uint64 branch_id = 1;
}
message AgentMergeBranchRequest {
uint64 branch_id = 1;
}
message AgentMergeBranchResponse {}
message AgentDiscardBranchRequest {
uint64 branch_id = 1;
}
message AgentDiscardBranchResponse {}
message AgentListBranchesRequest {}
message AgentBranchMetaProto {
uint64 id = 1;
uint64 parent_id = 2;
uint64 parent_snapshot_seq = 3;
uint64 created_at = 4;
string status = 5; // "Active", "Merged", "Discarded"
string label = 6;
}
message AgentListBranchesResponse {
repeated AgentBranchMetaProto branches = 1;
}
message AgentBranchPutRequest {
uint64 branch_id = 1;
uint32 cf = 2;
bytes key = 3;
bytes value = 4;
}
message AgentBranchPutResponse {}
message AgentBranchGetRequest {
uint64 branch_id = 1;
uint32 cf = 2;
bytes key = 3;
}
message AgentBranchGetResponse {
bool found = 1;
bytes value = 2;
}
// ─── Agent: Causal graph messages ───────────────────────────────────────────
message AgentAddStepRequest {
string agent_id = 1;
string step_type = 2; // "Observe", "Think", "Act", "Tool", "Result"
uint64 branch_id = 3;
bytes content = 4;
bytes metadata = 5;
repeated float embedding = 6; // optional embedding vector
// Memory scope (issue #697 / #849). Empty `scope` defaults to "world"
// (backward compatible). `scope_owner` is the team id for "team", the agent
// id for "private", empty for "world". `field_acl_json` is an optional
// JSON-encoded `[{"json_pointer","min_scope"}]` array of field-level ACL rules.
string scope = 7; // "world" | "team" | "private"
string scope_owner = 8; // team id / agent id; empty for world
bytes field_acl_json = 9; // optional JSON [{json_pointer, min_scope}]
}
message AgentAddStepResponse {
uint64 step_id = 1;
}
message AgentAddEdgeRequest {
uint64 src_step_id = 1;
uint64 dst_step_id = 2;
string edge_type = 3; // "Triggers", "Informs", "Branches", "Merges"
bytes props = 4;
uint64 valid_from = 5; // 0 = use current time (backward compatible)
uint64 valid_to = 6; // 0 = permanent (backward compatible)
// Provenance: the agent whose write created this belief. Empty = unknown
// author (backward compatible); set to enable per-agent belief queries.
string author_agent_id = 7;
}
message AgentAddEdgeResponse {}
message AgentGetStepRequest {
uint64 step_id = 1;
}
message AgentGetStepResponse {
bool found = 1;
bytes step_json = 2; // JSON-serialized CausalStep
}
message AgentGetContentRequest {
uint64 step_id = 1;
}
message AgentGetContentResponse {
bool found = 1;
bytes content = 2;
}
message AgentGetEdgesRequest {
uint64 step_id = 1;
string direction = 2; // "forward", "backward"
string edge_type = 3; // optional filter, empty = all types
uint64 at_timestamp = 4; // 0 = no temporal filter (all edges)
uint64 window_start = 5; // >0 with window_end = window query
uint64 window_end = 6;
}
message AgentEdgeProto {
uint64 peer_step_id = 1;
string edge_type = 2;
bytes props = 3;
uint64 valid_from = 4;
uint64 valid_to = 5;
}
message AgentGetEdgesResponse {
repeated AgentEdgeProto edges = 1;
}
message AgentLocalTraverseRequest {
uint64 start_step_id = 1;
string direction = 2; // "forward", "backward", "both"
uint32 max_depth = 3;
}
message AgentLocalTraverseResponse {
repeated bytes step_jsons = 1; // JSON-serialized CausalStep array
repeated AgentEdgeProto edges = 2;
}
message AgentTraverseRequest {
uint64 start_step_id = 1;
string direction = 2;
uint32 max_depth = 3;
}
// ── Memory-scope provenance audit (#697 phase 4) ──────────────────────────
// Query the immutable provenance log for a time window, optionally narrowed to
// a single target step. Empty/zero `to_ts` means "now". `after_ts`/`after_seq`
// resume after the last record of a previous page; `limit` 0 = server default.
message AgentQueryProvenanceRequest {
uint64 from_ts = 1; // inclusive lower bound (ms since epoch); 0 = beginning
uint64 to_ts = 2; // inclusive upper bound (ms since epoch); 0 = now
uint64 step_id = 3; // optional: only records for this target step (0 = any)
uint64 after_ts = 4; // pagination cursor: resume strictly after (after_ts, after_seq)
uint64 after_seq = 5;
uint32 limit = 6; // max records to return (0 = server default)
}
// One immutable provenance record (mirrors auth::scope::ProvenanceRecord). The
// payload is also carried as canonical JSON in `record_json` so the offline
// re-check tool can deserialize the exact `ProvenanceRecord` without re-deriving
// fields.
message AgentProvenanceRecordProto {
uint64 decision_ts = 1;
uint64 seq = 2; // per-record monotonic sequence (key tiebreaker)
string caller_user_id = 3;
string caller_agent_id = 4; // empty = no agent principal on the token
repeated string caller_team_ids = 5;
string op = 6; // add_step | get_step | traverse | find_similar | get_edges
uint64 target_step_id = 7;
string target_scope = 8; // world | team | private
string target_owner = 9;
string decision = 10; // allowed | denied_scope | denied_field | admin_bypass
repeated string denied_fields = 11; // populated for denied_field decisions
string grants_snapshot_hash = 12; // SHA-256 hex of the caller's grant set
bytes record_json = 13; // canonical JSON of the stored ProvenanceRecord
}
message AgentQueryProvenanceResponse {
repeated AgentProvenanceRecordProto records = 1;
uint64 next_after_ts = 2; // pagination cursor for the next page (0 = exhausted)
uint64 next_after_seq = 3;
}
// Grant or revoke an agent's membership of a team (#697 phase 2c / #794).
message AgentManageTeamGrantRequest {
string agent_id = 1;
string team_id = 2;
bool revoke = 3; // false = grant, true = revoke
}
message AgentManageTeamGrantResponse {
bool ok = 1;
uint64 effective_at = 2; // ms since epoch the change took effect
}
message AgentTraverseResponse {
repeated bytes step_jsons = 1;
repeated AgentEdgeProto edges = 2;
}
message AgentFindSimilarChainsRequest {
repeated float query_embedding = 1;
uint32 k = 2; // number of similar chains to return
uint32 chain_depth = 3; // BFS depth per anchor
uint32 ef = 4; // vector search ef (0 = default)
// Optional HLC snapshot timestamp (packed u64) for cross-shard txn read-path
// lock resolution (epic #1478, Phase 4). 0 = the gateway uses a fresh now().
// Only consulted when STATELET_CROSS_SHARD_TXN is ON; otherwise ignored and
// reads behave exactly as today.
uint64 read_ts = 5;
}
message AgentCausalChainProto {
bytes anchor_step_json = 1;
float distance = 2;
repeated bytes step_jsons = 3;
repeated AgentEdgeProto edges = 4;
}
message AgentFindSimilarChainsResponse {
repeated AgentCausalChainProto chains = 1;
}
// ─── Agent: Reactive state messages ─────────────────────────────────────────
message AgentCasPutRequest {
uint32 cf = 1;
bytes key = 2;
uint64 expected_seq = 3; // logical version from get_with_seq or current_version
bytes new_value = 4;
}
message AgentCasPutResponse {
bool success = 1;
uint64 new_seq = 2; // on success, the new logical version
uint64 actual_seq = 3; // on conflict, the current logical version
}
// One read observed by an optimistic transaction: the (cf, key) and the
// snapshot seq it was read at.
message AgentTxnRead {
uint32 cf = 1;
bytes key = 2;
uint64 observed_seq = 3; // snapshot seq at read time
}
// One buffered write in an optimistic transaction. `delete = true` makes this a
// tombstone (the `value` field is ignored).
message AgentTxnWrite {
uint32 cf = 1;
bytes key = 2;
bytes value = 3;
bool delete = 4;
}
message AgentTxnCommitRequest {
repeated AgentTxnRead read_set = 1;
repeated AgentTxnWrite write_set = 2;
}
message AgentTxnCommitResponse {
bool committed = 1; // true = applied, false = conflict-aborted
uint64 commit_seq = 2; // on commit, the engine MVCC seq after the write
// On conflict, the offending key and the latest seq that beat the snapshot.
uint32 conflict_cf = 3;
bytes conflict_key = 4;
uint64 conflict_seq = 5;
}
// ─── Agent: Coordination primitive messages (claim/lease/renew/release) ─────
//
// `key` is the caller-supplied coordination key; the server namespaces it into
// the dedicated coordination CF. `agent_id` is the claimant's identity. On a
// successful acquire the server returns a `fence` (the engine sequence the claim
// committed at) to carry on subsequent fenced writes; on a failed acquire it
// returns the current `holder` so the loser can observe who holds the key.
message AgentClaimRequest {
bytes key = 1;
string agent_id = 2;
}
message AgentClaimResponse {
bool acquired = 1; // true = caller now holds the key
string holder = 2; // on failure, the current holder's agent_id
uint64 fence = 3; // fencing token (acquire: caller's; failure: holder's)
}
message AgentLeaseRequest {
bytes key = 1;
string agent_id = 2;
uint64 ttl_ms = 3; // lease TTL in milliseconds; 0 = no expiry (plain claim)
}
message AgentLeaseResponse {
bool acquired = 1;
string holder = 2;
uint64 fence = 3;
}
message AgentRenewRequest {
bytes key = 1;
string agent_id = 2;
uint64 fence = 3; // the holder's current fence (must match to renew)
uint64 ttl_ms = 4; // new TTL window from now
}
message AgentRenewResponse {
bool acquired = 1; // true = lease extended; false = fence no longer matches
string holder = 2;
uint64 fence = 3; // refreshed fence on success
}
message AgentReleaseRequest {
bytes key = 1;
uint64 fence = 2; // the holder's fence (must match to release)
}
message AgentReleaseResponse {
bool released = 1; // true = key dropped; false = fence no longer matches
}
// ─── Cross-shard transactions: prewrite (epic #1478, Phase 2) ───────────────
//
// The internal Percolator prewrite: place a LockRecord intent on the user key
// and stage the provisional value, both as one atomic conditional write in the
// coordination CF. Aborts on a conflicting lock or a write-pointer carrying a
// commit_ts >= start_ts. INTERNAL/admin-only and gated behind
// STATELET_CROSS_SHARD_TXN (default OFF).
message AgentPrewriteRequest {
uint32 cf = 1; // target CF of the user key
bytes key = 2; // user key being prewritten
bytes primary_ref = 3; // the txn's primary key (every secondary points back)
uint64 start_ts = 4; // HLC start_ts (packed u64), from Phase 1
uint64 ttl_ms = 5; // lock TTL in milliseconds
uint64 fence = 6; // primary claim's fencing token
bytes value = 7; // provisional value for a Put (ignored when delete)
bool delete = 8; // true = Delete intent (the value field is ignored)
// true = READ intent: lock the key so no concurrent transaction can commit
// over it, stage nothing, and release (not roll forward) at commit. This is
// how a cross-shard transaction detects READ-WRITE conflicts; `value` and
// `delete` are ignored. Re-reading the key before commit would not do — a
// concurrent txn could commit between the check and the primary CAS.
bool read_only = 9;
}
message AgentPrewriteResponse {
bool locked = 1; // true = intent placed; false = conflict (lock or newer ver)
uint64 fence = 2; // on lock: the per-key seq the lock committed at
}
// ─── Cross-shard transactions: commit + resolve (epic #1478, Phase 3) ───────
//
// Internal/admin-only drivers (gated behind STATELET_CROSS_SHARD_TXN, default
// OFF) the gateway coordinator fans to the pinned coordination shard.
// The commit point: CAS the primary TxnStatus Prewritten->Committed at commit_ts,
// gated IfSeqEquals(fence). A coordinator that lost its lease (the per-key seq
// advanced past `fence`) is rejected (#784/#894).
message AgentCommitPrimaryRequest {
uint32 primary_cf = 1; // CF of the primary key
bytes primary_key = 2; // the txn's primary key (holds the single commit point)
uint64 fence = 3; // primary claim's fencing token (gates the CAS)
uint64 commit_ts = 4; // HLC commit_ts (packed u64)
// The participants (cf, key, delete-op) the txn prewrote, recorded on the status
// so a resolver can finish any secondary the coordinator didn't roll forward —
// each participant's `delete` flag records its op so the resolver can decide
// put-vs-tombstone without consulting the (possibly reaped) lock (#1626).
repeated CrossShardTxnWrite participants = 5;
// The txn's HLC start_ts (packed u64), recorded so a resolver can locate each
// participant's staged value after the prewrite lock TTL-expired (#1626).
uint64 start_ts = 6;
}
message AgentCommitPrimaryResponse {
bool committed = 1; // true = TxnStatus flipped to Committed; false = rejected
uint64 commit_seq = 2; // per-key seq the commit record committed at
}
// Roll a single committed secondary forward: replace its LockRecord with a
// WriteRecord{commit_ts}, advance the write pointer, materialize the staged
// value, clear the lock. Idempotent.
message AgentRollForwardRequest {
uint32 cf = 1; // target CF of the secondary user key
bytes key = 2; // the secondary user key
uint64 start_ts = 3; // the txn's HLC start_ts (locates the staged value)
uint64 commit_ts = 4; // the txn's HLC commit_ts
// The op the prewrite recorded (true = Delete/tombstone, false = Put). Carried
// so a resolver materializing a committed write after the lock TTL-expired does
// not have to consult the reaped lock to decide put-vs-tombstone (issue #1626).
bool delete = 5;
// true = this participant was a READ intent: release the lock and write
// NOTHING (no value, no write-pointer, no WriteRecord). Without this the
// two-valued `delete` would make a read intent look like a Put and the
// roll-forward would fail closed looking for a staged value that, by
// design, never existed.
bool read_only = 6;
}
message AgentRollForwardResponse {
bool rolled = 1; // true = roll-forward applied (always true on success)
}
// Roll a single secondary back: drop its LockRecord intent + staged value.
// Idempotent.
message AgentRollbackRequest {
uint32 cf = 1; // target CF of the secondary user key
bytes key = 2; // the secondary user key
uint64 start_ts = 3; // the txn's HLC start_ts (locates the staged value)
}
message AgentRollbackResponse {
bool rolled_back = 1; // true = intent + staged value dropped
}
// ─── Cross-shard transactions: read-path lock resolution (epic #1478, Phase 4) ─
//
// The read-path resolver: consult any prewrite lock on (cf, key) for a snapshot
// read at read_ts and resolve it via the primary TxnStatus. Idempotent and
// callable by any reader. Internal/admin-only and gated behind
// STATELET_CROSS_SHARD_TXN (default OFF).
message AgentResolveLockRequest {
uint32 cf = 1; // target CF of the user key being read
bytes key = 2; // the user key
uint64 read_ts = 3; // HLC snapshot read timestamp (packed u64)
}
// The resolution outcome (mirrors reactive::ResolveOutcome).
enum ResolveLockOutcome {
RESOLVE_LOCK_CLEAR = 0; // no blocking lock (or cleaned): read normally
RESOLVE_LOCK_ROLLED_FORWARD = 1; // committed secondary rolled forward
RESOLVE_LOCK_PENDING = 2; // live lock, decision pending: back off + retry
}
message AgentResolveLockResponse {
ResolveLockOutcome outcome = 1;
// On ROLLED_FORWARD, the HLC commit_ts the version became visible at.
uint64 commit_ts = 2;
}
// ─── Cross-shard transactions: coordination-shard primary-status read (#1598) ─
//
// Read the primary TxnStatus for `primary_key` from the receiving node's pinned
// coordination shard. The owner-shard read-path/recovery resolver consults this
// (against the coordination-shard leader) so a committed primary is observed
// even when the owner shard's node is not in the coordination shard's replica
// set. Internal/admin-only and gated behind STATELET_CROSS_SHARD_TXN (default
// OFF).
message AgentReadTxnStatusRequest {
bytes primary_key = 1; // the txn's primary key (raw suffix bytes)
}
// Mirrors reactive::TxnState. `present = false` means no primary status record
// exists yet (still mid-prewrite or the coordinator crashed before the commit
// point) — the resolver treats it the same as a node-local absent status.
enum TxnStatusState {
TXN_STATUS_PREWRITTEN = 0; // primary still undecided
TXN_STATUS_COMMITTED = 1; // primary committed at commit_ts
TXN_STATUS_ABORTED = 2; // primary aborted
}
message AgentReadTxnStatusResponse {
bool present = 1; // false = no TxnStatus record present
TxnStatusState state = 2; // the decision (only meaningful when present)
uint64 commit_ts = 3; // HLC commit_ts (packed u64), 0 unless committed
// Every participant (cf, user_key, delete-op) the txn prewrote, so the recovery
// driver can fan a roll-forward / roll-back to each participant's OWNER shard
// (#1598). Each participant's `delete` flag records its prewrite op so a
// resolver can roll forward (put-vs-tombstone) without the reaped lock (#1626).
repeated CrossShardTxnWrite participants = 4;
// The txn's HLC start_ts (packed u64), recorded on the status so a resolver can
// locate each participant's staged value even after the lock TTL-expired (#1626).
uint64 start_ts = 5;
}
// One decided primary returned by AgentListDecidedPrimaries (#1598): the raw
// primary suffix bytes plus its decision + participants.
message DecidedPrimary {
bytes primary_ref = 1; // raw primary suffix bytes (Claim-role key body)
TxnStatusState state = 2; // COMMITTED or ABORTED (never PREWRITTEN)
uint64 commit_ts = 3; // HLC commit_ts (packed u64), 0 unless committed
// (cf, user_key, delete-op) each participant prewrote; `delete` lets a resolver
// roll forward put-vs-tombstone without the reaped lock (#1626).
repeated CrossShardTxnWrite participants = 4;
uint64 start_ts = 5; // txn HLC start_ts: locate staged values post lock-expiry (#1626)
}
message AgentListDecidedPrimariesRequest {
uint64 max_primaries = 1; // cap the enumeration (0 = unbounded)
}
message AgentListDecidedPrimariesResponse {
repeated DecidedPrimary primaries = 1;
}
// ─── Cross-shard transactions: gateway coordinator API (epic #1478, Phase 3) ─
// One write in a cross-shard transaction's write_set. `delete = true` makes this
// a tombstone (the `value` field is ignored).
message CrossShardTxnWrite {
uint32 cf = 1;
bytes key = 2;
bytes value = 3;
bool delete = 4;
// Only meaningful in `AgentCommitPrimaryRequest.participants`: this
// participant is a lock-only READ intent, to be RELEASED rather than rolled
// forward. Recording it is what lets a post-crash resolver clean up read
// locks at all — a participant list of writes only leaves them stranded on
// their owner shards, blocking every other prewrite on those keys until the
// lock TTL expires. Always false in a `write_set`.
bool read_only = 5;
}
message CrossShardCommitRequest {
repeated CrossShardTxnWrite write_set = 1; // the atomically-committed writes
repeated AgentTxnRead read_set = 2; // optional read-set (snapshot seqs)
// The transaction's primary key (every secondary lock points back to it). When
// empty the coordinator synthesizes a per-txn primary in the coordination CF.
bytes primary_key = 3;
uint64 ttl_ms = 4; // lock TTL for the prewritten intents
}
message CrossShardCommitResponse {
bool committed = 1; // true = the txn committed atomically across all shards
uint64 commit_ts = 2; // on commit, the HLC commit_ts (packed u64)
string abort_reason = 3; // on abort, why (conflicting lock / newer version / lost lease)
}
// ─── Cross-shard transactions: recovery & liveness (epic #1478, Phase 5) ────
// Admin request to resolve stale / orphaned cross-shard locks. When
// `primary_key` is supplied, only that transaction is resolved (roll forward if
// its primary committed, roll back if aborted). Otherwise the whole `lock/`
// prefix of the coordination CF is swept and each lock resolved against its
// primary TxnStatus. Gated behind STATELET_CROSS_SHARD_TXN (default OFF) and
// restricted to cluster administrators.
message ResolveStaleTxnRequest {
bytes primary_key = 1; // resolve only this txn's primary; empty = sweep all locks
uint64 max_locks = 2; // cap the sweep (0 = unlimited); ignored when primary_key set
}
message ResolveStaleTxnResponse {
bool enabled = 1; // false = STATELET_CROSS_SHARD_TXN is OFF (nothing scanned)
uint64 scanned = 2; // locks scanned in the sweep
uint64 rolled_forward = 3; // locks whose primary committed (rolled forward)
uint64 rolled_back = 4; // stale/aborted locks reclaimed (rolled back)
uint64 still_pending = 5; // live locks left untouched (primary undecided, TTL live)
}
// Internal per-coordination-shard driver (same fields as the admin RPC). The
// gateway routes this to the pinned coordination shard, which runs the resolver
// over its local coordination CF.
message AgentResolveStaleTxnRequest {
bytes primary_key = 1;
uint64 max_locks = 2;
}
message AgentResolveStaleTxnResponse {
uint64 scanned = 1;
uint64 rolled_forward = 2;
uint64 rolled_back = 3;
uint64 still_pending = 4;
}
// Internal per-owner-shard expired-lock sweep (issue #1795). The gateway fans
// this to every owner shard of the participant CFs; each data node scans the
// TTL-expired prewrite locks in its shard's collapsed coordination CF and
// resolves each against the coordination-shard primary status (a still-undecided
// or aborted primary => roll back; a committed primary is left for the read-path
// roll-forward). Reclaims the still-Prewritten orphans the decided-primaries scan
// never enumerates.
message AgentGcExpiredLocksRequest {
uint64 shard_id = 1; // the owner shard to sweep (resolved on the data node)
uint64 max_locks = 2; // cap the per-shard sweep (0 = unbounded)
}
message AgentGcExpiredLocksResponse {
uint64 reclaimed = 1; // expired, never-committed locks rolled back on this shard
}
// ─── Cross-shard transactions: BEGIN / COMMIT / ROLLBACK (epic #1478, Phase 6) ─
//
// TiKV-style optimistic 2PC: the client buffers its write_set locally between
// BEGIN and COMMIT and submits it in one shot at COMMIT, which runs the same
// prewrite->commit->roll-forward as CrossShardTxnCommit. Gated behind
// STATELET_CROSS_SHARD_TXN, DEFAULT OFF.
message TxnBeginRequest {
// Optional caller-supplied primary key (a coordination-CF claim every prewrite
// fences on). When empty the server synthesizes a per-txn primary.
bytes primary_key = 1;
}
message TxnBeginResponse {
// The transaction handle: the primary key the matching TxnCommit must pass so
// its prewrites fence on the same claim. Opaque to the client.
bytes txn_id = 1;
}
message TxnCommitRequest {
bytes txn_id = 1; // the handle from TxnBegin
repeated CrossShardTxnWrite write_set = 2; // the client-buffered writes
repeated AgentTxnRead read_set = 3; // optional read-set (snapshot seqs)
uint64 ttl_ms = 4; // lock TTL for the prewritten intents
}
message TxnCommitResponse {
bool committed = 1; // true = the txn committed atomically across all shards
uint64 commit_ts = 2; // on commit, the HLC commit_ts (packed u64)
string abort_reason = 3; // on abort, why
}
message TxnRollbackRequest {
bytes txn_id = 1; // the handle from TxnBegin
}
message TxnRollbackResponse {
bool rolled_back = 1; // true = the handle was discarded (optimistic 2PC: a no-op)
}
message AgentWatchPrefixRequest {
string agent_id = 1;
uint32 cf = 2;
bytes prefix = 3;
}
message AgentWatchEventProto {
string event_type = 1; // "put", "delete"
uint32 cf = 2;
bytes key = 3;
bytes value = 4;
uint64 seq = 5;
}
// DEPRECATED (CDC Phase 5b, issue #823): request for the deprecated
// AgentSubscribeWrites RPC. Use SubscribeCommittedRequest instead — the durable
// committed change-feed is a drop-in superset (offset-addressable + resumable).
message AgentSubscribeWritesRequest {
option deprecated = true;
uint64 shard_id = 1;
uint32 cf = 2;
bytes prefix = 3;
}
message AgentWriteEventProto {
uint32 cf = 1;
bytes key = 2;
string op = 3; // "put" or "delete"
uint64 seq = 4;
}
// ─── Durable committed change-feed (CDC) — issue #692 ───────────────────────
message SubscribeCommittedRequest {
uint64 shard_id = 1;
uint64 from_offset = 2; // first Raft index to deliver; 0 = live-only (from current commit)
uint32 cf = 3; // 0 = all column families
bytes key_prefix = 4; // empty = no prefix filter
bool include_values = 5; // include the value bytes for Put/Merge changes
}
message CommittedChangeProto {
uint64 offset = 1; // = LogEntry.index (stable Raft offset)
uint64 term = 2; // LogEntry.term
uint32 seq_in_entry = 3; // position of this key within the batch (tiebreaker)
uint32 cf = 4;
bytes key = 5;
string op = 6; // "put" | "delete" | "merge"
bytes value = 7; // present for put/merge when include_values is set
}
// Requested offset is no longer in the durable log (compacted away); the client
// should full-rescan its prefix and resume from earliest_offset.
message CompactedNotice {
uint64 earliest_offset = 1; // new compaction floor (snapshot_index)
uint64 snapshot_offset = 2; // state-machine watermark (last_applied) at notice time:
// Scan() reflects state at >= this offset, so a Scan-then-resume
// client rebuilds baseline <= snapshot_offset then resumes from
// snapshot_offset+1. Old clients default this to 0 → resume at
// earliest_offset (the pre-Phase-5a behavior).
}
// One item in the SubscribeCommitted stream: either a change, a heartbeat that
// advances a filtered consumer's high-watermark, or a compaction notice.
message CommittedFeedItem {
oneof item {
CommittedChangeProto change = 1;
uint64 heartbeat = 2; // high-watermark offset (no matching change)
CompactedNotice compacted = 3;
}
}
// ─── Agent: Temporal graph messages ─────────────────────────────────────────
message AgentExpireEdgeRequest {
uint64 src_step_id = 1;
uint64 dst_step_id = 2;
string edge_type = 3;
uint64 expire_at = 4; // timestamp at which the edge becomes invalid
}
message AgentExpireEdgeResponse {}
message AgentCascadeExpireRequest {
uint64 root_fact = 1; // fact to retract; its dependents cascade-close
uint64 expire_at = 2; // valid_to timestamp (ms); 0 = now
string triggered_by_agent = 3; // provenance: which agent triggered it
string triggered_by_run = 4; // provenance: which run
bool follow_informs = 5; // also follow soft Informs deps (default false)
uint32 max_depth = 6; // 0 = default (64)
uint64 max_nodes = 7; // 0 = default (100000)
// Phase 5 cross-shard fan-out: when true, only close `root_fact`'s dependents
// (the root is already retired on its home shard) — do NOT re-retract the
// root. The gateway sets this when forwarding the cascade frontier to peer
// shards. Default false = retract the root + cascade (single-shard path).
bool dependents_only = 10;
}
message AgentCascadeExpireResponse {
uint64 root = 1;
repeated uint64 closed_facts = 2; // dependent facts whose valid_to closed
uint32 closed_edges = 3; // number of support edges severed
uint32 max_depth_reached = 4;
bool truncated = 5; // a depth/node guard stopped the walk
}
message AgentSupersedeFactRequest {
uint64 new_fact = 1; // replacement fact
uint64 old_fact = 2; // fact being superseded
uint64 expire_at = 3; // valid_to timestamp (ms); 0 = now
string triggered_by_agent = 4;
string triggered_by_run = 5;
bool cascade = 6; // also cascade-close old_fact's derived deps
bool follow_informs = 7; // (cascade only) follow soft Informs deps
uint32 max_depth = 8; // (cascade only) 0 = default (64)
uint64 max_nodes = 9; // (cascade only) 0 = default (100000)
}
message AgentSupersedeFactResponse {
// Populated only when cascade = true (same shape as cascade-expire).
repeated uint64 closed_facts = 1;
uint32 closed_edges = 2;
uint32 max_depth_reached = 3;
bool truncated = 4;
}
// ─── Agent: transactional memory ingest (#780) ──────────────────────────────
// One ANN candidate the caller already retrieved + scope-filtered.
message AgentIngestCandidate {
uint64 fact_id = 1; // existing fact id
float sim = 2; // cosine similarity to the incoming content
}
message AgentMemoryIngestRequest {
string scope = 1;
string content = 2;
optional uint64 embedding_id = 3;
repeated AgentIngestCandidate candidates = 4; // (existing_fact_id, cosine_sim)
repeated uint64 provenance_steps = 5; // DerivedFrom episode steps
float dedup_threshold = 6;
float supersede_threshold = 7;
uint64 fence = 8; // Phase 3 (#784) fencing token; 0 = ungated
// Optional typed attribution recorded on a newly-created fact body.
string author_agent_id = 9;
float confidence = 10;
string run_id = 11;
// Phase 3 (#784): the claim-key bytes the lease was taken on (exactly what
// was passed to AgentClaim/AgentLease). Required when fence != 0 — the handler
// adds (coord_cf, lease_key, observed=fence) to the txn read-set so the commit
// aborts if the lease moved. Ignored when fence == 0.
bytes lease_key = 12;
}
message AgentMemoryIngestResponse {
uint32 action = 1; // 0=Added 1=Deduplicated 2=Superseded 3=Conflict
uint64 fact_id = 2; // current fact id (new, or deduped existing)
repeated uint64 superseded = 3; // facts whose valid_to this ingest closed
bool committed = 4; // false iff action==Conflict (txn aborted)
uint64 conflict_fact_id = 5; // on data Conflict, the candidate whose version moved; 0 on fence loss
bool fence_lost = 6; // Phase 3 (#784): true iff Conflict was a lost lease (fence moved), not a data conflict
}
message AgentEdgeHistoryRequest {
uint64 src_step_id = 1;
uint64 dst_step_id = 2;
string edge_type = 3;
}
message AgentTemporalEdgeProto {
uint64 src = 1;
uint64 dst = 2;
string edge_type = 3;
uint64 valid_from = 4;
uint64 valid_to = 5;
bytes props = 6;
// Bitemporal transaction-time (recorded-at), ms. tx_from = when this edge
// revision became believed (0 = always known / legacy). tx_to = when it was
// superseded/corrected (0 = still believed).
uint64 tx_from = 7;
uint64 tx_to = 8;
// Provenance: the authoring agent id (empty = unknown author).
string author_agent_id = 9;
}
message AgentEdgeHistoryResponse {
repeated AgentTemporalEdgeProto edges = 1;
}
// ─── Durable agent execution (#846, epic #699 / sub-epic #792) ───────────────
//
// Raft-backed run/step home. RunStep/CompleteStep are split because the server
// must not execute client code over the wire: RunStep records `Started` (B1) +
// replay-check, the client runs the effect, CompleteStep records `Completed` +
// idempotency index + advances next_step_seq (B2). This preserves the
// Temporal/DBOS record-before-effect invariant across the network.
message AgentStartRunRequest {
string agent_id = 1;
uint64 parent_branch_id = 2;
bytes input = 3;
// Optional routing/idempotency key; when empty the shard is picked by hashing
// agent_id through the ShardRouter.
string run_key = 4;
}
message AgentStartRunResponse {
// run_id = (shard_id << 40) | local_seq — self-routing, cluster-unique.
uint64 run_id = 1;
}
message AgentRunStepRequest {
uint64 run_id = 1;
uint64 step_seq = 2;
bytes input = 3;
bytes idempotency_key = 4; // optional; default = hash(run_id, step_seq, input)
}
message AgentRunStepResponse {
// True when a Completed event already exists (idempotency hit / resume): the
// client skips the effect and uses recorded_result.
bool already_completed = 1;
bytes recorded_result = 2;
uint32 attempt = 3;
uint64 causal_step_id = 4;
}
message AgentCompleteStepRequest {
uint64 run_id = 1;
uint64 step_seq = 2;
bytes result = 3;
}
message AgentCompleteStepResponse {
uint64 next_step_seq = 1;
}
message AgentSemanticCheckpointProto {
uint64 run_id = 1;
uint64 step_seq = 2;
uint64 causal_step_id = 3;
uint64 tx_at = 4;
uint64 belief_raft_index = 5;
bool has_belief_raft_index = 6;
repeated string artifact_refs = 7;
bytes summary = 8;
bytes result_digest = 9;
string warning = 10;
}
message AgentCheckpointGetRequest {
uint64 run_id = 1;
uint64 step_seq = 2;
}
message AgentCheckpointGetResponse {
bool found = 1;
AgentSemanticCheckpointProto checkpoint = 2;
}
message AgentCheckpointLatestRequest {
uint64 run_id = 1;
}
message AgentCheckpointLatestResponse {
bool found = 1;
AgentSemanticCheckpointProto checkpoint = 2;
}
message AgentProvenanceChainNodeProto {
string kind = 1; // decision, memory, graph_write, redacted, missing
uint64 step_id = 2;
string agent_id = 3;
string step_type = 4;
uint64 timestamp = 5;
uint64 branch_id = 6;
bytes metadata = 7;
bool redacted = 8;
}
message AgentProvenanceChainQueryRequest {
uint64 run_id = 1;
uint64 step_seq = 2;
// When true, step_seq is ignored and the latest semantic checkpoint for the
// run is used.
bool latest = 3;
// Server-capped bounds for fan-out from the decision node.
uint32 max_memory_nodes = 4;
uint32 max_graph_nodes = 5;
}
message AgentProvenanceChainQueryResponse {
bool found = 1;
AgentSemanticCheckpointProto checkpoint = 2;
AgentProvenanceChainNodeProto decision = 3;
repeated AgentProvenanceChainNodeProto memories = 4;
repeated AgentArtifactMetadataProto artifacts = 5;
repeated AgentProvenanceChainNodeProto graph_writes = 6;
repeated string missing = 7;
bool partial = 8;
}
message AgentResumeFromStepRequest {
uint64 run_id = 1;
uint64 consumer_offset = 2; // consumer's checkpointed change-feed offset
}
message AgentResumeFromStepResponse {
uint64 resume_seq = 1;
repeated AgentStepEventProto replayed = 2; // recorded results below resume_seq
// Phase 4 (#795): the consumer's checkpointed offset exceeded the engine's
// durable high-water and was clamped (Kafka OffsetOutOfRange -> auto.offset.reset).
// Additive + optional: old clients ignore it and still resume safely.
bool consumer_offset_ahead = 3;
// The offset actually used after max(consumer_offset, high_water) + clamp.
// Equals resume_seq; surfaced separately for observability/symmetry.
uint64 effective_offset = 4;
}
message AgentSemanticCandidateProto {
string candidate_id = 1;
bytes value = 2;
float confidence = 3;
float probability = 4;
bytes metadata = 5;
}
message AgentCandidateForkProto {
AgentSemanticCandidateProto candidate = 1;
uint64 run_id = 2;
uint64 branch_id = 3;
uint64 fork_step_seq = 4;
uint64 inherited_steps = 5;
}
message AgentResumeSemanticRequest {
uint64 run_id = 1;
repeated AgentSemanticCandidateProto candidates = 2;
float confidence_threshold = 3; // 0/NaN => server default
uint32 top_k = 4; // 0 => server default, capped
string label_prefix = 5;
}
message AgentResumeSemanticResponse {
AgentSemanticCheckpointProto checkpoint = 1;
uint64 resume_seq = 2;
repeated AgentStepEventProto replayed = 3;
AgentSemanticCandidateProto selected_candidate = 4;
repeated AgentCandidateForkProto forks = 5;
bool consumer_offset_ahead = 6;
uint64 effective_offset = 7;
}
message AgentGetRunStatusRequest {
uint64 run_id = 1;
}
message AgentGetRunStatusResponse {
bool found = 1;
uint64 run_id = 2;
string agent_id = 3;
string status = 4; // "Running", "Completed", "Failed", "Suspended"
uint64 next_step_seq = 5;
uint64 created_at = 6;
uint64 updated_at = 7;
}
message AgentStepEventProto {
uint64 step_seq = 1;
uint64 causal_step_id = 2;
uint32 attempt = 3;
uint64 ts = 4;
bytes result = 5;
}
// Phase 5 (#797): branch/time-travel resume request. Fork `source_run_id` at the
// historical `fork_step_seq` into a NEW AgentFork branch + child run. The source
// run is left untouched; the child inherits the parent's recorded Completed
// prefix [0..fork_step_seq) verbatim (no re-execution) and continues forward
// execution from fork_step_seq on the new timeline.
message AgentForkRunRequest {
uint64 source_run_id = 1;
uint64 fork_step_seq = 2; // must be <= source.next_step_seq
string label = 3; // optional branch label; defaults to a descriptive one
}
message AgentForkRunResponse {
// Self-routing child run_id (lives on the same shard as the source run).
uint64 run_id = 1;
// The newly allocated AgentFork BranchId carried on the child's RunRecord and
// every inherited/forward CausalStep.
uint64 branch_id = 2;
// The child's next_step_seq == fork point; forward execution continues here.
uint64 fork_step_seq = 3;
// Count of parent Completed events copied into the child (== fork_step_seq).
uint64 inherited_steps = 4;
}
message AgentForkAcrossCandidatesRequest {
uint64 source_run_id = 1;
uint64 fork_step_seq = 2; // 0 => latest semantic checkpoint + 1
repeated AgentSemanticCandidateProto candidates = 3;
uint32 top_k = 4; // 0 => server default, capped
string label_prefix = 5;
}
message AgentForkAcrossCandidatesResponse {
uint64 fork_step_seq = 1;
repeated AgentCandidateForkProto forks = 2;
}
message AgentArtifactPutRequest {
uint64 run_id = 1;
uint64 step_seq = 2;
bytes content = 3; // inline payload; leave empty when uri is set
string uri = 4; // external payload reference; requires sha256
bytes sha256 = 5; // optional for inline, required for uri; 32 bytes
uint64 size = 6; // external payload size; inline size is derived
string mime_type = 7;
}
message AgentArtifactMetadataProto {
string artifact_id = 1; // lowercase hex sha256
uint64 run_id = 2;
uint64 step_seq = 3;
bytes sha256 = 4;
uint64 size = 5;
string mime_type = 6;
string uri = 7;
bool inline = 8;
uint64 created_at = 9;
}
message AgentArtifactPutResponse {
AgentArtifactMetadataProto artifact = 1;
}
message AgentArtifactGetRequest {
uint64 run_id = 1;
string artifact_id = 2; // lowercase hex sha256
}
message AgentArtifactGetResponse {
bool found = 1;
AgentArtifactMetadataProto artifact = 2;
bytes content = 3; // populated for inline artifacts
}
message AgentArtifactResolveRequest {
uint64 run_id = 1;
string artifact_id = 2;
}
message AgentArtifactResolveResponse {
bool found = 1;
AgentArtifactMetadataProto artifact = 2;
}
// ─── Team time-travel (#787, epic #698 Phase 3) ──────────────────────────────
//
// "As-of-then" replay of what a team knew at (V, T) over the wire. The leaf
// handlers call the already-shipped engine operators (PR #724); the gateway
// fans out, pins a per-shard committed ordinal (FoundationDB-style read
// version), merges/dedupes, paginates on the global sort key, and reports
// partial coverage when a shard is unreachable (Elasticsearch `_shards` style).
// How a single logical edge's belief changed in a team diff window. Mirrors
// `KnowledgeChangeType` in src/agent/types.rs 1:1 (Dolt `dolt_diff_*` shape).
enum ChangeType {
ASSERTED = 0; // newly believed in the window
RETRACTED = 1; // believed at T1, no longer at T2
REVALUED = 2; // believed at both, different revision
}
// Full bitemporal edge tuple carried by team time-travel. A superset of
// AgentTemporalEdgeProto (which lacks no field but is reused name-wise for
// belief queries); kept distinct so the team surface can evolve independently.
message TeamTemporalEdgeProto {
uint64 src = 1;
uint64 dst = 2;
string edge_type = 3;
uint64 valid_from = 4;
uint64 valid_to = 5;
bytes props = 6;
uint64 tx_from = 7;
uint64 tx_to = 8;
string agent_id = 9; // authoring agent id (empty = unknown)
}
// One classified change in the team's knowledge between two transaction
// instants, with the before/after edge revisions where applicable.
message KnowledgeChangeProto {
ChangeType change_type = 1;
TeamTemporalEdgeProto before = 2; // present for RETRACTED, REVALUED
TeamTemporalEdgeProto after = 3; // present for ASSERTED, REVALUED
}
// ─── Raw agent-state row access (P3) ─────────────────────────────────────────
// Which agent column family to read. A NAME, not an id: the agent CFs are
// created directly on the shared DB, so their physical ids are a node-local
// detail that a coordinator must not have to know (and must not be able to
// guess wrong — naming the CF is what makes the allowlist check possible).
message AgentStateGetRequest {
string cf_name = 1;
bytes key = 2;
// Logical row address. When `role` is non-zero the server builds the physical
// key itself from (`entity_id`, `role`, `key`-as-suffix) and `cf_name` is
// ignored.
//
// Prefer this over a raw `key`. Whether a row lives in the collapsed causal CF
// under an `[entity_id][role][suffix]` key or in a legacy per-role CF under
// the bare suffix is SERVER state (`collapsed_active()`), and it differs
// between stores. A coordinator that hand-built physical keys would have to
// track the storage layout to stay correct — which is exactly the coupling
// that keeps agent semantics stuck inside the storage process. Addressing a
// row logically leaves layout where it belongs.
uint32 role = 3; // CausalRole tag byte; 0 = address by raw key instead
uint64 entity_id = 4; // ignored unless `role` is set
bool system = 5; // address the SYSTEM_ENTITY namespace, not `entity_id`
// Snapshot-bounded read: resolve the row as of this engine sequence, ignoring
// every version committed after it. 0 = read the latest committed version.
//
// Required for an optimistic transaction driven from outside the storage
// process. Such a transaction baselines its read set on ONE snapshot; reading
// "latest" instead would let a write committed after the snapshot leak into
// the decision while the read set still claims the older baseline — the value
// and the version it is validated against would describe different instants.
uint64 snapshot_seq = 6;
}
message AgentStateGetResponse {
bool found = 1;
bytes value = 2;
// Engine sequence of the row's latest version; 0 when absent. Same clock and
// same purpose as `GetResponse.seq` — this is how a gateway-side agent
// transaction builds `AgentTxnRead.observed_seq` for a row it read.
uint64 seq = 3;
}
message AgentStateScanRequest {
string cf_name = 1;
bytes prefix = 2;
bytes cursor = 3; // resume cursor (exclusive); empty = from the beginning
uint32 limit = 4; // 0 = server default
}
message AgentStateScanEntry {
bytes key = 1;
bytes value = 2;
uint64 seq = 3;
}
// A JSON-serialized `ProvenanceRecord`, built by the coordinator that made the
// decision. Sent whole rather than field-by-field: the record's shape
// (caller identity, target scope, grant-set hash, decision) is the audit
// contract, and re-marshalling it through a parallel proto message would give
// it a second definition to drift against.
message AgentAppendProvenanceRequest {
// JSON-serialized `ProvenanceRecord`s. Batched because a scope-pruning
// traversal decides one per visited step: sending them individually would put
// a round trip on every node of a BFS.
repeated bytes records = 1;
}
message AgentAppendProvenanceResponse {
// How many records were appended. Failure is reported in-band, not as an RPC
// error: the read that produced these decisions has already been answered, so
// an audit-write failure must not retroactively fail it (matching
// `record_decision`, which counts + logs and continues).
uint64 appended = 1;
bool applied = 2; // false if ANY record failed to append
}
message AgentStateEdgesRequest {
// Batched for the same reason as the provenance append: a traversal expands a
// whole BFS level at once, and one round trip per anchor would make depth ×
// fan-out round trips out of what is one index lookup per anchor locally.
repeated uint64 anchor_step_ids = 1;
string direction = 2; // "forward" | "backward" | "both"
string edge_type = 3; // empty = every type
// `at_timestamp > 0` selects edges valid at that instant; otherwise a
// non-zero `window_end` selects edges overlapping [window_start, window_end];
// with neither, every revision is returned.
uint64 at_timestamp = 4;
uint64 window_start = 5;
uint64 window_end = 6;
// Bitemporal belief form (`AgentBeliefQuery`): when `bitemporal` is set the
// three fields above are ignored and the server instead resolves the LATEST
// believed revision of each edge at (`as_of` valid-time, `tx_as_of`
// transaction-time), optionally narrowed to one author.
//
// This cannot be done by the caller from the raw edge list: "latest revision
// per edge" is a reduction over the revision chain, not a per-edge predicate,
// and getting it wrong silently returns a superseded belief as current.
bool bitemporal = 7;
uint64 as_of = 8; // 0 = now
uint64 tx_as_of = 9; // 0 = latest known
string author_agent_id = 10; // empty = any author
}
message AgentStateEdgeProto {
// Which requested anchor this edge belongs to. Explicit rather than inferred
// from src/dst: under "both" an edge can attach to an anchor from either end,
// and a self-edge attaches from both.
uint64 anchor = 10;
uint64 src = 1;
uint64 dst = 2;
string edge_type = 3;
uint64 valid_from = 4;
uint64 valid_to = 5; // 0 = permanently valid
uint64 tx_from = 6;
uint64 tx_to = 7; // 0 = still believed
string author_agent_id = 8;
bytes props = 9;
}
message AgentStateEdgesResponse {
repeated AgentStateEdgeProto edges = 1;
}
message AgentStateVersionKey {
uint32 cf = 1;
bytes key = 2;
}
message AgentStateAllocIdsRequest {
uint32 count = 1; // ids to reserve; 0 is rejected
}
message AgentStateAllocIdsResponse {
// The block is `[start, start + count)`. Ids are never reissued, so an
// unused tail is a harmless gap.
uint64 start = 1;
uint32 count = 2;
}
message AgentStateVersionsRequest {
repeated AgentStateVersionKey keys = 1;
}
message AgentStateVersionsResponse {
// Positional: `seqs[i]` is the version of `keys[i]`; 0 for a key that has
// never been written.
repeated uint64 seqs = 1;
// The coordination shard's current engine sequence, captured with the probe.
// A read-only optimistic commit reports this as its `commit_seq`.
uint64 read_version = 2;
}
// One LOGICAL causal row, named by (namespace, entity, role, suffix).
//
// The server expands it into every physical row the write must produce. While
// the collapsed-CF migration window is open that is TWO rows — the legacy
// per-role CF and the entity-major mirror — and emitting only one of them
// produces a write that reads back as absent, or a layout a rollback can no
// longer trust. Naming the row logically keeps that expansion where the reads
// are.
message AgentStateRoleEntry {
uint32 role = 1; // CausalRole tag byte
uint64 entity_id = 2;
bool system = 3; // address the SYSTEM_ENTITY namespace
bytes suffix = 4; // the legacy per-CF key
bytes value = 5;
bool delete = 6;
// Absolute expiry (ms since epoch); 0 = no TTL. A lease acquire needs the
// expiry on the SAME write as the value.
uint64 expire_at = 7;
}
message AgentStateConditionalWriteRequest {
// Same shape as ConditionalBatchWriteRequest minus the shard fields — the
// shard is not the caller's to choose here, it is the pinned coordination
// shard by construction.
WriteEntry gate = 1;
WriteConditionKind condition = 2;
bytes condition_value = 3;
repeated WriteEntry entries = 4;
repeated AgentTxnRead read_set = 5;
uint64 condition_seq = 6;
// Logical causal rows to apply alongside `entries`, expanded server-side.
// Prefer these over raw `entries` for anything in a causal CF.
repeated AgentStateRoleEntry role_entries = 7;
// The GATE, named logically. Use this instead of `gate` for anything in a
// causal CF: the coordination CF is created node-locally and is NOT in the
// metadata registry, so a coordinator has no way to learn its physical id —
// and any id it guessed would be a different CF on a different node.
//
// The server puts the predicate on the row reads resolve to, and any
// additional mirror rows ride along as plain entries so the whole batch is
// still decided by the one predicate.
AgentStateRoleEntry role_gate = 8;
// Read-set entries named logically, resolved server-side and merged into
// `read_set`. Needed for the same reason `role_gate` is: a causal row's
// physical address depends on the store's layout, and the coordination CF has
// no metadata id at all — a coordinator that built these itself would either
// guess wrong or be unable to name the row.
repeated AgentStateRoleRead role_read_set = 9;
}
// One read-set entry, named by (namespace, entity, role, suffix).
message AgentStateRoleRead {
uint32 role = 1; // CausalRole tag byte
uint64 entity_id = 2;
bool system = 3;
bytes suffix = 4;
// The version the caller observed. The batch applies only if this row has not
// advanced past it.
uint64 observed_seq = 5;
}
message AgentStateTeamReadRequest {
string team_id = 1;
repeated string agent_ids = 2;
uint64 valid_at = 3;
// Snapshot form: the belief instant. Diff form: the window.
bool diff = 4;
uint64 tx_at = 5; // snapshot only
uint64 tx_from = 6; // diff only
uint64 tx_to = 7; // diff only
}
message AgentStateTeamReadResponse {
// Snapshot form.
repeated bytes step_jsons = 1;
repeated TeamTemporalEdgeProto edges = 2;
// Diff form.
repeated KnowledgeChangeProto changes = 3;
// This shard's committed ordinal at the instant the answer was computed.
uint64 read_version = 4;
}
message AgentRunCheckpointGetRequest {
uint64 run_id = 1;
// When `latest` is false, the checkpoint at exactly this step_seq.
uint64 step_seq = 2;
bool latest = 3;
}
message AgentRunCheckpointGetResponse {
// The run record, JSON-serialized. Absent when the run does not exist. The
// caller authorizes against this (owner agent + namespace/database scope)
// before looking at the checkpoint.
bool run_found = 1;
bytes run_json = 2;
bool checkpoint_found = 3;
bytes checkpoint_json = 4;
}
message AgentStateBeliefDivergenceRequest {
uint64 src_step_id = 1;
uint64 dst_step_id = 2; // 0 = any destination
string edge_type = 3; // empty = every type
uint64 as_of = 4; // 0 = now
uint64 tx_as_of = 5; // 0 = latest known
}
message AgentStateAuthorBelief {
string author_agent_id = 1;
// An author who is KNOWN to hold no belief here is distinct from one whose
// belief was filtered out: the divergence verdict depends on that difference.
//
// NOT named `has_edge`: protoc's C++ generator names a scalar getter after
// the bare field and a message field's presence check `has_<field>`, so
// `has_edge` alongside `edge` emits the same method twice and the generated
// header does not compile. Field number 2 is unchanged, so the rename is
// binary-wire compatible.
bool edge_present = 2;
AgentStateEdgeProto edge = 3;
}
message AgentStateBeliefDivergenceResponse {
repeated AgentStateAuthorBelief beliefs = 1;
// Whether the authors actually disagree, computed BEFORE scope filtering.
// The caller must recompute it after filtering — two beliefs that disagree
// are not a disagreement the caller can see if one of them is invisible.
bool divergent = 2;
}
message AgentStateBatchGetRequest {
repeated uint64 entity_ids = 1;
uint32 role = 2; // CausalRole tag byte
}
message AgentStateBatchGetEntry {
uint64 entity_id = 1;
bool found = 2;
bytes value = 3;
uint64 seq = 4;
}
message AgentStateBatchGetResponse {
repeated AgentStateBatchGetEntry entries = 1;
}
message AgentStateScanResponse {
repeated AgentStateScanEntry entries = 1;
bytes next_cursor = 2; // empty = no more rows
bool has_more = 3;
// The coordination shard this page was served from, and the engine sequence
// it was read at. A multi-page walk that sees `read_seq` move has raced a
// concurrent write and, if it needs a consistent view, must restart.
uint64 shard_id = 4;
uint64 read_seq = 5;
}
message TeamSnapshotLocalRequest {
string team_id = 1; // empty => all agents on this shard
repeated string agent_ids = 2; // optional explicit membership (AND with team_id)
uint64 valid_at = 3; // V (ms); 0 => now
uint64 tx_at = 4; // T; 0 => latest belief
uint64 tx_index = 5; // explicit belief ordinal (overrides tx_at); set by gateway fence
uint32 max_edges = 6; // page size (0 => leaf default)
bytes page_token = 7; // opaque resume cursor
}
message TeamSnapshotLocalResponse {
repeated bytes step_jsons = 1; // JSON-serialized CausalStep (AgentLocalTraverseResponse convention)
repeated TeamTemporalEdgeProto edges = 2;
uint64 resolved_tx_index = 3; // per-shard ordinal this answer was pinned at (fence stamp)
bytes next_page_token = 4; // empty => last page
bool partial = 5; // a required shard was unreachable => snapshot is incomplete
repeated string missing_agents = 6; // agents whose shard could not be contacted (parity with HTTP 206)
}
// Which data path served (or should serve) a team diff. Additive enum: AUTO is
// the wire default (tag 0) so existing callers are unchanged. See issue #838.
enum DiffMode {
DIFF_AUTO = 0; // changefeed if the window is fully retained, else version-scan head + changefeed tail
DIFF_CHANGEFEED = 1; // force the windowed durable-changefeed scan
DIFF_REPLAY = 2; // alias of changefeed (replay_committed-backed); reserved for future divergence
DIFF_VERSION_SCAN = 3; // force the #724 full-version-scan oracle path
}
message TeamDiffLocalRequest {
string team_id = 1;
repeated string agent_ids = 2;
uint64 tx_from = 3; // T1
uint64 tx_to = 4; // T2; 0 => up to latest belief
uint64 valid_at = 5; // optional domain filter
uint32 max_changes = 6;
bytes page_token = 7;
DiffMode mode = 10; // default AUTO (issue #838)
uint64 tx_from_index = 11; // explicit belief ordinal i1 (skip _belief_index resolve)
uint64 tx_to_index = 12; // explicit belief ordinal i2
}
message TeamDiffLocalResponse {
repeated KnowledgeChangeProto changes = 1;
uint64 resolved_from_index = 2;
uint64 resolved_to_index = 3; // = fence ordinal when tx_to==0
bytes next_page_token = 4;
DiffMode served_by = 10; // which path answered (observability, issue #838)
bool partial = 11; // a required shard was unreachable => diff is incomplete
uint64 earliest_offset = 12; // earliest retained Raft offset when partial
repeated string missing_agents = 13; // agents whose shard could not be contacted (parity with TeamSnapshotLocalResponse)
}
// ─── Agent: Bitemporal belief queries ("who believed what, when") ────────────
message AgentBeliefQueryRequest {
uint64 start_step_id = 1;
string direction = 2; // "forward" | "backward" | "both"
uint32 max_depth = 3; // 0 => single-hop edge list, >0 => traverse
uint64 as_of = 4; // valid-time V (0 => now)
uint64 tx_as_of = 5; // transaction-time T (0 => now == "current belief")
string author_agent_id = 6; // empty => team / authoritative view
string edge_type = 7; // optional
}
message AgentBeliefQueryResponse {
repeated AgentTemporalEdgeProto edges = 1;
repeated bytes step_jsons = 2; // when max_depth > 0
}
message AgentBeliefDivergenceRequest {
uint64 src_step_id = 1;
uint64 dst_step_id = 2; // 0 => all neighbors
string edge_type = 3; // optional
uint64 as_of = 4; // valid-time V (0 => now)
uint64 tx_as_of = 5; // transaction-time T (0 => now)
}
// One author's resolved belief at (V, T). `edge` is absent (edge_present =
// false) when that author has no believed edge there ("believes ¬X").
//
// `edge_present` is deliberately not called `has_edge` — see
// AgentStateAuthorBelief for why that name breaks the C++ codegen.
message AgentAuthorBeliefProto {
string author_agent_id = 1;
bool edge_present = 2;
AgentTemporalEdgeProto edge = 3;
}
message AgentBeliefDivergenceResponse {
repeated AgentAuthorBeliefProto beliefs = 1;
bool divergent = 2;
}
// ─── Graph Index ────────────────────────────────────────────────────────────
message CreateGraphIndexRequest {
string name = 1; // graph index name (e.g., "news")
uint32 dim = 2; // vector dimensionality
uint32 m = 3; // HNSW M parameter
uint32 m_max0 = 4; // HNSW max neighbors at layer 0
uint32 ef_construction = 5; // HNSW ef_construction
uint32 ef_search = 6; // HNSW ef_search
string metric = 7; // "l2", "cosine", "ip"
uint64 edge_segment_duration = 8; // time segment for edge partitioning (ms)
string backend = 9; // "disk_hnsw" or "spfresh" (default: "spfresh")
// Per-graph default recency half-life (ms) for graph_rag_search recency
// decay. 0 = use the server ~180d constant. A GraphRagSearch sending
// decay_half_life_ms == 0 resolves to this persisted value when set.
uint64 default_decay_half_life_ms = 10;
// In-list vector quantizer: "sq8", "pq", or "rabitq". "" = backend default
// (disk_hnsw -> sq8, spfresh -> pq), which is byte-identical to pre-field
// behavior. Part of the graph's schema identity: a re-create with a
// different quantizer is rejected (change it via an explicit rebuild).
string quantizer = 11;
// P5b: shared-CF sub-index id (+1 encoding: 0 = dedicated CF, N = sub N-1)
// so the field is optional without proto3 optional syntax.
uint32 shared_sub_plus1 = 12;
// The metadata-registry cf_id this graph is registered under. A data node
// creates its physical CF at exactly this id, keeping the node-local
// physical id space and the cluster-wide metadata space identical — a
// replicated write batch names its CF by number, so letting each node pick
// its own would wedge apply or land rows in the wrong column family.
//
// The gateway reserves the id (MetadataOp::ReserveCfId) before fanning out,
// so this is authoritative rather than advisory. 0 means the reservation
// could not be obtained; the node then allocates locally and the applier's
// id translation covers the difference.
uint32 cf_id_hint = 13;
}
message CreateGraphIndexResponse {
// The 6 CF IDs allocated for this graph index.
uint32 adj_cf = 1;
uint32 vec_cf = 2;
uint32 sq_cf = 3;
uint32 edge_cf = 4;
uint32 erev_cf = 5;
uint32 node_cf = 6;
}
message DropGraphIndexRequest {
string name = 1;
}
message DropGraphIndexResponse {}
message GraphAddNodeRequest {
string graph_name = 1;
uint64 node_id = 2;
bytes properties = 3;
repeated float vector = 4; // optional; empty = no vector
repeated string labels = 5; // optional first-class node labels (multi-label)
}
message GraphAddNodeResponse {}
message GraphBatchAddNodeRequest {
string graph_name = 1;
repeated GraphAddNodeEntry nodes = 2;
}
message GraphAddNodeEntry {
uint64 node_id = 1;
bytes properties = 2;
repeated float vector = 3;
repeated string labels = 4; // optional first-class node labels (multi-label)
}
message GraphBatchAddNodeResponse {}
message GraphRemoveNodeRequest {
string graph_name = 1;
uint64 node_id = 2;
}
message GraphRemoveNodeResponse {}
message GraphAddEdgeRequest {
string graph_name = 1;
uint64 src = 2;
uint64 dst = 3;
string edge_type = 4;
uint64 valid_from = 5; // timestamp (ms)
uint64 valid_to = 6; // 0 = no expiry
bytes properties = 7;
// Bitemporal transaction-time (recorded-at), ms. tx_from = when this edge
// revision became believed (0 = default to the write time at the data node).
// tx_to = when it was superseded/corrected (0 = still believed).
uint64 tx_from = 8;
uint64 tx_to = 9;
}
message GraphAddEdgeResponse {}
message GraphBatchAddEdgeRequest {
string graph_name = 1;
repeated GraphAddEdgeEntry edges = 2;
}
message GraphAddEdgeEntry {
uint64 src = 1;
uint64 dst = 2;
string edge_type = 3;
uint64 valid_from = 4; // timestamp (ms)
uint64 valid_to = 5; // 0 = no expiry
bytes properties = 6;
uint64 tx_from = 7; // 0 = default to write time at the data node
uint64 tx_to = 8; // 0 = still believed
}
message GraphBatchAddEdgeResponse {}
message GraphBatchWriteEntry {
string cf_name = 1;
WriteOp op = 2;
bytes key = 3;
bytes value = 4;
uint64 expire_at = 5;
}
message GraphBatchWriteRequest {
string graph_name = 1;
repeated GraphBatchWriteEntry entries = 2;
}
message GraphBatchWriteResponse {}
message GraphBatchReadEntry {
string cf_name = 1;
bytes key = 2;
}
message GraphBatchReadRequest {
string graph_name = 1;
repeated GraphBatchReadEntry entries = 2;
}
message GraphBatchReadValue {
bool found = 1;
bytes value = 2;
uint64 expire_at = 3;
}
message GraphBatchReadResponse {
// One value per request entry, in the same order.
repeated GraphBatchReadValue values = 1;
}
message GraphSearchRequest {
string graph_name = 1;
repeated float query = 2;
uint32 k = 3;
uint32 ef = 4; // 0 = use default ef_search
}
message GraphSearchResult {
uint64 node_id = 1;
float distance = 2;
}
message GraphSearchResponse {
repeated GraphSearchResult results = 1;
}
// ─── Vector-anchored multi-hop expansion (GraphRAG) ───────────────────────────
message GraphSearchExpandRequest {
string graph_name = 1;
repeated float query = 2; // query embedding for anchor selection
uint32 k = 3; // number of vector anchors (top-k)
uint32 ef = 4; // 0 = use default ef_search
uint32 depth = 5; // BFS hop count from each anchor (0 = anchors only)
string edge_type = 6; // empty = all edge types
uint64 as_of = 7; // 0 = no time bound; only edges with valid_from <= as_of
bool reverse = 8; // true = expand along incoming edges
// Upper bound on total nodes returned (anchors + expanded). 0 = engine
// default (DEFAULT_MAX_EXPAND_NODES = 10000, never unbounded); pass an
// explicit large value (e.g. u32 max) to raise the cap.
uint32 max_nodes = 9;
// Per-node edge fanout cap: when expanding a single frontier node, scan at
// most this many of its edges, so a high-degree supernode cannot be
// materialized in full (OOM / latency). 0 = engine default.
uint32 max_degree = 10;
// Transaction-time (as-of-then belief) point, ms. 0 = current belief (no
// bound). When set, only edges believed at `tx_as_of` are traversed/returned
// (tx_from <= tx_as_of && (tx_to == 0 || tx_to > tx_as_of)); legacy edges
// with tx_from == 0 are treated as always known. This excludes edges that
// were transactionally corrected/retracted after `tx_as_of`, matching the
// bitemporal predicate the adjacent traversal path already enforces.
uint64 tx_as_of = 11;
}
message GraphExpandNode {
uint64 node_id = 1;
// Vector distance to the query for anchor nodes; for nodes reached only via
// BFS this is the distance of the anchor they were discovered from.
float distance = 2;
uint32 hop = 3; // 0 = anchor, 1 = one hop away, ...
bool is_anchor = 4;
}
message GraphSearchExpandResponse {
repeated GraphExpandNode nodes = 1;
repeated GraphEdge edges = 2;
}
// ─── Unified GraphRAG retrieval (#696) ────────────────────────────────────────
// One call: vector seed -> bitemporal graph expansion -> blended rerank
// (similarity + recency + graph-distance) -> facts with provenance + validity.
// All new fields default to 0/empty ⇒ backward compatible.
// How the three component scores are fused into one scalar.
enum ScoreFusion {
RRF = 0; // rank-based reciprocal-rank fusion (robust across score scales)
LINEAR = 1; // weighted linear blend of normalized component scores
}
message GraphRagSearchRequest {
string graph_name = 1;
// Exactly one of {query_text, query} is required. query_text needs an
// embedding model on the gateway; query is a pre-embedded vector (model-free).
string query_text = 2;
repeated float query = 3;
repeated string extra_queries = 4; // optional multi-query; embedded + searched too
// ── seed + expansion (mirrors GraphSearchExpandRequest) ──
uint32 k = 5; // vector anchors (top-k)
uint32 ef = 6; // 0 = default ef_search
uint32 depth = 7; // BFS hops (0 = anchors only)
string edge_type = 8; // empty = all
bool reverse = 9;
uint32 max_nodes = 10; // cap on expanded set
uint32 max_degree = 11; // per-node fanout cap
// ── bitemporal scope (reused on the durable graph edge codec) ──
uint64 as_of = 12; // valid-time point (0 = no bound)
uint64 tx_as_of = 13; // transaction-time point (0 = current belief)
// ── memory scope (residual node-property predicate; not an ACL) ──
string scope = 14; // matches props.scope (empty = any)
string user = 15; // matches props.user (empty = any)
string session_id = 16; // matches props.session_id (empty = any)
// ── blending ──
ScoreFusion fusion = 17; // RRF (default) | LINEAR
float w_similarity = 18; // linear weights (used when fusion = LINEAR)
float w_recency = 19;
float w_graph_distance = 20;
uint64 decay_half_life_ms = 21; // recency half-life; 0 = server default (~180d)
uint64 now_ms = 22; // reference epoch for recency (0 = wall clock)
bool use_reranker = 23; // optional cross-encoder pass (no-op if absent)
bool use_mmr = 24; // optional MMR diversification
float mmr_lambda = 25;
uint32 final_k = 26; // results returned after rerank (0 = k)
// graph_distance = Personalized PageRank stationary mass over the induced
// anchor-seeded subgraph (Zep node-distance / mention-reranker analogue,
// best-accuracy). unset/false ⇒ cheap 1/(1+hop) default. The server flag
// STATELET_GRAPHRAG_PPR=1 forces PPR on for all calls.
bool use_ppr_distance = 27;
}
message GraphRagFact {
uint64 node_id = 1;
bytes properties = 2; // hydrated ROLE_NODE JSON (provenance source)
float score = 3; // final blended score (higher = better)
// component scores for explainability / benchmark ablation:
float similarity = 4; // 1 - normalized vector distance
float recency = 5; // decay weight in [0,1]
float graph_distance = 6; // PPR mass when use_ppr_distance, else 1/(1+hop)
uint32 hop = 7; // 0 = anchor
bool is_anchor = 8;
// validity of the strongest contributing edge (bitemporal provenance):
uint64 valid_from = 9;
uint64 valid_to = 10;
uint64 tx_from = 11;
uint64 tx_to = 12;
repeated GraphEdge supporting_edges = 13; // edges that reached this fact
}
message GraphRagSearchResponse {
repeated GraphRagFact facts = 1;
bool truncated = 2; // true if max_nodes / frontier cap hit during expansion
}
message GraphGetNodeRequest {
string graph_name = 1;
uint64 node_id = 2;
// Optional bitemporal visibility point for the node's property blob. 0 means
// no filter for that time axis.
uint64 as_of = 3;
uint64 tx_as_of = 4;
}
message GraphGetNodeResponse {
bool found = 1;
bytes properties = 2;
repeated uint32 label_ids = 3; // node's first-class label ids (empty if none)
}
message GraphQueryEdgesRequest {
string graph_name = 1;
uint64 node_id = 2;
string edge_type = 3; // empty = all types
uint64 time_start = 4; // 0 = no lower bound
uint64 time_end = 5; // 0 = no upper bound
bool reverse = 6; // true = incoming edges
// Per-node edge fanout cap: scan at most this many of the node's edges so a
// high-degree supernode cannot be materialized in full over RPC (OOM /
// latency). 0 = engine default (DEFAULT_MAX_DEGREE).
uint32 max_degree = 7;
}
message GraphEdge {
uint64 src = 1;
uint64 dst = 2;
string edge_type = 3;
uint64 valid_from = 4;
uint64 valid_to = 5;
bytes properties = 6;
// Bitemporal transaction-time (recorded-at), ms. tx_from = 0 for legacy edges
// with no encoded transaction time (== "always known"). tx_to = 0 = still
// believed.
uint64 tx_from = 7;
uint64 tx_to = 8;
// Raw interned edge-type id as decoded from the on-disk edge key. Unlike
// `edge_type` (the human-facing resolution, which can be a synthetic
// `type_{id}` fallback), this is authoritative for reconstructing the exact
// stored key — internal removal paths use it to tombstone an edge's
// forward/reverse rows on their owning shards. 0 for legacy producers and
// for edge sources that carry no interned key (e.g. the JSON gedge store).
uint32 type_id = 9;
}
message GraphQueryEdgesResponse {
repeated GraphEdge edges = 1;
}
// Batched edge query for a set of nodes that all live on the same shard.
message GraphQueryEdgesBatchRequest {
string graph_name = 1;
// Nodes whose edges to fetch (all routed to the same shard by the caller).
repeated uint64 node_ids = 2;
string edge_type = 3; // empty = all types (applied at the data node)
uint64 time_start = 4; // 0 = no lower bound
uint64 time_end = 5; // 0 = no upper bound
bool reverse = 6; // true = incoming edges
// Point-in-time filter (ms), applied at the data node:
// valid_from <= as_of && (valid_to == 0 || valid_to > as_of). 0 = no filter.
uint64 as_of = 7;
// As-of-then transaction-time filter (ms), applied at the data node:
// tx_from <= tx_as_of && (tx_to == 0 || tx_to > tx_as_of). 0 = no filter.
uint64 tx_as_of = 8;
// Per-node edge fanout cap, applied at the data node: scan at most this many
// of each node's edges so a high-degree supernode cannot be materialized in
// full over RPC (OOM / latency). 0 = engine default (DEFAULT_MAX_DEGREE).
uint32 max_degree = 9;
// Optional HLC snapshot timestamp (packed u64) for cross-shard txn read-path
// lock resolution (epic #1478, Phase 4). 0 = the gateway uses a fresh now().
// Only consulted when STATELET_CROSS_SHARD_TXN is ON; otherwise ignored and
// edge reads behave exactly as today.
uint64 read_ts = 10;
}
// Per-node edge list (mirrors the order of GraphQueryEdgesBatchRequest.node_ids).
message GraphNodeEdges {
uint64 node_id = 1;
repeated GraphEdge edges = 2;
}
message GraphQueryEdgesBatchResponse {
repeated GraphNodeEdges nodes = 1;
}
// Batched node-properties fetch for nodes on the same shard.
message GraphGetNodesBatchRequest {
string graph_name = 1;
repeated uint64 node_ids = 2;
}
message GraphNodeProps {
uint64 node_id = 1;
bool found = 2;
bytes properties = 3;
repeated uint32 label_ids = 4; // node's first-class label ids (empty if none)
repeated string labels = 5; // resolved label strings (for label-filtered match)
}
message GraphGetNodesBatchResponse {
repeated GraphNodeProps nodes = 1;
}
// ─── Multi-hop Traversal ────────────────────────────────────────────────────
// Direction of edge expansion during a multi-hop traversal.
enum GraphTraverseDirection {
GRAPH_TRAVERSE_FORWARD = 0; // follow outgoing edges (ROLE_EDGE)
GRAPH_TRAVERSE_REVERSE = 1; // follow incoming edges (ROLE_EREV)
GRAPH_TRAVERSE_BOTH = 2; // follow both
}
message GraphTraverseRequest {
string graph_name = 1;
uint64 start_node = 2;
GraphTraverseDirection direction = 3;
// Maximum BFS depth (hops) from the start node. 0 = start node only.
uint32 max_depth = 4;
// Only follow edges of this type (empty = all types).
string edge_type = 5;
// Point-in-time filter (ms): only follow edges valid at this instant
// (valid_from <= as_of && (valid_to == 0 || valid_to > as_of)). 0 = no filter.
uint64 as_of = 6;
// Cap on the total number of expanded/visited nodes to bound latency.
// 0 = use a server default (currently 100000). Pass uint32 max (0xFFFFFFFF)
// to explicitly opt out of the cap (unbounded traversal).
uint32 max_frontier = 7;
// When true, populate `path` (the edge chain from start to each node).
bool return_paths = 8;
// Per-node edge fanout cap: when expanding a single frontier node, scan at
// most this many of its edges, so a high-degree supernode cannot be
// materialized in full (OOM / latency). 0 = engine default.
uint32 max_degree = 9;
// As-of-then transaction-time filter (ms): only follow edges that were already
// believed at this recorded-time
// (tx_from <= tx_as_of && (tx_to == 0 || tx_to > tx_as_of)). Combined with
// `as_of` this gives bitemporal "what was believed at tx_as_of about valid-time
// as_of". 0 = no transaction-time filter (legacy edges are always known).
uint64 tx_as_of = 10;
}
// A node reached by the traversal, with its depth, hydrated properties and
// (optionally) the path of edges from the start node.
message GraphTraverseNode {
uint64 node_id = 1;
uint32 depth = 2;
bytes properties = 3; // hydrated ROLE_NODE props (empty if none)
bool has_props = 4;
// The chain of edges from the start node to this node (only when
// return_paths is set). Empty for the start node itself.
repeated GraphEdge path = 5;
// The node's first-class label ids (resolved client-side via the dict, or
// used by the gateway for a cheap u32 membership check during label-filtered
// matching). Empty for legacy/unlabeled nodes.
repeated uint32 label_ids = 6;
}
message GraphTraverseResponse {
// All visited nodes (including the start node), in BFS order.
repeated GraphTraverseNode nodes = 1;
// All edges traversed during expansion (deduplicated tree edges).
repeated GraphEdge edges = 2;
// True if the traversal stopped early because max_frontier was reached.
bool truncated = 3;
}
// ─── Nodes-by-label scan ──────────────────────────────────────────────────────
message GraphNodesByLabelRequest {
string graph_name = 1;
// Conjunctive (AND) label set: a node is returned only if it carries every
// label. The engine intersects the posting lists, scanning the smallest
// first (selectivity heuristic). An unknown label ⇒ empty result.
repeated string labels = 2;
// Cap on the number of returned node ids. 0 = engine default
// (effective_max_frontier). Applied per shard; the gateway re-applies the
// global cap after merging shard results.
uint32 max_nodes = 3;
// When true, include each node's ROLE_NodeProp JSON so the caller can apply
// a property residual (WHERE / inline map) without a second round trip.
bool hydrate_props = 4;
// Reserved for temporal label validity. 0 = now.
uint64 as_of = 5;
}
message GraphNodesByLabelNode {
uint64 node_id = 1;
// Hydrated ROLE_NodeProp JSON (empty when hydrate_props is false or the node
// has no props).
bytes properties = 2;
}
message GraphNodesByLabelResponse {
repeated GraphNodesByLabelNode nodes = 1;
// True if the (per-shard) cap was hit, so the result may be incomplete.
bool truncated = 2;
}
// ─── Temporal Join ──────────────────────────────────────────────────────────
message GraphTemporalJoinRequest {
string graph_name = 1; // graph index name
uint64 node_id = 2; // source node (e.g., symbol node)
string edge_type = 3; // filter by edge type (empty = all)
uint64 time_start = 4; // time range start (ms), 0 = no bound
uint64 time_end = 5; // time range end (ms), 0 = no bound
// KV lookup config: for each edge timestamp, look up these KV keys.
// Key template supports {timestamp} placeholder, e.g. "AAPL:{timestamp}".
string kv_key_template = 6;
uint32 kv_cf = 7; // column family for KV lookups (0 = default)
}
message TemporalJoinEntry {
// The graph edge.
GraphEdge edge = 1;
// The KV value at the edge's timestamp (empty if not found).
bytes kv_value = 2;
bool kv_found = 3;
// The KV key that was looked up.
string kv_key = 4;
}
message GraphTemporalJoinResponse {
repeated TemporalJoinEntry entries = 1;
}
// ─── Graph Analytics ──────────────────────────────────────────────────────────
enum GraphAnalyticsAlgorithm {
GRAPH_ANALYTICS_PAGERANK = 0;
GRAPH_ANALYTICS_WCC = 1; // weakly connected components
GRAPH_ANALYTICS_DEGREE_CENTRALITY = 2;
GRAPH_ANALYTICS_LABEL_PROPAGATION = 3; // deferred (community detection)
}
message GraphAnalyticsRequest {
string graph_name = 1;
GraphAnalyticsAlgorithm algorithm = 2;
// Optional: keep only edges of this type (empty = all types).
string edge_type = 3;
// Optional: only edges live at this timestamp (ms); 0 = current/all.
uint64 as_of = 4;
// PageRank / iterative params (0 = engine default).
uint32 iterations = 5;
float tolerance = 6;
float damping = 7; // PageRank damping factor (default 0.85)
// If true, persist scores back into each node's properties under a reserved
// "__analytics" sub-key so later traversals/searches can order by them.
bool write_back = 8;
// Optional cap on returned rows (0 = return all). Write-back still applies to
// every node regardless of this cap.
uint32 top_k = 9;
// Optional transaction-time (recorded-at / belief) point (ms); 0 = current
// belief / no filter. When set, only edges believed at `tx_as_of` are counted
// (tx_from <= tx_as_of && (tx_to == 0 || tx_to > tx_as_of)); legacy edges
// (tx_from == 0) are always known. This is the same predicate the bitemporal
// traversal read path applies, so analytics excludes belief-retracted edges
// consistently with every other graph reader (issue #1920).
uint64 tx_as_of = 10;
}
message GraphAnalyticsScore {
uint64 node_id = 1;
double score = 2;
// For WCC: the component label (smallest node id in the component). 0 for
// score-based algorithms.
uint64 component = 3;
}
message GraphAnalyticsResponse {
string algorithm = 1; // canonical name actually run
uint64 node_count = 2;
uint64 edge_count = 3;
uint32 iterations = 4; // iterations performed (0 for non-iterative)
bool wrote_back = 5;
repeated GraphAnalyticsScore scores = 6;
}
// Internal: ask one shard to dump its local analytics edge list. Mirrors the
// edge_type / as_of filters of GraphAnalyticsRequest so each shard applies the
// same temporal/type filtering before returning its (src,dst) pairs.
message GraphAnalyticsEdgesRequest {
string graph_name = 1;
string edge_type = 2; // empty = all types
uint64 as_of = 3; // 0 = current/all
// Per-shard ownership filter (issue #1601). When non-empty, the data node
// dumps ONLY edges whose source's forward-edge routing key
// (`[ROLE_EDGE][src:u64 BE]`) falls inside one of these half-open
// `[start_key, end_key)` ranges — i.e. the shards this node is the leader of.
// Without it, a node that leads some shards but also holds follower replicas
// of OTHER shards (replication factor > 1) dumps the replicated edges too, so
// the gateway union double-counts every replicated directed edge. An empty
// list disables the filter (full local dump) for backward compatibility.
repeated GraphKeyRange owned_ranges = 4;
// Transaction-time (recorded-at / belief) point (ms); 0 = current belief / no
// filter. Mirrors GraphAnalyticsRequest.tx_as_of so each shard applies the
// same belief filter before dumping its (src,dst) pairs (issue #1920).
uint64 tx_as_of = 5;
}
// A half-open `[start_key, end_key)` key range. An empty `end_key` means
// "to the end" (no upper bound); an empty `start_key` means "from the start".
message GraphKeyRange {
bytes start_key = 1;
bytes end_key = 2;
}
// Packed parallel arrays: src[i] -> dst[i] is one directed edge. Kept as two
// `repeated uint64` (rather than repeated GraphEdge) so the per-shard dump is
// compact — analytics only needs endpoints, not types/timestamps/properties.
message GraphAnalyticsEdgesResponse {
repeated uint64 src = 1;
repeated uint64 dst = 2;
}
// Internal: write a batch of analytics scores back into node properties for the
// nodes this shard owns. The gateway routes each node to its owning shard, so
// a data node only ever receives its own node ids here.
message GraphAnalyticsWriteScoresRequest {
string graph_name = 1;
string algorithm = 2; // canonical name (e.g. "PageRank", "WCC")
repeated GraphAnalyticsScore scores = 3;
}
message GraphAnalyticsWriteScoresResponse {
uint64 wrote = 1; // number of node-property rows written
}
// ─── Weighted shortest path / pathfinding ───────────────────────────────────
//
// Cost-ordered (BinaryHeap / Dijkstra) traversal over user graph edges. The
// per-edge cost is decoded from the edge's `properties` bytes according to the
// chosen `weight_encoding`. Paths are computed as-of `as_of` by reusing the
// temporal valid_from/valid_to filter.
// How to decode a non-negative edge cost (weight) from an edge's properties.
enum GraphWeightEncoding {
// No weight is read; every edge costs 1.0 (== unweighted hop count).
GRAPH_WEIGHT_UNIT = 0;
// Properties are the UTF-8 text "<key>:<float>" (e.g. "weight:0.9"). The
// `weight_key` field selects <key> (default "weight"). The first matching
// "<key>:<number>" token wins; missing key falls back to `default_weight`.
GRAPH_WEIGHT_TEXT_KEY = 1;
// Properties are a UTF-8 JSON object; `weight_key` names a top-level numeric
// field (default "weight"). Missing/non-numeric falls back to `default_weight`.
GRAPH_WEIGHT_JSON_KEY = 2;
// Properties begin with a little-endian f32 cost in the first 4 bytes. This
// is a NEW convention defined by this RPC (no such prefix existed before);
// writers that want it must prepend the 4 weight bytes themselves.
GRAPH_WEIGHT_F32_LE_PREFIX = 3;
}
// Pathfinding algorithm. A* currently shares Dijkstra's relaxation (no spatial
// heuristic is available for arbitrary node ids), so it behaves as Dijkstra
// but is accepted for forward compatibility.
enum GraphPathAlgorithm {
GRAPH_PATH_DIJKSTRA = 0;
GRAPH_PATH_ASTAR = 1;
}
message GraphShortestPathRequest {
string graph_name = 1;
uint64 src = 2;
// Single destination. Used when `targets` is empty.
uint64 dst = 3;
// Optional destination set; the search stops once the k cheapest of these
// are settled. When non-empty, `dst` is ignored.
repeated uint64 targets = 4;
string edge_type = 5; // optional: only traverse this edge type
GraphWeightEncoding weight_encoding = 6;
string weight_key = 7; // key for TEXT_KEY / JSON_KEY (default "weight")
double default_weight = 8; // fallback cost when no weight decodes (default 1.0)
GraphPathAlgorithm algorithm = 9;
// k-shortest loopless paths via Yen's algorithm (0 or 1 = single best path).
uint32 k = 10;
// As-of timestamp (ms): only edges live at this instant are traversed
// (valid_from <= as_of < valid_to, valid_to == 0 meaning no expiry).
// 0 = no temporal filter.
uint64 as_of = 11;
// Prune any path whose accumulated cost exceeds this (0 = no limit).
double max_cost = 12;
// Safety bound on settled nodes per Dijkstra run (0 = engine default).
uint64 max_expansions = 13;
// As-of-then transaction-time filter (ms): only traverse edges believed at
// this recorded-time (tx_from <= tx_as_of && (tx_to == 0 || tx_to > tx_as_of)).
// 0 = no transaction-time filter.
uint64 tx_as_of = 14;
}
message GraphPathEdge {
uint64 src = 1;
uint64 dst = 2;
string edge_type = 3;
double weight = 4; // decoded edge cost
uint64 valid_from = 5;
}
message GraphPath {
repeated uint64 nodes = 1; // ordered node ids from src to dst
repeated GraphPathEdge edges = 2; // edges between consecutive nodes
double total_cost = 3;
}
message GraphShortestPathResponse {
repeated GraphPath paths = 1; // ascending by total_cost (best first)
}
// ─── Declarative pattern-match graph query (openCypher subset) ───────────────
//
// Read-only. The gateway parses a fixed openCypher subset and compiles each
// MATCH segment to engine traversal primitives:
// * linear path patterns -> GraphTraverse (edge_type / as_of / depth)
// * anchored vector hints -> GraphSearchExpand (expand_from_anchors)
// * shortestPath(...) patterns -> GraphShortestPath
// WHERE predicates over node properties and an `as_of(<ms>)` temporal predicate
// are evaluated against hydrated ROLE_NodeProp JSON at the gateway. RETURN /
// LIMIT shape the rows. CREATE / MERGE are rejected.
message GraphQueryRequest {
// The graph index to query. When empty the gateway resolves a default graph
// (same policy as the other Graph* RPCs).
string graph_name = 1;
// The openCypher-subset query text.
string cypher = 2;
// Hard cap on returned rows regardless of any LIMIT in the query (0 = no
// extra cap; the parsed LIMIT, if any, still applies).
uint32 max_rows = 3;
// Full bitemporal point-in-time (epic #1635, Phase 4). Out-of-band defaults
// for the valid-time (`as_of`) and transaction-time (`tx_as_of`) the query is
// evaluated against. `0` means "current" (no temporal filter) for each axis,
// matching every other Graph* read path. An `AS OF <valid>, <tx>` clause in
// the `cypher` text overrides these when present; otherwise these supply the
// temporal context out-of-band. `tx_as_of` is threaded into every downstream
// read-path request that carries the field (traverse / shortest-path /
// expand / rag / triple BGP), replacing the previously hardcoded `0`.
uint64 as_of = 4;
uint64 tx_as_of = 5;
}
// One projected column value. Exactly one of the typed fields is meaningful per
// `kind`; `json` carries hydrated node properties verbatim.
message GraphQueryValue {
enum Kind {
NULL = 0;
INT = 1; // node id / integer property
DOUBLE = 2; // numeric property
STRING = 3; // string property / edge type
BOOL = 4; // boolean property
JSON = 5; // raw ROLE_NodeProp JSON bytes for a whole node
}
Kind kind = 1;
int64 int_value = 2;
double dbl_value = 3;
string str_value = 4;
bool bool_value = 5;
bytes json_value = 6;
}
message GraphQueryRow {
repeated GraphQueryValue values = 1;
}
message GraphQueryResponse {
// RETURN column names, in projection order.
repeated string columns = 1;
// Result rows, in projection order.
repeated GraphQueryRow rows = 2;
// Non-fatal query warnings (epic #1429, Phase 4). Populated when a label-scan
// anchor resolution hit the per-shard frontier cap so the anchor set was
// truncated (the worst case is a hub label, e.g. `:Person`, with no property
// residual to narrow it). Surfaced — never silently truncated — so the client
// knows the result may be incomplete.
repeated string warnings = 3;
}
// ─── Text Embedding + Search (gateway-side) ─────────────────────────────────
// The gateway runs the embedding model locally, then routes to data nodes
// for vector index and KV storage. MCP clients send raw text, never vectors.
message TextPutRequest {
string index_name = 1; // vector index name
uint64 vector_id = 2; // unique ID for this entry
string text = 3; // raw text — gateway embeds this
bytes metadata = 4; // JSON metadata stored alongside in KV
uint32 kv_cf = 5; // column family for KV metadata (0 = default)
}
message TextPutResponse {}
message TextSearchRequest {
string index_name = 1; // vector index name
string query = 2; // raw text query — gateway embeds this
uint32 k = 3; // number of nearest neighbors
uint32 ef_search = 4; // optional ef_search override (0 = default)
uint32 kv_cf = 5; // column family for KV hydration (0 = default)
bytes kv_prefix = 6; // optional: only hydrate keys matching this prefix
}
message TextSearchResult {
uint64 id = 1;
float distance = 2;
bytes metadata = 3; // hydrated KV value (JSON metadata)
}
message TextSearchResponse {
repeated TextSearchResult results = 1;
}
// ─── Text + Graph (gateway-side embedding + graph storage) ──────────────────
message TextGraphPutRequest {
string graph_name = 1; // graph index name (e.g. "default")
uint64 node_id = 2; // unique node ID (microsecond timestamp)
string text = 3; // raw text — gateway embeds this
bytes properties = 4; // JSON properties stored on the graph node
// Optional: create an edge from this node to another
uint64 edge_target = 5; // 0 = no edge
string edge_type = 6; // e.g. "supersedes", "related_to"
uint64 edge_valid_from = 7; // 0 = now
uint64 edge_valid_to = 8; // 0 = permanent
// Backward-compatibility flag only. Gateway now uses server-side unified
// inference mode configuration for all requests.
bool skip_server_llm = 9;
}
message TextGraphPutResponse {
uint64 node_id = 1;
}
message TextGraphSearchRequest {
string graph_name = 1;
string query = 2; // raw text — gateway embeds this
uint32 k = 3;
uint32 ef = 4; // 0 = default
// Backward-compatibility flag only. Gateway now uses server-side unified
// inference mode configuration for all requests.
bool skip_server_llm = 5;
// Optional caller-provided rewritten queries for better retrieval.
// Each entry is embedded and searched alongside the main query.
// Used by MCP plugins where the host LLM generates rewrites at zero cost.
repeated string extra_queries = 6;
// Per-type result limits. When set (> 0), the response additionally
// populates `fact_results` and `chunk_results` with at most this many
// entries each. The combined `results` field is always populated for
// backward compatibility.
uint32 fact_k = 10; // max extracted-fact results (0 = default 7)
uint32 chunk_k = 11; // max raw-chunk results (0 = default 5)
// When true, the gateway also returns a response-level answer-oriented
// evidence bundle built from the final ranked result set.
bool include_answer_bundle = 12;
// (#827 LongMemEval Phase 5a) Multi-granularity ingest + RRF fusion. When
// non-empty, the gateway runs one ANN ranking per requested granularity
// (sentence/round/session/fact), folds each to sessions, and fuses the
// per-granularity session rankings via Reciprocal Rank Fusion. Empty ⇒
// legacy sentence-only behavior (zero regression). Recognized values:
// "sentence", "round", "session", "fact".
repeated string granularities = 13;
// RRF constant `k` in score(d) = Σ_g weight_g / (rrf_k + rank_g(d)).
// 0 ⇒ default 60 (the Elasticsearch/OpenSearch `rrf` retriever default).
uint32 rrf_k = 14;
// Conflict-as-data per-request opt-in (issue #812 / #783). When set to a
// value other than UNSPECIFIED, the gateway runs a read-time conflict
// re-rank over the final result set: it scans `contradicts` edges, groups
// contradictory claims into conflict sets, arbitrates each set with this
// policy (PR #711), and demotes the losing claims below the winner (never
// removing them) before truncating to `k`. UNSPECIFIED = use the graph/env
// default (off unless configured) → byte-identical to pre-#812 behaviour.
ConflictPolicy conflict_policy = 15;
// When true, `conflict_resolutions_json` includes the full per-loser detail
// (demoted_score, reason) for every demoted claim. When false, the JSON is
// still emitted for any resolved set but with the loser list collapsed to
// node ids only. No effect unless `conflict_policy` fires.
bool include_dissent = 16;
// (#830 LongMemEval Phase 5d) Read-time entity consolidation. When true, the
// gateway dereferences each query entity term through the persisted `gcanon:`
// identity cluster (Phase 5c) and OR-expands retrieval over the cluster's
// surface forms via the existing `load_alias_map` query-expansion seam, so
// evidence written under *any* surface in the cluster is recalled regardless
// of which surface the query used. Each consolidated result is stamped with
// `canonical_entity_id`. Default false ⇒ byte-identical to pre-#830 behaviour;
// a `gcanon:` miss / resolution-off falls back to today's recall (never an
// error). Honoured only when entity resolution is enabled (this flag OR the
// `STATELET_ENTITY_RESOLVE` env default).
bool entity_consolidate = 17;
// Optional caller-supplied reference date for relative-time queries and the
// response-level answer-oriented memory pack. Kept out of `query` so date
// tokens do not perturb retrieval ranking.
string context_date = 18;
}
message TextGraphSearchResult {
uint64 node_id = 1;
float distance = 2;
bytes properties = 3; // hydrated JSON properties
// (#827) Granularities whose per-granularity session ranking contributed to
// this result's fused RRF score (e.g. ["sentence","round"]). Empty when
// multi-granularity fusion was not used.
repeated string contributing_granularities = 4;
// (#830 LongMemEval Phase 5d) The canonical entity id this result resolved to
// when `entity_consolidate` was set and one of the result's surface forms
// dereferenced through a persisted `gcanon:` identity cluster. 0 when the
// result was not consolidated (resolution off, no cluster hit, or no surface
// overlap) — so existing clients reading 0 see the pre-#830 behaviour.
uint64 canonical_entity_id = 5;
}
message TextGraphSearchResponse {
repeated TextGraphSearchResult results = 1;
// Per-type results (populated when fact_k / chunk_k > 0 in the request).
repeated TextGraphSearchResult fact_results = 2; // extracted facts only
repeated TextGraphSearchResult chunk_results = 3; // raw session chunks only
// Optional UTF-8 JSON answer bundle built by the gateway when requested.
bytes answer_bundle_json = 4;
// Optional UTF-8 JSON object for the gateway-selected primary answer result.
bytes primary_answer_result_json = 5;
// Optional ordered UTF-8 JSON answer-result objects when answer bundling is enabled.
repeated bytes answer_results_json = 6;
// Conflict-as-data resolution log (issue #812 / #783). UTF-8 JSON array of
// ResolvedClaim objects (#781 shape), one per arbitrated conflict set:
// `{ winner, losers:[{node_id, demoted_score, reason}], policy, set_id }`.
// Empty string when `conflict_policy` was UNSPECIFIED / the default was off,
// the graph was conflict-cold, or no conflict set was found.
string conflict_resolutions_json = 7;
// Ready-to-read PLAIN-TEXT evidence block assembled by the gateway when
// `STATELET_READER_BLOCK=1`, for feeding an LLM reader directly (no client-side
// formatting). Empty when the flag is off or no compact memory was built.
string memories = 8;
}
message TextGraphQueryEdgesRequest {
string graph_name = 1;
uint64 node_id = 2;
string edge_type = 3; // empty = all types
uint64 time_start = 4; // 0 = no lower bound
uint64 time_end = 5; // 0 = no upper bound
bool reverse = 6; // true = incoming edges
// Conflict-as-data per-request opt-in (issue #813 / #783, Phase 3b). When set
// to a value other than UNSPECIFIED, the gateway runs a read-time conflict
// re-order over the returned edge set: it scans `contradicts` edges among the
// edge endpoints, groups contradictory claims into conflict sets, arbitrates
// each set with this policy (PR #711), and re-orders the edges so winner edges
// precede loser edges (edges have no distance, so demotion is ordering, not
// re-scoring — losers are never removed). UNSPECIFIED = use the graph/env
// default (off unless configured) → byte-identical to pre-#813 behaviour.
ConflictPolicy conflict_policy = 7;
// When true, `conflict_resolutions_json` includes the full per-loser detail
// (reason) for every demoted edge endpoint. When false, the JSON is still
// emitted for any resolved set but with the loser list collapsed to node ids
// only. No effect unless `conflict_policy` fires.
bool include_dissent = 8;
}
message TextGraphQueryEdgesResponse {
repeated GraphEdge edges = 1;
// Conflict-as-data resolution log (issue #813 / #783, Phase 3b). UTF-8 JSON
// array of ResolvedClaim objects (#781 shape), one per arbitrated conflict
// set: `{ winner, losers:[{node_id, reason}], policy, set_id }`. Empty string
// when `conflict_policy` was UNSPECIFIED / the default was off, the graph was
// conflict-cold, or no conflict set was found.
string conflict_resolutions_json = 2;
}
message TextGraphGetNodeRequest {
string graph_name = 1;
uint64 node_id = 2;
}
message TextGraphGetNodeResponse {
bool found = 1;
bytes properties = 2;
}
// ── Embed: pure text→vector (gateway-only) ───────────────────────────────────
message EmbedRequest {
repeated string texts = 1; // texts to embed; response vectors match this order
// Embed as a search query (true) vs a document/passage (false, default).
// Asymmetric models (e5/gte with query/doc prefixes) score better when the
// side matches; for a symmetric model the two paths are identical.
bool is_query = 2;
}
// One embedding vector. Wrapper because proto3 forbids a directly-nested
// `repeated repeated float`.
message EmbedVector {
repeated float values = 1;
}
message EmbedResponse {
repeated EmbedVector vectors = 1; // one per input text, in request order
uint32 dim = 2; // embedding dimension (0 if no model loaded)
}
// ── Conflict-as-data: read-time authority resolution (gateway-only) ──────────
// Which policy arbitrates a conflict set at read time. 1:1 with the policy
// core (PR #711, `ResolutionPolicy`). `UNSPECIFIED` means "use the graph/env
// default", which is itself off unless an `STATELET_CONFLICT_POLICY` default is
// configured — so a request that leaves this unset gets the pre-feature behaviour.
enum ConflictPolicy {
CONFLICT_POLICY_UNSPECIFIED = 0;
RECENCY = 1;
TRUST = 2;
CONFIDENCE = 3;
}
message ResolveConflictRequest {
string graph_name = 1;
uint64 node_id = 2; // any member of the conflict set
string policy = 3; // "" = graph default; trust|recency|confidence|quorum
uint64 as_of = 4; // bitemporal basis (ms); 0 = now
}
message ResolveConflictResponse {
bool found = 1; // false when node has no props row
uint64 authoritative = 2; // winning claim node id
repeated uint64 dissenting = 3; // every other claim, live + retired (never dropped)
string policy = 4; // policy actually applied (resolved default)
float score = 5; // authoritative claim's policy score
string rationale = 6; // human-readable, e.g. trust(author=admin)=0.90 ...
bool truncated = 7; // conflict set exceeded the per-set cap
// One Vote per normalised value; populated only for the quorum policy.
repeated ConflictVote votes = 8;
// Normalized candidate hypotheses for semantic replay/forking. Probabilities
// sum to 1.0 when non-empty; existing winner/dissent fields remain authoritative.
repeated ConflictCandidate candidates = 9;
}
message ConflictVote {
string value = 1;
float weight = 2;
repeated uint64 supporters = 3;
}
message ConflictCandidate {
string value = 1;
uint64 representative = 2;
float confidence = 3;
float weight = 4;
float probability = 5;
repeated uint64 supporters = 6;
}
// ─── ResolveEntities (#828 LongMemEval Phase 5b entity-resolution) ──────────
message ResolveEntitiesRequest {
string graph_name = 1;
// Optional surface forms / query terms to resolve. When empty, the resolver
// scans the entity-mention index for the whole graph and clusters all surfaces.
repeated string queries = 2;
// ANN neighbors fetched per query surface (blocking fan-out). 0 = default.
uint32 k = 3;
// Similarity threshold override (0 = STATELET_ENTITY_SIM_THRESHOLD / default).
float threshold = 4;
}
message ResolveEntitiesResponse {
repeated EntityCluster clusters = 1;
// True when resolution ran with the embedder available; false ⇒ alias/lexical
// fallback only (embedding/index unavailable — never an error).
bool embedder_used = 2;
}
message EntityCluster {
uint64 canonical_id = 1; // stable hash of the canonical surface form
string canonical = 2; // display canonical surface
repeated EntityClusterMember members = 3;
}
message EntityClusterMember {
string surface = 1;
string method = 2; // alias_rule | vector_nn | lexical
float score = 3;
}
// ─── Triple store (epic #1432, Phase 1) ──────────────────────────────────────
// The object position of a triple — either a resource (another interned term,
// distinguished on disk by a clear high tag bit) or a literal value carrying a
// datatype byte (distinguished by the OBJECT_LITERAL_TAG high bit).
message TripleObject {
oneof value {
// Resource: an interned term identical to a subject/predicate term.
string resource = 1;
// Literal: an opaque value (UTF-8 here) tagged with a 1-byte datatype.
bytes literal = 2;
}
// Datatype byte for a literal object (ignored for a resource object).
uint32 datatype = 3;
}
message TriplePutRequest {
string graph = 1; // triple graph name → CF `t:{graph}` (CfType::User)
string subject = 2; // interned through the term dictionary
string predicate = 3; // interned through the term dictionary
TripleObject object = 4; // resource (interned) or literal (LIT-stored)
// Bitemporal validity window. `valid_to == 0` is treated as open-ended
// (encoded as u64::MAX on disk). Keys end with the inverted `valid_from` so
// the newest version of a given (s,p,o) sorts first within its prefix.
uint64 valid_from = 5;
uint64 valid_to = 6;
bytes props = 7; // opaque property bytes stored on the SPO value
}
message TriplePutResponse {
uint64 subject_id = 1; // interned subject term id
uint64 predicate_id = 2; // interned predicate term id
// Interned object id. For a resource this is its term id; for a literal it is
// the allocated lit_id (the on-disk object id is this value, MSB-tagged).
uint64 object_id = 3;
bool object_is_literal = 4;
uint32 cf_id = 5; // committed cf_id of `t:{graph}`
}
// ─── Triple store (epic #1432, Phase 2) ──────────────────────────────────────
// A single bound/unbound triple pattern. Any of subject/predicate/object may be
// left empty (unbound, a variable); the gateway selects the SPO/POS/OSP index
// whose leading columns are bound and prefix-scans it.
message TripleQueryRequest {
string graph = 1; // triple graph name → CF `t:{graph}`
// Leave empty/unset to make a position UNBOUND (a `?` variable). A bound
// subject/predicate is interned through the dictionary; an unbound object is
// signalled by leaving `object` unset.
string subject = 2; // bound subject term (empty ⇒ unbound)
string predicate = 3; // bound predicate term (empty ⇒ unbound)
TripleObject object = 4; // bound object (unset ⇒ unbound)
// Optional point-in-time filter: keep only triples whose validity window
// `[valid_from, valid_to)` contains `as_of`. `0` disables the filter and
// returns the newest version of every (s,p,o).
uint64 as_of = 5;
// Cap on returned triples (0 ⇒ a server default). Newest-first by valid_from.
uint32 limit = 6;
}
// One resolved triple — TermIds resolved back to strings through ID2T / LIT.
message ResolvedTriple {
string subject = 1; // resolved subject string
string predicate = 2; // resolved predicate string
string object = 3; // resolved object string (resource term or literal text)
bool object_is_literal = 4; // true ⇒ object is a literal value
uint32 datatype = 5; // literal datatype byte (0 for a resource object)
uint64 valid_from = 6;
uint64 valid_to = 7; // u64::MAX on disk ⇒ open-ended (returned verbatim)
bytes props = 8; // opaque property bytes from the SPO value
uint64 subject_id = 9; // interned subject term id
uint64 predicate_id = 10; // interned predicate term id
uint64 object_id = 11; // interned object id (term id, or literal lit_id)
}
message TripleQueryResponse {
repeated ResolvedTriple triples = 1;
uint32 cf_id = 2; // committed cf_id of `t:{graph}` (0 if the graph has none)
}
// ─── Triple store (epic #1432, Phase 3): BGP join ────────────────────────────
// One position of a BGP triple pattern: either a bound constant term, or a
// named variable that joins across patterns. A position is treated as a
// VARIABLE iff `var` is non-empty; otherwise it is the bound constant carried by
// the position's typed field. Variables sharing the same name across patterns
// are the same join variable.
message TripleBgpTerm {
// Variable name (e.g. "x"). Non-empty ⇒ this position is an unbound/join
// variable and the bound fields below are ignored.
string var = 1;
// Bound subject/predicate constant (used when this term is in the s/p
// position and `var` is empty).
string term = 2;
// Bound object constant (used when this term is in the o position and `var`
// is empty); resource or literal.
TripleObject object = 3;
}
// One triple pattern in the BGP. `subject`/`predicate` use the term/var fields
// of TripleBgpTerm; `object` uses the object/var fields.
message TripleBgpPattern {
TripleBgpTerm subject = 1;
TripleBgpTerm predicate = 2;
TripleBgpTerm object = 3;
}
message TripleBgpRequest {
string graph = 1; // triple graph name → CF `t:{graph}`
repeated TripleBgpPattern patterns = 2; // patterns sharing variables → join
// Optional point-in-time filter applied to every pattern's scan (see
// TripleQueryRequest.as_of). 0 disables it (newest version of each triple).
uint64 as_of = 3;
// Cap on returned binding rows (0 ⇒ a server default).
uint32 limit = 4;
// Optional transaction-time filter applied to every pattern's scan. 0
// disables it (current belief / legacy behavior).
uint64 tx_as_of = 5;
}
// One join solution: a variable name → resolved value assignment.
message TripleBgpBinding {
// var name → resolved string value (resource term or literal text).
map<string, string> values = 1;
// var name → interned id (term id, or literal lit_id).
map<string, uint64> ids = 2;
// var name → true when ids[var] is a literal id, not a graph/entity node id.
map<string, bool> literal_vars = 3;
}
message TripleBgpResponse {
repeated TripleBgpBinding bindings = 1;
uint32 cf_id = 2; // committed cf_id of `t:{graph}` (0 if the graph has none)
bool truncated = 3; // true when an internal cap may have dropped solutions
}
// ─── Triple store (epic #1432, Phase 3): vector↔triple linkage ───────────────
enum TripleLinkMode {
// HNSW.search(query,k) → candidate node ids → SPO prefix-scan (id, P?, ?) to
// keep only ids that have the requested triple structure, re-ranked by the
// vector distance.
TRIPLE_LINK_VECTOR_TO_TRIPLE = 0;
// Single-pattern/seed ids → HNSW.search around each → similar entities NOT
// already connected through the requested predicate (similar-but-unconnected).
TRIPLE_LINK_TRIPLE_TO_VECTOR = 1;
// k-hop expansion from a seed over a predicate, frontier ranked by vector
// similarity to the query; return the top-N most semantically relevant.
TRIPLE_LINK_VECTOR_GUIDED_KHOP = 2;
}
message TripleLinkRequest {
string graph = 1; // triple graph name → CF `t:{graph}`
string index_name = 2; // vector index to search (HNSW/SpFresh)
TripleLinkMode mode = 3;
repeated float query = 4; // query vector (VECTOR_TO_TRIPLE / VECTOR_GUIDED_KHOP)
uint32 k = 5; // ANN fan-out / candidate pool size
uint32 ef_search = 6; // optional HNSW ef_search override (0 ⇒ default)
// The predicate constraining the linkage. For VECTOR_TO_TRIPLE the candidate
// must have an outgoing (id, predicate, ?) triple; for TRIPLE_TO_VECTOR the
// seed's existing (seed, predicate, ?) objects are excluded from results; for
// VECTOR_GUIDED_KHOP it is the edge predicate to expand.
string predicate = 7;
// Seed subject term (TRIPLE_TO_VECTOR / VECTOR_GUIDED_KHOP).
string seed = 8;
// Hops to expand for VECTOR_GUIDED_KHOP (>=1; 0 ⇒ 1).
uint32 hops = 9;
// Cap on returned linked entities (0 ⇒ a server default).
uint32 limit = 10;
// Optional point-in-time filter for the triple-side scans (see as_of above).
uint64 as_of = 11;
}
// One linked entity: an id in the shared term/node id space plus its resolved
// string and the vector distance that ranked it.
message TripleLinkResult {
uint64 id = 1; // term-id == node-id (shared id space)
string value = 2; // resolved string (via ID2T)
float distance = 3; // vector distance (smaller ⇒ closer)
// True if this id was confirmed to have the requested triple structure
// (VECTOR_TO_TRIPLE) / was reached by k-hop expansion (VECTOR_GUIDED_KHOP).
bool connected = 4;
}
message TripleLinkResponse {
repeated TripleLinkResult results = 1;
uint32 cf_id = 2; // committed cf_id of `t:{graph}` (0 if the graph has none)
}