feat: improve ai sdk tools (#394)

This commit is contained in:
Nicolò Boschi 2026-02-18 13:06:03 +01:00 committed by GitHub
parent be8728b313
commit d06a0259cc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 496 additions and 749 deletions

View file

@ -68,7 +68,7 @@ Hindsight supports three PostgreSQL vector extensions:
#### **pgvector** (HNSW - default)
- In-memory index using Hierarchical Navigable Small World algorithm
- Works well for most embeddings and dataset sizes
- Fast for small-medium datasets (<10M vectors)
- Fast for small-medium datasets (&lt;10M vectors)
- Higher memory usage for large datasets
- Most widely deployed and supported
@ -97,7 +97,7 @@ Hindsight supports three PostgreSQL vector extensions:
- When disk I/O is not a bottleneck
**When to use pgvector (HNSW):**
- Small-medium datasets (<10M vectors)
- Small-medium datasets (&lt;10M vectors)
- Maximum query speed when all data fits in memory
- Simple nearest-neighbor queries without filters
- Standard PostgreSQL deployment preference

View file

@ -1,366 +0,0 @@
---
sidebar_position: 4
---
# Vercel AI SDK
Official Hindsight integration for the [Vercel AI SDK](https://ai-sdk.dev).
## Features
- **7 Memory Tools**: Core memory operations (retain, recall, reflect), mental models (create, query), documents (get), and directives (create)
- **AI SDK 6 Native**: Works seamlessly with `generateText`, `streamText`, and `ToolLoopAgent`
- **Multi-User Support**: Dynamic bank IDs per tool call for multi-user/multi-tenant scenarios
- **Full Parameter Support**: Complete access to all Hindsight API parameters
- **Type-Safe**: Full TypeScript support with Zod schemas for validation
## Installation
```bash
npm install @vectorize-io/hindsight-ai-sdk @vectorize-io/hindsight-client ai zod
```
## Quick Start
### 1. Set up your Hindsight client
```typescript
import { HindsightClient } from '@vectorize-io/hindsight-client';
const hindsightClient = new HindsightClient({
apiUrl: process.env.HINDSIGHT_API_URL || 'http://localhost:8000',
});
```
### 2. Create Hindsight tools
```typescript
import { createHindsightTools } from '@vectorize-io/hindsight-ai-sdk';
const tools = createHindsightTools({
client: hindsightClient,
});
```
### 3. Use with AI SDK
```typescript
import { generateText } from 'ai';
import { anthropic } from '@ai-sdk/anthropic';
const result = await generateText({
model: anthropic('claude-sonnet-4-20250514'),
tools,
prompt: 'Remember that Alice loves hiking and prefers spicy food',
});
console.log(result.text);
```
## Memory Tools
The integration provides seven tools that the AI model can use to manage memory:
### `retain` - Store Information
The model calls this tool to store information for future recall.
**Parameters:**
- `bankId` (required): Memory bank ID (usually the user ID)
- `content` (required): Content to store
- `documentId` (optional): Document ID for grouping/upserting related memories
- `timestamp` (optional): ISO timestamp for when the memory occurred
- `context` (optional): Additional context about the memory
- `metadata` (optional): Key-value metadata for filtering
**Example tool call:**
```typescript
{
bankId: "user-123",
content: "Alice loves hiking and goes to Yosemite every summer",
context: "User preferences",
timestamp: "2024-01-15T10:30:00Z"
}
```
**Returns:**
```typescript
{
success: true,
itemsCount: 1
}
```
### `recall` - Search Memories
The model calls this tool to search for relevant information in memory.
**Parameters:**
- `bankId` (required): Memory bank ID
- `query` (required): What to search for
- `types` (optional): Filter by fact types (`['world', 'experience', 'opinion']`)
- `maxTokens` (optional): Maximum tokens to return (default: 4096)
- `budget` (optional): Processing budget - `'low'`, `'mid'`, or `'high'`
- `queryTimestamp` (optional): Query from a specific time (ISO format)
- `includeEntities` (optional): Include entity observations
- `includeChunks` (optional): Include raw document chunks
**Example tool call:**
```typescript
{
bankId: "user-123",
query: "What does Alice like to do outdoors?",
types: ["world", "experience"],
maxTokens: 2048,
budget: "mid"
}
```
**Returns:**
```typescript
{
results: [
{
id: "mem-123",
text: "Alice loves hiking",
type: "world",
entities: ["Alice"],
context: "User preferences",
occurred_start: "2024-01-15T10:30:00Z",
document_id: "doc-456",
metadata: { source: "chat" }
}
],
entities: {
"Alice": {
canonical_name: "Alice",
mention_count: 15,
observations: [...]
}
}
}
```
### `reflect` - Synthesize Insights
The model calls this tool to analyze memories and generate contextual insights.
**Parameters:**
- `bankId` (required): Memory bank ID
- `query` (required): Question to reflect on
- `context` (optional): Additional context for reflection
- `budget` (optional): Processing budget - `'low'`, `'mid'`, or `'high'`
**Example tool call:**
```typescript
{
bankId: "user-123",
query: "What outdoor activities does Alice enjoy?",
context: "Planning a weekend trip",
budget: "mid"
}
```
**Returns:**
```typescript
{
text: "Alice is an avid hiker who particularly enjoys visiting Yosemite National Park during summer months. She has expressed strong preferences for mountain trails over beach activities.",
basedOn: [
{
id: "mem-123",
text: "Alice loves hiking",
type: "world",
context: "User preferences",
occurred_start: "2024-01-15T10:30:00Z"
}
]
}
```
### `createMentalModel` - Create Knowledge Consolidation
The model calls this tool to create a mental model that automatically consolidates memories into structured knowledge.
**Parameters:**
- `bankId` (required): Memory bank ID
- `mentalModelId` (optional): Custom ID for the mental model (auto-generated if not provided)
- `name` (optional): Name for the mental model
- `sourceQuery` (optional): Query defining which memories to consolidate
- `tags` (optional): Tags for organizing mental models
- `maxTokens` (optional): Maximum tokens for the content
- `autoRefresh` (optional): Auto-refresh after new consolidations (default: false)
**Example tool call:**
```typescript
{
bankId: "user-123",
name: "User Preferences",
sourceQuery: "What are the user's preferences?",
tags: ["preferences"],
autoRefresh: true
}
```
**Returns:**
```typescript
{
mentalModelId: "mm-456",
createdAt: "2024-01-15T10:30:00Z"
}
```
### `queryMentalModel` - Retrieve Consolidated Knowledge
The model calls this tool to retrieve synthesized insights from an existing mental model.
**Parameters:**
- `bankId` (required): Memory bank ID
- `mentalModelId` (required): ID of the mental model to query
**Example tool call:**
```typescript
{
bankId: "user-123",
mentalModelId: "mm-456"
}
```
**Returns:**
```typescript
{
content: "The user prefers outdoor activities, particularly hiking. They enjoy mountain trails and visit Yosemite regularly during summer.",
name: "User Preferences",
updatedAt: "2024-01-20T15:45:00Z"
}
```
### `getDocument` - Retrieve Stored Document
The model calls this tool to retrieve a stored document by its ID.
**Parameters:**
- `bankId` (required): Memory bank ID
- `documentId` (required): ID of the document to retrieve
**Example tool call:**
```typescript
{
bankId: "user-123",
documentId: "doc-789"
}
```
**Returns:**
```typescript
{
originalText: "User profile: Alice, Software Engineer, loves hiking...",
id: "doc-789",
createdAt: "2024-01-10T09:00:00Z",
updatedAt: "2024-01-15T14:30:00Z"
}
```
### `createDirective` - Create Behavioral Rule
The model calls this tool to create a directive—a hard rule injected into prompts during reflect operations.
**Parameters:**
- `bankId` (required): Memory bank ID
- `name` (required): Human-readable name for the directive
- `content` (required): The directive text to inject
- `priority` (optional): Higher priority directives are injected first (default: 0)
- `isActive` (optional): Whether this directive is active (default: true)
- `tags` (optional): Tags for filtering (e.g., user-specific directives)
**Example tool call:**
```typescript
{
bankId: "user-123",
name: "Response Format",
content: "Always provide responses in bullet-point format",
priority: 10,
tags: ["formatting"]
}
```
**Returns:**
```typescript
{
id: "dir-321",
name: "Response Format",
content: "Always provide responses in bullet-point format",
tags: ["formatting"],
createdAt: "2024-01-15T10:30:00Z"
}
```
## Usage Examples
### Using with `generateText`
```typescript
import { HindsightClient } from '@vectorize-io/hindsight-client';
import { createHindsightTools } from '@vectorize-io/hindsight-ai-sdk';
import { generateText } from 'ai';
import { anthropic } from '@ai-sdk/anthropic';
const hindsightClient = new HindsightClient({
apiUrl: 'http://localhost:8000',
});
const tools = createHindsightTools({ client: hindsightClient });
const result = await generateText({
model: anthropic('claude-sonnet-4-20250514'),
tools,
system: `You are a helpful assistant with long-term memory. Use the recall tool to check for relevant memories before responding.`,
prompt: 'Remember that Alice loves hiking and prefers spicy food',
});
console.log(result.text);
```
### Using with `streamText`
```typescript
import { streamText } from 'ai';
const result = streamText({
model: anthropic('claude-sonnet-4-20250514'),
tools,
system: `You have persistent memory. Use retain to store important information and recall to retrieve it.`,
prompt: 'What do you know about Alice?',
});
for await (const chunk of result.textStream) {
process.stdout.write(chunk);
}
```
### Using with `ToolLoopAgent`
```typescript
import { ToolLoopAgent, stopWhen, stepCountIs } from 'ai';
const agent = new ToolLoopAgent({
model: anthropic('claude-sonnet-4-20250514'),
tools,
instructions: `You are a personal assistant with long-term memory. Always check recall before responding and use retain to store important information.`,
stopWhen: stepCountIs(10),
});
const result = await agent.generate({
prompt: 'What did I say I wanted to work on this week?',
});
```
### Multi-User Support
```typescript
const result = await generateText({
model: anthropic('claude-sonnet-4-20250514'),
tools,
system: `You are a helpful assistant. The user's ID is: ${userId}. Always pass this as the bankId parameter to memory tools.`,
prompt: 'Remember that I prefer dark mode',
});
```

View file

@ -0,0 +1,97 @@
---
sidebar_position: 4
---
# Vercel AI SDK
The `@vectorize-io/hindsight-ai-sdk` package integrates [Hindsight](https://hindsight.vectorize.io) memory with the [Vercel AI SDK](https://ai-sdk.dev). It provides five ready-to-use tools for retaining, recalling, and reflecting on long-term memories.
import CodeSnippet from '@site/src/components/CodeSnippet';
import aiSdkTs from '!!raw-loader!@site/examples/integrations/ai-sdk.ts';
## Installation
```bash
npm install @vectorize-io/hindsight-ai-sdk @vectorize-io/hindsight-client ai
```
## Setup
Create a Hindsight client and pass it to `createHindsightTools` along with a `bankId`. The `bankId` identifies the memory store for this session—typically a user ID.
<CodeSnippet code={aiSdkTs} section="setup" language="typescript" />
:::tip Per-request bank IDs
In multi-user applications, create `tools` inside your request handler so each request closes over the correct `bankId`. See the [Next.js example](#in-a-nextjs-route-handler) below.
:::
## Usage
### With `generateText`
<CodeSnippet code={aiSdkTs} section="generate-text" language="typescript" />
### With `streamText`
<CodeSnippet code={aiSdkTs} section="stream-text" language="typescript" />
### With `ToolLoopAgent`
<CodeSnippet code={aiSdkTs} section="tool-loop-agent" language="typescript" />
### In a Next.js Route Handler
<CodeSnippet code={aiSdkTs} section="next-api-route" language="typescript" />
---
## Tools Reference
Five tools are registered. The `bankId` is fixed at creation time—the agent cannot change it.
| Tool | What the agent provides | What the constructor controls |
|------|------------------------|-------------------------------|
| `retain` | `content`, `documentId`, `timestamp`, `context` | `async`, `tags`, `metadata` |
| `recall` | `query`, `queryTimestamp` | `budget`, `types`, `maxTokens`, `includeEntities`, `includeChunks` |
| `reflect` | `query`, `context` | `budget` |
| `getMentalModel` | `mentalModelId` | — |
| `getDocument` | `documentId` | — |
**Why this split?** Semantic inputs (what to remember, what to search for) belong to the agent. Infrastructure concerns (cost budget, tagging strategy, async mode) belong to the application.
---
## Constructor Options
All options except `client` and `bankId` are optional. Each tool's options are grouped under the tool name.
<CodeSnippet code={aiSdkTs} section="constructor-options" language="typescript" />
### `retain`
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `async` | `boolean` | `false` | Fire-and-forget — do not wait for ingestion to complete |
| `tags` | `string[]` | — | Tags attached to every retained memory |
| `metadata` | `Record<string, string>` | — | Metadata attached to every retained memory |
| `description` | `string` | built-in | Override the tool description shown to the model |
### `recall`
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `budget` | `'low' \| 'mid' \| 'high'` | `'mid'` | Controls retrieval depth and latency |
| `types` | `('world' \| 'experience' \| 'observation')[]` | all | Restrict results to these fact types |
| `maxTokens` | `number` | API default | Cap the total tokens returned |
| `includeEntities` | `boolean` | `false` | Include entity observations in results |
| `includeChunks` | `boolean` | `false` | Include raw source chunks in results |
| `description` | `string` | built-in | Override the tool description shown to the model |
### `reflect`
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `budget` | `'low' \| 'mid' \| 'high'` | `'mid'` | Controls synthesis depth and latency |
| `maxTokens` | `number` | API default | Maximum tokens for the response |
| `description` | `string` | built-in | Override the tool description shown to the model |

View file

@ -0,0 +1,122 @@
/**
* Hindsight AI SDK integration examples
* These snippets are embedded in the documentation via CodeSnippet.
*/
// [docs:setup]
import { HindsightClient } from '@vectorize-io/hindsight-client';
import { createHindsightTools } from '@vectorize-io/hindsight-ai-sdk';
const client = new HindsightClient({ baseUrl: 'http://localhost:8888' });
const tools = createHindsightTools({
client,
bankId: 'user-123',
});
// [/docs:setup]
// [docs:generate-text]
import { generateText } from 'ai';
import { openai } from '@ai-sdk/openai';
const { text } = await generateText({
model: openai('gpt-4o'),
tools,
maxSteps: 5,
system: 'You are a helpful assistant with long-term memory.',
prompt: 'Remember that I prefer dark mode and large fonts.',
});
// [/docs:generate-text]
// [docs:stream-text]
import { streamText } from 'ai';
const result = streamText({
model: openai('gpt-4o'),
tools,
maxSteps: 5,
system: 'You are a helpful assistant with long-term memory.',
prompt: 'What are my display preferences?',
});
for await (const chunk of result.textStream) {
process.stdout.write(chunk);
}
// [/docs:stream-text]
// [docs:tool-loop-agent]
import { generateText, ToolLoopAgent, stepCountIs } from 'ai';
import { openai } from '@ai-sdk/openai';
import { HindsightClient } from '@vectorize-io/hindsight-client';
import { createHindsightTools } from '@vectorize-io/hindsight-ai-sdk';
const client = new HindsightClient({ baseUrl: process.env.HINDSIGHT_API_URL! });
const agent = new ToolLoopAgent({
model: openai('gpt-4o'),
tools: createHindsightTools({ client, bankId: 'user-123' }),
stopWhen: stepCountIs(10),
system: 'You are a helpful assistant with long-term memory.',
});
const result = await agent.generate({
prompt: 'Remember that my favorite editor is Neovim',
});
// [/docs:tool-loop-agent]
// [docs:next-api-route]
// app/api/chat/route.ts
import { streamText } from 'ai';
import { openai } from '@ai-sdk/openai';
import { HindsightClient } from '@vectorize-io/hindsight-client';
import { createHindsightTools } from '@vectorize-io/hindsight-ai-sdk';
const hindsightClient = new HindsightClient({
baseUrl: process.env.HINDSIGHT_API_URL!,
});
export async function POST(req: Request) {
const { messages, userId } = await req.json();
// Tools are created per-request, closing over the current user's bankId
const tools = createHindsightTools({
client: hindsightClient,
bankId: userId,
});
return streamText({
model: openai('gpt-4o'),
tools,
maxSteps: 5,
system: 'You are a helpful assistant with long-term memory.',
messages,
}).toDataStreamResponse();
}
// [/docs:next-api-route]
// [docs:constructor-options]
const tools = createHindsightTools({
client,
bankId: userId,
retain: {
async: true, // fire-and-forget (default: false)
tags: ['env:prod', 'app:support'], // always attached to every retained memory
metadata: { version: '2.0' }, // always attached to every retained memory
},
recall: {
budget: 'high', // processing depth: low | mid | high (default: 'mid')
types: ['experience', 'world'], // restrict to these fact types (default: all)
maxTokens: 2048, // cap token budget (default: API default)
includeEntities: true, // include entity observations (default: false)
includeChunks: true, // include raw source chunks (default: false)
},
reflect: {
budget: 'mid', // processing depth (default: 'mid')
},
});
// [/docs:constructor-options]

View file

@ -1,12 +1,12 @@
{
"name": "@vectorize-io/hindsight-ai-sdk",
"version": "0.4.8",
"version": "0.4.11",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@vectorize-io/hindsight-ai-sdk",
"version": "0.4.8",
"version": "0.4.11",
"license": "MIT",
"devDependencies": {
"@types/node": "^22.0.0",

View file

@ -13,31 +13,34 @@ describe('createHindsightTools', () => {
});
describe('tool creation', () => {
it('should create all three tools', () => {
const tools = createHindsightTools({ client: mockClient });
it('should create all tools', () => {
const tools = createHindsightTools({ client: mockClient, bankId: 'test-bank' });
expect(tools).toHaveProperty('retain');
expect(tools).toHaveProperty('recall');
expect(tools).toHaveProperty('reflect');
expect(tools).toHaveProperty('getMentalModel');
expect(tools).toHaveProperty('getDocument');
expect(typeof tools.retain.execute).toBe('function');
expect(typeof tools.recall.execute).toBe('function');
expect(typeof tools.reflect.execute).toBe('function');
});
it('should use default descriptions when not provided', () => {
const tools = createHindsightTools({ client: mockClient });
const tools = createHindsightTools({ client: mockClient, bankId: 'test-bank' });
expect(tools.retain.description).toContain('Store information in long-term memory');
expect(tools.recall.description).toContain('Search memory for relevant information');
expect(tools.reflect.description).toContain('Analyze memories to form insights');
});
it('should use custom descriptions when provided', () => {
it('should use custom descriptions from nested options', () => {
const tools = createHindsightTools({
client: mockClient,
retainDescription: 'Custom retain description',
recallDescription: 'Custom recall description',
reflectDescription: 'Custom reflect description',
bankId: 'test-bank',
retain: { description: 'Custom retain description' },
recall: { description: 'Custom recall description' },
reflect: { description: 'Custom reflect description' },
});
expect(tools.retain.description).toBe('Custom retain description');
@ -47,8 +50,8 @@ describe('createHindsightTools', () => {
});
describe('retain tool', () => {
it('should call client.retain with correct parameters', async () => {
const tools = createHindsightTools({ client: mockClient });
it('should call client.retain with agent inputs and constructor defaults', async () => {
const tools = createHindsightTools({ client: mockClient, bankId: 'test-bank' });
vi.mocked(mockClient.retain).mockResolvedValue({
success: true,
bank_id: 'test-bank',
@ -56,21 +59,21 @@ describe('createHindsightTools', () => {
async: false,
});
const result = await tools.retain.execute({
bankId: 'test-bank',
content: 'Test content',
});
const result = await tools.retain.execute({ content: 'Test content' });
expect(mockClient.retain).toHaveBeenCalledWith('test-bank', 'Test content', {
documentId: undefined,
timestamp: undefined,
context: undefined,
tags: undefined,
metadata: undefined,
async: false,
});
expect(result).toEqual({ success: true, itemsCount: 5 });
});
it('should pass optional parameters to client.retain', async () => {
const tools = createHindsightTools({ client: mockClient });
it('should pass agent-provided optional inputs', async () => {
const tools = createHindsightTools({ client: mockClient, bankId: 'test-bank' });
vi.mocked(mockClient.retain).mockResolvedValue({
success: true,
bank_id: 'test-bank',
@ -79,7 +82,6 @@ describe('createHindsightTools', () => {
});
await tools.retain.execute({
bankId: 'test-bank',
content: 'Test content',
documentId: 'doc-123',
timestamp: '2024-01-01T00:00:00Z',
@ -90,100 +92,123 @@ describe('createHindsightTools', () => {
documentId: 'doc-123',
timestamp: '2024-01-01T00:00:00Z',
context: 'Test context',
tags: undefined,
metadata: undefined,
async: false,
});
});
it('should transform response correctly', async () => {
const tools = createHindsightTools({ client: mockClient });
it('should apply constructor-level retain options', async () => {
const tools = createHindsightTools({
client: mockClient,
bankId: 'test-bank',
retain: {
async: true,
tags: ['env:prod', 'app:support'],
metadata: { version: '1.0' },
},
});
vi.mocked(mockClient.retain).mockResolvedValue({
success: true,
bank_id: 'test-bank',
items_count: 10,
async: false,
items_count: 1,
async: true,
});
const result = await tools.retain.execute({
bankId: 'test-bank',
content: 'Test content',
});
await tools.retain.execute({ content: 'Test content' });
expect(result).toEqual({ success: true, itemsCount: 10 });
expect(mockClient.retain).toHaveBeenCalledWith('test-bank', 'Test content', {
documentId: undefined,
timestamp: undefined,
context: undefined,
tags: ['env:prod', 'app:support'],
metadata: { version: '1.0' },
async: true,
});
});
});
describe('recall tool', () => {
it('should call client.recall with correct parameters', async () => {
const tools = createHindsightTools({ client: mockClient });
it('should call client.recall with agent inputs and constructor defaults', async () => {
const tools = createHindsightTools({ client: mockClient, bankId: 'test-bank' });
vi.mocked(mockClient.recall).mockResolvedValue({
results: [
{
id: 'fact-1',
text: 'Test fact',
type: 'preference',
},
],
results: [{ id: 'fact-1', text: 'Test fact', type: 'preference' }],
});
const result = await tools.recall.execute({
bankId: 'test-bank',
query: 'Test query',
});
const result = await tools.recall.execute({ query: 'Test query' });
expect(mockClient.recall).toHaveBeenCalledWith('test-bank', 'Test query', {
types: undefined,
maxTokens: undefined,
budget: undefined,
budget: 'mid',
queryTimestamp: undefined,
includeEntities: undefined,
includeChunks: undefined,
includeEntities: false,
includeChunks: false,
});
expect(result.results).toHaveLength(1);
expect(result.results[0].id).toBe('fact-1');
});
it('should pass all optional parameters to client.recall', async () => {
const tools = createHindsightTools({ client: mockClient });
vi.mocked(mockClient.recall).mockResolvedValue({
results: [],
});
it('should pass agent-provided queryTimestamp', async () => {
const tools = createHindsightTools({ client: mockClient, bankId: 'test-bank' });
vi.mocked(mockClient.recall).mockResolvedValue({ results: [] });
await tools.recall.execute({
bankId: 'test-bank',
query: 'Test query',
types: ['preference', 'fact'],
maxTokens: 1000,
budget: 'high',
queryTimestamp: '2024-01-01T00:00:00Z',
includeEntities: true,
includeChunks: true,
});
expect(mockClient.recall).toHaveBeenCalledWith('test-bank', 'Test query', {
types: undefined,
maxTokens: undefined,
budget: 'mid',
queryTimestamp: '2024-01-01T00:00:00Z',
includeEntities: false,
includeChunks: false,
});
});
it('should apply constructor-level recall options', async () => {
const tools = createHindsightTools({
client: mockClient,
bankId: 'test-bank',
recall: {
types: ['preference', 'fact'],
maxTokens: 1000,
budget: 'high',
includeEntities: true,
includeChunks: true,
},
});
vi.mocked(mockClient.recall).mockResolvedValue({ results: [] });
await tools.recall.execute({ query: 'Test query' });
expect(mockClient.recall).toHaveBeenCalledWith('test-bank', 'Test query', {
types: ['preference', 'fact'],
maxTokens: 1000,
budget: 'high',
queryTimestamp: '2024-01-01T00:00:00Z',
queryTimestamp: undefined,
includeEntities: true,
includeChunks: true,
});
});
it('should handle empty results', async () => {
const tools = createHindsightTools({ client: mockClient });
vi.mocked(mockClient.recall).mockResolvedValue({
results: undefined as any,
});
const tools = createHindsightTools({ client: mockClient, bankId: 'test-bank' });
vi.mocked(mockClient.recall).mockResolvedValue({ results: undefined as any });
const result = await tools.recall.execute({
bankId: 'test-bank',
query: 'Test query',
});
const result = await tools.recall.execute({ query: 'Test query' });
expect(result.results).toEqual([]);
});
it('should include entities when present', async () => {
const tools = createHindsightTools({ client: mockClient });
const tools = createHindsightTools({
client: mockClient,
bankId: 'test-bank',
recall: { includeEntities: true },
});
const entities = {
'entity-1': {
entity_id: 'entity-1',
@ -191,59 +216,39 @@ describe('createHindsightTools', () => {
observations: [{ text: 'Alice loves hiking' }],
},
};
vi.mocked(mockClient.recall).mockResolvedValue({ results: [], entities });
vi.mocked(mockClient.recall).mockResolvedValue({
results: [],
entities,
});
const result = await tools.recall.execute({
bankId: 'test-bank',
query: 'Test query',
includeEntities: true,
});
const result = await tools.recall.execute({ query: 'Test query' });
expect(result.entities).toEqual(entities);
});
});
describe('reflect tool', () => {
it('should call client.reflect with correct parameters', async () => {
const tools = createHindsightTools({ client: mockClient });
it('should call client.reflect with agent inputs and constructor defaults', async () => {
const tools = createHindsightTools({ client: mockClient, bankId: 'test-bank' });
vi.mocked(mockClient.reflect).mockResolvedValue({
text: 'Reflection result',
based_on: [
{
id: 'fact-1',
text: 'Supporting fact',
},
],
based_on: [{ id: 'fact-1', text: 'Supporting fact' }],
});
const result = await tools.reflect.execute({
bankId: 'test-bank',
query: 'What are my preferences?',
});
const result = await tools.reflect.execute({ query: 'What are my preferences?' });
expect(mockClient.reflect).toHaveBeenCalledWith('test-bank', 'What are my preferences?', {
context: undefined,
budget: undefined,
budget: 'mid',
});
expect(result.text).toBe('Reflection result');
expect(result.basedOn).toHaveLength(1);
});
it('should pass optional parameters to client.reflect', async () => {
const tools = createHindsightTools({ client: mockClient });
vi.mocked(mockClient.reflect).mockResolvedValue({
text: 'Reflection result',
});
it('should pass agent-provided context', async () => {
const tools = createHindsightTools({ client: mockClient, bankId: 'test-bank' });
vi.mocked(mockClient.reflect).mockResolvedValue({ text: 'Reflection result' });
await tools.reflect.execute({
bankId: 'test-bank',
query: 'What are my preferences?',
context: 'User context',
budget: 'mid',
});
expect(mockClient.reflect).toHaveBeenCalledWith('test-bank', 'What are my preferences?', {
@ -252,44 +257,43 @@ describe('createHindsightTools', () => {
});
});
it('should handle empty text response with fallback', async () => {
const tools = createHindsightTools({ client: mockClient });
vi.mocked(mockClient.reflect).mockResolvedValue({
text: undefined as any,
});
const result = await tools.reflect.execute({
it('should apply constructor-level reflect budget', async () => {
const tools = createHindsightTools({
client: mockClient,
bankId: 'test-bank',
query: 'Test query',
reflect: { budget: 'low' },
});
vi.mocked(mockClient.reflect).mockResolvedValue({ text: 'Reflection result' });
await tools.reflect.execute({ query: 'Test query' });
expect(mockClient.reflect).toHaveBeenCalledWith('test-bank', 'Test query', {
context: undefined,
budget: 'low',
});
});
it('should handle empty text response with fallback', async () => {
const tools = createHindsightTools({ client: mockClient, bankId: 'test-bank' });
vi.mocked(mockClient.reflect).mockResolvedValue({ text: undefined as any });
const result = await tools.reflect.execute({ query: 'Test query' });
expect(result.text).toBe('No insights available yet.');
});
it('should include basedOn facts when present', async () => {
const tools = createHindsightTools({ client: mockClient });
const tools = createHindsightTools({ client: mockClient, bankId: 'test-bank' });
const basedOn = [
{
id: 'fact-1',
text: 'User prefers spicy food',
type: 'preference',
},
{
id: 'fact-2',
text: 'User is allergic to nuts',
type: 'health',
},
{ id: 'fact-1', text: 'User prefers spicy food', type: 'preference' },
{ id: 'fact-2', text: 'User is allergic to nuts', type: 'health' },
];
vi.mocked(mockClient.reflect).mockResolvedValue({
text: 'Based on your history, you prefer spicy Asian cuisine',
based_on: basedOn,
});
const result = await tools.reflect.execute({
bankId: 'test-bank',
query: 'What do I like?',
});
const result = await tools.reflect.execute({ query: 'What do I like?' });
expect(result.basedOn).toEqual(basedOn);
});
@ -297,66 +301,70 @@ describe('createHindsightTools', () => {
describe('error handling', () => {
it('should propagate errors from client.retain', async () => {
const tools = createHindsightTools({ client: mockClient });
const error = new Error('Retain failed');
vi.mocked(mockClient.retain).mockRejectedValue(error);
const tools = createHindsightTools({ client: mockClient, bankId: 'test-bank' });
vi.mocked(mockClient.retain).mockRejectedValue(new Error('Retain failed'));
await expect(
tools.retain.execute({
bankId: 'test-bank',
content: 'Test content',
})
).rejects.toThrow('Retain failed');
await expect(tools.retain.execute({ content: 'Test content' })).rejects.toThrow('Retain failed');
});
it('should propagate errors from client.recall', async () => {
const tools = createHindsightTools({ client: mockClient });
const error = new Error('Recall failed');
vi.mocked(mockClient.recall).mockRejectedValue(error);
const tools = createHindsightTools({ client: mockClient, bankId: 'test-bank' });
vi.mocked(mockClient.recall).mockRejectedValue(new Error('Recall failed'));
await expect(
tools.recall.execute({
bankId: 'test-bank',
query: 'Test query',
})
).rejects.toThrow('Recall failed');
await expect(tools.recall.execute({ query: 'Test query' })).rejects.toThrow('Recall failed');
});
it('should propagate errors from client.reflect', async () => {
const tools = createHindsightTools({ client: mockClient });
const error = new Error('Reflect failed');
vi.mocked(mockClient.reflect).mockRejectedValue(error);
const tools = createHindsightTools({ client: mockClient, bankId: 'test-bank' });
vi.mocked(mockClient.reflect).mockRejectedValue(new Error('Reflect failed'));
await expect(
tools.reflect.execute({
bankId: 'test-bank',
query: 'Test query',
})
).rejects.toThrow('Reflect failed');
await expect(tools.reflect.execute({ query: 'Test query' })).rejects.toThrow('Reflect failed');
});
});
describe('budget schema', () => {
it('should accept valid budget values', async () => {
const tools = createHindsightTools({ client: mockClient });
describe('budget defaults', () => {
it('should default recall budget to mid', async () => {
const tools = createHindsightTools({ client: mockClient, bankId: 'test-bank' });
vi.mocked(mockClient.recall).mockResolvedValue({ results: [] });
await tools.recall.execute({ query: 'Test' });
expect(mockClient.recall).toHaveBeenCalledWith('test-bank', 'Test', expect.objectContaining({ budget: 'mid' }));
});
it('should default reflect budget to mid', async () => {
const tools = createHindsightTools({ client: mockClient, bankId: 'test-bank' });
vi.mocked(mockClient.reflect).mockResolvedValue({ text: 'ok' });
await tools.reflect.execute({ query: 'Test' });
expect(mockClient.reflect).toHaveBeenCalledWith('test-bank', 'Test', expect.objectContaining({ budget: 'mid' }));
});
it('should accept low/mid/high budget values', async () => {
vi.mocked(mockClient.recall).mockResolvedValue({ results: [] });
for (const budget of ['low', 'mid', 'high'] as const) {
await tools.recall.execute({
bankId: 'test-bank',
query: 'Test',
budget,
});
expect(mockClient.recall).toHaveBeenCalledWith('test-bank', 'Test', {
types: undefined,
maxTokens: undefined,
budget,
queryTimestamp: undefined,
includeEntities: undefined,
includeChunks: undefined,
});
const tools = createHindsightTools({ client: mockClient, bankId: 'test-bank', recall: { budget } });
await tools.recall.execute({ query: 'Test' });
expect(mockClient.recall).toHaveBeenCalledWith('test-bank', 'Test', expect.objectContaining({ budget }));
}
});
});
describe('bankId enforcement', () => {
it('should always use the bankId from constructor options', async () => {
const tools = createHindsightTools({ client: mockClient, bankId: 'forced-bank' });
vi.mocked(mockClient.retain).mockResolvedValue({
success: true,
bank_id: 'forced-bank',
items_count: 1,
async: false,
});
await tools.retain.execute({ content: 'Test' });
expect(mockClient.retain).toHaveBeenCalledWith('forced-bank', 'Test', expect.anything());
});
});
});

View file

@ -7,6 +7,12 @@ import { z } from 'zod';
export const BudgetSchema = z.enum(['low', 'mid', 'high']);
export type Budget = z.infer<typeof BudgetSchema>;
/**
* Fact types for filtering recall results.
*/
export const FactTypeSchema = z.enum(['world', 'experience', 'observation']);
export type FactType = z.infer<typeof FactTypeSchema>;
/**
* Recall result item from Hindsight
*/
@ -105,15 +111,6 @@ export interface MentalModelResponse {
trigger?: MentalModelTrigger;
}
/**
* Create mental model response from Hindsight
*/
export interface CreateMentalModelResponse {
mental_model_id: string;
bank_id: string;
created_at: string;
}
/**
* Document response from Hindsight
*/
@ -128,36 +125,6 @@ export interface DocumentResponse {
tags?: string[];
}
/**
* Directive response from Hindsight
*/
export interface DirectiveResponse {
id: string;
bank_id: string;
name: string;
content: string;
priority: number;
is_active: boolean;
tags: string[];
created_at: string;
updated_at: string;
}
/**
* Create directive response from Hindsight
*/
export interface CreateDirectiveResponse {
id: string;
bank_id: string;
name: string;
content: string;
priority: number;
is_active: boolean;
tags: string[];
created_at: string;
updated_at: string;
}
/**
* Hindsight client interface - matches @vectorize-io/hindsight-client
*/
@ -179,7 +146,7 @@ export interface HindsightClient {
bankId: string,
query: string,
options?: {
types?: string[];
types?: FactType[];
maxTokens?: number;
budget?: Budget;
trace?: boolean;
@ -197,21 +164,10 @@ export interface HindsightClient {
options?: {
context?: string;
budget?: Budget;
maxTokens?: number;
}
): Promise<ReflectResponse>;
createMentalModel(
bankId: string,
options?: {
id?: string;
name?: string;
sourceQuery?: string;
tags?: string[];
maxTokens?: number;
trigger?: MentalModelTrigger;
}
): Promise<CreateMentalModelResponse>;
getMentalModel(
bankId: string,
mentalModelId: string
@ -221,148 +177,125 @@ export interface HindsightClient {
bankId: string,
documentId: string
): Promise<DocumentResponse | null>;
createDirective(
bankId: string,
options: {
name: string;
content: string;
priority?: number;
isActive?: boolean;
tags?: string[];
}
): Promise<CreateDirectiveResponse>;
listDirectives(
bankId: string,
options?: {
tags?: string[];
tagsMatch?: 'any' | 'all' | 'exact';
activeOnly?: boolean;
limit?: number;
offset?: number;
}
): Promise<{ directives: DirectiveResponse[]; total: number }>;
}
export interface HindsightToolsOptions {
/** Hindsight client instance */
client: HindsightClient;
/**
* Custom description for the retain tool.
*/
retainDescription?: string;
/**
* Custom description for the recall tool.
*/
recallDescription?: string;
/**
* Custom description for the reflect tool.
*/
reflectDescription?: string;
/**
* Custom description for the createMentalModel tool.
*/
createMentalModelDescription?: string;
/**
* Custom description for the queryMentalModel tool.
*/
queryMentalModelDescription?: string;
/**
* Custom description for the getDocument tool.
*/
getDocumentDescription?: string;
/** Memory bank ID to use for all tool calls (e.g. the user ID) */
bankId: string;
/** Options for the retain tool */
retain?: {
/** Fire-and-forget retain without waiting for completion (default: false) */
async?: boolean;
/** Tags always attached to every retained memory (default: undefined) */
tags?: string[];
/** Metadata always attached to every retained memory (default: undefined) */
metadata?: Record<string, string>;
/** Custom tool description */
description?: string;
};
/** Options for the recall tool */
recall?: {
/** Restrict results to these fact types: 'world', 'experience', 'observation' (default: undefined = all types) */
types?: FactType[];
/** Maximum tokens to return (default: undefined = API default) */
maxTokens?: number;
/** Processing budget controlling latency vs. depth (default: 'mid') */
budget?: Budget;
/** Include entity observations in results (default: false) */
includeEntities?: boolean;
/** Include raw source chunks in results (default: false) */
includeChunks?: boolean;
/** Custom tool description */
description?: string;
};
/** Options for the reflect tool */
reflect?: {
/** Processing budget controlling latency vs. depth (default: 'mid') */
budget?: Budget;
/** Maximum tokens for the response (default: undefined = API default) */
maxTokens?: number;
/** Custom tool description */
description?: string;
};
/** Options for the getMentalModel tool */
getMentalModel?: {
/** Custom tool description */
description?: string;
};
/** Options for the getDocument tool */
getDocument?: {
/** Custom tool description */
description?: string;
};
}
/**
* Creates AI SDK tools for Hindsight memory operations.
*
* Features:
* - Dynamic bank ID per call (supports multi-user/multi-bank scenarios)
* - Full API parameter support for retain, recall, and reflect
* - Ready to use with streamText, generateText, or ToolLoopAgent
* The bank ID and all infrastructure concerns (budget, tags, async mode, etc.)
* are fixed at creation time. The agent only controls semantic inputs:
* content, queries, names, and timestamps.
*
* @example
* ```ts
* const tools = createHindsightTools({
* client: hindsightClient,
* bankId: userId,
* recall: { budget: 'high', includeEntities: true },
* retain: { async: true, tags: ['env:prod'] },
* });
*
* // Use with AI SDK
* const result = await generateText({
* model: openai('gpt-4'),
* model: openai('gpt-4o'),
* tools,
* prompt: 'Remember that Alice loves hiking',
* messages,
* });
* ```
*/
export function createHindsightTools({
client,
retainDescription,
recallDescription,
reflectDescription,
createMentalModelDescription,
queryMentalModelDescription,
getDocumentDescription,
bankId,
retain: retainOpts = {},
recall: recallOpts = {},
reflect: reflectOpts = {},
getMentalModel: getMentalModelOpts = {},
getDocument: getDocumentOpts = {},
}: HindsightToolsOptions) {
// Agent-controlled params only: content, timestamp, documentId, context
const retainParams = z.object({
bankId: z.string().describe('Memory bank ID (usually the user ID)'),
content: z.string().describe('Content to store in memory'),
documentId: z.string().optional().describe('Optional document ID for grouping/upserting content'),
timestamp: z.string().optional().describe('Optional ISO timestamp for when the memory occurred'),
context: z.string().optional().describe('Optional context about the memory'),
tags: z.array(z.string()).optional().describe('Optional tags for visibility scoping'),
metadata: z.record(z.string(), z.string()).optional().describe('Optional user-defined metadata'),
});
// Agent-controlled params only: query, queryTimestamp
const recallParams = z.object({
bankId: z.string().describe('Memory bank ID (usually the user ID)'),
query: z.string().describe('What to search for in memory'),
types: z.array(z.string()).optional().describe('Filter by fact types'),
maxTokens: z.number().optional().describe('Maximum tokens to return'),
budget: BudgetSchema.optional().describe('Processing budget: low, mid, or high'),
queryTimestamp: z.string().optional().describe('Query from a specific point in time (ISO format)'),
includeEntities: z.boolean().optional().describe('Include entity observations in results'),
includeChunks: z.boolean().optional().describe('Include raw chunks in results'),
});
// Agent-controlled params only: query, context
const reflectParams = z.object({
bankId: z.string().describe('Memory bank ID (usually the user ID)'),
query: z.string().describe('Question to reflect on based on memories'),
context: z.string().optional().describe('Additional context for the reflection'),
budget: BudgetSchema.optional().describe('Processing budget: low, mid, or high'),
});
const createMentalModelParams = z.object({
bankId: z.string().describe('Memory bank ID (usually the user ID)'),
mentalModelId: z.string().optional().describe('Optional custom ID for the mental model (auto-generated if not provided)'),
name: z.string().optional().describe('Optional name for the mental model'),
sourceQuery: z.string().optional().describe('Query to define what memories to consolidate'),
tags: z.array(z.string()).optional().describe('Optional tags for organizing mental models'),
maxTokens: z.number().optional().describe('Maximum tokens for the mental model content'),
autoRefresh: z.boolean().optional().describe('Auto-refresh mental model after new consolidations (default: false)'),
});
const queryMentalModelParams = z.object({
bankId: z.string().describe('Memory bank ID (usually the user ID)'),
mentalModelId: z.string().describe('ID of the mental model to query'),
const getMentalModelParams = z.object({
mentalModelId: z.string().describe('ID of the mental model to retrieve'),
});
const getDocumentParams = z.object({
bankId: z.string().describe('Memory bank ID (usually the user ID)'),
documentId: z.string().describe('ID of the document to retrieve'),
});
const createDirectiveParams = z.object({
bankId: z.string().describe('Memory bank ID (usually the user ID)'),
name: z.string().describe('Human-readable name for the directive'),
content: z.string().describe('The directive text to inject into prompts'),
priority: z.number().optional().describe('Higher priority directives are injected first (default 0)'),
isActive: z.boolean().optional().describe('Whether this directive is active (default true)'),
tags: z.array(z.string()).optional().describe('Tags for filtering'),
});
type RetainInput = z.infer<typeof retainParams>;
type RetainOutput = { success: boolean; itemsCount: number };
@ -372,37 +305,31 @@ export function createHindsightTools({
type ReflectInput = z.infer<typeof reflectParams>;
type ReflectOutput = { text: string; basedOn?: ReflectFact[] };
type CreateMentalModelInput = z.infer<typeof createMentalModelParams>;
type CreateMentalModelOutput = { mentalModelId: string; createdAt: string };
type QueryMentalModelInput = z.infer<typeof queryMentalModelParams>;
type QueryMentalModelOutput = { content: string; name?: string; updatedAt: string };
type GetMentalModelInput = z.infer<typeof getMentalModelParams>;
type GetMentalModelOutput = { content: string; name?: string; updatedAt: string };
type GetDocumentInput = z.infer<typeof getDocumentParams>;
type GetDocumentOutput = { originalText: string; id: string; createdAt: string; updatedAt: string } | null;
type CreateDirectiveInput = z.infer<typeof createDirectiveParams>;
type CreateDirectiveOutput = { id: string; name: string; content: string; tags: string[]; createdAt: string };
return {
retain: tool<RetainInput, RetainOutput>({
description:
retainDescription ??
retainOpts.description ??
`Store information in long-term memory. Use this when information should be remembered for future interactions, such as user preferences, facts, experiences, or important context.`,
inputSchema: retainParams,
execute: async (input) => {
console.log('[AI SDK Tool] Retain input:', {
bankId: input.bankId,
bankId,
documentId: input.documentId,
tags: input.tags,
hasContent: !!input.content,
});
const result = await client.retain(input.bankId, input.content, {
const result = await client.retain(bankId, input.content, {
documentId: input.documentId,
timestamp: input.timestamp,
context: input.context,
tags: input.tags,
metadata: input.metadata as Record<string, string> | undefined,
tags: retainOpts.tags,
metadata: retainOpts.metadata,
async: retainOpts.async ?? false,
});
return { success: result.success, itemsCount: result.items_count };
},
@ -410,17 +337,17 @@ export function createHindsightTools({
recall: tool<RecallInput, RecallOutput>({
description:
recallDescription ??
recallOpts.description ??
`Search memory for relevant information. Use this to find previously stored information that can help personalize responses or provide context.`,
inputSchema: recallParams,
execute: async (input) => {
const result = await client.recall(input.bankId, input.query, {
types: input.types,
maxTokens: input.maxTokens,
budget: input.budget,
const result = await client.recall(bankId, input.query, {
types: recallOpts.types,
maxTokens: recallOpts.maxTokens,
budget: recallOpts.budget ?? 'mid',
queryTimestamp: input.queryTimestamp,
includeEntities: input.includeEntities,
includeChunks: input.includeChunks,
includeEntities: recallOpts.includeEntities ?? false,
includeChunks: recallOpts.includeChunks ?? false,
});
return {
results: result.results ?? [],
@ -431,13 +358,14 @@ export function createHindsightTools({
reflect: tool<ReflectInput, ReflectOutput>({
description:
reflectDescription ??
reflectOpts.description ??
`Analyze memories to form insights and generate contextual answers. Use this to understand patterns, synthesize information, or answer questions that require reasoning over stored memories.`,
inputSchema: reflectParams,
execute: async (input) => {
const result = await client.reflect(input.bankId, input.query, {
const result = await client.reflect(bankId, input.query, {
context: input.context,
budget: input.budget,
budget: reflectOpts.budget ?? 'mid',
maxTokens: reflectOpts.maxTokens,
});
return {
text: result.text ?? 'No insights available yet.',
@ -446,34 +374,13 @@ export function createHindsightTools({
},
}),
createMentalModel: tool<CreateMentalModelInput, CreateMentalModelOutput>({
getMentalModel: tool<GetMentalModelInput, GetMentalModelOutput>({
description:
createMentalModelDescription ??
`Create a mental model that automatically consolidates memories into structured knowledge. Mental models are continuously updated as new memories are added, making them ideal for maintaining up-to-date user preferences, behavioral patterns, and accumulated wisdom.`,
inputSchema: createMentalModelParams,
getMentalModelOpts.description ??
`Retrieve a mental model to get consolidated knowledge synthesized from memories. Mental models provide synthesized insights that are faster and more efficient to retrieve than searching through raw memories.`,
inputSchema: getMentalModelParams,
execute: async (input) => {
const result = await client.createMentalModel(input.bankId, {
id: input.mentalModelId,
name: input.name,
sourceQuery: input.sourceQuery,
tags: input.tags,
maxTokens: input.maxTokens,
trigger: input.autoRefresh !== undefined ? { refresh_after_consolidation: input.autoRefresh } : undefined,
});
return {
mentalModelId: result.mental_model_id,
createdAt: result.created_at,
};
},
}),
queryMentalModel: tool<QueryMentalModelInput, QueryMentalModelOutput>({
description:
queryMentalModelDescription ??
`Query an existing mental model to retrieve consolidated knowledge. Mental models provide synthesized insights from memories, making them faster and more efficient than searching through raw memories.`,
inputSchema: queryMentalModelParams,
execute: async (input) => {
const result = await client.getMentalModel(input.bankId, input.mentalModelId);
const result = await client.getMentalModel(bankId, input.mentalModelId);
return {
content: result.content ?? 'No content available yet.',
name: result.name,
@ -484,11 +391,11 @@ export function createHindsightTools({
getDocument: tool<GetDocumentInput, GetDocumentOutput>({
description:
getDocumentDescription ??
getDocumentOpts.description ??
`Retrieve a stored document by its ID. Documents are used to store structured data like application state, user profiles, or any data that needs exact retrieval.`,
inputSchema: getDocumentParams,
execute: async (input) => {
const result = await client.getDocument(input.bankId, input.documentId);
const result = await client.getDocument(bankId, input.documentId);
if (!result) {
return null;
}
@ -501,27 +408,6 @@ export function createHindsightTools({
},
}),
createDirective: tool<CreateDirectiveInput, CreateDirectiveOutput>({
description:
`Create a directive - a hard rule that is injected into prompts during reflect operations. Directives are explicit instructions that guide agent behavior. Use tags to control when directives are applied (e.g., user-specific directives with 'user:username' tags).`,
inputSchema: createDirectiveParams,
execute: async (input) => {
const result = await client.createDirective(input.bankId, {
name: input.name,
content: input.content,
priority: input.priority,
isActive: input.isActive,
tags: input.tags,
});
return {
id: result.id,
name: result.name,
content: result.content,
tags: result.tags,
createdAt: result.created_at,
};
},
}),
};
}