feat: claude one-shot port from nanobot python codebase (v0.1.4.post4)
This commit is contained in:
141
src/agent/context.ts
Normal file
141
src/agent/context.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
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');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user