import { NestedIndexSchema, FlatIndexSchema, SearchIndex, Redis, InferFilterFromSchema } from '@upstash/redis'; import { Ratelimit } from '@upstash/ratelimit'; export { Duration, Ratelimit } from '@upstash/ratelimit'; import { z } from 'zod'; /** Deterministic, order-insensitive hash of an arbitrary JSON value. */ declare function stableHash(value: unknown): string; /** JSON.stringify with object keys sorted recursively, so equal values hash equally. */ declare function stableStringify(value: unknown): string; /** Build a colon-joined Redis key from parts, skipping empty segments. */ declare function key(...parts: (string | number | undefined | null)[]): string; /** Current epoch milliseconds. Wrapped so tests can stay deterministic if needed. */ declare function now(): number; /** The telemetry tag of this (core) package. Adapters pass their own package tag instead. */ declare const SDK_TELEMETRY = "@upstash/agentkit-sdk@0.9.0"; /** * Reports the sdk name and version to Upstash through the redis client's telemetry headers. The * redis client already reports the platform and the runtime, so we only append our own sdk tag, * resulting in a header like * `@upstash/redis@1.38.0,@upstash/agentkit-sdk@0.2.0,@upstash/agentkit-ai-sdk@0.2.0`. * * Opt out with `enableTelemetry: false` on any AgentKit config, with the same option on the redis * client itself, or with the `UPSTASH_DISABLE_TELEMETRY` env var. */ declare const addTelemetry: (redis: unknown, options?: { /** The sdk tag to report. Defaults to {@link SDK_TELEMETRY} (this package). */ sdk?: string; /** Set `false` to skip reporting. Defaults to `true`. */ enabled?: boolean; }) => void; declare const VERSION = "0.9.0"; /** The schema type accepted by `redis.search.index` / `createIndex` (an `s.object({...})`). */ type AnySearchSchema = NestedIndexSchema | FlatIndexSchema; interface ReactiveSearchIndexConfig { /** The Upstash Redis client. */ redis: Redis; /** The Redis Search index name. */ indexName: string; /** The JSON-document key prefix the index covers (e.g. `"agentkit:memory:"`). */ prefix: string; /** The Upstash Redis Search schema (an `s.object({...})`). */ schema: TSchema; /** * Report the sdk name + version to Upstash as a header on the requests made by your redis client. * Can also be disabled with the `UPSTASH_DISABLE_TELEMETRY` env var. Defaults to `true`. */ enableTelemetry?: boolean; } /** * A {@link SearchIndex} wrapper that **provisions the index reactively on the read path**. Writes go * straight to `redis.json.set` and never need the index to exist — so features don't call any * `ensure()` when saving. The first `query`/`aggregate`/`count` that hits a missing index creates it * (+ `waitIndexing`) and retries the op once; subsequent reads skip straight through. * * It mirrors the `SearchIndex` read surface (`query`/`aggregate`/`count`) plus `waitIndexing`/`drop`/ * `describe`, and exposes the raw handle as `.index` for anything else. */ declare class ReactiveSearchIndex { /** The underlying Upstash `SearchIndex` (for advanced, non-reactive use). */ readonly index: SearchIndex; private redis; private indexName; private prefix; private schema; private created?; constructor(config: ReactiveSearchIndexConfig); /** Create the index (idempotent via `existsOk`). */ private createIndex; private ensure; /** Create the index and wait until it's queryable — the recovery path on a missing index. */ private provision; /** Run a read op; on a missing index (sentinel return or thrown error) provision and retry once. */ private reactive; /** Query the index, creating it first if it doesn't exist yet. */ query(...args: Parameters["query"]>): ReturnType["query"]>; /** Aggregate over the index, creating it first if it doesn't exist yet. */ aggregate(...args: Parameters["aggregate"]>): ReturnType["aggregate"]>; /** Count documents, creating the index first if it doesn't exist yet. */ count(...args: Parameters["count"]>): ReturnType["count"]>; /** Block until pending writes are indexed. */ waitIndexing(): ReturnType["waitIndexing"]>; /** Drop the index. */ drop(): ReturnType["drop"]>; /** Describe the index (or `null` if it doesn't exist). */ describe(): ReturnType["describe"]>; } /** A store that declares no `metadataSchema`: it carries no metadata and has nothing to filter on. */ type EmptySchema = Record; /** * The built field object a schema builder produces — `{type: "TEXT", …}` for `s.string()`, * `{type: "BOOL"}` for `s.boolean()`, and so on. * * `@upstash/redis` does not export the builder classes, so the built shape is recovered * structurally rather than by naming them: every builder carries exactly one zero-argument method * (keyed by an internal symbol) that returns the field object, and none of its other public methods * returns anything with a `type` property — `noTokenize()`/`fast()` return builders, and `from()` * takes an argument. Deriving it this way means the field-type mapping below is the only thing * restated from the library. */ type BuiltField = Extract { type: string; }> extends () => infer TField ? TField : never; /** The JS value an indexed field carries, mirroring Upstash's own field-type table. */ type FieldValue = TField extends { type: infer TType; } ? TType extends "TEXT" | "KEYWORD" | "DATE" | "FACET" ? string : TType extends "U64" | "I64" | "F64" ? number : TType extends "BOOL" ? boolean : never : never; /** * Constraint for `metadataSchema`: every value must be a builder `s` produces. Self-referential the * same way `s.object`'s own parameter is, so a bad entry is reported on the offending key instead of * collapsing the whole object. */ type MetadataSchemaShape = { [K in keyof TSchema]: [FieldValue>] extends [never] ? never : TSchema[K]; }; /** * The `metadata` object a store carries, derived from the fields its `metadataSchema` declares: * `s.string()` → `string`, `s.number()` → `number`, `s.boolean()` → `boolean`. */ type MetadataOf = { [K in keyof TSchema]: FieldValue>; }; /** The built schema, as Upstash's own filter/query types want to see it. */ type BuiltSchema = { [K in keyof TSchema]: BuiltField; }; /** * Filter clauses over the declared metadata fields — the field names come from `metadataSchema`, and * each operand is checked against that field's type (`{deleted: {$eq: false}}` is accepted, * `{deleted: {$eq: "false"}}` and `{notAField: …}` are not). */ type MetadataFilter = BuiltSchema extends infer TBuilt ? TBuilt extends FlatIndexSchema ? InferFilterFromSchema : never : never; interface MemoryRecord> { id: string; text: string; createdAt: number; /** * Extra fields this store was configured to carry, via * {@link AgentMemoryConfig.metadataSchema}. They are stored as **top-level, indexed** fields, so * unlike `createdAt` they can be filtered on — that is the whole point of declaring them. */ metadata?: TMetadata; } interface RecalledMemory> extends MemoryRecord { score: number; } interface AgentMemoryConfig { /** The Upstash Redis client. The search index is created and managed internally. */ redis: Redis; /** Base key prefix for stored memories. Defaults to `agentkit:memory`. */ prefix?: string; /** Redis Search index name. Defaults to the (identifier-safe) `prefix`. */ indexName?: string; /** * Extra indexed fields to carry on every record, as Upstash Search schema builders — e.g. * `{ source: s.string().noTokenize(), deleted: s.boolean() }`. The store's `metadata` type is * derived from what you declare here, so `add` and the `filter` on {@link AgentMemory.recall}, * {@link AgentMemory.list} and {@link AgentMemory.count} are all checked against these fields and * their types — there is no second type to keep in sync. * * **Give an extended store its own `prefix`.** The schema describes an index, and an index covers * a keyspace: pointing a stricter schema at a keyspace that already holds records written without * these fields makes those records permanently invisible, because Upstash Search does not match a * missing field against `{$eq: …}` and has no `$ne`. Its own prefix means its own keyspace and its * own index, and nothing written earlier is in scope. * * Omit it and this is exactly the store it always was — same two indexed fields, same index, no * re-index, existing records untouched. */ metadataSchema?: TSchema & MetadataSchemaShape; /** Default relevance floor for {@link AgentMemory.recall} (BM25 score). */ minScore?: number; /** * Report the sdk name + version to Upstash as a header on the requests made by your redis client. * Can also be disabled with the `UPSTASH_DISABLE_TELEMETRY` env var. Defaults to `true`. */ enableTelemetry?: boolean; } /** * Long-term agent memory with fuzzy recall, backed entirely by Upstash Redis Search. You pass only * the `redis` client; the memory creates and owns its search index internally (reactively, on the * first recall) and exposes the handle via {@link AgentMemory.searchIndex} for advanced use. * * Each memory is one JSON doc at `::`. Memories are scoped per user via the * exact-match `userId` filter, and recalled with the `$smart` operator (phrase/term/fuzzy/prefix). */ declare class AgentMemory = MetadataOf> { private redis; private keyPrefix; private index; private minScore; private metadataFields; constructor(config: AgentMemoryConfig); /** The underlying (reactive) Upstash Redis Search index handle. */ get searchIndex(): ReactiveSearchIndex<{ text: { type: "TEXT"; }; userId: { type: "TEXT"; noTokenize: true; }; }>; private keyFor; /** * Store a memory for `userId` (required, non-empty — unique per user). Returns the persisted record. * Key: `::`. Writes go straight to Redis; the index is created on first recall. */ add(params: { text: string; userId: string; id?: string; metadata?: TMetadata; }): Promise>; /** * Fuzzily recall the memories most relevant to `query` for `userId`. Omit `query` (or pass an empty * string) to return everything for the user, unfiltered by relevance. * * A `query` that matches nothing returns **nothing**. There is no fallback to "everything for the * user": a search that answers a miss with unrelated memories cannot be told apart from a hit, and * a model will report whatever came back as a result. Pass no query when you want the whole set. */ recall(params: { userId: string; query?: string; topK?: number; minScore?: number; /** Extra clauses over {@link AgentMemoryConfig.metadataSchema} fields, e.g. `{source: {$eq: "agent"}}`. */ filter?: MetadataFilter; }): Promise[]>; /** * Records matching a metadata filter, unranked — the filter-first read, where {@link * AgentMemory.recall} is the relevance-first one. Ordering is the caller's business: sort the * result by whatever fields they put in `metadata`. */ list(params: { userId: string; filter?: MetadataFilter; limit?: number; }): Promise[]>; /** How many records match, without fetching them. */ count(params: { userId: string; filter?: MetadataFilter; }): Promise; /** Run a `userId`-scoped query and normalize the rows back into records. */ private query; /** Rebuild a stored document into a record. Metadata is stored flat so it can be indexed. */ private toRecord; /** * One memory by id, or `null` if there is none. * * This reads the key directly rather than going through the index, which is the point: a search * returns a bounded, unordered page, so looking a known id up by listing and filtering can miss a * record that exists purely because it fell outside the page. It also sees records the index has * not caught up with yet, and records a `filter` would have excluded. */ get(params: { userId: string; id: string; }): Promise | null>; /** Delete a memory by id for `userId` (required, non-empty). */ forget(id: string, opts: { userId: string; }): Promise; } interface ToolCacheConfig { redis: Redis; /** Base key prefix; defaults to `agentkit:toolCache`. */ prefix?: string; /** Default TTL (seconds) for cached results. Omit for no expiry. */ ttlSeconds?: number; /** * Report the sdk name + version to Upstash as a header on the requests made by your redis client. * Can also be disabled with the `UPSTASH_DISABLE_TELEMETRY` env var. Defaults to `true`. */ enableTelemetry?: boolean; } /** A cached tool result. `null`/`undefined` results are cached too, hence the wrapper object. */ interface ToolCacheHit { value: T; } /** * Memoizes deterministic tool-call results in Redis, keyed by the tool name plus a stable hash of * its arguments. Saves repeated work (and cost) when an agent calls the same tool with the same * arguments — common with retries, parallel branches, and multi-step reasoning loops. */ declare class ToolCache { private redis; private prefix; private ttlSeconds?; constructor(config: ToolCacheConfig); /** Key shape: `:::` — scoped per user, then per tool. */ private entryKey; /** Fetch a cached result, or `null` if absent. The hit is wrapped so a cached `null` is distinct. */ get(userId: string, toolName: string, args: unknown): Promise | null>; /** Cache a tool result. */ set(userId: string, toolName: string, args: unknown, value: T, opts?: { ttlSeconds?: number; }): Promise; /** Invalidate a single cached result. */ invalidate(userId: string, toolName: string, args: unknown): Promise; /** * Wrap a tool's execute function so results are cached automatically. The returned function checks * the cache first, runs the original on a miss, and stores the result. The cache entry is scoped to * `userId` then `toolName`. */ wrap(userId: string, toolName: string, execute: (args: A) => Promise, opts?: { ttlSeconds?: number; }): (args: A) => Promise; } /** Limiter algorithm accepted by `@upstash/ratelimit` (the value `Ratelimit.slidingWindow(...)` etc. returns). */ type Limiter = ConstructorParameters[0]["limiter"]; /** Configuration for {@link createRateLimit}. */ interface RateLimitConfig { /** Upstash Redis client backing the limiter. Defaults to `Redis.fromEnv()`. */ redis?: Redis; /** The limiter algorithm, e.g. `Ratelimit.slidingWindow(10, "60 s")` or `Ratelimit.fixedWindow(...)`. */ limiter: Limiter; /** Key prefix for the limiter. Defaults to `agentkit:rateLimit`; keys are `:`. */ prefix?: string; /** * Report the sdk name + version to Upstash as a header on the requests made by your redis client. * Can also be disabled with the `UPSTASH_DISABLE_TELEMETRY` env var. Defaults to `true`. */ enableTelemetry?: boolean; } /** * Build a configured [Upstash Ratelimit](https://github.com/upstash/ratelimit-js) `Ratelimit` with * AgentKit defaults. The returned value is a plain `Ratelimit`, so call `.limit(identifier)` yourself * before doing work (e.g. before `generateText`). Keys are `agentkit:rateLimit:`. * * ```ts * const ratelimit = createRateLimit({ redis, limiter: Ratelimit.slidingWindow(30, "1 m") }); * const { success } = await ratelimit.limit("user-123"); * if (!success) throw new Error("rate limited"); * ``` */ declare function createRateLimit(config: RateLimitConfig): Ratelimit; /** Lightweight summary of a chat, returned by list/search (no raw messages). */ interface ChatSummary { /** The chat/session id (the AI SDK `useChat` id, or an auto-generated one). */ sessionId: string; /** The user/owner this chat belongs to. */ userId: string; /** Human-readable title (e.g. derived from the first user message). */ title?: string; createdAt: number; updatedAt: number; /** Number of stored messages. */ messageCount: number; } /** A full chat record: its summary plus the raw stored messages. */ interface ChatRecord extends ChatSummary { /** The raw transcript exactly as the framework needs it (e.g. AI SDK `UIMessage[]`). Not indexed. */ messages: TMessage[]; } /** A search hit: a chat summary plus its BM25 relevance score. */ interface ChatSearchHit extends ChatSummary { score: number; } /** Merged, searchable text pulled out of a message list. */ interface ExtractedText { userMessages: string; modelMessages: string; } interface ChatHistoryConfig { /** The Upstash Redis client. The search index is created and managed internally. */ redis: Redis; /** Base key prefix for stored chats; defaults to `agentkit:chat`. */ prefix?: string; /** Redis Search index name. Defaults to the (identifier-safe) `prefix`. */ indexName?: string; /** Optional TTL (seconds) per chat. The search index self-syncs when a chat key expires. Omit for no expiry. */ ttlSeconds?: number; /** * Merge messages into the two indexed text fields. The default handles the `UIMessage`/`EveMessage` * convention (`{ role, parts: [{ type: "text", text }] }`), so adapters rarely override it. */ extractText?: (messages: TMessage[]) => ExtractedText; /** * Report the sdk name + version to Upstash as a header on the requests made by your redis client. * Can also be disabled with the `UPSTASH_DISABLE_TELEMETRY` env var. Defaults to `true`. */ enableTelemetry?: boolean; } /** * Durable chat history backed by **Upstash Redis Search** — the source of truth for a conversation's * transcript, framework-agnostic and generic over the message type `TMessage` (the AI SDK adapter * specializes it to `UIMessage`, eve to `EveMessage`). * * Each chat is one JSON doc at `agentkit:chat::` — keyed per user, so two users * can never collide on a `sessionId`. The index covers four fields: `userId` + `sessionId` (exact-match * filters) and `userMessages` + `modelMessages` (fuzzy `$smart` text). The raw `messages` array rides * along in the same doc but is **not** indexed. So you can filter by `userId` to list a user's chats, * and `$smart`-search within what the user or the model said. */ declare class ChatHistory { private redis; private keyPrefix; private index; private ttlSeconds?; private extract; constructor(config: ChatHistoryConfig); /** The underlying (reactive) Upstash Redis Search index handle. */ get searchIndex(): ReactiveSearchIndex<{ userId: { type: "TEXT"; noTokenize: true; }; sessionId: { type: "TEXT"; noTokenize: true; }; userMessages: { type: "TEXT"; }; modelMessages: { type: "TEXT"; }; }>; /** * Per-user key: each user has its own keyspace, so two users can never collide on a `sessionId` and * one user's write can't reach another's chat. The `userId` is the tenant boundary, structurally. */ private keyFor; private toSummary; private toRecord; /** * Persist a chat by **replacing** its whole message array with the one you pass — `saveChat` is an * overwrite, not an append, so hand it the complete transcript (there's no delta to merge). Upserts * the chat if new, re-derives the searchable text, and bumps `updatedAt`. `sessionId` is the AI SDK * `useChat` id (or any stable id you choose). Typically you call this server-side once a turn * finishes (e.g. the AI SDK route's `onFinish`), with the full request + reply message list. * * The chat lives under a per-user key (`::`), so a write only ever * touches **this** user's chat — another user reusing the same `sessionId` has a separate chat. */ saveChat(params: { /** The chat owner. Must be unique per user (it's the tenant boundary). Required, non-empty. */ userId: string; /** The chat/session id (the AI SDK `useChat` id, or any stable id). Required, non-empty. */ sessionId: string; /** The full transcript (the whole conversation — there is no delta merge). */ messages: TMessage[]; title?: string; }): Promise>; private getDoc; /** Fetch a single chat (full transcript), or `null` if this user has no chat with that `sessionId`. */ getChat(params: { userId: string; sessionId: string; }): Promise | null>; /** * Run a filtered query (the index is provisioned reactively on first read) and return its rows. The * typed index narrows `data` to the indexed schema fields, but at runtime `query` returns the **full * stored JSON** as `data` (verified against live Redis). This one cast bridges that — everywhere * else `r.data` is a fully-typed {@link ChatDoc}. */ private queryChats; /** List a user's chats (summaries only), newest-updated first. Filters the index by `userId`. */ listChats(params: { userId: string; limit?: number; }): Promise; /** * Fuzzily search a user's chats by what was said. `target` picks which side to match: `"user"`, * `"model"`, or `"both"` (default). Returns summaries with BM25 scores, most relevant first. */ searchChats(params: { userId: string; query: string; limit?: number; minScore?: number; target?: "user" | "model" | "both"; }): Promise; /** Delete this user's chat (also removes it from the index). No-op if it doesn't exist. */ deleteChat(params: { userId: string; sessionId: string; }): Promise; } interface SearchToolDefsConfig { /** The Upstash Redis Search schema (built with `s` from `@upstash/redis`). */ schema: TSchema; /** Upstash Redis client. */ redis: Redis; /** Index name. Defaults to `"agentkit:search"`. */ indexName?: string; /** Key prefix for indexed JSON documents. Defaults to `":"`. */ prefix?: string; /** Default page size for the `search` tool. Defaults to 10. */ defaultLimit?: number; /** * Report the sdk name + version to Upstash as a header on the requests made by your redis client. * Can also be disabled with the `UPSTASH_DISABLE_TELEMETRY` env var. Defaults to `true`. */ enableTelemetry?: boolean; } /** One framework-agnostic search-tool definition (wrap with AI SDK `tool()` or eve `defineTool()`). */ interface SearchToolDef { description: string; inputSchema: z.ZodTypeAny; execute: (input: Record) => Promise; } /** The three search-tool definitions over one index: `search`, `aggregate`, `count`. */ interface SearchToolDefs { search: SearchToolDef; aggregate: SearchToolDef; count: SearchToolDef; } /** * Build the framework-agnostic `search` / `aggregate` / `count` tool definitions over an Upstash Redis * Search index. Each returned def is `{ description, inputSchema (zod), execute }`; the AI SDK adapter * wraps them with `tool()` and the eve adapter with `defineTool()`. Descriptions are generated from the * `schema` so the model learns the fields, their types, and which filter operators apply. * * The index is provisioned **reactively**: each op runs straight away, and only if the index doesn't * exist yet does it get created (+ `waitIndexing`) and the op retried — see {@link ReactiveSearchIndex}. */ declare function createSearchToolDefs(config: SearchToolDefsConfig): SearchToolDefs; export { AgentMemory, type AgentMemoryConfig, type AnySearchSchema, ChatHistory, type ChatHistoryConfig, type ChatRecord, type ChatSearchHit, type ChatSummary, type ExtractedText, type MemoryRecord, type MetadataFilter, type MetadataOf, type MetadataSchemaShape, type RateLimitConfig, ReactiveSearchIndex, type ReactiveSearchIndexConfig, type RecalledMemory, SDK_TELEMETRY, type SearchToolDef, type SearchToolDefs, type SearchToolDefsConfig, ToolCache, type ToolCacheConfig, type ToolCacheHit, VERSION, addTelemetry, createRateLimit, createSearchToolDefs, key, now, stableHash, stableStringify };