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
72 changes: 72 additions & 0 deletions .changeset/soft-taxis-dress.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
---
"@voltagent/serverless-hono": patch
"@voltagent/server-elysia": patch
"@voltagent/server-core": patch
"@voltagent/server-hono": patch
---

feat: add memory HTTP endpoints for conversations, messages, working memory, and search across server-core, Hono, Elysia, and serverless runtimes.

### Endpoints

- `GET /api/memory/conversations`
- `POST /api/memory/conversations`
- `GET /api/memory/conversations/:conversationId`
- `PATCH /api/memory/conversations/:conversationId`
- `DELETE /api/memory/conversations/:conversationId`
- `POST /api/memory/conversations/:conversationId/clone`
- `GET /api/memory/conversations/:conversationId/messages`
- `GET /api/memory/conversations/:conversationId/working-memory`
- `POST /api/memory/conversations/:conversationId/working-memory`
- `POST /api/memory/save-messages`
- `POST /api/memory/messages/delete`
- `GET /api/memory/search`

Note: include `agentId` (query/body) when multiple agents are registered or no global memory is configured.

### Examples

Create a conversation:

```bash
curl -X POST http://localhost:3141/api/memory/conversations \
-H "Content-Type: application/json" \
-d '{
"userId": "user-123",
"resourceId": "assistant",
"title": "Support Chat",
"metadata": { "channel": "web" }
}'
```

Save messages into the conversation:

```bash
curl -X POST http://localhost:3141/api/memory/save-messages \
-H "Content-Type: application/json" \
-d '{
"userId": "user-123",
"conversationId": "conv-001",
"messages": [
{ "role": "user", "content": "Hi there" },
{ "role": "assistant", "content": "Hello!" }
]
}'
```

Update working memory (append mode):

```bash
curl -X POST http://localhost:3141/api/memory/conversations/conv-001/working-memory \
-H "Content-Type: application/json" \
-d '{
"content": "Customer prefers email follow-ups.",
"mode": "append"
}'
```

Search memory (requires embedding + vector adapters):

```bash
curl "http://localhost:3141/api/memory/search?searchQuery=refund%20policy&limit=5"
```
38 changes: 38 additions & 0 deletions packages/cloudflare-d1/src/memory-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -789,6 +789,23 @@ export class D1MemoryAdapter implements StorageAdapter {
);
}

async deleteMessages(
messageIds: string[],
userId: string,
conversationId: string,
): Promise<void> {
await this.ensureInitialized();

if (messageIds.length === 0) {
return;
}

const messagesTable = `${this.tablePrefix}_messages`;
const placeholders = messageIds.map(() => "?").join(",");
const sql = `DELETE FROM ${messagesTable} WHERE conversation_id = ? AND user_id = ? AND message_id IN (${placeholders})`;
await this.run(sql, [conversationId, userId, ...messageIds]);
}

// ==========================================================================
// Conversation Operations
// ==========================================================================
Expand Down Expand Up @@ -962,6 +979,27 @@ export class D1MemoryAdapter implements StorageAdapter {
}));
}

async countConversations(options: ConversationQueryOptions): Promise<number> {
await this.ensureInitialized();

const conversationsTable = `${this.tablePrefix}_conversations`;
let sql = `SELECT COUNT(*) as count FROM ${conversationsTable} WHERE 1=1`;
const args: unknown[] = [];

if (options.userId) {
sql += " AND user_id = ?";
args.push(options.userId);
}

if (options.resourceId) {
sql += " AND resource_id = ?";
args.push(options.resourceId);
}

const rows = await this.all<{ count?: number }>(sql, args);
return rows[0]?.count ?? 0;
}

async updateConversation(
id: string,
updates: Partial<Omit<Conversation, "id" | "createdAt" | "updatedAt">>,
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,7 @@ export type {
ManagedMemoryAddMessagesInput,
ManagedMemoryGetMessagesInput,
ManagedMemoryClearMessagesInput,
ManagedMemoryDeleteMessagesInput,
ManagedMemoryUpdateConversationInput,
ManagedMemoryWorkingMemoryInput,
ManagedMemorySetWorkingMemoryInput,
Expand Down
38 changes: 37 additions & 1 deletion packages/core/src/memory/adapters/storage/in-memory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ export class InMemoryStorageAdapter implements StorageAdapter {
options?: GetMessagesOptions,
_context?: OperationContext,
): Promise<UIMessage<{ createdAt: Date }>[]> {
const { limit = 100, before, after, roles } = options || {};
const { limit, before, after, roles } = options || {};

// Get user's messages or return empty array
const userMessages = this.storage[userId] || {};
Expand Down Expand Up @@ -208,6 +208,25 @@ export class InMemoryStorageAdapter implements StorageAdapter {
}));
}

