Skip to content
Open
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
2 changes: 2 additions & 0 deletions examples/upstash-vector-agentskit-memory/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
UPSTASH_VECTOR_REST_URL=
UPSTASH_VECTOR_REST_TOKEN=
3 changes: 3 additions & 0 deletions examples/upstash-vector-agentskit-memory/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.env
node_modules/
dist/
55 changes: 55 additions & 0 deletions examples/upstash-vector-agentskit-memory/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
---
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. 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

```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)
23 changes: 23 additions & 0 deletions examples/upstash-vector-agentskit-memory/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
17 changes: 17 additions & 0 deletions examples/upstash-vector-agentskit-memory/src/index.ts
Original file line number Diff line number Diff line change
@@ -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))
55 changes: 55 additions & 0 deletions examples/upstash-vector-agentskit-memory/src/lifecycle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import type { upstashVector } from '@agentskit/memory'

type UpstashMemory = ReturnType<typeof upstashVector>

type LifecycleOptions = {
maxSearchAttempts?: number
retryDelayMs?: number
sleep?: (delayMs: number) => Promise<void>
}

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, 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 {
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)
}
}
105 changes: 105 additions & 0 deletions examples/upstash-vector-agentskit-memory/tests/lifecycle.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { describe, expect, it, vi } from 'vitest'
import { upstashVector } from '@agentskit/memory'
import { runMemoryLifecycle, sampleDocuments } from '../src/lifecycle.js'

describe('Upstash Vector memory lifecycle', () => {
function createMemory(queryResults: Array<Array<Record<string, unknown>>>) {
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: queryResults[queryCall++] ?? [] }
: { 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,
})

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)
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,
},
])
})

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)
})
})
12 changes: 12 additions & 0 deletions examples/upstash-vector-agentskit-memory/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"noEmit": true,
"skipLibCheck": true,
"types": ["node"]
},
"include": ["src", "tests"]
}