Files
nanobot-ts/src/agent/context.ts

142 lines
4.9 KiB
TypeScript

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<Record<string, unknown>>;
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');
}
}