Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
44f21b8053 | ||
|
|
0801f9cbd8 | ||
|
|
4a40703fa8 | ||
|
|
420cde7fa4 |
@@ -32,6 +32,7 @@ jobs:
|
||||
integration-tests: ${{ steps.filter.outputs.integration-tests }}
|
||||
integrations-openclaw: ${{ steps.filter.outputs.integrations-openclaw }}
|
||||
integrations-ai-sdk: ${{ steps.filter.outputs.integrations-ai-sdk }}
|
||||
integrations-agentos: ${{ steps.filter.outputs.integrations-agentos }}
|
||||
integrations-agent-framework: ${{ steps.filter.outputs.integrations-agent-framework }}
|
||||
integrations-composio: ${{ steps.filter.outputs.integrations-composio }}
|
||||
integrations-chat: ${{ steps.filter.outputs.integrations-chat }}
|
||||
@@ -135,6 +136,8 @@ jobs:
|
||||
- 'hindsight-integrations/openclaw/**'
|
||||
integrations-ai-sdk:
|
||||
- 'hindsight-integrations/ai-sdk/**'
|
||||
integrations-agentos:
|
||||
- 'hindsight-integrations/agentos/**'
|
||||
integrations-agent-framework:
|
||||
- 'hindsight-integrations/agent-framework/**'
|
||||
integrations-composio:
|
||||
@@ -800,6 +803,37 @@ jobs:
|
||||
working-directory: ./hindsight-integrations/ai-sdk
|
||||
run: npm run test:deno
|
||||
|
||||
build-agentos-integration:
|
||||
needs: [detect-changes]
|
||||
if: >-
|
||||
(github.event_name == 'workflow_dispatch' ||
|
||||
needs.detect-changes.outputs.integrations-agentos == 'true' ||
|
||||
needs.detect-changes.outputs.ci == 'true')
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha || '' }}
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '22'
|
||||
|
||||
- name: Install dependencies
|
||||
working-directory: ./hindsight-integrations/agentos
|
||||
run: npm ci
|
||||
|
||||
- name: Run tests
|
||||
working-directory: ./hindsight-integrations/agentos
|
||||
run: npm test
|
||||
|
||||
- name: Build
|
||||
working-directory: ./hindsight-integrations/agentos
|
||||
run: npm run build
|
||||
|
||||
test-opencode-integration:
|
||||
needs: [detect-changes]
|
||||
if: >-
|
||||
@@ -4948,6 +4982,7 @@ jobs:
|
||||
- test-zcode-integration
|
||||
- build-ai-sdk-integration
|
||||
- test-ai-sdk-integration-deno
|
||||
- build-agentos-integration
|
||||
- test-opencode-integration
|
||||
- test-eve-integration
|
||||
- test-omo-integration
|
||||
|
||||
@@ -39,6 +39,7 @@ class IntegrationMeta:
|
||||
# enough to make this script accept it — no parallel lists to keep in sync.
|
||||
INTEGRATIONS: dict[str, IntegrationMeta] = {
|
||||
"litellm": IntegrationMeta("hindsight-litellm", "LiteLLM"),
|
||||
"agentos": IntegrationMeta("@vectorize-io/hindsight-agentos", "AgentOS"),
|
||||
"pydantic-ai": IntegrationMeta("hindsight-pydantic-ai", "Pydantic AI"),
|
||||
"crewai": IntegrationMeta("hindsight-crewai", "CrewAI"),
|
||||
"agent-framework": IntegrationMeta("hindsight-agent-framework", "Microsoft Agent Framework"),
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
---
|
||||
title: "AgentOS Long-Term Memory with Hindsight | Integration"
|
||||
description: "Give AgentOS agents persistent long-term memory. The Hindsight memory provider recalls relevant memories into context before each model call and retains turns after each turn."
|
||||
---
|
||||
|
||||
# AgentOS
|
||||
|
||||
The `@vectorize-io/hindsight-agentos` package gives [AgentOS](https://github.com/framerslab/agentos) agents long-term memory backed by [Hindsight](https://hindsight.vectorize.io).
|
||||
|
||||
`createHindsightMemory` returns an AgentOS `AgentMemoryProvider`. Attach it to an agent via `memoryProvider` and AgentOS auto-wires it on every call path (`generate`, `stream`, and `session.send` / `session.stream`):
|
||||
|
||||
- **`getContext`** runs *before* each model call — it recalls relevant memories from Hindsight and injects them into the system prompt.
|
||||
- **`observe`** runs *after* each turn — it retains the exchange to Hindsight, where entity extraction and consolidation into world/experience facts and mental models happen server-side.
|
||||
|
||||
Both are enabled by default and fail safe — a memory-service hiccup never blocks your agent from responding, and retains are fire-and-forget so they never add turn latency.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install @vectorize-io/hindsight-agentos @vectorize-io/hindsight-client
|
||||
```
|
||||
|
||||
This package targets `@framers/agentos` `>=0.9.0` (declared as a peer dependency).
|
||||
|
||||
## Usage
|
||||
|
||||
```ts
|
||||
import { agent } from "@framers/agentos";
|
||||
import { createHindsightMemory } from "@vectorize-io/hindsight-agentos";
|
||||
import { Hindsight } from "@vectorize-io/hindsight-client";
|
||||
|
||||
const memory = createHindsightMemory({
|
||||
client: new Hindsight({ apiKey: process.env.HINDSIGHT_API_KEY }),
|
||||
bank: "ada",
|
||||
recall: { budget: "high", includeEntities: true },
|
||||
retain: { tags: ["source:agentos"] },
|
||||
});
|
||||
|
||||
const ada = agent({ name: "Ada", memoryProvider: memory });
|
||||
|
||||
await ada.generate("What theme do I prefer?");
|
||||
```
|
||||
|
||||
AgentOS memory-provider hooks receive only turn text (no per-message routing
|
||||
context), so a provider instance maps to exactly one memory **bank**. Give each
|
||||
agent (or user) its own bank for isolation — it defaults to `"default"`.
|
||||
|
||||
## Configuration
|
||||
|
||||
```ts
|
||||
createHindsightMemory({
|
||||
client,
|
||||
|
||||
// Which memory bank this agent reads/writes. Defaults to "default".
|
||||
bank: "ada",
|
||||
|
||||
recall: {
|
||||
enabled: true, // set false to disable recall
|
||||
budget: "mid", // "low" | "mid" | "high" — latency vs. depth
|
||||
types: ["world", "experience"], // restrict to fact types
|
||||
maxTokens: 1000, // cap recalled tokens (else uses AgentOS tokenBudget)
|
||||
includeEntities: false, // include entity observations
|
||||
labelTypes: false, // prefix each memory with its fact kind, e.g. [world]
|
||||
heading: "# Relevant long-term memories", // context-block heading
|
||||
},
|
||||
|
||||
retain: {
|
||||
enabled: true, // set false to disable retain
|
||||
async: true, // fire-and-forget; never adds turn latency
|
||||
tags: ["source:agentos"], // tags on every retained memory
|
||||
metadata: { env: "prod" },
|
||||
includeAgentMessages: false, // also store the agent's own replies
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Using only recall or only retain
|
||||
|
||||
Disable either side with `recall.enabled: false` or `retain.enabled: false`.
|
||||
|
||||
## How it works
|
||||
|
||||
| Hook | AgentOS seam | When it runs | What it does |
|
||||
| --- | --- | --- | --- |
|
||||
| `getContext` | Before generation | Before each model call | Calls Hindsight `recall` with the turn text and returns a context block AgentOS injects into the prompt |
|
||||
| `observe` | After generation | After each turn | Calls Hindsight `retain` to persist the user turn (and optionally the agent's reply) |
|
||||
@@ -1,5 +1,15 @@
|
||||
{
|
||||
"integrations": [
|
||||
{
|
||||
"id": "agentos",
|
||||
"name": "AgentOS",
|
||||
"description": "Give AgentOS agents long-term memory. A memory provider that recalls relevant memories into context before each model call and retains turns to Hindsight.",
|
||||
"type": "official",
|
||||
"by": "hindsight",
|
||||
"category": "framework",
|
||||
"link": "/sdks/integrations/agentos",
|
||||
"icon": "/img/icons/agentos.svg"
|
||||
},
|
||||
{
|
||||
"id": "litellm",
|
||||
"name": "LiteLLM",
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="64" height="64" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="iconGradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#6366F1;stop-opacity:1" />
|
||||
<stop offset="50%" style="stop-color:#8B5CF6;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#EC4899;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
<radialGradient id="centerGradient">
|
||||
<stop offset="0%" style="stop-color:#8B5CF6;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#6366F1;stop-opacity:0.8" />
|
||||
</radialGradient>
|
||||
</defs>
|
||||
<g id="icon-mark">
|
||||
<circle cx="32" cy="32" r="8" fill="url(#centerGradient)"/>
|
||||
<circle cx="32" cy="12" r="5" fill="#6366F1" opacity="0.9"/>
|
||||
<circle cx="48" cy="20" r="5" fill="#8B5CF6" opacity="0.9"/>
|
||||
<circle cx="48" cy="44" r="5" fill="#EC4899" opacity="0.9"/>
|
||||
<circle cx="32" cy="52" r="5" fill="#06B6D4" opacity="0.9"/>
|
||||
<circle cx="16" cy="44" r="5" fill="#8B5CF6" opacity="0.9"/>
|
||||
<circle cx="16" cy="20" r="5" fill="#6366F1" opacity="0.9"/>
|
||||
<path d="M 32 32 L 32 12" stroke="url(#iconGradient)" stroke-width="2" opacity="0.7"/>
|
||||
<path d="M 32 32 L 48 20" stroke="url(#iconGradient)" stroke-width="2" opacity="0.7"/>
|
||||
<path d="M 32 32 L 48 44" stroke="url(#iconGradient)" stroke-width="2" opacity="0.7"/>
|
||||
<path d="M 32 32 L 32 52" stroke="url(#iconGradient)" stroke-width="2" opacity="0.7"/>
|
||||
<path d="M 32 32 L 16 44" stroke="url(#iconGradient)" stroke-width="2" opacity="0.7"/>
|
||||
<path d="M 32 32 L 16 20" stroke="url(#iconGradient)" stroke-width="2" opacity="0.7"/>
|
||||
<circle cx="32" cy="32" r="24" fill="none" stroke="url(#iconGradient)" stroke-width="1" opacity="0.3" stroke-dasharray="2 4"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
@@ -29,6 +29,7 @@ Each integration lives in its own subdirectory with its own README, configuratio
|
||||
| [**Pydantic AI**](./pydantic-ai) | Dependency-injected memory for Pydantic AI agents. |
|
||||
| [**Vercel AI SDK**](./ai-sdk) | Persistent memory for Vercel AI SDK apps. |
|
||||
| [**Vercel Chat**](./chat) | Drop-in memory for the Vercel AI Chatbot. |
|
||||
| [**AgentOS**](./agentos) | `AgentMemoryProvider` for AgentOS — recalls memories into context before each model call, retains turns after. |
|
||||
| [**LangGraph / LangChain**](./langgraph) | Memory Tools, Graph Nodes, and BaseStore adapter patterns. |
|
||||
| [**LlamaIndex**](./llamaindex) | Agent-driven (BaseToolSpec) and automatic (BaseMemory) memory. |
|
||||
| [**Google ADK**](./google-adk) | `BaseMemoryService` implementation for ADK. |
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
node_modules
|
||||
dist
|
||||
*.tsbuildinfo
|
||||
.DS_Store
|
||||
@@ -0,0 +1,83 @@
|
||||
# @vectorize-io/hindsight-agentos
|
||||
|
||||
Long-term memory for [AgentOS](https://github.com/framerslab/agentos) agents, backed by [Hindsight](https://hindsight.vectorize.io).
|
||||
|
||||
`createHindsightMemory` returns an AgentOS [`AgentMemoryProvider`](https://agentos.sh). Attach it to an agent via `memoryProvider` and AgentOS auto-wires it on every call path (`generate`, `stream`, and `session.send` / `session.stream`):
|
||||
|
||||
- **`getContext`** runs _before_ each model call — it recalls relevant memories from Hindsight and injects them into the system prompt.
|
||||
- **`observe`** runs _after_ each turn — it retains the exchange to Hindsight, where entity extraction and consolidation into world/experience facts and mental models happen server-side.
|
||||
|
||||
Both sides are enabled by default and fail safe — a Hindsight outage never blocks the agent from responding, and retains are fire-and-forget so they never add turn latency.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install @vectorize-io/hindsight-agentos @vectorize-io/hindsight-client
|
||||
```
|
||||
|
||||
Requires `@framers/agentos` `>=0.9.0` (peer dependency).
|
||||
|
||||
## Usage
|
||||
|
||||
```ts
|
||||
import { agent } from "@framers/agentos";
|
||||
import { createHindsightMemory } from "@vectorize-io/hindsight-agentos";
|
||||
import { Hindsight } from "@vectorize-io/hindsight-client";
|
||||
|
||||
const memory = createHindsightMemory({
|
||||
client: new Hindsight({ apiKey: process.env.HINDSIGHT_API_KEY }),
|
||||
bank: "ada",
|
||||
recall: { budget: "high", includeEntities: true },
|
||||
retain: { tags: ["source:agentos"] },
|
||||
});
|
||||
|
||||
const ada = agent({ name: "Ada", memoryProvider: memory });
|
||||
|
||||
await ada.generate("What theme do I prefer?");
|
||||
```
|
||||
|
||||
AgentOS memory-provider hooks receive only turn text (no per-message routing
|
||||
context), so a provider instance maps to exactly one memory **bank**. Give each
|
||||
agent (or user) its own bank for isolation — it defaults to `"default"`.
|
||||
|
||||
## Options
|
||||
|
||||
| Option | Description | Default |
|
||||
| ----------------------------- | ----------------------------------------------------- | ------------------------------- |
|
||||
| `client` | A Hindsight client instance | required |
|
||||
| `bank` | Memory bank this agent reads/writes | `"default"` |
|
||||
| `recall.enabled` | Enable memory recall / context injection | `true` |
|
||||
| `recall.budget` | `"low" \| "mid" \| "high"` — latency vs. depth | `"mid"` |
|
||||
| `recall.types` | Restrict to fact types | all |
|
||||
| `recall.maxTokens` | Cap recalled tokens (else uses AgentOS `tokenBudget`) | AgentOS budget |
|
||||
| `recall.includeEntities` | Include entity observations | `false` |
|
||||
| `recall.labelTypes` | Prefix each memory with its fact kind, e.g. `[world]` | `false` |
|
||||
| `recall.heading` | Heading above recalled memories | `# Relevant long-term memories` |
|
||||
| `retain.enabled` | Enable retaining turns | `true` |
|
||||
| `retain.async` | Fire-and-forget (no turn latency) | `true` |
|
||||
| `retain.tags` | Tags on every retained memory | — |
|
||||
| `retain.metadata` | Metadata on every retained memory | — |
|
||||
| `retain.includeAgentMessages` | Also store the agent's replies | `false` |
|
||||
|
||||
### Using only recall or only retain
|
||||
|
||||
Disable either side with `recall.enabled: false` or `retain.enabled: false`.
|
||||
|
||||
## How it works
|
||||
|
||||
| Hook | AgentOS seam | When it runs | What it does |
|
||||
| ------------ | ---------------------------------------- | ---------------------- | ------------------------------------------------------------------------------------ |
|
||||
| `getContext` | Before generation (`onBeforeGeneration`) | Before each model call | Calls Hindsight `recall` and returns a context block AgentOS injects into the prompt |
|
||||
| `observe` | After generation (`onAfterGeneration`) | After each turn | Calls Hindsight `retain` to persist the user turn (and optionally the agent's reply) |
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm test
|
||||
npm run build
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
+5888
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"name": "@vectorize-io/hindsight-agentos",
|
||||
"version": "0.1.0",
|
||||
"description": "Hindsight long-term memory for AgentOS agents - recall relevant memories into context and retain turns via a memory provider",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"keywords": [
|
||||
"agentos",
|
||||
"framers",
|
||||
"memory",
|
||||
"hindsight",
|
||||
"agents",
|
||||
"llm",
|
||||
"long-term-memory",
|
||||
"cognitive-memory"
|
||||
],
|
||||
"author": "Vectorize <[email protected]>",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/vectorize-io/hindsight.git",
|
||||
"directory": "hindsight-integrations/agentos"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"README.md"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsup",
|
||||
"dev": "tsc --watch",
|
||||
"clean": "rm -rf dist",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"prepublishOnly": "npm run clean && npm run build"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@framers/agentos": ">=0.9.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@framers/agentos": "^0.9.164",
|
||||
"@types/node": "^22.0.0",
|
||||
"tsup": "^8.5.1",
|
||||
"typescript": "^5.7.0",
|
||||
"vitest": "^4.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* Minimal Hindsight client surface used by this integration.
|
||||
*
|
||||
* It is a structural subset of `@vectorize-io/hindsight-client` so that
|
||||
* consumers can pass a real client instance without this package taking a hard
|
||||
* dependency on it. Only `recall` and `retain` are required.
|
||||
*/
|
||||
|
||||
/** Processing budget controlling latency vs. depth. */
|
||||
export type Budget = "low" | "mid" | "high";
|
||||
|
||||
/** Fact types for filtering recall results. */
|
||||
export type FactType = "world" | "experience" | "observation";
|
||||
|
||||
/** A single recalled memory. */
|
||||
export interface RecallResult {
|
||||
id: string;
|
||||
text: string;
|
||||
type?: string | null;
|
||||
entities?: string[] | null;
|
||||
context?: string | null;
|
||||
occurred_start?: string | null;
|
||||
occurred_end?: string | null;
|
||||
mentioned_at?: string | null;
|
||||
document_id?: string | null;
|
||||
metadata?: Record<string, string> | null;
|
||||
chunk_id?: string | null;
|
||||
}
|
||||
|
||||
export interface RecallResponse {
|
||||
results: RecallResult[];
|
||||
trace?: Record<string, unknown> | null;
|
||||
entities?: Record<string, unknown> | null;
|
||||
chunks?: Record<string, unknown> | null;
|
||||
}
|
||||
|
||||
export interface RetainResponse {
|
||||
success: boolean;
|
||||
bank_id: string;
|
||||
items_count: number;
|
||||
async: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hindsight client interface - matches `@vectorize-io/hindsight-client`.
|
||||
*/
|
||||
export interface HindsightClient {
|
||||
retain(
|
||||
bankId: string,
|
||||
content: string,
|
||||
options?: {
|
||||
timestamp?: Date | string;
|
||||
context?: string;
|
||||
metadata?: Record<string, string>;
|
||||
documentId?: string;
|
||||
tags?: string[];
|
||||
async?: boolean;
|
||||
}
|
||||
): Promise<RetainResponse>;
|
||||
|
||||
recall(
|
||||
bankId: string,
|
||||
query: string,
|
||||
options?: {
|
||||
types?: FactType[];
|
||||
maxTokens?: number;
|
||||
budget?: Budget;
|
||||
includeEntities?: boolean;
|
||||
includeChunks?: boolean;
|
||||
}
|
||||
): Promise<RecallResponse>;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
export { createHindsightMemory } from "./memory.js";
|
||||
export { DEFAULT_BANK } from "./options.js";
|
||||
export type { HindsightMemoryOptions, RecallOptions, RetainOptions } from "./options.js";
|
||||
export type {
|
||||
HindsightClient,
|
||||
RecallResult,
|
||||
RecallResponse,
|
||||
RetainResponse,
|
||||
Budget,
|
||||
FactType,
|
||||
} from "./client.js";
|
||||
@@ -0,0 +1,105 @@
|
||||
import type { AgentMemoryProvider } from "@framers/agentos";
|
||||
import type { RecallResult } from "./client.js";
|
||||
import { DEFAULT_BANK, type HindsightMemoryOptions } from "./options.js";
|
||||
|
||||
const DEFAULT_HEADING = "# Relevant long-term memories";
|
||||
|
||||
/** Render recalled memories as a markdown bullet list for context injection. */
|
||||
function formatMemories(results: RecallResult[], heading: string, labelTypes: boolean): string {
|
||||
const lines = results
|
||||
.map((r) => {
|
||||
const text = r.text?.trim();
|
||||
if (!text) return undefined;
|
||||
const label = labelTypes && r.type ? `[${r.type}] ` : "";
|
||||
return `- ${label}${text}`;
|
||||
})
|
||||
.filter((line): line is string => Boolean(line));
|
||||
if (lines.length === 0) return "";
|
||||
return `${heading}\n${lines.join("\n")}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an {@link AgentMemoryProvider} that gives an AgentOS agent long-term
|
||||
* memory backed by Hindsight.
|
||||
*
|
||||
* Wire the returned provider into an agent via `memoryProvider`; AgentOS then
|
||||
* auto-invokes it on every call path:
|
||||
*
|
||||
* - `getContext` runs **before** each model call — it recalls relevant memories
|
||||
* from Hindsight and returns them as a context block that AgentOS injects into
|
||||
* the system prompt.
|
||||
* - `observe` runs **after** each turn (once for the user turn, once for the
|
||||
* assistant reply) — it retains the turn to Hindsight so entity extraction and
|
||||
* consolidation happen server-side.
|
||||
*
|
||||
* Both sides fail safe: a recall failure returns no context (the agent still
|
||||
* responds) and retains are fire-and-forget by default so they never add turn
|
||||
* latency. Disable either side with `recall.enabled: false` /
|
||||
* `retain.enabled: false`.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import { agent } from "@framers/agentos";
|
||||
* import { createHindsightMemory } from "@vectorize-io/hindsight-agentos";
|
||||
* import { Hindsight } from "@vectorize-io/hindsight-client";
|
||||
*
|
||||
* const memory = createHindsightMemory({
|
||||
* client: new Hindsight({ apiKey: process.env.HINDSIGHT_API_KEY }),
|
||||
* bank: "ada",
|
||||
* recall: { budget: "high", includeEntities: true },
|
||||
* retain: { tags: ["source:agentos"] },
|
||||
* });
|
||||
*
|
||||
* const ada = agent({ name: "Ada", memoryProvider: memory });
|
||||
* ```
|
||||
*/
|
||||
export function createHindsightMemory(options: HindsightMemoryOptions): AgentMemoryProvider {
|
||||
const { client, bank = DEFAULT_BANK, recall = {}, retain = {} } = options;
|
||||
|
||||
const provider: AgentMemoryProvider = {};
|
||||
|
||||
if (recall.enabled !== false) {
|
||||
const heading = recall.heading ?? DEFAULT_HEADING;
|
||||
provider.getContext = async (text, opts) => {
|
||||
const query = text?.trim();
|
||||
if (!query) return null;
|
||||
try {
|
||||
const response = await client.recall(bank, query, {
|
||||
types: recall.types,
|
||||
maxTokens: recall.maxTokens ?? opts?.tokenBudget,
|
||||
budget: recall.budget,
|
||||
includeEntities: recall.includeEntities,
|
||||
});
|
||||
const results = response.results ?? [];
|
||||
const contextText = formatMemories(results, heading, recall.labelTypes ?? false);
|
||||
return contextText ? { contextText } : null;
|
||||
} catch {
|
||||
// Recall failure is non-fatal; the agent proceeds without memory.
|
||||
return null;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (retain.enabled !== false) {
|
||||
const isAsync = retain.async ?? true;
|
||||
provider.observe = async (role, text) => {
|
||||
const content = text?.trim();
|
||||
if (!content) return;
|
||||
if (role === "assistant" && !retain.includeAgentMessages) return;
|
||||
|
||||
const write = client
|
||||
.retain(bank, content, {
|
||||
async: isAsync,
|
||||
tags: retain.tags,
|
||||
metadata: retain.metadata,
|
||||
})
|
||||
.catch(() => undefined);
|
||||
|
||||
// Fire-and-forget in async mode so retain never adds turn latency;
|
||||
// await only when the caller opted into synchronous writes.
|
||||
if (!isAsync) await write;
|
||||
};
|
||||
}
|
||||
|
||||
return provider;
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import type { Budget, FactType, HindsightClient } from "./client.js";
|
||||
|
||||
export interface RecallOptions {
|
||||
/** Disable memory recall / context injection (default: enabled). */
|
||||
enabled?: boolean;
|
||||
/** Restrict recall to these fact types (default: all). */
|
||||
types?: FactType[];
|
||||
/**
|
||||
* Maximum tokens to return. When omitted, the `tokenBudget` AgentOS passes to
|
||||
* `getContext` is used (defaults to AgentOS's own budget).
|
||||
*/
|
||||
maxTokens?: number;
|
||||
/** Processing budget controlling latency vs. depth (default: 'mid'). */
|
||||
budget?: Budget;
|
||||
/** Include entity observations in recall (default: false). */
|
||||
includeEntities?: boolean;
|
||||
/** Heading rendered above the recalled memories in the injected context. */
|
||||
heading?: string;
|
||||
/**
|
||||
* Prefix each recalled memory with its fact kind, e.g. `[world]` / `[experience]`
|
||||
* (default: false). Keeps provenance visible when the model reasons over the
|
||||
* injected block.
|
||||
*/
|
||||
labelTypes?: boolean;
|
||||
}
|
||||
|
||||
export interface RetainOptions {
|
||||
/** Disable retaining turns (default: enabled). */
|
||||
enabled?: boolean;
|
||||
/** Fire-and-forget retain without awaiting completion (default: true). */
|
||||
async?: boolean;
|
||||
/** Tags attached to every retained memory. */
|
||||
tags?: string[];
|
||||
/** Metadata attached to every retained memory. */
|
||||
metadata?: Record<string, string>;
|
||||
/**
|
||||
* Also retain the agent's own replies, not just the user's turns
|
||||
* (default: false).
|
||||
*/
|
||||
includeAgentMessages?: boolean;
|
||||
}
|
||||
|
||||
export interface HindsightMemoryOptions {
|
||||
/** A Hindsight client instance (e.g. from `@vectorize-io/hindsight-client`). */
|
||||
client: HindsightClient;
|
||||
/**
|
||||
* The memory bank this agent reads and writes. AgentOS memory-provider hooks
|
||||
* receive only turn text (no per-message routing context), so a provider
|
||||
* instance maps to exactly one bank — give each agent/user its own bank for
|
||||
* isolation. Defaults to `"default"`.
|
||||
*/
|
||||
bank?: string;
|
||||
/** Recall (read) behaviour. */
|
||||
recall?: RecallOptions;
|
||||
/** Retain (write) behaviour. */
|
||||
retain?: RetainOptions;
|
||||
}
|
||||
|
||||
/** Default bank used when none is configured. */
|
||||
export const DEFAULT_BANK = "default";
|
||||
@@ -0,0 +1,241 @@
|
||||
import type { AgentMemoryProvider } from "@framers/agentos";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
createHindsightMemory,
|
||||
type HindsightClient,
|
||||
type RecallResponse,
|
||||
type RetainResponse,
|
||||
} from "../src/index.js";
|
||||
|
||||
function mockClient(): HindsightClient {
|
||||
return {
|
||||
recall: vi.fn(
|
||||
async (): Promise<RecallResponse> => ({
|
||||
results: [
|
||||
{ id: "1", text: "User prefers dark mode", type: "world" },
|
||||
{ id: "2", text: "User lives in Berlin", type: "experience" },
|
||||
],
|
||||
})
|
||||
),
|
||||
retain: vi.fn(
|
||||
async (bankId: string): Promise<RetainResponse> => ({
|
||||
success: true,
|
||||
bank_id: bankId,
|
||||
items_count: 1,
|
||||
async: true,
|
||||
})
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Minimal re-implementation of AgentOS's own `applyMemoryProvider` wiring:
|
||||
* call `getContext` before generation and inject the returned block as a
|
||||
* system message, then call `observe` for the user turn and assistant reply
|
||||
* after. Lets the test exercise the provider exactly the way AgentOS does.
|
||||
*/
|
||||
async function runTurn(
|
||||
provider: AgentMemoryProvider,
|
||||
userText: string,
|
||||
assistantText: string,
|
||||
tokenBudget = 2000
|
||||
): Promise<{ role: string; content: string }[]> {
|
||||
const messages: { role: string; content: string }[] = [
|
||||
{ role: "system", content: "You are Ada." },
|
||||
{ role: "user", content: userText },
|
||||
];
|
||||
if (provider.getContext) {
|
||||
const ctx = await provider.getContext(userText, { tokenBudget });
|
||||
if (ctx?.contextText) {
|
||||
messages.splice(1, 0, { role: "system", content: ctx.contextText });
|
||||
}
|
||||
}
|
||||
if (provider.observe) {
|
||||
await provider.observe("user", userText);
|
||||
await provider.observe("assistant", assistantText);
|
||||
}
|
||||
return messages;
|
||||
}
|
||||
|
||||
describe("createHindsightMemory", () => {
|
||||
let client: HindsightClient;
|
||||
|
||||
beforeEach(() => {
|
||||
client = mockClient();
|
||||
});
|
||||
|
||||
it("returns a provider exposing getContext and observe by default", () => {
|
||||
const provider = createHindsightMemory({ client });
|
||||
expect(typeof provider.getContext).toBe("function");
|
||||
expect(typeof provider.observe).toBe("function");
|
||||
});
|
||||
|
||||
it("can disable recall or retain", () => {
|
||||
const provider = createHindsightMemory({
|
||||
client,
|
||||
recall: { enabled: false },
|
||||
retain: { enabled: false },
|
||||
});
|
||||
expect(provider.getContext).toBeUndefined();
|
||||
expect(provider.observe).toBeUndefined();
|
||||
});
|
||||
|
||||
describe("getContext (recall)", () => {
|
||||
it("recalls memories and formats them as a context block", async () => {
|
||||
const provider = createHindsightMemory({ client });
|
||||
const ctx = await provider.getContext!("What theme do I like?", { tokenBudget: 500 });
|
||||
expect(client.recall).toHaveBeenCalledWith("default", "What theme do I like?", {
|
||||
types: undefined,
|
||||
maxTokens: 500,
|
||||
budget: undefined,
|
||||
includeEntities: undefined,
|
||||
});
|
||||
expect(ctx?.contextText).toContain("User prefers dark mode");
|
||||
expect(ctx?.contextText).toContain("User lives in Berlin");
|
||||
});
|
||||
|
||||
it("uses the configured bank", async () => {
|
||||
const provider = createHindsightMemory({ client, bank: "team-bank" });
|
||||
await provider.getContext!("hi");
|
||||
expect(client.recall).toHaveBeenCalledWith("team-bank", "hi", expect.any(Object));
|
||||
});
|
||||
|
||||
it("prefers a configured maxTokens over the hook tokenBudget", async () => {
|
||||
const provider = createHindsightMemory({ client, recall: { maxTokens: 128 } });
|
||||
await provider.getContext!("q", { tokenBudget: 4096 });
|
||||
expect(client.recall).toHaveBeenCalledWith(
|
||||
"default",
|
||||
"q",
|
||||
expect.objectContaining({ maxTokens: 128 })
|
||||
);
|
||||
});
|
||||
|
||||
it("labels fact types when labelTypes is set", async () => {
|
||||
const provider = createHindsightMemory({ client, recall: { labelTypes: true } });
|
||||
const ctx = await provider.getContext!("q");
|
||||
expect(ctx?.contextText).toContain("[world] User prefers dark mode");
|
||||
expect(ctx?.contextText).toContain("[experience] User lives in Berlin");
|
||||
});
|
||||
|
||||
it("returns null for an empty query without calling recall", async () => {
|
||||
const provider = createHindsightMemory({ client });
|
||||
const ctx = await provider.getContext!(" ");
|
||||
expect(client.recall).not.toHaveBeenCalled();
|
||||
expect(ctx).toBeNull();
|
||||
});
|
||||
|
||||
it("returns null when recall yields no memories", async () => {
|
||||
(client.recall as ReturnType<typeof vi.fn>).mockResolvedValueOnce({ results: [] });
|
||||
const provider = createHindsightMemory({ client });
|
||||
expect(await provider.getContext!("q")).toBeNull();
|
||||
});
|
||||
|
||||
it("never throws when recall fails", async () => {
|
||||
(client.recall as ReturnType<typeof vi.fn>).mockRejectedValueOnce(new Error("down"));
|
||||
const provider = createHindsightMemory({ client });
|
||||
expect(await provider.getContext!("hello")).toBeNull();
|
||||
});
|
||||
|
||||
it("passes recall options through to the client", async () => {
|
||||
const provider = createHindsightMemory({
|
||||
client,
|
||||
recall: { budget: "high", types: ["world"], includeEntities: true, maxTokens: 500 },
|
||||
});
|
||||
await provider.getContext!("q", { tokenBudget: 4096 });
|
||||
expect(client.recall).toHaveBeenCalledWith("default", "q", {
|
||||
budget: "high",
|
||||
types: ["world"],
|
||||
includeEntities: true,
|
||||
maxTokens: 500,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("observe (retain)", () => {
|
||||
it("retains the user turn", async () => {
|
||||
const provider = createHindsightMemory({ client, retain: { async: false } });
|
||||
await provider.observe!("user", "Remember I like dark mode");
|
||||
expect(client.retain).toHaveBeenCalledWith("default", "Remember I like dark mode", {
|
||||
async: false,
|
||||
tags: undefined,
|
||||
metadata: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it("skips the assistant reply by default", async () => {
|
||||
const provider = createHindsightMemory({ client, retain: { async: false } });
|
||||
await provider.observe!("assistant", "my reply");
|
||||
expect(client.retain).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("retains assistant replies when includeAgentMessages is set", async () => {
|
||||
const provider = createHindsightMemory({
|
||||
client,
|
||||
retain: { async: false, includeAgentMessages: true },
|
||||
});
|
||||
await provider.observe!("assistant", "agent answer");
|
||||
expect(client.retain).toHaveBeenCalledWith("default", "agent answer", expect.any(Object));
|
||||
});
|
||||
|
||||
it("ignores empty content", async () => {
|
||||
const provider = createHindsightMemory({ client, retain: { async: false } });
|
||||
await provider.observe!("user", " ");
|
||||
expect(client.retain).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("passes tags and metadata through", async () => {
|
||||
const provider = createHindsightMemory({
|
||||
client,
|
||||
retain: { async: false, tags: ["source:agentos"], metadata: { env: "prod" } },
|
||||
});
|
||||
await provider.observe!("user", "hi");
|
||||
expect(client.retain).toHaveBeenCalledWith("default", "hi", {
|
||||
async: false,
|
||||
tags: ["source:agentos"],
|
||||
metadata: { env: "prod" },
|
||||
});
|
||||
});
|
||||
|
||||
it("does not reject when retain fails (async mode)", async () => {
|
||||
(client.retain as ReturnType<typeof vi.fn>).mockRejectedValue(new Error("boom"));
|
||||
const provider = createHindsightMemory({ client });
|
||||
await expect(provider.observe!("user", "hi")).resolves.toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("AgentOS turn wiring", () => {
|
||||
it("injects recalled memory into the prompt and retains the user turn", async () => {
|
||||
const provider = createHindsightMemory({ client, bank: "ada", retain: { async: false } });
|
||||
const messages = await runTurn(provider, "What theme do I like?", "You like dark mode.");
|
||||
|
||||
// Recall block injected after the leading system message, before the user turn.
|
||||
expect(messages[1].role).toBe("system");
|
||||
expect(messages[1].content).toContain("User prefers dark mode");
|
||||
expect(messages[0].content).toBe("You are Ada.");
|
||||
|
||||
expect(client.recall).toHaveBeenCalledWith(
|
||||
"ada",
|
||||
"What theme do I like?",
|
||||
expect.any(Object)
|
||||
);
|
||||
// User turn retained; assistant reply skipped by default.
|
||||
expect(client.retain).toHaveBeenCalledTimes(1);
|
||||
expect(client.retain).toHaveBeenCalledWith(
|
||||
"ada",
|
||||
"What theme do I like?",
|
||||
expect.any(Object)
|
||||
);
|
||||
});
|
||||
|
||||
it("retains both turns when includeAgentMessages is set", async () => {
|
||||
const provider = createHindsightMemory({
|
||||
client,
|
||||
bank: "ada",
|
||||
retain: { async: false, includeAgentMessages: true },
|
||||
});
|
||||
await runTurn(provider, "hi", "hello there");
|
||||
expect(client.retain).toHaveBeenCalledWith("ada", "hi", expect.any(Object));
|
||||
expect(client.retain).toHaveBeenCalledWith("ada", "hello there", expect.any(Object));
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ES2022",
|
||||
"lib": ["ES2022"],
|
||||
"moduleResolution": "node",
|
||||
"declaration": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist", "src/**/*.test.ts", "tests"]
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { defineConfig } from "tsup";
|
||||
|
||||
export default defineConfig({
|
||||
entry: ["src/index.ts"],
|
||||
format: ["esm"],
|
||||
dts: true,
|
||||
outDir: "dist",
|
||||
clean: true,
|
||||
sourcemap: true,
|
||||
bundle: true,
|
||||
external: ["@framers/agentos"],
|
||||
});
|
||||
@@ -0,0 +1,9 @@
|
||||
import { defineConfig } from "vitest/config";
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
globals: true,
|
||||
environment: "node",
|
||||
include: ["tests/**/*.test.ts", "src/**/*.test.ts"],
|
||||
},
|
||||
});
|
||||
@@ -13,7 +13,7 @@ print_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
|
||||
print_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
|
||||
print_error() { echo -e "${RED}[ERROR]${NC} $1"; }
|
||||
|
||||
VALID_INTEGRATIONS=("ag2" "agent-framework" "agentcore" "agno" "aider" "ai-sdk" "autogen" "chat" "claude-agent-sdk" "claude-code" "cline" "cloudflare-oauth-proxy" "codex" "composio" "continue" "crewai" "cursor" "cursor-cli" "devin-desktop" "dify" "eve" "flowise" "gemini-spark" "github-copilot" "google-adk" "haystack" "langgraph" "litellm" "llamaindex" "n8n" "nemoclaw" "obsidian" "omo" "openai-agents" "openclaw" "opencode" "openhands" "paperclip" "pipecat" "pydantic-ai" "roo-code" "smolagents" "strands" "superagent" "vapi" "zcode" "zed")
|
||||
VALID_INTEGRATIONS=("ag2" "agent-framework" "agentcore" "agentos" "agno" "aider" "ai-sdk" "autogen" "chat" "claude-agent-sdk" "claude-code" "cline" "cloudflare-oauth-proxy" "codex" "composio" "continue" "crewai" "cursor" "cursor-cli" "devin-desktop" "dify" "eve" "flowise" "gemini-spark" "github-copilot" "google-adk" "haystack" "langgraph" "litellm" "llamaindex" "n8n" "nemoclaw" "obsidian" "omo" "openai-agents" "openclaw" "opencode" "openhands" "paperclip" "pipecat" "pydantic-ai" "roo-code" "smolagents" "strands" "superagent" "vapi" "zcode" "zed")
|
||||
|
||||
usage() {
|
||||
print_error "Usage: $0 <integration> <version>"
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
|
||||
# AgentOS
|
||||
|
||||
The `@vectorize-io/hindsight-agentos` package gives [AgentOS](https://github.com/framerslab/agentos) agents long-term memory backed by [Hindsight](https://hindsight.vectorize.io).
|
||||
|
||||
`createHindsightMemory` returns an AgentOS `AgentMemoryProvider`. Attach it to an agent via `memoryProvider` and AgentOS auto-wires it on every call path (`generate`, `stream`, and `session.send` / `session.stream`):
|
||||
|
||||
- **`getContext`** runs *before* each model call — it recalls relevant memories from Hindsight and injects them into the system prompt.
|
||||
- **`observe`** runs *after* each turn — it retains the exchange to Hindsight, where entity extraction and consolidation into world/experience facts and mental models happen server-side.
|
||||
|
||||
Both are enabled by default and fail safe — a memory-service hiccup never blocks your agent from responding, and retains are fire-and-forget so they never add turn latency.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install @vectorize-io/hindsight-agentos @vectorize-io/hindsight-client
|
||||
```
|
||||
|
||||
This package targets `@framers/agentos` `>=0.9.0` (declared as a peer dependency).
|
||||
|
||||
## Usage
|
||||
|
||||
```ts
|
||||
import { agent } from "@framers/agentos";
|
||||
import { createHindsightMemory } from "@vectorize-io/hindsight-agentos";
|
||||
import { Hindsight } from "@vectorize-io/hindsight-client";
|
||||
|
||||
const memory = createHindsightMemory({
|
||||
client: new Hindsight({ apiKey: process.env.HINDSIGHT_API_KEY }),
|
||||
bank: "ada",
|
||||
recall: { budget: "high", includeEntities: true },
|
||||
retain: { tags: ["source:agentos"] },
|
||||
});
|
||||
|
||||
const ada = agent({ name: "Ada", memoryProvider: memory });
|
||||
|
||||
await ada.generate("What theme do I prefer?");
|
||||
```
|
||||
|
||||
AgentOS memory-provider hooks receive only turn text (no per-message routing
|
||||
context), so a provider instance maps to exactly one memory **bank**. Give each
|
||||
agent (or user) its own bank for isolation — it defaults to `"default"`.
|
||||
|
||||
## Configuration
|
||||
|
||||
```ts
|
||||
createHindsightMemory({
|
||||
client,
|
||||
|
||||
// Which memory bank this agent reads/writes. Defaults to "default".
|
||||
bank: "ada",
|
||||
|
||||
recall: {
|
||||
enabled: true, // set false to disable recall
|
||||
budget: "mid", // "low" | "mid" | "high" — latency vs. depth
|
||||
types: ["world", "experience"], // restrict to fact types
|
||||
maxTokens: 1000, // cap recalled tokens (else uses AgentOS tokenBudget)
|
||||
includeEntities: false, // include entity observations
|
||||
labelTypes: false, // prefix each memory with its fact kind, e.g. [world]
|
||||
heading: "# Relevant long-term memories", // context-block heading
|
||||
},
|
||||
|
||||
retain: {
|
||||
enabled: true, // set false to disable retain
|
||||
async: true, // fire-and-forget; never adds turn latency
|
||||
tags: ["source:agentos"], // tags on every retained memory
|
||||
metadata: { env: "prod" },
|
||||
includeAgentMessages: false, // also store the agent's own replies
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Using only recall or only retain
|
||||
|
||||
Disable either side with `recall.enabled: false` or `retain.enabled: false`.
|
||||
|
||||
## How it works
|
||||
|
||||
| Hook | AgentOS seam | When it runs | What it does |
|
||||
| --- | --- | --- | --- |
|
||||
| `getContext` | Before generation | Before each model call | Calls Hindsight `recall` with the turn text and returns a context block AgentOS injects into the prompt |
|
||||
| `observe` | After generation | After each turn | Calls Hindsight `retain` to persist the user turn (and optionally the agent's reply) |
|
||||
Reference in New Issue
Block a user