From 6e0b575fc3ef5e13bc0522cbc777ddf29e81b5b7 Mon Sep 17 00:00:00 2001 From: rrader2890 Date: Tue, 7 Jul 2026 22:20:17 -0400 Subject: [PATCH] feat(sdk): context.batchBuild() + context.queryGraph() - context.batchBuild({ subjects, ... }) -> ContextBundle[] for the BatchBuildContext RPC (bulk context in one call, <=500 subjects). - context.queryGraph({ subjectId?, predicate?, asOf?, limit? }) -> GraphEdge[] for the point-in-time QueryGraph RPC. check:proto green (47 RPCs, 42 covered). Routes /lattice/context/batch + /lattice/graph/query need TS-shell wiring (follow-up). Co-Authored-By: Claude Opus 4.8 (1M context) --- proto/coverage.json | 4 ++- proto/memory.proto | 57 ++++++++++++++++++++++++++++++++++++ src/index.ts | 3 ++ src/resources/context.ts | 63 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 126 insertions(+), 1 deletion(-) diff --git a/proto/coverage.json b/proto/coverage.json index 679082b..1bf3ab4 100644 --- a/proto/coverage.json +++ b/proto/coverage.json @@ -1,5 +1,5 @@ { - "_comment": "Tracks how each engine RPC (proto/memory.proto) is surfaced in the SDK. check-proto-drift.mjs fails CI if the proto gains an RPC that is in neither list, so the SDK can't silently fall behind the engine. Vendored from thinkfleet-memory-engine main @ bc5c9ec.", + "_comment": "Tracks how each engine RPC (proto/memory.proto) is surfaced in the SDK. check-proto-drift.mjs fails CI if the proto gains an RPC that is in neither list, so the SDK can't silently fall behind the engine. Vendored from thinkfleet-memory-engine main @ 7b8b80f.", "covered": { "Save": "memory.admin.create / memory.observe", "Observe": "memory.observe", @@ -15,6 +15,8 @@ "GetCalibration": "lattice.getCalibration", "GetProfile": "lattice.getProfile", "BuildContext": "context.build", + "BatchBuildContext": "context.batchBuild", + "QueryGraph": "context.queryGraph", "EmitEvent": "events.emit", "PollEvents": "events.poll", "CreateAlertRule": "alerts.create", diff --git a/proto/memory.proto b/proto/memory.proto index dc16b25..87f0e3c 100644 --- a/proto/memory.proto +++ b/proto/memory.proto @@ -114,6 +114,12 @@ service Memory { // most likely needed next before it's asked for. Deterministic; read-only. rpc PrefetchRelated(PrefetchRelatedRequest) returns (SearchResponse); + // Point-in-time knowledge-graph query. Returns the edges valid AT `as_of` + // (or, if omitted, the current ones) matching the subject/predicate filter + // — "what did we believe about X on date Y". Backed by the bi-temporal + // valid_from/valid_to on each edge. Read-only. + rpc QueryGraph(QueryGraphRequest) returns (QueryGraphResult); + // Generate embeddings for items that don't have one yet (the backfill // work-list). generate-on-save only covers new writes; this catches up the // existing corpus. No-op if the engine's embedding provider is disabled. @@ -156,6 +162,12 @@ service Memory { // carries source ids so any inference can be traced. rpc BuildContext(BuildContextRequest) returns (ContextBundle); + // Batch context assembly — build bundles for many subjects in ONE call. + // Replaces the N-round-trip / direct-DB bulk load (e.g. EngageIt loading + // memory for hundreds of customers at once, which is what killed the old + // path). Bundles are returned in request order; capped at 500 subjects. + rpc BatchBuildContext(BatchBuildContextRequest) returns (BatchContextBundle); + // ─── Events (the proactive event log) ─────────────────────────── // Append an event to the durable event log. Idempotent — @@ -842,6 +854,34 @@ message PrefetchRelatedRequest { optional uint32 limit = 2; } +message QueryGraphRequest { + optional string platform_id = 10; + optional string project_id = 11; + // Narrow to a subject entity and/or predicate (both optional). + optional string subject_id = 1; + optional string predicate = 2; + // RFC3339 instant for a point-in-time view. Omit for the current graph. + optional string as_of = 3; + // Max edges. Default 100, clamped [1, 1000]. + optional uint32 limit = 4; +} + +message GraphEdge { + string id = 1; + string subject_id = 2; + string predicate = 3; + // Exactly one of object_id (entity) or object_literal is set. + optional string object_id = 4; + optional string object_literal = 5; + double weight = 6; + string valid_from = 7; // RFC3339 + optional string valid_to = 8; // RFC3339; absent = still current +} + +message QueryGraphResult { + repeated GraphEdge edges = 1; +} + message BackfillRequest { // Max items to embed this call. Default 500, clamped [1, 10000]. Call // repeatedly until embedded == 0 to drain a large corpus. @@ -1032,6 +1072,23 @@ message BuildContextRequest { optional string project_id = 11; } +message BatchBuildContextRequest { + // Subjects to build context for (<= 500). The options below apply to all. + repeated Subject subjects = 1; + repeated string include = 2; + optional uint32 max_tokens = 3; + optional uint32 memory_limit = 4; + optional uint32 prediction_limit = 5; + repeated string exclude_categories = 6; + optional string platform_id = 10; + optional string project_id = 11; +} + +message BatchContextBundle { + // One bundle per requested subject, in request order. + repeated ContextBundle bundles = 1; +} + message ContextProfileSummary { optional string rfm_segment = 1; optional uint32 recency_score = 2; diff --git a/src/index.ts b/src/index.ts index 6655bca..9b53130 100644 --- a/src/index.ts +++ b/src/index.ts @@ -33,6 +33,9 @@ export type { ContextPrediction, ContextMemory, ContextObservation, + BatchContextBuildRequest, + QueryGraphRequest, + GraphEdge, } from './resources/context.js' export { EventsResource } from './resources/events.js' diff --git a/src/resources/context.ts b/src/resources/context.ts index 2b1fa41..b0a0510 100644 --- a/src/resources/context.ts +++ b/src/resources/context.ts @@ -132,6 +132,40 @@ export interface ContextObservation { * const prompt = `Subject context:\n${JSON.stringify(ctx, null, 2)}\n\nUser question: ...` * ``` */ +/** One edge of the temporal knowledge graph. */ +export interface GraphEdge { + id: string + subjectId: string + predicate: string + /** Exactly one of objectId (entity) or objectLiteral is set. */ + objectId?: string + objectLiteral?: string + weight: number + validFrom: string + /** Absent = still current. */ + validTo?: string +} + +export interface QueryGraphRequest { + /** Narrow to a subject entity and/or predicate. */ + subjectId?: string + predicate?: string + /** RFC3339 instant for a point-in-time view. Omit for the current graph. */ + asOf?: string + /** Max edges. Default 100, clamped [1, 1000]. */ + limit?: number +} + +export interface BatchContextBuildRequest { + /** Subjects to build context for (<= 500). Options below apply to all. */ + subjects: Array<{ kind: string; externalId: string }> + include?: ContextSection[] + maxTokens?: number + memoryLimit?: number + predictionLimit?: number + excludeCategories?: string[] +} + export class ContextResource { constructor(private readonly http: HttpClient) {} @@ -141,4 +175,33 @@ export class ContextResource { ): Promise { return this.http.post('/lattice/context', body, options) } + + /** + * Build context bundles for many subjects in ONE call (up to 500). Replaces + * the N-round-trip bulk load — return order matches `subjects`. + */ + async batchBuild( + body: BatchContextBuildRequest, + options?: RequestOptions, + ): Promise { + const r = await this.http.post<{ bundles: ContextBundle[] }>( + '/lattice/context/batch', + body, + options, + ) + return r.bundles + } + + /** + * Point-in-time knowledge-graph query: edges valid AT `asOf` (or current if + * omitted), filtered by subject/predicate. "What did we believe about X on + * date Y." + */ + async queryGraph( + body: QueryGraphRequest = {}, + options?: RequestOptions, + ): Promise { + const r = await this.http.post<{ edges: GraphEdge[] }>('/lattice/graph/query', body, options) + return r.edges + } }