diff --git a/llms-full.txt b/llms-full.txt index eac0f6f99..1e01b0383 100644 --- a/llms-full.txt +++ b/llms-full.txt @@ -96037,13 +96037,14 @@ Next.js app that runs the same tool on QStash and on Workflow, with a live log o Source: https://upstash.com/docs/redis/sdks/agentkit/tanstack-ai [TanStack AI](https://tanstack.com/ai) defines the contracts for an agent's production state — chat -persistence, resumable streaming, locks, memory — and ships in-memory implementations that only work +persistence, session history, stored credentials, resumable streaming, locks, memory — and ships in-memory implementations that only work inside one process. `@upstash/agentkit-tanstack-ai` implements them on Upstash Redis, so they hold across serverless instances, page reloads, and devices. | Import | Plugs into | Feature | | --- | --- | --- | -| `upstashPersistence` | `withPersistence()`, `withGenerationPersistence()` | Transcripts, runs, human-in-the-loop interrupts, metadata, generation jobs, and generated files. | +| `upstashPersistence` | `withPersistence()`, `withGenerationPersistence()` | Transcripts, runs, human-in-the-loop interrupts, metadata, generation jobs, generated files, the session index, and the session log. | +| `upstashCredentials` | `CredentialStore` (`@tanstack/ai/byok`) | Provider API keys and OAuth tokens that users or organizations saved, optionally encrypted at rest. | | `upstashStream` | `durability` on the response | Resume a stream after a reload, or open the same thread on another device. | | `upstashLocks` | `withLocks()` | Distributed locks for TanStack AI middleware, such as its sandbox setup. | | `upstashMemory` | `memoryMiddleware()` | Long-term memory ranked in [Redis Search](/docs/redis/search/introduction). | @@ -96054,6 +96055,11 @@ across serverless instances, page reloads, and devices. npm install @upstash/agentkit-tanstack-ai @tanstack/ai ``` + + Version 0.3.0 and later need **TanStack AI 1.0 or later**: `@tanstack/ai` and `@tanstack/ai-persistence` must be + `>=1.0.0`. If you are still on TanStack AI 0.x, stay on `@upstash/agentkit-tanstack-ai@0.2.0`. + + AgentKit reads `UPSTASH_REDIS_REST_URL` / `UPSTASH_REDIS_REST_TOKEN` from the environment by default. Pass a `redis` client to any helper to use a different one. @@ -96079,7 +96085,8 @@ chat({ adapter, messages, threadId, middleware: [withPersistence(persistence)] } ``` This covers every TanStack AI persistence store: `messages`, `runs`, `interrupts`, and `metadata` for -chats, plus `generationRuns` and `artifacts` for one-shot generation jobs such as images or speech. The +chats, `generationRuns` and `artifacts` for one-shot generation jobs such as images or speech, and the +`sessions` and `log` stores described [below](#how-to-keep-a-session-index-in-redis). The `blobs` store for generated bytes is added when you pass an Upstash Blob bucket: ```ts @@ -96102,6 +96109,7 @@ Each write is one command or one Lua script, so concurrent instances cannot inte prefix: "agentkit:tanstack", // optional: base key prefix messagesTtlSeconds: 60 * 60 * 24 * 30, // optional: expire idle transcripts (default: never) bucket: Bucket.fromEnv(), // optional: Upstash Blob bucket for generated files + logPollIntervalMs: 250, // optional: how often the session log checks for writes from other machines }); ``` @@ -96109,6 +96117,122 @@ Each write is one command or one Lua script, so concurrent instances cannot inte [Upstash Blob](/docs/blob/overall/quickstart). Without it, there is no `blobs` store. +## How to keep a session index in Redis + +`upstashPersistence()` also returns a `sessions` store, TanStack AI's `SessionIndexStore`. It holds one +entry per `(tenantId, threadId)` that a "your chats" list reads: members, title, harness, token usage, +pinned and archived flags, and your own `metadata`. It holds no messages, which stay in the other stores. + +```ts +const { sessions } = upstashPersistence().stores; + +await sessions.upsert({ + tenantId, + threadId, + members: [{ userId, role: "owner" }], + title: "Refund question", + createdAt: Date.now(), + updatedAt: Date.now(), +}); + +// Newest first, one page at a time. +const { entries, cursor, truncated } = await sessions.list({ tenantId, memberId: userId, limit: 20 }); +``` + +* `list` is newest-first per tenant, and filters by `memberId`, `search` (title, case-insensitive), `harness`, + `parentThreadId`, `metadata`, and `withArchived`. Pass the returned `cursor` back for the next page. +* `upsert` replaces the whole entry. To change one field safely from several machines, read the entry with + `getVersioned` and write the changed copy with `upsertIf`, which only writes at the revision you read. +* `delete` removes only the entry, not the thread's data. + +To use the index on its own, import `upstashSessionIndex(redis, prefix)` from +`@upstash/agentkit-tanstack-ai/persistence`. + +## How to keep the session log in Redis + +`upstashPersistence()` also returns a `log` store, TanStack AI's `LogStore`: the append-only log a host +keeps for a session tree (events, transcript, inputs, and tool steps), with what several machines need to +share it safely. + +```ts +const { log } = upstashPersistence().stores; + +// One owner at a time holds the log; every fenced write carries its epoch. +const lease = await log.acquireOwnership({ logId, harness: "support", ownerId: hostId, ttlMs: 30_000 }); +if (lease) { + await log.append({ logId, seq: 1, epoch: lease.epoch, records: [{ type: "message", text: "hi" }] }); + const entries = await log.read({ logId, after: 0 }); +} +``` + +* **Append:** `append` writes a batch at the next position, together with the pending inputs it consumes, in + one atomic step. A second writer at the same position gets a `LogConflictError` and nothing is written. + One call takes at most 10,000 records. +* **Ownership:** `acquireOwnership`, `renewOwnership`, and `releaseOwnership` give one host a lease with an + epoch. A host that lost the log gets an `OwnershipLostError` on its next fenced write. Lease expiry uses + the Redis server clock, so your machines need no clock sync. +* **Input lane:** `offerInput` lets any machine queue an input, and `listLogsWithoutOwner` finds logs whose + owner expired, that have pending inputs, or whose wake time is due. `markHostAlive` and `pinToWorkers` + keep a log on one group of workers. +* **Snapshots:** `putSnapshot` and `truncate` keep a log short. Reading before the truncation point throws + `LogTruncatedError`. +* **Subscriptions:** `subscribe` calls your listener right after writes through the same store, and polls + for writes from other machines every `logPollIntervalMs` (250 ms by default), because the REST API keeps + no blocking reads. Treat a call as "something changed" and `read` to catch up. + +The store passes all of TanStack AI's `log`, ownership, input-lane, snapshot, and cross-instance +conformance cases. `upstashLogStore(redis, prefix, { pollIntervalMs })` is also exported from +`@upstash/agentkit-tanstack-ai/persistence` for standalone use. + +## How to store provider API keys and OAuth tokens + +`upstashCredentials()` is a `CredentialStore` (from `@tanstack/ai/byok`) for API keys and OAuth tokens that +users or organizations saved. TanStack AI's `storedProviderKeys()` reads from it. + +```ts +import { storedProviderKeys } from "@tanstack/ai/byok/server"; +import { upstashCredentials } from "@upstash/agentkit-tanstack-ai/byok"; + +const credentials = upstashCredentials({ encryptionKey: process.env.CREDENTIALS_KEY }); + +await credentials.set({ + scope: { tenantId, userId }, + id: "openai", + credential: { type: "api_key", value: apiKey }, +}); + +// The user's own key, else the tenant's. +const keys = storedProviderKeys(credentials, { tenantId, userId }); +``` + +Each owner (`tenantId` plus `userId`) has its own hash, so `list` returns ids and types only and never a +secret. `getVersioned` and `setIf` are compare-and-set on a revision that never comes back after a +`delete`, so several instances refreshing one OAuth token cannot overwrite each other. + + + Set `encryptionKey` to encrypt secrets with AES-256-GCM. It is 32 bytes: a `Uint8Array`, or a base64, + base64url, or hex string (`openssl rand -base64 32`). Without it, secrets are stored as plain JSON. + + ```ts + upstashCredentials({ + redis, // optional: defaults to Redis.fromEnv() + prefix: "agentkit:tanstack", // optional: base key prefix + encryptionKey: process.env.CREDENTIALS_KEY, // optional but strongly recommended + requireEncryption: true, // optional: refuse plaintext rows once a key is set + }); + ``` + + * The ciphertext is bound to the key `prefix`, the owner, the credential id, and the type and expiry that + `list` shows. **Changing `prefix`, or copying rows to another prefix, makes every encrypted row + undecryptable.** + * Credentials saved before you added a key are still read as plaintext. Anyone who can *write* to your + Redis could swap an encrypted credential for a plaintext one. After you re-save your credentials with + the key, set `requireEncryption: true` to refuse plaintext rows. + * The revision is not part of the ciphertext, so a writer to your Redis could replay an older ciphertext of + the same credential. Encryption protects the data from being read, not from a writer. + * The store does not expire `secret_answer` credentials. Delete them after reading. + + ## How to resume a TanStack AI stream after a reload ```ts @@ -96275,13 +96399,14 @@ off, set `UPSTASH_DISABLE_TELEMETRY`, or pass `enableTelemetry: false` to a help Source: https://upstash.com/docs/redis/sdks/agentkit/tanstack-ai [TanStack AI](https://tanstack.com/ai) defines the contracts for an agent's production state — chat -persistence, resumable streaming, locks, memory — and ships in-memory implementations that only work +persistence, session history, stored credentials, resumable streaming, locks, memory — and ships in-memory implementations that only work inside one process. `@upstash/agentkit-tanstack-ai` implements them on Upstash Redis, so they hold across serverless instances, page reloads, and devices. | Import | Plugs into | Feature | | --- | --- | --- | -| `upstashPersistence` | `withPersistence()`, `withGenerationPersistence()` | Transcripts, runs, human-in-the-loop interrupts, metadata, generation jobs, and generated files. | +| `upstashPersistence` | `withPersistence()`, `withGenerationPersistence()` | Transcripts, runs, human-in-the-loop interrupts, metadata, generation jobs, generated files, the session index, and the session log. | +| `upstashCredentials` | `CredentialStore` (`@tanstack/ai/byok`) | Provider API keys and OAuth tokens that users or organizations saved, optionally encrypted at rest. | | `upstashStream` | `durability` on the response | Resume a stream after a reload, or open the same thread on another device. | | `upstashLocks` | `withLocks()` | Distributed locks for TanStack AI middleware, such as its sandbox setup. | | `upstashMemory` | `memoryMiddleware()` | Long-term memory ranked in [Redis Search](/docs/redis/search/introduction). | @@ -96292,6 +96417,11 @@ across serverless instances, page reloads, and devices. npm install @upstash/agentkit-tanstack-ai @tanstack/ai ``` + + Version 0.3.0 and later need **TanStack AI 1.0 or later**: `@tanstack/ai` and `@tanstack/ai-persistence` must be + `>=1.0.0`. If you are still on TanStack AI 0.x, stay on `@upstash/agentkit-tanstack-ai@0.2.0`. + + AgentKit reads `UPSTASH_REDIS_REST_URL` / `UPSTASH_REDIS_REST_TOKEN` from the environment by default. Pass a `redis` client to any helper to use a different one. @@ -96317,7 +96447,8 @@ chat({ adapter, messages, threadId, middleware: [withPersistence(persistence)] } ``` This covers every TanStack AI persistence store: `messages`, `runs`, `interrupts`, and `metadata` for -chats, plus `generationRuns` and `artifacts` for one-shot generation jobs such as images or speech. The +chats, `generationRuns` and `artifacts` for one-shot generation jobs such as images or speech, and the +`sessions` and `log` stores described [below](#how-to-keep-a-session-index-in-redis). The `blobs` store for generated bytes is added when you pass an Upstash Blob bucket: ```ts @@ -96340,6 +96471,7 @@ Each write is one command or one Lua script, so concurrent instances cannot inte prefix: "agentkit:tanstack", // optional: base key prefix messagesTtlSeconds: 60 * 60 * 24 * 30, // optional: expire idle transcripts (default: never) bucket: Bucket.fromEnv(), // optional: Upstash Blob bucket for generated files + logPollIntervalMs: 250, // optional: how often the session log checks for writes from other machines }); ``` @@ -96347,6 +96479,122 @@ Each write is one command or one Lua script, so concurrent instances cannot inte [Upstash Blob](/docs/blob/overall/quickstart). Without it, there is no `blobs` store. +## How to keep a session index in Redis + +`upstashPersistence()` also returns a `sessions` store, TanStack AI's `SessionIndexStore`. It holds one +entry per `(tenantId, threadId)` that a "your chats" list reads: members, title, harness, token usage, +pinned and archived flags, and your own `metadata`. It holds no messages, which stay in the other stores. + +```ts +const { sessions } = upstashPersistence().stores; + +await sessions.upsert({ + tenantId, + threadId, + members: [{ userId, role: "owner" }], + title: "Refund question", + createdAt: Date.now(), + updatedAt: Date.now(), +}); + +// Newest first, one page at a time. +const { entries, cursor, truncated } = await sessions.list({ tenantId, memberId: userId, limit: 20 }); +``` + +* `list` is newest-first per tenant, and filters by `memberId`, `search` (title, case-insensitive), `harness`, + `parentThreadId`, `metadata`, and `withArchived`. Pass the returned `cursor` back for the next page. +* `upsert` replaces the whole entry. To change one field safely from several machines, read the entry with + `getVersioned` and write the changed copy with `upsertIf`, which only writes at the revision you read. +* `delete` removes only the entry, not the thread's data. + +To use the index on its own, import `upstashSessionIndex(redis, prefix)` from +`@upstash/agentkit-tanstack-ai/persistence`. + +## How to keep the session log in Redis + +`upstashPersistence()` also returns a `log` store, TanStack AI's `LogStore`: the append-only log a host +keeps for a session tree (events, transcript, inputs, and tool steps), with what several machines need to +share it safely. + +```ts +const { log } = upstashPersistence().stores; + +// One owner at a time holds the log; every fenced write carries its epoch. +const lease = await log.acquireOwnership({ logId, harness: "support", ownerId: hostId, ttlMs: 30_000 }); +if (lease) { + await log.append({ logId, seq: 1, epoch: lease.epoch, records: [{ type: "message", text: "hi" }] }); + const entries = await log.read({ logId, after: 0 }); +} +``` + +* **Append:** `append` writes a batch at the next position, together with the pending inputs it consumes, in + one atomic step. A second writer at the same position gets a `LogConflictError` and nothing is written. + One call takes at most 10,000 records. +* **Ownership:** `acquireOwnership`, `renewOwnership`, and `releaseOwnership` give one host a lease with an + epoch. A host that lost the log gets an `OwnershipLostError` on its next fenced write. Lease expiry uses + the Redis server clock, so your machines need no clock sync. +* **Input lane:** `offerInput` lets any machine queue an input, and `listLogsWithoutOwner` finds logs whose + owner expired, that have pending inputs, or whose wake time is due. `markHostAlive` and `pinToWorkers` + keep a log on one group of workers. +* **Snapshots:** `putSnapshot` and `truncate` keep a log short. Reading before the truncation point throws + `LogTruncatedError`. +* **Subscriptions:** `subscribe` calls your listener right after writes through the same store, and polls + for writes from other machines every `logPollIntervalMs` (250 ms by default), because the REST API keeps + no blocking reads. Treat a call as "something changed" and `read` to catch up. + +The store passes all of TanStack AI's `log`, ownership, input-lane, snapshot, and cross-instance +conformance cases. `upstashLogStore(redis, prefix, { pollIntervalMs })` is also exported from +`@upstash/agentkit-tanstack-ai/persistence` for standalone use. + +## How to store provider API keys and OAuth tokens + +`upstashCredentials()` is a `CredentialStore` (from `@tanstack/ai/byok`) for API keys and OAuth tokens that +users or organizations saved. TanStack AI's `storedProviderKeys()` reads from it. + +```ts +import { storedProviderKeys } from "@tanstack/ai/byok/server"; +import { upstashCredentials } from "@upstash/agentkit-tanstack-ai/byok"; + +const credentials = upstashCredentials({ encryptionKey: process.env.CREDENTIALS_KEY }); + +await credentials.set({ + scope: { tenantId, userId }, + id: "openai", + credential: { type: "api_key", value: apiKey }, +}); + +// The user's own key, else the tenant's. +const keys = storedProviderKeys(credentials, { tenantId, userId }); +``` + +Each owner (`tenantId` plus `userId`) has its own hash, so `list` returns ids and types only and never a +secret. `getVersioned` and `setIf` are compare-and-set on a revision that never comes back after a +`delete`, so several instances refreshing one OAuth token cannot overwrite each other. + + + Set `encryptionKey` to encrypt secrets with AES-256-GCM. It is 32 bytes: a `Uint8Array`, or a base64, + base64url, or hex string (`openssl rand -base64 32`). Without it, secrets are stored as plain JSON. + + ```ts + upstashCredentials({ + redis, // optional: defaults to Redis.fromEnv() + prefix: "agentkit:tanstack", // optional: base key prefix + encryptionKey: process.env.CREDENTIALS_KEY, // optional but strongly recommended + requireEncryption: true, // optional: refuse plaintext rows once a key is set + }); + ``` + + * The ciphertext is bound to the key `prefix`, the owner, the credential id, and the type and expiry that + `list` shows. **Changing `prefix`, or copying rows to another prefix, makes every encrypted row + undecryptable.** + * Credentials saved before you added a key are still read as plaintext. Anyone who can *write* to your + Redis could swap an encrypted credential for a plaintext one. After you re-save your credentials with + the key, set `requireEncryption: true` to refuse plaintext rows. + * The revision is not part of the ciphertext, so a writer to your Redis could replay an older ciphertext of + the same credential. Encryption protects the data from being read, not from a writer. + * The store does not expire `secret_answer` credentials. Delete them after reading. + + ## How to resume a TanStack AI stream after a reload ```ts diff --git a/llms.txt b/llms.txt index 270ca1187..371e9535e 100644 --- a/llms.txt +++ b/llms.txt @@ -774,8 +774,8 @@ Search these docs: GET https://upstash.com/docs/search?q= returns JSON { - [MCP Events with Redis and QStash](https://upstash.com/docs/redis/sdks/agentkit/mcp-events.md): Let agents subscribe to events on your MCP server and get a signed webhook when one happens. Subscriptions live in Upstash Redis and deliveries are retried by QStash. - [Long-Running MCP Tools with Redis and QStash](https://upstash.com/docs/redis/sdks/agentkit/mcp-tasks.md): Long-running tools for MCP servers: start in memory with no setup, then make tasks durable with Upstash Redis and QStash or Workflow. Clients that support MCP Tasks poll natively, and every other client gets a task id the model can poll. - [Long-Running MCP Tools with Redis and QStash](https://upstash.com/docs/redis/sdks/agentkit/mcp-tasks.md): Long-running tools for MCP servers: start in memory with no setup, then make tasks durable with Upstash Redis and QStash or Workflow. Clients that support MCP Tasks poll natively, and every other client gets a task id the model can poll. -- [TanStack AI Persistence, Resumable Streams & Memory with Redis](https://upstash.com/docs/redis/sdks/agentkit/tanstack-ai.md): Production backends for TanStack AI on Upstash Redis — chat persistence, resumable streams, distributed locks, long-term memory, tool caching, rate limiting, and RAG tools. -- [TanStack AI Persistence, Resumable Streams & Memory with Redis](https://upstash.com/docs/redis/sdks/agentkit/tanstack-ai.md): Production backends for TanStack AI on Upstash Redis — chat persistence, resumable streams, distributed locks, long-term memory, tool caching, rate limiting, and RAG tools. +- [TanStack AI Persistence, Resumable Streams & Memory with Redis](https://upstash.com/docs/redis/sdks/agentkit/tanstack-ai.md): Production backends for TanStack AI on Upstash Redis — chat persistence, session index and session log, stored provider keys, resumable streams, distributed locks, long-term memory, tool caching, rate limiting, and RAG tools. +- [TanStack AI Persistence, Resumable Streams & Memory with Redis](https://upstash.com/docs/redis/sdks/agentkit/tanstack-ai.md): Production backends for TanStack AI on Upstash Redis — chat persistence, session index and session log, stored provider keys, resumable streams, distributed locks, long-term memory, tool caching, rate limiting, and RAG tools. - [Upstash Redis MCP](https://upstash.com/docs/redis/sdks/mcp.md) - [ARCOUNT](https://upstash.com/docs/redis/sdks/py/commands/array/arcount.md): Count the occupied slots in an array. - [ARDEL](https://upstash.com/docs/redis/sdks/py/commands/array/ardel.md): Delete array values without shifting later indexes. diff --git a/redis/sdks/agentkit/tanstack-ai.mdx b/redis/sdks/agentkit/tanstack-ai.mdx index c89473317..f2fc94e31 100644 --- a/redis/sdks/agentkit/tanstack-ai.mdx +++ b/redis/sdks/agentkit/tanstack-ai.mdx @@ -1,17 +1,18 @@ --- title: "TanStack AI Persistence, Resumable Streams & Memory with Redis" sidebarTitle: "TanStack AI" -description: "Production backends for TanStack AI on Upstash Redis — chat persistence, resumable streams, distributed locks, long-term memory, tool caching, rate limiting, and RAG tools." +description: "Production backends for TanStack AI on Upstash Redis — chat persistence, session index and session log, stored provider keys, resumable streams, distributed locks, long-term memory, tool caching, rate limiting, and RAG tools." --- [TanStack AI](https://tanstack.com/ai) defines the contracts for an agent's production state — chat -persistence, resumable streaming, locks, memory — and ships in-memory implementations that only work +persistence, session history, stored credentials, resumable streaming, locks, memory — and ships in-memory implementations that only work inside one process. `@upstash/agentkit-tanstack-ai` implements them on Upstash Redis, so they hold across serverless instances, page reloads, and devices. | Import | Plugs into | Feature | | --- | --- | --- | -| `upstashPersistence` | `withPersistence()`, `withGenerationPersistence()` | Transcripts, runs, human-in-the-loop interrupts, metadata, generation jobs, and generated files. | +| `upstashPersistence` | `withPersistence()`, `withGenerationPersistence()` | Transcripts, runs, human-in-the-loop interrupts, metadata, generation jobs, generated files, the session index, and the session log. | +| `upstashCredentials` | `CredentialStore` (`@tanstack/ai/byok`) | Provider API keys and OAuth tokens that users or organizations saved, optionally encrypted at rest. | | `upstashStream` | `durability` on the response | Resume a stream after a reload, or open the same thread on another device. | | `upstashLocks` | `withLocks()` | Distributed locks for TanStack AI middleware, such as its sandbox setup. | | `upstashMemory` | `memoryMiddleware()` | Long-term memory ranked in [Redis Search](/redis/search/introduction). | @@ -22,6 +23,11 @@ across serverless instances, page reloads, and devices. npm install @upstash/agentkit-tanstack-ai @tanstack/ai ``` + + Version 0.3.0 and later need **TanStack AI 1.0 or later**: `@tanstack/ai` and `@tanstack/ai-persistence` must be + `>=1.0.0`. If you are still on TanStack AI 0.x, stay on `@upstash/agentkit-tanstack-ai@0.2.0`. + + AgentKit reads `UPSTASH_REDIS_REST_URL` / `UPSTASH_REDIS_REST_TOKEN` from the environment by default. Pass a `redis` client to any helper to use a different one. @@ -47,7 +53,8 @@ chat({ adapter, messages, threadId, middleware: [withPersistence(persistence)] } ``` This covers every TanStack AI persistence store: `messages`, `runs`, `interrupts`, and `metadata` for -chats, plus `generationRuns` and `artifacts` for one-shot generation jobs such as images or speech. The +chats, `generationRuns` and `artifacts` for one-shot generation jobs such as images or speech, and the +`sessions` and `log` stores described [below](#how-to-keep-a-session-index-in-redis). The `blobs` store for generated bytes is added when you pass an Upstash Blob bucket: ```ts @@ -70,6 +77,7 @@ Each write is one command or one Lua script, so concurrent instances cannot inte prefix: "agentkit:tanstack", // optional: base key prefix messagesTtlSeconds: 60 * 60 * 24 * 30, // optional: expire idle transcripts (default: never) bucket: Bucket.fromEnv(), // optional: Upstash Blob bucket for generated files + logPollIntervalMs: 250, // optional: how often the session log checks for writes from other machines }); ``` @@ -77,6 +85,122 @@ Each write is one command or one Lua script, so concurrent instances cannot inte [Upstash Blob](/blob/overall/quickstart). Without it, there is no `blobs` store. +## How to keep a session index in Redis + +`upstashPersistence()` also returns a `sessions` store, TanStack AI's `SessionIndexStore`. It holds one +entry per `(tenantId, threadId)` that a "your chats" list reads: members, title, harness, token usage, +pinned and archived flags, and your own `metadata`. It holds no messages, which stay in the other stores. + +```ts +const { sessions } = upstashPersistence().stores; + +await sessions.upsert({ + tenantId, + threadId, + members: [{ userId, role: "owner" }], + title: "Refund question", + createdAt: Date.now(), + updatedAt: Date.now(), +}); + +// Newest first, one page at a time. +const { entries, cursor, truncated } = await sessions.list({ tenantId, memberId: userId, limit: 20 }); +``` + +- `list` is newest-first per tenant, and filters by `memberId`, `search` (title, case-insensitive), `harness`, + `parentThreadId`, `metadata`, and `withArchived`. Pass the returned `cursor` back for the next page. +- `upsert` replaces the whole entry. To change one field safely from several machines, read the entry with + `getVersioned` and write the changed copy with `upsertIf`, which only writes at the revision you read. +- `delete` removes only the entry, not the thread's data. + +To use the index on its own, import `upstashSessionIndex(redis, prefix)` from +`@upstash/agentkit-tanstack-ai/persistence`. + +## How to keep the session log in Redis + +`upstashPersistence()` also returns a `log` store, TanStack AI's `LogStore`: the append-only log a host +keeps for a session tree (events, transcript, inputs, and tool steps), with what several machines need to +share it safely. + +```ts +const { log } = upstashPersistence().stores; + +// One owner at a time holds the log; every fenced write carries its epoch. +const lease = await log.acquireOwnership({ logId, harness: "support", ownerId: hostId, ttlMs: 30_000 }); +if (lease) { + await log.append({ logId, seq: 1, epoch: lease.epoch, records: [{ type: "message", text: "hi" }] }); + const entries = await log.read({ logId, after: 0 }); +} +``` + +- **Append:** `append` writes a batch at the next position, together with the pending inputs it consumes, in + one atomic step. A second writer at the same position gets a `LogConflictError` and nothing is written. + One call takes at most 10,000 records. +- **Ownership:** `acquireOwnership`, `renewOwnership`, and `releaseOwnership` give one host a lease with an + epoch. A host that lost the log gets an `OwnershipLostError` on its next fenced write. Lease expiry uses + the Redis server clock, so your machines need no clock sync. +- **Input lane:** `offerInput` lets any machine queue an input, and `listLogsWithoutOwner` finds logs whose + owner expired, that have pending inputs, or whose wake time is due. `markHostAlive` and `pinToWorkers` + keep a log on one group of workers. +- **Snapshots:** `putSnapshot` and `truncate` keep a log short. Reading before the truncation point throws + `LogTruncatedError`. +- **Subscriptions:** `subscribe` calls your listener right after writes through the same store, and polls + for writes from other machines every `logPollIntervalMs` (250 ms by default), because the REST API keeps + no blocking reads. Treat a call as "something changed" and `read` to catch up. + +The store passes all of TanStack AI's `log`, ownership, input-lane, snapshot, and cross-instance +conformance cases. `upstashLogStore(redis, prefix, { pollIntervalMs })` is also exported from +`@upstash/agentkit-tanstack-ai/persistence` for standalone use. + +## How to store provider API keys and OAuth tokens + +`upstashCredentials()` is a `CredentialStore` (from `@tanstack/ai/byok`) for API keys and OAuth tokens that +users or organizations saved. TanStack AI's `storedProviderKeys()` reads from it. + +```ts +import { storedProviderKeys } from "@tanstack/ai/byok/server"; +import { upstashCredentials } from "@upstash/agentkit-tanstack-ai/byok"; + +const credentials = upstashCredentials({ encryptionKey: process.env.CREDENTIALS_KEY }); + +await credentials.set({ + scope: { tenantId, userId }, + id: "openai", + credential: { type: "api_key", value: apiKey }, +}); + +// The user's own key, else the tenant's. +const keys = storedProviderKeys(credentials, { tenantId, userId }); +``` + +Each owner (`tenantId` plus `userId`) has its own hash, so `list` returns ids and types only and never a +secret. `getVersioned` and `setIf` are compare-and-set on a revision that never comes back after a +`delete`, so several instances refreshing one OAuth token cannot overwrite each other. + + + Set `encryptionKey` to encrypt secrets with AES-256-GCM. It is 32 bytes: a `Uint8Array`, or a base64, + base64url, or hex string (`openssl rand -base64 32`). Without it, secrets are stored as plain JSON. + + ```ts + upstashCredentials({ + redis, // optional: defaults to Redis.fromEnv() + prefix: "agentkit:tanstack", // optional: base key prefix + encryptionKey: process.env.CREDENTIALS_KEY, // optional but strongly recommended + requireEncryption: true, // optional: refuse plaintext rows once a key is set + }); + ``` + + - The ciphertext is bound to the key `prefix`, the owner, the credential id, and the type and expiry that + `list` shows. **Changing `prefix`, or copying rows to another prefix, makes every encrypted row + undecryptable.** + - Credentials saved before you added a key are still read as plaintext. Anyone who can *write* to your + Redis could swap an encrypted credential for a plaintext one. After you re-save your credentials with + the key, set `requireEncryption: true` to refuse plaintext rows. + - The revision is not part of the ciphertext, so a writer to your Redis could replay an older ciphertext of + the same credential. Encryption protects the data from being read, not from a writer. + - The store does not expire `secret_answer` credentials. Delete them after reading. + + ## How to resume a TanStack AI stream after a reload ```ts