- 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
161 lines
5.6 KiB
TypeScript
161 lines
5.6 KiB
TypeScript
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}` };
|
|
},
|
|
};
|