{"_id":"@cortadel/vercel-ai-provider","name":"@cortadel/vercel-ai-provider","dist-tags":{"latest":"0.1.0"},"versions":{"0.1.0":{"name":"@cortadel/vercel-ai-provider","version":"0.1.0","description":"Cortadel long-term memory for the Vercel AI SDK — memory tools plus a language-model middleware that recalls and persists automatically.","license":"Apache-2.0","type":"module","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"types":"./dist/index.d.ts","engines":{"node":">=22"},"homepage":"https://cortadel.ai","bugs":{"url":"https://github.com/cortadel/cortadel/issues"},"repository":{"type":"git","url":"git+https://github.com/cortadel/cortadel.git","directory":"integrations/vercel-ai-sdk"},"keywords":["ai","ai-sdk","vercel","memory","agents","middleware","tools","llm","rag"],"publishConfig":{"access":"public","provenance":true},"dependencies":{"@cortadel/sdk":"^1.0.0"},"peerDependencies":{"ai":"^7.0.0","zod":"^3.25.76 || ^4.1.8"},"devDependencies":{"@types/json-schema":"^7.0.15","@types/node":"^22.20.1","ai":"^7.0.64","typescript":"^7.0.2","vitest":"^4.1.10","zod":"^4.4.3"},"scripts":{"build":"tsc","typecheck":"tsc --noEmit && tsc -p tsconfig.test.json","test":"vitest run"},"_id":"@cortadel/vercel-ai-provider@0.1.0","_integrity":"sha512-JDqvaOS+ocuB7mz/wVmX/JMT836QgjktC4sAWemzKoWyqiK+WyTbC7buzWAQWBl1YSPhUj1kTici4Tx1FHj7EQ==","_resolved":"/tmp/7293bbe893f91cb36735613a9a41f53b/cortadel-vercel-ai-provider-0.1.0.tgz","_from":"file:cortadel-vercel-ai-provider-0.1.0.tgz","_nodeVersion":"24.19.0","_npmVersion":"11.17.0","dist":{"integrity":"sha512-JDqvaOS+ocuB7mz/wVmX/JMT836QgjktC4sAWemzKoWyqiK+WyTbC7buzWAQWBl1YSPhUj1kTici4Tx1FHj7EQ==","shasum":"e3df5431a14a9c58c7298ceb76771b8209f6d21f","tarball":"https://registry.npmjs.org/@cortadel/vercel-ai-provider/-/vercel-ai-provider-0.1.0.tgz","fileCount":24,"unpackedSize":83166,"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@cortadel%2fvercel-ai-provider@0.1.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIB1w5vnu5uIMFIaofuv1YYnjnfZZTw7Vdxqp4Dn//mPdAiEA41CL3HC+HqaObZfVUO2OpYDviUIIZMQGrUuyxu5cp/s="}]},"_npmUser":{"name":"serhii-seletskyi","email":"admin@cortadel.com"},"directories":{},"maintainers":[{"name":"serhii-seletskyi","email":"admin@cortadel.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/vercel-ai-provider_0.1.0_1786714090274_0.4473410152287258"},"_hasShrinkwrap":false}},"time":{"created":"2026-08-14T13:28:10.156Z","0.1.0":"2026-08-14T13:28:10.415Z","modified":"2026-08-14T13:28:10.802Z"},"maintainers":[{"name":"serhii-seletskyi","email":"admin@cortadel.com"}],"description":"Cortadel long-term memory for the Vercel AI SDK — memory tools plus a language-model middleware that recalls and persists automatically.","homepage":"https://cortadel.ai","keywords":["ai","ai-sdk","vercel","memory","agents","middleware","tools","llm","rag"],"repository":{"type":"git","url":"git+https://github.com/cortadel/cortadel.git","directory":"integrations/vercel-ai-sdk"},"bugs":{"url":"https://github.com/cortadel/cortadel/issues"},"license":"Apache-2.0","readme":"# Cortadel × Vercel AI SDK\n\nLong-term memory for [AI SDK](https://ai-sdk.dev) agents, backed by\n[**Cortadel**](https://github.com/cortadel/cortadel) — self-hosted long-term temporal graph memory\nfor AI agents (a bi-temporal graph store with hybrid BM25 + vector search). Models are stateless:\nevery `generateText` call starts from nothing, so an agent forgets a user's preferences the moment\nthe conversation ends. This package closes that gap in the two places the AI SDK gives you for it —\na **language-model middleware** that recalls before each call and stores after each turn without\nyour code asking, and a pair of **tools** the agent can call when it wants to remember something\ndeliberately.\n\n## Install\n\n```bash\nnpm install @cortadel/vercel-ai-provider\n# or: pnpm add @cortadel/vercel-ai-provider / yarn add @cortadel/vercel-ai-provider\n```\n\nESM-only, Node ≥ 22. `ai` and `zod` are peer dependencies — you almost certainly have both already.\n\n## Quickstart\n\n```ts\nimport { openai } from \"@ai-sdk/openai\";\nimport { generateText, wrapLanguageModel } from \"ai\";\nimport { cortadelMemory } from \"@cortadel/vercel-ai-provider\";\n\nconst model = wrapLanguageModel({\n  model: openai(\"gpt-4.1-mini\"),\n  middleware: cortadelMemory({\n    baseUrl: \"http://localhost:3001\",\n    userId: \"e2e-alice\",\n    // apiKey: \"<token>\",   // omit when the server runs with auth disabled\n  }),\n});\n\n// Turn 1 — nothing here mentions memory.\nawait generateText({ model, prompt: \"I always deploy on Fridays and I prefer metric units.\" });\n\n// Turn 2 — a new conversation with no message history, and it still knows.\nconst { text } = await generateText({ model, prompt: \"When do I usually ship?\" });\nconsole.log(text);\n```\n\nAny AI SDK model works — the middleware wraps whatever you already use, and `wrapProvider` /\n`createProviderRegistry` accept it too if you would rather apply memory across a whole provider:\n\n```ts\nconst provider = wrapProvider({ provider: openai, languageModelMiddleware: memory });\n```\n\n## What you get\n\n### 1. `cortadelMemory(options)` — automatic memory\n\nA `LanguageModelMiddleware` for `wrapLanguageModel`. It hooks all three middleware seams:\n\n| Seam | What it does |\n|---|---|\n| `transformParams` | Searches Cortadel with the latest user message and injects the hits as a system message, placed after your own system prompt and before the conversation. |\n| `wrapGenerate` | Hands the finished turn to `addConversation`, which distills it into atomic facts. |\n| `wrapStream` | The same, accumulated from the token stream and written when it closes. |\n\nThree behaviours worth knowing, because they are the difference between this and a naive wrapper:\n\n- **One search per turn, not per step.** Every step of a tool-calling loop re-enters the\n  middleware with the same trailing user message. A short-lived cache keyed on\n  `(user, session, query)` collapses those into a single search.\n- **Unfinished turns are not stored.** A generation that ends in `tool-calls` is mid-loop, so the\n  write is deferred until the step that actually answers. Turns are also fingerprinted, so a retry\n  cannot write the same exchange twice.\n- **Memory never takes the agent down.** Both halves are wrapped. If Cortadel is unreachable,\n  recall returns the prompt untouched and persistence is dropped; you hear about it through\n  `onError` — or, if you set none, through `console.warn` — and the model call proceeds regardless.\n  Set `throwOnError: true` when you would rather the run fail than answer without memory.\n\n### 2. `cortadelTools(options)` — memory the agent drives\n\nA `ToolSet` with `search_memory` and `add_memories`, named to match Cortadel's MCP tools so an\nagent behaves the same whichever surface it reaches memory through.\n\n```ts\nimport { generateText, stepCountIs } from \"ai\";\nimport { cortadelTools } from \"@cortadel/vercel-ai-provider\";\n\nconst result = await generateText({\n  model: openai(\"gpt-4.1-mini\"),\n  tools: cortadelTools({ baseUrl: \"http://localhost:3001\", userId: \"e2e-alice\" }),\n  stopWhen: stepCountIs(5),\n  prompt: \"What do you remember about my deployment habits?\",\n});\n```\n\n| Tool | Input | Returns |\n|---|---|---|\n| `search_memory` | `{ query: string, topK?: number }` | `{ memories: [{ id, content, score?, createdAt? }], count, error? }` |\n| `add_memories` | `{ memories: string[] }` | `{ stored, duplicates, ids, error? }` |\n\nFailures come back as an `error` field rather than a thrown exception, so a memory outage degrades\ninto an agent that can still answer instead of a run that aborts. `throwOnError: true` flips that\nif you want the tool call to fail loudly instead.\n\nThe two compose: use the middleware for always-on context and the tools for deliberate recall, both\nagainst one shared `client`.\n\n## Scoping memory to a user\n\nA Cortadel client is bound to **one user id at construction** — no method takes a user id. So\nper-user scoping means one client per user, and this package handles that for you: give it\n`baseUrl` and it builds and caches a client per user id.\n\n```ts\n// Default user, overridden per request:\nawait generateText({\n  model,\n  prompt: \"What do you remember about me?\",\n  providerOptions: { cortadel: { userId: \"e2e-bob\", sessionId: \"thread-42\" } },\n});\n```\n\n`providerOptions.cortadel` accepts `userId`, `sessionId`, `recall` and `persist`. Other providers\nignore the namespace, so it is safe to leave in place. Note that passing a pre-built `client`\ninstead of `baseUrl` pins the middleware to that client's single user — a per-request `userId` is\nthen ignored, because a client's own user id is fixed and not readable back out.\n\n**Tools are always single-user**, deliberately: the user id is not part of any tool's input schema,\nso nothing the model reads can talk it into fetching another user's memories. For a multi-tenant\nserver, build a tool set per request.\n\n## Configuration\n\n### Connection (both `cortadelMemory` and `cortadelTools`)\n\n| Option | Type | Default | Meaning |\n|---|---|---|---|\n| `baseUrl` | `string` | — | Cortadel server URL, e.g. `https://app.cortadel.ai` or `http://localhost:3001`. Required unless `client` is given. |\n| `userId` | `string` | — | User that owns the memories. Required with `baseUrl`. |\n| `apiKey` | `string` | — | Sent as `Authorization: Bearer <key>`. Omit when auth is disabled. |\n| `appName` | `string` | `\"cortadel-vercel-ai-provider\"` | App name recorded for access logging on searches. Defaults to this package's own npm name. |\n| `client` | `CortadelClient` | — | A client you built yourself. Mutually exclusive with `baseUrl`; pins to one user. |\n| `timeoutMs` | `number` | `100000` | Per-request timeout. `0` disables it. |\n| `fetch` | `typeof fetch` | global | Custom fetch. Never mutated. |\n| `clientCacheSize` | `number` | `64` | Max per-user clients kept alive. |\n| `onError` | `(error, { phase, userId }) => void` | — | Observes any Cortadel failure. `phase` is `\"recall\"` or `\"persist\"`. With no callback, a swallowed failure is logged through `console.warn`. |\n| `throwOnError` | `boolean` | `false` | Propagate memory failures to the caller instead of degrading. See below. |\n\n### `cortadelMemory` — recall\n\n| Option | Type | Default | Meaning |\n|---|---|---|---|\n| `recall` | `boolean` | `true` | Search and inject before each model call. |\n| `topK` | `number` | `5` | Memories to inject (1–50). Tighter than the `search_memory` tool's `10`, because automatic injection pays for every hit in every prompt. |\n| `mode` | `\"hybrid\" \\| \"text\" \\| \"vector\"` | `\"hybrid\"` | Retrieval arm. Hybrid fuses BM25 + vector with RRF. |\n| `rerank` | `\"cross_encoder\"` | — | Rerank with the server's local cross-encoder. Costs a model pass. |\n| `memoryType` | `\"episodic\" \\| \"semantic\" \\| \"procedural\"` | — | Restrict recall to one cognitive type. |\n| `sessionId` | `string` | — | Group recalled and stored facts under a session. |\n| `minScore` | `number` | — | Drop hits below this `rrfScore`. Unscored hits are kept. |\n| `formatMemories` | `(hits, { query, userId }) => string` | built-in | Render the injected block. |\n| `recallCacheTtlMs` | `number` | `60000` | How long an identical `(user, session, query)` recall is reused. `0` disables. |\n| `recallCacheSize` | `number` | `32` | Max cached recalls. |\n\n### `cortadelMemory` — persistence\n\n| Option | Type | Default | Meaning |\n|---|---|---|---|\n| `persist` | `boolean` | `true` | Store each completed turn with `addConversation`. |\n| `awaitPersist` | `boolean` | `false` | Await the write before the call resolves. **Turn this on in serverless/edge handlers**, where a background promise is killed when the handler returns. |\n| `isAgentMemory` | `boolean` | `false` | Extract facts about the assistant rather than the user. |\n| `tags` | `string[]` | — | Tags applied to every stored fact. |\n| `project` | `string` | — | Project scope applied to every stored fact. |\n\n### `cortadelTools`\n\n| Option | Type | Default | Meaning |\n|---|---|---|---|\n| `search.topK` | `number` | `10` | Default result count when the model does not ask for one (the Cortadel SDK's own search default). The model can override it per call. |\n| `search.mode` | `\"hybrid\" \\| \"text\" \\| \"vector\"` | `\"hybrid\"` | Retrieval arm. |\n| `search.rerank` | `\"cross_encoder\"` | — | Cross-encoder rerank. |\n| `search.memoryType` | `\"episodic\" \\| \"semantic\" \\| \"procedural\"` | — | Restrict to one cognitive type. |\n| `search.sessionId` | `string` | — | Restrict to one session. |\n| `search.minScore` | `number` | — | Drop hits below this `rrfScore`. |\n| `add.app` | `string` | — | App name recorded as each memory's creator. |\n| `add.infer` | `boolean` | `true` | When `false`, store verbatim and skip background extraction. Dedup still applies. |\n| `add.memoryType` | `\"episodic\" \\| \"semantic\" \\| \"procedural\"` | — | Pin stored memories to a cognitive type. |\n| `add.metadata` | `Record<string, unknown>` | — | Metadata attached to every stored memory. |\n\n### Failure handling\n\nFail-open is the default across the whole package: a memory outage must never be the reason an\nagent stops working.\n\n| You set | Recall fails | Persistence fails |\n|---|---|---|\n| nothing | prompt goes through unmodified, `console.warn` | write dropped, `console.warn` |\n| `onError` | prompt goes through unmodified, callback fires | write dropped, callback fires |\n| `throwOnError: true` | the model call rejects | rejects **only with `awaitPersist: true`** |\n\n`onError` is an observer, not a switch — it fires either way, and it is `throwOnError` alone that\ndecides whether the error also reaches your `await`. A fire-and-forget write (`awaitPersist: false`,\nthe default) has already returned to the caller by the time it fails, so it can only ever be\nreported; it is never left as an unhandled rejection.\n\nThe same pair works on `cortadelTools`, where the default is a result carrying `error` and\n`throwOnError: true` makes the tool call reject instead.\n\n## Examples\n\n- [`examples/chat-with-memory.ts`](examples/chat-with-memory.ts) — the middleware: the same\n  question across two runs that share no message history, plus a per-request user override.\n- [`examples/memory-tools.ts`](examples/memory-tools.ts) — the tools: the agent decides when to\n  store and when to recall.\n\nBoth need a running server and a model provider:\n\n```bash\npnpm add @ai-sdk/openai\nexport OPENAI_API_KEY=...\npnpm exec tsx examples/chat-with-memory.ts\n```\n\n## Running the tests\n\n```bash\npnpm install\npnpm test           # vitest run\npnpm run typecheck  # tsc --noEmit over src/ and test/\npnpm run build\n```\n\nThe suite is fully offline — no server, no network, no API keys. It stubs the Cortadel client at\nthe `CortadelMemoryClient` interface and drives the real AI SDK end to end with\n`MockLanguageModelV4` from `ai/test`, so the assertions are made on what actually reached the model\nafter `wrapLanguageModel` ran.\n\n## Requirements\n\n- **Node ≥ 22** (the floor `ai@7` sets).\n- **`ai` ≥ 7.0.0** and **`zod`** `^3.25.76 || ^4.1.8` — peer dependencies, matching the range `ai`\n  itself declares.\n- **A running Cortadel server**: the hosted service at `https://app.cortadel.ai`, or self-host with\n  `docker compose up` → `http://localhost:3001`. See\n  [self-hosting](https://github.com/cortadel/cortadel/blob/main/docs/self-hosting.md).\n\nBuilt on [`@cortadel/sdk`](https://www.npmjs.com/package/@cortadel/sdk).\n\n## Links\n\n- [github.com/cortadel/cortadel](https://github.com/cortadel/cortadel)\n- [cortadel.ai](https://cortadel.ai)\n\nApache-2.0.\n","readmeFilename":"README.md","_rev":"1-83c568012c7915b125066511459000e0"}