Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/lazy-poets-relax.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@rushdb/agent-memory-contract': minor
---

Fix CI for memory agent contract
35 changes: 9 additions & 26 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ concurrency:
# ✓ Build & Type-check
# ✓ Lint
# ✓ Test
# ✓ Docs Build
# ──────────────────────────────────────────────────────────────────────────────

jobs:
Expand Down Expand Up @@ -46,6 +45,12 @@ jobs:
- name: Build javascript-sdk
run: pnpm --filter ./packages/javascript-sdk build

- name: Build agent-memory-contract
run: pnpm --filter ./packages/agent-memory-contract build

- name: Verify agent-memory-contract package
run: pnpm --filter ./packages/agent-memory-contract pack:check

- name: Build mcp-server
run: pnpm --filter ./packages/mcp-server build

Expand All @@ -60,9 +65,6 @@ jobs:
- name: Type-check javascript-sdk
run: pnpm --filter ./packages/javascript-sdk types:check

- name: Type-check docs
run: pnpm --filter ./docs types:check

# ── ESLint across all packages that define a lint script ────────────────────
lint:
name: Lint
Expand Down Expand Up @@ -116,30 +118,11 @@ jobs:
- name: Build javascript-sdk
run: pnpm --filter ./packages/javascript-sdk build

- name: Test agent-memory-contract
run: pnpm --filter ./packages/agent-memory-contract test

# 9 pure unit tests for the Cypher query parser — no DB or network required.
- name: Test platform/core (unit)
run: pnpm --filter ./platform/core test
env:
NODE_ENV: test

# ── Docusaurus build (validates all internal links and MDX syntax) ───────────
docs:
name: Docs Build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: pnpm/action-setup@v4
with:
version: 10.1.0

- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Build docs
run: pnpm --filter ./docs build
7 changes: 5 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,10 @@ jobs:
run: pnpm install --frozen-lockfile

- name: Build packages
run: pnpm --filter ./packages/javascript-sdk... --filter ./packages/mcp-server... build
run: pnpm --filter ./packages/javascript-sdk... --filter ./packages/agent-memory-contract... --filter ./packages/mcp-server... build

- name: Verify agent-memory-contract package
run: pnpm --filter ./packages/agent-memory-contract pack:check

- name: Setup .npmrc
run: |
Expand All @@ -106,7 +109,7 @@ jobs:

