feat: bootstrap context из AGENT.md и memory/ файлов

- loadBootstrapContext() загружает AGENT.md, memory/notes.md, memory/projects/*.md
- Лимит 15K символов, автотрункейт
- agentHome передаётся из router в agent options
- System prompt из agent.json сохранён как fallback
This commit is contained in:
Markov 2026-02-24 22:04:29 +01:00
parent dea2bcaef6
commit db90b64f54
2 changed files with 60 additions and 1 deletions

View File

@ -29,6 +29,8 @@ export interface AgentOptions {
allowedPaths?: string[];
/** Additional custom tools (e.g. tracker tools) */
customTools?: ToolDefinition[];
/** Agent home directory for loading bootstrap files (AGENT.md, memory/) */
agentHome?: string;
}
export interface AgentMessage {
@ -37,6 +39,53 @@ export interface AgentMessage {
sessionId?: string;
}
// --- Bootstrap context loader ---
const BOOTSTRAP_FILES = ['AGENT.md', 'memory/notes.md'];
const BOOTSTRAP_MAX_CHARS = 15_000;
function loadBootstrapContext(agentHome: string): string {
const parts: string[] = [];
let totalChars = 0;
for (const relPath of BOOTSTRAP_FILES) {
const filePath = path.join(agentHome, relPath);
try {
if (!fs.existsSync(filePath)) continue;
const content = fs.readFileSync(filePath, 'utf-8').trim();
if (!content) continue;
const remaining = BOOTSTRAP_MAX_CHARS - totalChars;
if (remaining <= 0) break;
const truncated = content.length > remaining ? content.slice(0, remaining) + '\n[...truncated]' : content;
parts.push(`## ${relPath}\n${truncated}`);
totalChars += truncated.length;
} catch {
// skip unreadable files
}
}
// Also load per-project memory files
const projectMemDir = path.join(agentHome, 'memory', 'projects');
try {
if (fs.existsSync(projectMemDir)) {
for (const file of fs.readdirSync(projectMemDir).filter(f => f.endsWith('.md'))) {
const filePath = path.join(projectMemDir, file);
const content = fs.readFileSync(filePath, 'utf-8').trim();
if (!content) continue;
const remaining = BOOTSTRAP_MAX_CHARS - totalChars;
if (remaining <= 200) break;
const truncated = content.length > remaining ? content.slice(0, remaining) + '\n[...truncated]' : content;
parts.push(`## memory/projects/${file}\n${truncated}`);
totalChars += truncated.length;
}
}
} catch {
// skip
}
if (parts.length === 0) return '';
return `# Agent Context (bootstrap)\n\n${parts.join('\n\n')}`;
}
// --- Model alias map: short name → full model ID ---
const MODEL_ALIASES: Record<string, string> = {
'sonnet': 'claude-sonnet-4-6',
@ -185,6 +234,15 @@ export async function* runAgent(
// Build system prompt additions
const promptParts: string[] = [];
// Load bootstrap context from agent home (AGENT.md, memory/)
if (options.agentHome) {
const bootstrapContext = loadBootstrapContext(options.agentHome);
if (bootstrapContext) {
promptParts.push(bootstrapContext);
log.info({ chars: bootstrapContext.length }, 'Bootstrap context loaded');
}
}
if (skillsXml) {
promptParts.push(
'## Skills (mandatory)',

View File

@ -100,13 +100,14 @@ export class EventRouter {
sessionId: this.config.sessionId,
model: this.config.model,
provider: this.config.provider,
systemPrompt: this.config.prompt || undefined,
systemPrompt: this.config.prompt || undefined, // fallback if no AGENT.md
skillsDir: this.config.agentHome,
sessionDir: path.join(this.config.agentHome, 'sessions'),
allowedPaths: this.config.allowedPaths.length > 0
? this.config.allowedPaths
: [this.config.agentHome],
customTools: this.trackerTools,
agentHome: this.config.agentHome,
})) {
if (msg.type === 'error') {
this.log.error({ error: msg.content }, 'Agent error');