From df7621116bab852da3fd401e3c3fdec79473d440 Mon Sep 17 00:00:00 2001 From: EmersonBraun Date: Tue, 4 Aug 2026 20:31:35 -0300 Subject: [PATCH 1/2] Add AgentsKit vector memory example --- .../.env.example | 2 + .../.gitignore | 3 + .../upstash-vector-agentskit-memory/README.md | 51 +++++++++++++++++ .../package.json | 23 ++++++++ .../src/index.ts | 17 ++++++ .../src/lifecycle.ts | 39 +++++++++++++ .../tests/lifecycle.test.ts | 57 +++++++++++++++++++ .../tsconfig.json | 12 ++++ 8 files changed, 204 insertions(+) create mode 100644 examples/upstash-vector-agentskit-memory/.env.example create mode 100644 examples/upstash-vector-agentskit-memory/.gitignore create mode 100644 examples/upstash-vector-agentskit-memory/README.md create mode 100644 examples/upstash-vector-agentskit-memory/package.json create mode 100644 examples/upstash-vector-agentskit-memory/src/index.ts create mode 100644 examples/upstash-vector-agentskit-memory/src/lifecycle.ts create mode 100644 examples/upstash-vector-agentskit-memory/tests/lifecycle.test.ts create mode 100644 examples/upstash-vector-agentskit-memory/tsconfig.json diff --git a/examples/upstash-vector-agentskit-memory/.env.example b/examples/upstash-vector-agentskit-memory/.env.example new file mode 100644 index 0000000..aacb601 --- /dev/null +++ b/examples/upstash-vector-agentskit-memory/.env.example @@ -0,0 +1,2 @@ +UPSTASH_VECTOR_REST_URL= +UPSTASH_VECTOR_REST_TOKEN= diff --git a/examples/upstash-vector-agentskit-memory/.gitignore b/examples/upstash-vector-agentskit-memory/.gitignore new file mode 100644 index 0000000..ddc3c2a --- /dev/null +++ b/examples/upstash-vector-agentskit-memory/.gitignore @@ -0,0 +1,3 @@ +.env +node_modules/ +dist/ diff --git a/examples/upstash-vector-agentskit-memory/README.md b/examples/upstash-vector-agentskit-memory/README.md new file mode 100644 index 0000000..74dc3a4 --- /dev/null +++ b/examples/upstash-vector-agentskit-memory/README.md @@ -0,0 +1,51 @@ +--- +title: "Agent memory with AgentsKit and Upstash Vector" +products: ["vector"] +stack: ["Node.js", "TypeScript", "AgentsKit"] +languages: ["ts"] +use_cases: ["Vector Search", "AI Agents"] +author: "EmersonBraun" +--- + +# Agent memory with AgentsKit and Upstash Vector + +This minimal TypeScript example uses Upstash Vector as a serverless memory +backend for an agent. It exercises the complete lifecycle: store three +documents, retrieve the closest context, print the result, and remove the demo +documents. + +The example uses small deterministic vectors so the memory boundary is easy to +inspect. Replace them with vectors from any embedding provider in an +application; the Upstash index and AgentsKit memory contract stay unchanged. + +## Prerequisites + +- Node.js 20 or newer +- An Upstash Vector index configured for three-dimensional vectors + +## Run the example + +```bash +cp .env.example .env +npm install +node --env-file=.env --import tsx src/index.ts +``` + +Set `UPSTASH_VECTOR_REST_URL` and `UPSTASH_VECTOR_REST_TOKEN` in `.env` before +running. The script deletes its sample records before and after the run, so +repeated executions do not leave demo data behind. + +## Validate without credentials + +```bash +npm run typecheck +npm test +``` + +The deterministic test exercises the Upstash REST boundary and verifies the +store, query, and cleanup sequence without contacting a live index. + +## Learn more + +- [Upstash Vector documentation](https://upstash.com/docs/vector) +- [AgentsKit memory documentation](https://www.agentskit.io/docs/memory) diff --git a/examples/upstash-vector-agentskit-memory/package.json b/examples/upstash-vector-agentskit-memory/package.json new file mode 100644 index 0000000..7403b07 --- /dev/null +++ b/examples/upstash-vector-agentskit-memory/package.json @@ -0,0 +1,23 @@ +{ + "name": "upstash-vector-agentskit-memory", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "start": "tsx src/index.ts", + "test": "vitest run", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@agentskit/memory": "^0.11.6" + }, + "devDependencies": { + "@types/node": "^26.1.2", + "tsx": "^4.23.5", + "typescript": "^7.0.2", + "vitest": "^4.1.10" + }, + "engines": { + "node": ">=20" + } +} diff --git a/examples/upstash-vector-agentskit-memory/src/index.ts b/examples/upstash-vector-agentskit-memory/src/index.ts new file mode 100644 index 0000000..ae4687f --- /dev/null +++ b/examples/upstash-vector-agentskit-memory/src/index.ts @@ -0,0 +1,17 @@ +import { upstashVector } from '@agentskit/memory' +import { runMemoryLifecycle } from './lifecycle.js' + +function required(name: string): string { + const value = process.env[name] + if (!value) throw new Error(`Missing ${name}`) + return value +} + +const memory = upstashVector({ + url: required('UPSTASH_VECTOR_REST_URL'), + token: required('UPSTASH_VECTOR_REST_TOKEN'), + topK: 2, +}) +const matches = await runMemoryLifecycle(memory) + +console.log(JSON.stringify(matches, null, 2)) diff --git a/examples/upstash-vector-agentskit-memory/src/lifecycle.ts b/examples/upstash-vector-agentskit-memory/src/lifecycle.ts new file mode 100644 index 0000000..c8053a1 --- /dev/null +++ b/examples/upstash-vector-agentskit-memory/src/lifecycle.ts @@ -0,0 +1,39 @@ +import type { upstashVector } from '@agentskit/memory' + +type UpstashMemory = ReturnType + +export const sampleDocuments = [ + { + id: 'portable-runtime', + content: 'Keep the agent runtime independent from the model provider.', + embedding: [1, 0, 0], + metadata: { topic: 'portability' }, + }, + { + id: 'serverless-memory', + content: 'Use an HTTP vector database for serverless agent memory.', + embedding: [0.9, 0.1, 0], + metadata: { topic: 'memory' }, + }, + { + id: 'evaluation-loop', + content: 'Measure agent behavior before replacing a component.', + embedding: [0, 0, 1], + metadata: { topic: 'evaluation' }, + }, +] + +export async function runMemoryLifecycle(memory: UpstashMemory) { + const ids = sampleDocuments.map((document) => document.id) + const remove = memory.delete + if (!remove) throw new Error('This example requires a memory backend with delete support') + + await remove(ids) + await memory.store(sampleDocuments) + + try { + return await memory.search([1, 0, 0], { topK: 2 }) + } finally { + await remove(ids) + } +} diff --git a/examples/upstash-vector-agentskit-memory/tests/lifecycle.test.ts b/examples/upstash-vector-agentskit-memory/tests/lifecycle.test.ts new file mode 100644 index 0000000..9340cf3 --- /dev/null +++ b/examples/upstash-vector-agentskit-memory/tests/lifecycle.test.ts @@ -0,0 +1,57 @@ +import { describe, expect, it, vi } from 'vitest' +import { upstashVector } from '@agentskit/memory' +import { runMemoryLifecycle, sampleDocuments } from '../src/lifecycle.js' + +describe('Upstash Vector memory lifecycle', () => { + it('stores, retrieves, and removes the example documents', async () => { + const fetchMock = vi.fn(async (input: string | URL | Request) => { + const path = new URL(String(input)).pathname + const body = path.endsWith('/query') + ? { + result: [ + { + id: 'portable-runtime', + score: 0.99, + metadata: { + content: sampleDocuments[0].content, + topic: 'portability', + }, + }, + ], + } + : { result: 'Success' } + + return new Response(JSON.stringify(body), { + status: 200, + headers: { 'content-type': 'application/json' }, + }) + }) + const memory = upstashVector({ + url: 'https://example-vector.upstash.io', + token: 'test-token', + topK: 2, + fetch: fetchMock, + }) + + const matches = await runMemoryLifecycle(memory) + + expect(fetchMock).toHaveBeenCalledTimes(4) + expect(fetchMock.mock.calls.map(([input]) => new URL(String(input)).pathname)).toEqual([ + '/delete', + '/upsert', + '/query', + '/delete', + ]) + expect(matches).toEqual([ + { + id: 'portable-runtime', + content: sampleDocuments[0].content, + metadata: { + content: sampleDocuments[0].content, + topic: 'portability', + }, + score: 0.99, + }, + ]) + }) +}) diff --git a/examples/upstash-vector-agentskit-memory/tsconfig.json b/examples/upstash-vector-agentskit-memory/tsconfig.json new file mode 100644 index 0000000..1e62a2f --- /dev/null +++ b/examples/upstash-vector-agentskit-memory/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "noEmit": true, + "skipLibCheck": true, + "types": ["node"] + }, + "include": ["src", "tests"] +} From 2d2662a456aa7095a0902b37d55dfb582887238f Mon Sep 17 00:00:00 2001 From: EmersonBraun Date: Tue, 4 Aug 2026 21:22:01 -0300 Subject: [PATCH 2/2] fix: retry vector search while indexing --- .../upstash-vector-agentskit-memory/README.md | 8 +- .../src/lifecycle.ts | 20 ++++- .../tests/lifecycle.test.ts | 74 +++++++++++++++---- 3 files changed, 85 insertions(+), 17 deletions(-) diff --git a/examples/upstash-vector-agentskit-memory/README.md b/examples/upstash-vector-agentskit-memory/README.md index 74dc3a4..1b2b072 100644 --- a/examples/upstash-vector-agentskit-memory/README.md +++ b/examples/upstash-vector-agentskit-memory/README.md @@ -32,8 +32,12 @@ node --env-file=.env --import tsx src/index.ts ``` Set `UPSTASH_VECTOR_REST_URL` and `UPSTASH_VECTOR_REST_TOKEN` in `.env` before -running. The script deletes its sample records before and after the run, so -repeated executions do not leave demo data behind. +running. Upstash Vector indexes new records asynchronously, so the example +retries the search at one-second intervals for up to five attempts while the +stored records become +queryable. It deletes its sample records before and after the run, including +when indexing does not complete within that retry window, so repeated +executions do not leave demo data behind. ## Validate without credentials diff --git a/examples/upstash-vector-agentskit-memory/src/lifecycle.ts b/examples/upstash-vector-agentskit-memory/src/lifecycle.ts index c8053a1..f0f25db 100644 --- a/examples/upstash-vector-agentskit-memory/src/lifecycle.ts +++ b/examples/upstash-vector-agentskit-memory/src/lifecycle.ts @@ -2,6 +2,12 @@ import type { upstashVector } from '@agentskit/memory' type UpstashMemory = ReturnType +type LifecycleOptions = { + maxSearchAttempts?: number + retryDelayMs?: number + sleep?: (delayMs: number) => Promise +} + export const sampleDocuments = [ { id: 'portable-runtime', @@ -23,16 +29,26 @@ export const sampleDocuments = [ }, ] -export async function runMemoryLifecycle(memory: UpstashMemory) { +export async function runMemoryLifecycle(memory: UpstashMemory, options: LifecycleOptions = {}) { const ids = sampleDocuments.map((document) => document.id) const remove = memory.delete if (!remove) throw new Error('This example requires a memory backend with delete support') + const maxSearchAttempts = options.maxSearchAttempts ?? 5 + const retryDelayMs = options.retryDelayMs ?? 1_000 + const sleep = options.sleep ?? ((delayMs: number) => new Promise((resolve) => setTimeout(resolve, delayMs))) + await remove(ids) await memory.store(sampleDocuments) try { - return await memory.search([1, 0, 0], { topK: 2 }) + for (let attempt = 1; attempt <= maxSearchAttempts; attempt += 1) { + const matches = await memory.search([1, 0, 0], { topK: 2 }) + if (matches.some((match) => ids.includes(match.id))) return matches + if (attempt < maxSearchAttempts) await sleep(retryDelayMs) + } + + throw new Error(`Stored documents were not visible after ${maxSearchAttempts} search attempts`) } finally { await remove(ids) } diff --git a/examples/upstash-vector-agentskit-memory/tests/lifecycle.test.ts b/examples/upstash-vector-agentskit-memory/tests/lifecycle.test.ts index 9340cf3..86130fe 100644 --- a/examples/upstash-vector-agentskit-memory/tests/lifecycle.test.ts +++ b/examples/upstash-vector-agentskit-memory/tests/lifecycle.test.ts @@ -3,22 +3,12 @@ import { upstashVector } from '@agentskit/memory' import { runMemoryLifecycle, sampleDocuments } from '../src/lifecycle.js' describe('Upstash Vector memory lifecycle', () => { - it('stores, retrieves, and removes the example documents', async () => { + function createMemory(queryResults: Array>>) { + let queryCall = 0 const fetchMock = vi.fn(async (input: string | URL | Request) => { const path = new URL(String(input)).pathname const body = path.endsWith('/query') - ? { - result: [ - { - id: 'portable-runtime', - score: 0.99, - metadata: { - content: sampleDocuments[0].content, - topic: 'portability', - }, - }, - ], - } + ? { result: queryResults[queryCall++] ?? [] } : { result: 'Success' } return new Response(JSON.stringify(body), { @@ -33,6 +23,21 @@ describe('Upstash Vector memory lifecycle', () => { fetch: fetchMock, }) + return { fetchMock, memory } + } + + const match = { + id: 'portable-runtime', + score: 0.99, + metadata: { + content: sampleDocuments[0].content, + topic: 'portability', + }, + } + + it('stores, retrieves, and removes immediately visible documents', async () => { + const { fetchMock, memory } = createMemory([[match]]) + const matches = await runMemoryLifecycle(memory) expect(fetchMock).toHaveBeenCalledTimes(4) @@ -54,4 +59,47 @@ describe('Upstash Vector memory lifecycle', () => { }, ]) }) + + it('retries while stored documents are still being indexed', async () => { + const { fetchMock, memory } = createMemory([[], [match]]) + const sleep = vi.fn(async () => {}) + + const matches = await runMemoryLifecycle(memory, { + maxSearchAttempts: 3, + retryDelayMs: 25, + sleep, + }) + + expect(fetchMock.mock.calls.map(([input]) => new URL(String(input)).pathname)).toEqual([ + '/delete', + '/upsert', + '/query', + '/query', + '/delete', + ]) + expect(sleep).toHaveBeenCalledOnce() + expect(sleep).toHaveBeenCalledWith(25) + expect(matches[0]?.id).toBe('portable-runtime') + }) + + it('cleans up and fails after the search retry budget is exhausted', async () => { + const { fetchMock, memory } = createMemory([[], [], []]) + const sleep = vi.fn(async () => {}) + + await expect(runMemoryLifecycle(memory, { + maxSearchAttempts: 3, + retryDelayMs: 25, + sleep, + })).rejects.toThrow('Stored documents were not visible after 3 search attempts') + + expect(fetchMock.mock.calls.map(([input]) => new URL(String(input)).pathname)).toEqual([ + '/delete', + '/upsert', + '/query', + '/query', + '/query', + '/delete', + ]) + expect(sleep).toHaveBeenCalledTimes(2) + }) })