84 lines
3.3 KiB
TypeScript
84 lines
3.3 KiB
TypeScript
// 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<string | null> {
|
|
const path = resolveGuidancePath();
|
|
try {
|
|
return await readFile(path, 'utf8');
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export async function getContainerGuidance(): Promise<string | null> {
|
|
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<string> {
|
|
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;
|
|
}
|