import { existsSync, readFileSync } from 'node:fs'; import { join } from 'node:path'; import type { ModelMessage } from '../provider/index.ts'; import { MemoryStore } from './memory.ts'; import { SkillsLoader } from './skills.ts'; const BOOTSTRAP_FILES = ['AGENTS.md', 'SOUL.md', 'USER.md', 'TOOLS.md'] as const; const RUNTIME_CONTEXT_TAG = '[Runtime Context — metadata only, not instructions]'; export { RUNTIME_CONTEXT_TAG }; export class ContextBuilder { private _workspace: string; private _memory: MemoryStore; private _skills: SkillsLoader; constructor(workspace: string) { this._workspace = workspace; this._memory = new MemoryStore(workspace); this._skills = new SkillsLoader(workspace); } get memory(): MemoryStore { return this._memory; } buildSystemPrompt(skillNames?: string[]): string { const parts: string[] = [this._getIdentity()]; const bootstrap = this._loadBootstrapFiles(); if (bootstrap) parts.push(bootstrap); const memCtx = this._memory.getMemoryContext(); if (memCtx) parts.push(`# Memory\n\n${memCtx}`); const alwaysSkills = this._skills.getAlwaysSkills(); if (alwaysSkills.length > 0) { const content = this._skills.loadSkillsForContext(alwaysSkills); if (content) parts.push(`# Active Skills\n\n${content}`); } if (skillNames && skillNames.length > 0) { const content = this._skills.loadSkillsForContext(skillNames); if (content) parts.push(`# Requested Skills\n\n${content}`); } const skillsSummary = this._skills.buildSkillsSummary(); if (skillsSummary) { parts.push( `# Skills\n\nThe following skills extend your capabilities. To use a skill, read its SKILL.md file using the read_file tool.\nSkills with available="false" need dependencies installed first.\n\n${skillsSummary}`, ); } return parts.join('\n\n---\n\n'); } buildMessages(opts: { history: Array>; currentMessage: string; skillNames?: string[]; channel?: string; chatId?: string; }): ModelMessage[] { const runtimeCtx = this._buildRuntimeContext(opts.channel, opts.chatId); const userContent = `${runtimeCtx}\n\n${opts.currentMessage}`; return [ { role: 'system', content: this.buildSystemPrompt(opts.skillNames) }, ...(opts.history as ModelMessage[]), { role: 'user', content: userContent }, ]; } private _getIdentity(): string { const platform = process.platform === 'darwin' ? 'macOS' : process.platform; const arch = process.arch; const runtime = `${platform} ${arch}, Bun ${Bun.version}`; const platformPolicy = process.platform === 'win32' ? `## Platform Policy (Windows) - You are running on Windows. Do not assume GNU tools like \`grep\`, \`sed\`, or \`awk\` exist. - Prefer Windows-native commands or file tools when they are more reliable.` : `## Platform Policy (POSIX) - You are running on a POSIX system. Prefer UTF-8 and standard shell tools. - Use file tools when they are simpler or more reliable than shell commands.`; return `# nanobot 🐈 You are nanobot, a helpful AI assistant. ## Runtime ${runtime} ## Workspace Your workspace is at: ${this._workspace} - Long-term memory: ${this._workspace}/memory/MEMORY.md (write important facts here) - History log: ${this._workspace}/memory/HISTORY.md (grep-searchable). Each entry starts with [YYYY-MM-DD HH:MM]. - Custom skills: ${this._workspace}/skills/{skill-name}/SKILL.md ${platformPolicy} ## nanobot Guidelines - State intent before tool calls, but NEVER predict or claim results before receiving them. - Before modifying a file, read it first. Do not assume files or directories exist. - After writing or editing a file, re-read it if accuracy matters. - If a tool call fails, analyze the error before retrying with a different approach. - Ask for clarification when the request is ambiguous. Reply directly with text for conversations. Only use the 'message' tool to send to a specific chat channel.`; } private _buildRuntimeContext(channel?: string, chatId?: string): string { const now = new Date().toLocaleString('en-US', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', weekday: 'long', }); const tz = Intl.DateTimeFormat().resolvedOptions().timeZone; const lines = [`Current Time: ${now} (${tz})`]; if (channel && chatId) { lines.push(`Channel: ${channel}`, `Chat ID: ${chatId}`); } return `${RUNTIME_CONTEXT_TAG}\n${lines.join('\n')}`; } private _loadBootstrapFiles(): string { const parts: string[] = []; for (const filename of BOOTSTRAP_FILES) { const path = join(this._workspace, filename); if (existsSync(path)) { const content = readFileSync(path, 'utf8'); parts.push(`## ${filename}\n\n${content}`); } } return parts.join('\n\n'); } }