feat(server): inference state-graph + supervisor, memory tools, MCP client, schema, routes
- Add state-graph.ts: typed state machine for inference lifecycle - Add supervisor.ts: agent supervisor pattern for multi-agent coordination - Add export-formatter.ts: structured export formatting - Add manage_memory.ts: memory CRUD tool for agent persistence - Add get_wiki_article.ts: codecontext wiki article retrieval - Extend memory/index.ts: 3-tier memory (context/daily/core) - Extend MCP client: mcp-config.ts env-var substitution - Update schema.sql: agent_sessions, tasks, pending_changes extensions - Update API types: MessageMetadata, ErrorReason, AgentSessionConfig - Update routes: chats, messages, sessions — column renames and agent_session_id - Update inference: error handler, payload builder, stream phase, turn orchestrator
This commit is contained in:
160
apps/server/src/services/tools/manage_memory.ts
Normal file
160
apps/server/src/services/tools/manage_memory.ts
Normal file
@@ -0,0 +1,160 @@
|
||||
import { z } from 'zod';
|
||||
import { existsSync } from 'node:fs';
|
||||
import { writeFile, unlink } from 'node:fs/promises';
|
||||
import { join } from 'node:path';
|
||||
import type { ToolDef } from '../tools/types.js';
|
||||
import { ensureMemoryScaffold, getMemoryRoot } from '../memory/paths.js';
|
||||
import { writeEntry, readTopicFiles } from '../memory/store.js';
|
||||
|
||||
const ManageMemoryInput = z.object({
|
||||
topic: z.enum(['project', 'user', 'reference']).describe('Memory topic category'),
|
||||
title: z.string().min(1).max(200).describe('Entry title (used as identifier for update/delete)'),
|
||||
content: z.string().optional().describe('Memory content body (required for create/update)'),
|
||||
tags: z.array(z.string()).optional().describe('Optional tags for search'),
|
||||
action: z.enum(['create', 'update', 'delete']).describe('Action to perform'),
|
||||
});
|
||||
|
||||
type InputT = z.infer<typeof ManageMemoryInput>;
|
||||
|
||||
function titleToFilename(title: string): string {
|
||||
return (
|
||||
title
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.replace(/(^-|-$)/g, '') + '.md'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to update the CoreTier SQLite database in addition to the file store.
|
||||
* This is best-effort — CoreTier is optional (file store is primary).
|
||||
*/
|
||||
async function syncCoreTier(
|
||||
_root: string,
|
||||
_topic: string,
|
||||
_title: string,
|
||||
_content: string,
|
||||
_tags: string[],
|
||||
): Promise<void> {
|
||||
// CoreTier SQLite backend is not available in this build — file store only.
|
||||
}
|
||||
|
||||
export const manageMemoryTool: ToolDef<InputT> = {
|
||||
name: 'manage_memory',
|
||||
description:
|
||||
'Create, update, or delete memory entries in .boocode/memory/ for cross-session recall. ' +
|
||||
'Use to persist project conventions, user preferences, and architectural decisions. ' +
|
||||
'Actions: create (write new entry), update (modify existing entry), delete (remove entry).',
|
||||
inputSchema: ManageMemoryInput,
|
||||
jsonSchema: {
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'manage_memory',
|
||||
description: 'Manage memory entries — create, update, or delete',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
topic: {
|
||||
type: 'string',
|
||||
enum: ['project', 'user', 'reference'],
|
||||
description: 'Memory topic category',
|
||||
},
|
||||
title: { type: 'string', description: 'Entry title (identifier for update/delete)' },
|
||||
content: {
|
||||
type: 'string',
|
||||
description: 'Memory content body (required for create/update)',
|
||||
},
|
||||
tags: {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
description: 'Optional tags for search',
|
||||
},
|
||||
action: {
|
||||
type: 'string',
|
||||
enum: ['create', 'update', 'delete'],
|
||||
description: 'Action to perform',
|
||||
},
|
||||
},
|
||||
required: ['topic', 'title', 'action'],
|
||||
},
|
||||
},
|
||||
},
|
||||
async execute(input: InputT, projectRoot: string): Promise<unknown> {
|
||||
const root = getMemoryRoot(projectRoot);
|
||||
await ensureMemoryScaffold(root);
|
||||
const filename = titleToFilename(input.title);
|
||||
|
||||
if (input.action === 'create') {
|
||||
if (!input.content) {
|
||||
return { error: 'Content is required for create action.' };
|
||||
}
|
||||
await writeEntry(root, input.topic, input.title, input.content, input.tags ?? []);
|
||||
await syncCoreTier(root, input.topic, input.title, input.content, input.tags ?? []);
|
||||
return {
|
||||
result: `Memory entry "${input.title}" created in .boocode/memory/${input.topic}/`,
|
||||
};
|
||||
}
|
||||
|
||||
if (input.action === 'update') {
|
||||
if (!input.content) {
|
||||
return { error: 'Content is required for update action.' };
|
||||
}
|
||||
|
||||
// Resolve target file path — try computed filename first, then heading match
|
||||
let targetPath = join(root, input.topic, filename);
|
||||
if (!existsSync(targetPath)) {
|
||||
const files = await readTopicFiles(root, input.topic);
|
||||
const matched = [...files.keys()].find((name) => {
|
||||
const content = files.get(name);
|
||||
return content?.trimStart().startsWith(`## ${input.topic}: ${input.title}`);
|
||||
});
|
||||
if (matched) {
|
||||
targetPath = join(root, input.topic, matched);
|
||||
} else {
|
||||
return {
|
||||
error: `Memory entry "${input.title}" not found in .boocode/memory/${input.topic}/`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const tagLine =
|
||||
(input.tags ?? []).length > 0
|
||||
? `> tags: ${(input.tags ?? []).join(', ')}\n\n`
|
||||
: '\n';
|
||||
const entry = `## ${input.topic}: ${input.title}\n${tagLine}${input.content}\n`;
|
||||
await writeFile(targetPath, entry, 'utf8');
|
||||
|
||||
await syncCoreTier(root, input.topic, input.title, input.content, input.tags ?? []);
|
||||
return {
|
||||
result: `Memory entry "${input.title}" updated in .boocode/memory/${input.topic}/`,
|
||||
};
|
||||
}
|
||||
|
||||
if (input.action === 'delete') {
|
||||
// Resolve target file path
|
||||
let targetPath = join(root, input.topic, filename);
|
||||
if (!existsSync(targetPath)) {
|
||||
const files = await readTopicFiles(root, input.topic);
|
||||
const matched = [...files.keys()].find((name) => {
|
||||
const content = files.get(name);
|
||||
return content?.trimStart().startsWith(`## ${input.topic}: ${input.title}`);
|
||||
});
|
||||
if (matched) {
|
||||
targetPath = join(root, input.topic, matched);
|
||||
} else {
|
||||
return {
|
||||
error: `Memory entry "${input.title}" not found in .boocode/memory/${input.topic}/`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
await unlink(targetPath);
|
||||
|
||||
return {
|
||||
result: `Memory entry "${input.title}" deleted from .boocode/memory/${input.topic}/`,
|
||||
};
|
||||
}
|
||||
|
||||
return { error: `Unknown action: ${input.action}` };
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user