/**
* Delete specific messages by ID for a conversation
*/
async deleteMessages(
messageIds: string[],
userId: string,
conversationId: string,
_context?: OperationContext,
): Promise<void> {
if (!this.storage[userId]?.[conversationId]) {
return;
}

const ids = new Set(messageIds);
this.storage[userId][conversationId] = this.storage[userId][conversationId].filter(
(message) => !ids.has(message.id),
);
}

/**
* Clear messages for a user
*/
Expand Down Expand Up @@ -336,6 +355,23 @@ export class InMemoryStorageAdapter implements StorageAdapter {
return conversations.map((c) => deepClone(c));
}

/**
* Count conversations matching query filters
*/
async countConversations(options: ConversationQueryOptions): Promise<number> {
let conversations = Array.from(this.conversations.values());

if (options.userId) {
conversations = conversations.filter((c) => c.userId === options.userId);
}

if (options.resourceId) {
conversations = conversations.filter((c) => c.resourceId === options.resourceId);
}

return conversations.length;
}

/**
* Update a conversation
*/
Expand Down
32 changes: 32 additions & 0 deletions packages/core/src/memory/index.spec-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ describe("Memory V2 Type System", () => {
addMessages: async () => {},
getMessages: async () => [],
clearMessages: async () => {},
deleteMessages: async () => {},
createConversation: async () => ({
id: "test",
resourceId: "res",
Expand All @@ -38,6 +39,7 @@ describe("Memory V2 Type System", () => {
getConversations: async () => [],
getConversationsByUserId: async () => [],
queryConversations: async () => [],
countConversations: async () => 0,
updateConversation: async () => ({
id: "test",
resourceId: "res",
Expand Down Expand Up @@ -152,6 +154,15 @@ describe("Memory V2 Type System", () => {
expectTypeOf(adapter.getMessages).returns.toMatchTypeOf<Promise<UIMessage[]>>();
});

it("should enforce messageIds for deleteMessages", () => {
const adapter: StorageAdapter = mockStorageAdapter;

expectTypeOf(adapter.deleteMessages).parameters.toMatchTypeOf<
[string[], string, string, OperationContext?]
>();
expectTypeOf(adapter.deleteMessages).returns.toMatchTypeOf<Promise<void>>();
});

it("should enforce Conversation type for createConversation", () => {
const adapter: StorageAdapter = mockStorageAdapter;

Expand All @@ -169,6 +180,15 @@ describe("Memory V2 Type System", () => {
>();
expectTypeOf(adapter.queryConversations).returns.toMatchTypeOf<Promise<Conversation[]>>();
});

it("should return number from countConversations", () => {
const adapter: StorageAdapter = mockStorageAdapter;

expectTypeOf(adapter.countConversations).parameters.toMatchTypeOf<
[ConversationQueryOptions]
>();
expectTypeOf(adapter.countConversations).returns.toMatchTypeOf<Promise<number>>();
});
});

describe("EmbeddingAdapter Interface", () => {
Expand Down Expand Up @@ -245,6 +265,18 @@ describe("Memory V2 Type System", () => {

expectTypeOf(memory.clearMessages).returns.toMatchTypeOf<Promise<void>>();
});

it("should return void for deleteMessages", () => {
const memory = new Memory({ storage: mockStorageAdapter });

expectTypeOf(memory.deleteMessages).returns.toMatchTypeOf<Promise<void>>();
});

it("should return number for countConversations", () => {
const memory = new Memory({ storage: mockStorageAdapter });

expectTypeOf(memory.countConversations).returns.toMatchTypeOf<Promise<number>>();
});
});

describe("Type Parameter Constraints", () => {
Expand Down
32 changes: 32 additions & 0 deletions packages/core/src/memory/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,31 @@ export class Memory {
return this.storage.clearMessages(userId, conversationId, context);
}

/**
* Delete specific messages by ID for a conversation
* Adapters should delete atomically when possible; otherwise a best-effort delete may be used.
*/
async deleteMessages(
messageIds: string[],
userId: string,
conversationId: string,
context?: OperationContext,
): Promise<void> {
await this.storage.deleteMessages(messageIds, userId, conversationId, context);

if (this.vector && messageIds.length > 0) {
try {
const vectorIds = messageIds.map((id) => `msg_${conversationId}_${id}`);
await this.vector.deleteBatch(vectorIds);
} catch (error) {
console.warn(
`Failed to delete vectors for conversation ${conversationId} messages:`,
error,
);
}
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

async getConversationSteps(
userId: string,
conversationId: string,
Expand Down Expand Up @@ -180,6 +205,13 @@ export class Memory {
return this.storage.queryConversations(options);
}

/**
* Count conversations with the same filtering as queryConversations (ignores limit/offset)
*/
async countConversations(options: ConversationQueryOptions): Promise<number> {
return this.storage.countConversations(options);
}

/**
* Create a new conversation
*/
Expand Down
15 changes: 15 additions & 0 deletions packages/core/src/memory/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,17 @@ export interface StorageAdapter {
context?: OperationContext,
): Promise<UIMessage<{ createdAt: Date }>[]>;
clearMessages(userId: string, conversationId?: string, context?: OperationContext): Promise<void>;
/**
* Delete specific messages by ID for a conversation.
* Adapters should perform an atomic delete when possible. If atomic deletes or transactions
* are unavailable, a best-effort deletion (for example, clear + rehydrate) may be used.
*/
deleteMessages(
messageIds: string[],
userId: string,
conversationId: string,
context?: OperationContext,
): Promise<void>;

// Conversation operations
createConversation(input: CreateConversationInput): Promise<Conversation>;
Expand All @@ -368,6 +379,10 @@ export interface StorageAdapter {
options?: Omit<ConversationQueryOptions, "userId">,
): Promise<Conversation[]>;
queryConversations(options: ConversationQueryOptions): Promise<Conversation[]>;
/**
* Count conversations matching query filters (limit/offset ignored).
*/
countConversations(options: ConversationQueryOptions): Promise<number>;
updateConversation(
id: string,
updates: Partial<Omit<Conversation, "id" | "createdAt" | "updatedAt">>,
Expand Down
17 changes: 17 additions & 0 deletions packages/core/src/voltops/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import type {
ManagedMemoryCredentialCreateResult,
ManagedMemoryCredentialListResult,
ManagedMemoryDatabaseSummary,
ManagedMemoryDeleteMessagesInput,
ManagedMemoryDeleteVectorsInput,
ManagedMemoryGetConversationStepsInput,
ManagedMemoryGetMessagesInput,
Expand Down Expand Up @@ -439,6 +440,7 @@ export class VoltOpsClient implements IVoltOpsClient {
addBatch: (databaseId, input) => this.addManagedMemoryMessages(databaseId, input),
list: (databaseId, input) => this.getManagedMemoryMessages(databaseId, input),
clear: (databaseId, input) => this.clearManagedMemoryMessages(databaseId, input),
delete: (databaseId, input) => this.deleteManagedMemoryMessages(databaseId, input),
},
conversations: {
create: (databaseId, input) => this.createManagedMemoryConversation(databaseId, input),
Expand Down Expand Up @@ -600,6 +602,21 @@ export class VoltOpsClient implements IVoltOpsClient {
}
}

private async deleteManagedMemoryMessages(
databaseId: string,
input: ManagedMemoryDeleteMessagesInput,
): Promise<void> {
const payload = await this.request<{ success: boolean }>(
"POST",
`/managed-memory/projects/databases/${databaseId}/messages/delete`,
input,
);

if (!payload?.success) {
throw new Error("Failed to delete managed memory messages via VoltOps");
}
}

private async storeManagedMemoryVector(
databaseId: string,
input: ManagedMemoryStoreVectorInput,
Expand Down
7 changes: 7 additions & 0 deletions packages/core/src/voltops/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1116,6 +1116,12 @@ export interface ManagedMemoryClearMessagesInput {
conversationId?: string;
}

export interface ManagedMemoryDeleteMessagesInput {
conversationId: string;
userId: string;
messageIds: string[];
}

export interface ManagedMemoryGetConversationStepsInput {
conversationId: string;
userId: string;
Expand Down Expand Up @@ -1178,6 +1184,7 @@ export interface ManagedMemoryMessagesClient {
addBatch(databaseId: string, input: ManagedMemoryAddMessagesInput): Promise<void>;
list(databaseId: string, input: ManagedMemoryGetMessagesInput): Promise<UIMessage[]>;
clear(databaseId: string, input: ManagedMemoryClearMessagesInput): Promise<void>;
delete(databaseId: string, input: ManagedMemoryDeleteMessagesInput): Promise<void>;
}

export interface ManagedMemoryConversationsClient {
Expand Down
Loading