/** * Facts Store — persistent key/value memory for agents and sessions. * * Facts live in PostgreSQL and are designed for: * - session-scoped durable memory * - shared cross-agent knowledge * - session cleanup when a session is deleted */ export interface FactRecord { /** * Canonical scope key (`shared:` / `session::`). * Exposed so graph `evidence` arrays can reference a real fact and resolve * back via `readFacts({ scopeKeys })` (enhancedfactstore 02 §1c). */ scopeKey: string; key: string; value: unknown; agentId: string | null; sessionId: string | null; shared: boolean; tags: string[]; createdAt: Date; updatedAt: Date; deletedAt: Date | null; etag: number; } export interface StoreFactInput { key: string; value: unknown; tags?: string[]; shared?: boolean; agentId?: string | null; sessionId?: string | null; } export interface StoredFactResult { key: string; shared: boolean; stored: true; } export interface ReadFactsQuery { keyPattern?: string; /** * Bulk read of an explicit fact set by scope_key (enhancedfactstore 02 §1c). * ACL applies as for any read; inaccessible/unknown keys are silently * omitted. This is how graph `evidence` arrays resolve back into facts. */ scopeKeys?: string[]; tags?: string[]; sessionId?: string; agentId?: string; limit?: number; scope?: "accessible" | "shared" | "session" | "descendants"; } export interface DeleteFactInput { key: string; /** Required true to treat `key` as a pattern instead of an exact key. */ pattern?: boolean; /** Pattern-delete scope. Exact deletes infer scope from `shared`. */ scope?: "session" | "shared" | "all"; shared?: boolean; sessionId?: string | null; /** Required for scope="all"; used only by privileged callers. */ unrestricted?: boolean; } export interface DeletedFactResult { key: string; shared: boolean; deleted: boolean; } export interface DeletedFactsResult { keyPattern: string; scope: "session" | "shared" | "all"; deleted: number; } /** How visibility is resolved inside the read/search procs. */ export interface AccessContext { readerSessionId?: string | null; grantedSessionIds?: string[]; unrestricted?: boolean; } /** * One scope-key receipt for {@link FactStore.setFactsCrawled}. `etag` (optional) * makes the entry a conditional compare-and-set against `facts.etag` — the * receipt returned by `readUncrawledFacts`; omitting it stomps the crawl flag * regardless of source version. */ export interface SetFactsCrawledScopeKey { scopeKey: string; etag?: number; } /** * Input to {@link FactStore.setFactsCrawled}. Provide EXACTLY one selection of * `scopeKeys` (explicit batch) or `keyPrefix` (coarse prefix flush). */ export interface SetFactsCrawledInput { /** Explicit batch of 1..500 receipts. Each entry's etag, when present, is a conditional CAS. */ scopeKeys?: SetFactsCrawledScopeKey[]; /** Literal key prefix flipped in one shot (no per-row etag). Must be non-empty. */ keyPrefix?: string; /** Default true. false clears `last_crawled_at` to trigger a recrawl (includes tombstones). */ crawled?: boolean; } export interface FactsTombstoneStats { pendingTotal: number; unreconciled: number; ttlBlocked: number; oldestUnreconciledAgeSeconds: number | null; reconciledUnswept: number; } export interface ForcePurgeFactsInput { cutoff: Date; onlyUnreconciled?: boolean; keyPrefix?: string | null; limit?: number; } /** Knowledge-namespace bucket used by facts stats aggregations. */ export type FactsNamespace = "skills" | "asks" | "intake" | "config" | "(other)"; /** One row of facts-stats aggregation, returned by all three facts-stats procs. */ export interface FactsStatsRow { namespace: FactsNamespace; factCount: number; totalValueBytes: number; oldestCreatedAt: Date | null; newestUpdatedAt: Date | null; } export interface FactStore { initialize(): Promise; storeFact(input: StoreFactInput): Promise; storeFact(input: StoreFactInput[]): Promise<{ stored: number; facts: StoredFactResult[]; }>; readFacts(query: ReadFactsQuery, access?: AccessContext): Promise<{ count: number; facts: FactRecord[]; }>; deleteFact(input: DeleteFactInput & { pattern: true; }): Promise; deleteFact(input: DeleteFactInput & { pattern?: false | undefined; }): Promise; deleteFact(input: DeleteFactInput): Promise; deleteSessionFactsForSession(sessionId: string): Promise; /** Per-session non-shared facts, bucketed by namespace. */ getSessionFactsStats(sessionId: string): Promise; /** Same shape, aggregated across an array of session ids (used for spawn trees). */ getFactsStatsForSessions(sessionIds: string[]): Promise; /** Shared (cross-session) facts bucketed by namespace. */ getSharedFactsStats(): Promise; /** * PRIVILEGED crawl-queue read (base-store bookkeeping, enhancedfactstore 07 D3): * facts not yet incorporated into a graph (`last_crawled_at IS NULL`), across * ALL scopes. Each returned fact carries its `scopeKey` (and `etag`) — the * receipt for `setFactsCrawled`. Only useful when a graph harvester is running; inert otherwise. */ readUncrawledFacts(opts?: { keyPrefix?: string; namespace?: string; limit?: number; }): Promise<{ count: number; facts: FactRecord[]; }>; /** * PRIVILEGED crawl-queue write: set the crawled flag on a selection. Provide * EXACTLY one of: * - `scopeKeys`: up to 500 `{ scopeKey, etag? }` receipts. An entry with * `etag` is a conditional CAS (skipped on mismatch); without `etag` it * stomps. The `crawled: true` path is the per-fact receipt mark. * - `keyPrefix`: a non-empty literal key prefix flipped in one shot (coarse, * no per-row etag). * `crawled` defaults to true; `crawled: false` clears `last_crawled_at` to * requeue for recrawl (includes tombstones). Returns `affected` (rows that * changed state) and `skipped` (matched the selector but already in the * requested state, or etag mismatch). */ setFactsCrawled(input: SetFactsCrawledInput): Promise<{ affected: number; skipped: number; }>; purgeExpiredFacts(ttlSeconds: number, limit?: number): Promise; getFactsTombstoneStats(ttlSeconds?: number): Promise; forcePurgeFacts(input: ForcePurgeFactsInput): Promise; close(): Promise; } /** Facts-store-only retrieval modes. There is NO "graph" mode — graph * retrieval is the separate GraphStore (see graph-store.ts). */ export type SearchMode = "lexical" | "semantic" | "hybrid"; /** Relative weights for hybrid fusion. A missing signal contributes 0. */ export interface SearchWeights { lexical?: number; semantic?: number; } export interface SearchOpts { mode?: SearchMode; scope?: ReadFactsQuery["scope"]; namespace?: string; tags?: string[]; limit?: number; /** Candidate pool size per signal before fusion (default 50). ACL applies * INSIDE the proc, before this pool is cut. */ candidatePool?: number; weights?: SearchWeights; /** Minimum cosine similarity for semantic candidates (0..1). */ minSemanticScore?: number; } /** Options for similarFacts (semantic kNN of a known fact). */ export interface SimilarOpts { k?: number; minScore?: number; namespace?: string; } /** One fused, ACL-resolved hit. */ export interface ScoredFact extends FactRecord { /** Final fused score (higher = better). */ score: number; /** Per-signal contributions, for debugging/tuning fusion. */ signals: { lexical?: number; semantic?: number; }; } export interface SearchResult { count: number; mode: SearchMode; facts: ScoredFact[]; } export interface EmbedderStatus { running: boolean; instanceId?: string; status?: string; loops?: EmbedderLoopStatus[]; } export interface EmbedderLoopStatus { name: "batch" | "retry"; label: string; running: boolean; instanceId?: string; status?: string; } /** OpenAI/Azure-OpenAI-compatible embeddings endpoint (database-agnostic). */ export interface EmbeddingEndpointConfig { url: string; model: string; dim: number; apiKey?: string; apiKeyHeader?: string; bearer?: boolean; inputField?: string; headers?: Record; timeoutMs?: number; } /** Capability descriptor advertised by an EnhancedFactStore. Graph is NOT here * — it is the separate `graphStore` injection (enhancedfactstore 07 D2). */ export interface FactsCapabilities { search: boolean; embedder: boolean; } /** * Strict superset of `FactStore` adding multi-signal retrieval, semantic * similarity, and the durable embedder lifecycle. The crawl queue lives on the * base `FactStore` (07 D3), so it is inherited, not redeclared here. */ export interface EnhancedFactStore extends FactStore { /** Capability descriptor read by the runtime to gate enhanced tools. */ readonly capabilities: FactsCapabilities; /** Retrieval over the FACTS STORE ONLY: lexical (BM25) / semantic / hybrid. */ searchFacts(query: string, opts?: SearchOpts, access?: AccessContext): Promise; /** Semantic nearest-neighbours of a known fact (no re-embed). An * existing-but-inaccessible anchor returns empty (≡ unknown key). */ similarFacts(scopeKey: string, opts?: SimilarOpts, access?: AccessContext): Promise; /** Record/replace the embedding endpoint; restarts a running loop. */ configureEmbedder(endpoint: EmbeddingEndpointConfig, opts?: { restartIfRunning?: boolean; }): Promise; /** Start the durable batch and single-row retry loops. Idempotent. */ startEmbedder(opts?: { intervalSeconds?: number; batch?: number; }): Promise; /** Cancel the durable batch/retry loops. No-op if already stopped. */ stopEmbedder(reason?: string): Promise; /** Current lifecycle state. */ embedderStatus(): Promise; } /** * Thrown when an enhanced/graph method is called on a store that does not * support it. Providers don't grow throwing stubs (Liskov / ISP — 07 D1); this * is retained only for callers that bypass the `isEnhancedFactStore` guard and * hard-cast a base store. */ export declare class EnhancedFactsUnsupportedError extends Error { constructor(method: string); } /** * Structural type guard: is this store an `EnhancedFactStore` (search + embedder) * rather than a plain `FactStore`? The runtime asks this once at worker boot and * threads the answer through — it never sniffs per turn. Graph presence is a * SEPARATE question (`!!graphStore`), not derived from the fact store. */ export declare function isEnhancedFactStore(store: FactStore): store is EnhancedFactStore; export declare function computeScopeKey(key: string, shared: boolean, sessionId?: string | null): string; export declare function createFactStoreForUrl(storeUrl: string, schema?: string, opts?: { useManagedIdentity?: boolean; aadUser?: string; /** Provider selector. "pg" (default) = PgFactStore. "horizon" = * dynamically import pilotswarm-horizon-store's HorizonDBFactStore. */ provider?: "pg" | "horizon"; /** Embedding endpoint for the horizon provider's durable embedder. */ embedding?: EmbeddingEndpointConfig; }): Promise; /** * Construct an optional `GraphStore` provider (enhancedfactstore 07 D2/P3). * * SYMMETRIC peer of `createFactStoreForUrl` — its own URL, its own provider, no * `factStore` argument and no instance reuse. Returns `undefined` when no graph * URL is configured (⇒ no graph store, no graph tools). The only provider today * is `pilotswarm-horizon-store`'s AGE-backed `HorizonDBGraphStore`, loaded via a * guarded dynamic import so the SDK builds/runs without it. */ export declare function createGraphStoreForUrl(graphUrl: string | undefined, schema?: string, opts?: { useManagedIdentity?: boolean; aadUser?: string; registrySchema?: string; namespaceCacheTtlMs?: number; }): Promise; /** * Resolve where the facts store physically lives + which provider serves it, * shared by the worker, client, and management-client so they cannot drift * (enhancedfactstore 07 P3). Resolution is most-specific-first: * url = enhancedFactsDatabaseUrl ?? cmsFactsDatabaseUrl ?? store * provider = factsProvider ?? (enhancedFactsDatabaseUrl ? "horizon" : "pg") * schema = horizon ? (enhancedFactsSchema ?? factsSchema) : factsSchema */ export declare function resolveFactsTarget(cfg: { store: string; cmsFactsDatabaseUrl?: string; enhancedFactsDatabaseUrl?: string; factsProvider?: "pg" | "horizon"; factsSchema?: string; enhancedFactsSchema?: string; }): { url: string; provider: "pg" | "horizon"; schema?: string; }; export declare class PgFactStore implements FactStore { private pool; private initialized; private sql; private constructor(); static readonly DEFAULT_POOL_MAX = 3; static create(connectionString: string, schema?: string, opts?: { useManagedIdentity?: boolean; aadUser?: string; }): Promise; initialize(): Promise; storeFact(input: StoreFactInput): Promise; storeFact(input: StoreFactInput[]): Promise<{ stored: number; facts: StoredFactResult[]; }>; readFacts(query: ReadFactsQuery, access?: AccessContext): Promise<{ count: number; facts: FactRecord[]; }>; deleteFact(input: DeleteFactInput & { pattern: true; }): Promise; deleteFact(input: DeleteFactInput & { pattern?: false | undefined; }): Promise; deleteFact(input: DeleteFactInput): Promise; deleteSessionFactsForSession(sessionId: string): Promise; getSessionFactsStats(sessionId: string): Promise; getFactsStatsForSessions(sessionIds: string[]): Promise; getSharedFactsStats(): Promise; readUncrawledFacts(opts?: { keyPrefix?: string; namespace?: string; limit?: number; }): Promise<{ count: number; facts: FactRecord[]; }>; setFactsCrawled(input: SetFactsCrawledInput): Promise<{ affected: number; skipped: number; }>; purgeExpiredFacts(ttlSeconds: number, limit?: number): Promise; getFactsTombstoneStats(ttlSeconds?: number): Promise; forcePurgeFacts(input: ForcePurgeFactsInput): Promise; close(): Promise; } //# sourceMappingURL=facts-store.d.ts.map