// v1.12: extracted from inference.ts to give the prompt-assembly logic its // own home + test surface. Adds the container-guidance layer (BOOCHAT.md // baked into the Docker image, injected between the base prompt and the // agent block). // // Resolution order, last-wins on conflicts: // base prompt // + container guidance (this layer, NEW in v1.12) // + agent.system_prompt (resolved from data/AGENTS.md by getAgentById) // + session.system_prompt OR project.default_system_prompt import { readFile, stat } from 'node:fs/promises'; import type { Agent, Project, Session } from '../types/api.js'; const BASE_SYSTEM_PROMPT = (projectPath: string) => `You are BooCode Chat, a code investigation assistant. The user is working on a project located at ${projectPath}. Use the file-read tools (view_file, list_dir, grep, find_files) to investigate code when needed. Be concise. Cite file paths and line numbers when discussing code. Do not hallucinate file contents — read the file first. Tool results may be truncated; if so, narrow your query rather than guessing.`; // v1.12 mtime-watch cache. Mirrors the safeStat pattern in services/agents.ts. // On every call we stat the file; if the mtime matches the cached entry we // return the cached content without re-reading. If the file is missing we // cache { mtime: 0, content: null } so the not-found case still benefits // from caching (one stat per call, no readFile attempt on a known-missing // path). Because BOOCHAT.md is bind-mounted from the host, edits land // immediately on the next chat turn — no container restart needed. let cachedGuidance: { mtime: number; content: string | null } | null = null; function resolveGuidancePath(): string { return process.env['CONTAINER_GUIDANCE_FILE'] ?? '/app/BOOCHAT.md'; } export async function loadContainerGuidance(): Promise { const path = resolveGuidancePath(); try { return await readFile(path, 'utf8'); } catch { return null; } } export async function getContainerGuidance(): Promise { const path = resolveGuidancePath(); let mtimeMs: number; try { const s = await stat(path); mtimeMs = s.mtimeMs; } catch { cachedGuidance = { mtime: 0, content: null }; return null; } if (cachedGuidance && cachedGuidance.mtime === mtimeMs) { return cachedGuidance.content; } const content = await loadContainerGuidance(); cachedGuidance = { mtime: mtimeMs, content }; return content; } // Test-only: clear the cache so consecutive tests don't share state. export function _resetContainerGuidanceCacheForTests(): void { cachedGuidance = null; } export async function buildSystemPrompt( project: Project, session: Session, agent: Agent | null ): Promise { let out = BASE_SYSTEM_PROMPT(project.path); const guidance = await getContainerGuidance(); if (guidance) { out += `\n\n--- Container guidance ---\n${guidance}\n--- end container guidance ---\n`; } if (agent && agent.system_prompt.trim().length > 0) { out += '\n\n' + agent.system_prompt.trim(); } const sessionPrompt = session.system_prompt?.trim() ?? ''; const projectPrompt = project.default_system_prompt?.trim() ?? ''; const userPrompt = sessionPrompt || projectPrompt; if (userPrompt.length > 0) { out += '\n\n' + userPrompt; } return out; }