# Opens/updates the "version packages" PR when changesets are pending,
# publishes to npm when the versioned commit lands. `changeset publish`
# covers every public workspace package (sdk, mcp-server, skills).
# covers every public workspace package (sdk, memory contract, mcp-server, skills).
- name: Create Release PR or Publish
id: changesets
if: github.event_name == 'push'
Expand Down
3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,7 @@
},
"workspaces": [
"packages/*",
"platform/*",
"docs"
"platform/*"
],
"dependencies": {
"npm-run-all": "^4.1.5"
Expand Down
164 changes: 164 additions & 0 deletions packages/agent-memory-contract/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
<div align="center">

![RushDB Logo](https://raw.githubusercontent.com/rush-db/rushdb/main/rushdb-logo.svg)

# RushDB Agent Memory Contract

### One memory protocol for OpenClaw, Hermes, and custom agent runtimes.

[![npm](https://img.shields.io/npm/v/%40rushdb%2Fagent-memory-contract)](https://www.npmjs.com/package/@rushdb/agent-memory-contract)
[![license](https://img.shields.io/npm/l/%40rushdb%2Fagent-memory-contract)](#license)

[Website](https://rushdb.com) · [Documentation](https://docs.rushdb.com) · [RushDB Cloud](https://app.rushdb.com)

</div>

---

`@rushdb/agent-memory-contract` is the provider-neutral TypeScript boundary for durable agent memory
in RushDB. It gives runtime adapters a shared event model, deterministic identities, strict scope
filters, semantic recall primitives, and language-neutral conformance resources.

Use it when building a native memory integration for an agent runtime. It keeps stored episodes and
facts compatible across adapters without forcing runtimes to share lifecycle code.

## What it provides

- `AgentMemoryEvent v1`, with `EPISODE` and `MEMORY_FACT` variants
- deterministic SHA-256 `eventId` and `factId` generation
- participant-scope hashing and complete authorization query filters
- idempotent RushDB upserts for episodes and facts
- semantic recall across episode summaries and active facts
- a bounded recent-write cache for recall before embeddings become visible
- formatting that labels recalled memory as untrusted historical context
- a published JSON Schema and cross-language conformance fixture

## Installation

```bash
pnpm add @rushdb/agent-memory-contract
```

## Quick start

```typescript
import {
RushDBAgentMemory,
createEpisodeEvent,
formatRecalledMemories,
hashScope
} from '@rushdb/agent-memory-contract'

const scope = {
agentId: 'support-agent',
profileId: 'default',
privacyScope: 'private' as const,
participantScopeHash: hashScope(['discord', 'account-1', 'user-42'], process.env.MEMORY_SCOPE_SALT),
sandboxEligible: false
}

const memory = new RushDBAgentMemory({
apiKey: process.env.RUSHDB_API_KEY
})

await memory.ensureIndexes()

await memory.persistEpisode(
createEpisodeEvent({
...scope,
runtime: 'custom',
externalSessionId: 'session-42',
sourceEventId: 'turn-7',
turnIndex: 7,
userText: 'Prefer TypeScript for this service.',
assistantText: 'I will use TypeScript.',
summary: 'The user prefers TypeScript for this service.',
conversationKind: 'direct',
visibility: 'participant',
trustClass: 'mixed',
originClass: 'conversation',
observedAt: new Date().toISOString(),
provenance: 'custom:completed_turn'
})
)

const recalled = await memory.recall({
...scope,
query: 'Which language does the user prefer?',
excludeSessionId: 'session-42',
limit: 6
})

const context = formatRecalledMemories(recalled)
```

`ensureIndexes()` creates managed embedding indexes for `EPISODE.summary` and
`MEMORY_FACT.text` when they do not already exist.

## Event model

| Event | RushDB label | Purpose |
| -------------- | ------------- | ---------------------------------------------------------- |
| Episode | `EPISODE` | A bounded, completed user/assistant interaction |
| Canonical fact | `MEMORY_FACT` | A durable fact produced by an explicit, trusted write path |

Every event includes the authorization scope:

```text
agentId
profileId
privacyScope
participantScopeHash
sandboxEligible
```

Recall applies all five fields before similarity ranking. Fact recall additionally requires
`active: true`; episode recall can exclude the current external session.

Derive scope only from trusted host metadata. Never accept agent, participant, privacy, or sandbox
scope from model output or prompt text. Use separate RushDB projects when hard tenant isolation is
required.

## Package exports

| Export | Contents |
| ---------------------------------------- | ------------------------------------------------------ |
| `@rushdb/agent-memory-contract` | TypeScript types, canonical helpers, client, formatter |
| `@rushdb/agent-memory-contract/schema` | AgentMemoryEvent v1 JSON Schema |
| `@rushdb/agent-memory-contract/fixtures` | Deterministic cross-language conformance fixture |

Other languages should implement the published schema and produce the same IDs as the conformance
fixture.

## Adapter responsibilities

This package is a protocol and client primitive, not a complete runtime adapter. The host
integration remains responsible for:

- proving a conversation is eligible before deriving scope
- capturing only the latest successful, bounded user/assistant pair
- excluding system prompts, tool transcripts, secrets, command output, and local paths
- applying a short fail-open timeout to recall
- writing to a durable outbox before background persistence
- retrying and replaying pending writes without losing idempotency
- deactivating a prior fact when publishing a replacement via `supersedesFactId`
- clearing session state and performing bounded flushes on session end and shutdown

Treat formatted recall as historical data, never as instructions or policy.

## Development

From the RushDB monorepo root:

```bash
pnpm --filter ./packages/agent-memory-contract types:check
pnpm --filter ./packages/agent-memory-contract test
pnpm --filter ./packages/agent-memory-contract pack:check
```

`pack:check` builds the package and verifies that the npm tarball contains the README, runtime
JavaScript, TypeScript declarations, JSON Schema, and conformance fixture.

## License

Apache-2.0
3 changes: 3 additions & 0 deletions packages/agent-memory-contract/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,15 @@
"./fixtures": "./fixtures/conformance.v1.json"
},
"files": [
"README.md",
"dist",
"schema",
"fixtures"
],
"scripts": {
"build": "rimraf dist && tsc",
"pack:check": "node scripts/verify-package.mjs",
"prepack": "pnpm build",
"types:check": "tsc --noEmit",
"test": "vitest run"
},
Expand Down
51 changes: 51 additions & 0 deletions packages/agent-memory-contract/scripts/verify-package.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { spawnSync } from 'node:child_process'
import { mkdtempSync, readdirSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'

const packageDirectory = dirname(dirname(fileURLToPath(import.meta.url)))
const packDirectory = mkdtempSync(join(tmpdir(), 'rushdb-agent-memory-contract-'))
const requiredFiles = [
'package/README.md',
'package/dist/index.js',
'package/dist/index.d.ts',
'package/schema/agent-memory-event.v1.schema.json',
'package/fixtures/conformance.v1.json'
]

function run(command, args, options = {}) {
const result = spawnSync(command, args, {
cwd: packageDirectory,
encoding: 'utf8',
...options
})
if (result.status !== 0) {
process.stderr.write(result.stdout)
process.stderr.write(result.stderr)
process.exit(result.status ?? 1)
}
return result.stdout
}

try {
run('npm', ['pack', '--pack-destination', packDirectory], {
env: { ...process.env, npm_config_cache: join(packDirectory, 'npm-cache') }
})
const tarball = readdirSync(packDirectory).find((entry) => entry.endsWith('.tgz'))
if (!tarball) throw new Error('npm pack did not create a tarball')

const files = new Set(
run('tar', ['-tzf', join(packDirectory, tarball)])
.trim()
.split('\n')
)
const missing = requiredFiles.filter((file) => !files.has(file))
if (missing.length > 0) {
throw new Error(`Agent memory contract package is missing: ${missing.join(', ')}`)
}

console.log(`Verified ${tarball}: ${requiredFiles.length} required files are present`)
} finally {
rmSync(packDirectory, { recursive: true, force: true })
}
Loading