Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
260 changes: 254 additions & 6 deletions llms-full.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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). |
Expand All @@ -96054,6 +96055,11 @@ across serverless instances, page reloads, and devices.
npm install @upstash/agentkit-tanstack-ai @tanstack/ai
```

<Warning>
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`.
</Warning>

<Note>
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.
Expand All @@ -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
Expand All @@ -96102,13 +96109,130 @@ 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
});
```

Pass `bucket` to also store the bytes of generated files (images, audio, video) in
[Upstash Blob](/docs/blob/overall/quickstart). Without it, there is no `blobs` store.
</Accordion>

## 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.

<Accordion title="Encryption at rest">
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.
</Accordion>

## How to resume a TanStack AI stream after a reload

```ts
Expand Down Expand Up @@ -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). |
Expand All @@ -96292,6 +96417,11 @@ across serverless instances, page reloads, and devices.
npm install @upstash/agentkit-tanstack-ai @tanstack/ai
```

<Warning>
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`.
</Warning>

<Note>
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.
Expand All @@ -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
Expand All @@ -96340,13 +96471,130 @@ 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
});
```

Pass `bucket` to also store the bytes of generated files (images, audio, video) in
[Upstash Blob](/docs/blob/overall/quickstart). Without it, there is no `blobs` store.
</Accordion>

## 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.

<Accordion title="Encryption at rest">
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.
</Accordion>

## How to resume a TanStack AI stream after a reload

```ts
Expand Down
Loading
Loading