import { readFileSync, existsSync } from 'fs'; import { resolve } from 'path'; import type { ContextLevel, TruthfulnessMode } from '../config/schema.js'; import { getTruthfulnessGuidance } from '../backends/native/guardrails.js'; /** Ordered list of prompt template files to look for. */ const PROMPT_FILES = [ { name: 'SOUL.md', section: 'Identity', required: false }, { name: 'AGENTS.md', section: 'Agent Instructions', required: false }, { name: 'IDENTITY.md', section: 'Identity Customization', required: false }, { name: 'USER.md', section: 'User Context', required: false }, { name: 'TOOLS.md', section: 'Tool Instructions', required: false }, ] as const; export interface PromptTemplateConfig { /** Directories to search for template files, in priority order. */ searchDirs: string[]; /** Additional sections to inject (e.g., from config). */ extraSections?: Array<{ name: string; content: string }>; /** Prompt context depth. Defaults to normal. */ contextLevel?: ContextLevel; /** Truthfulness enforcement mode. Defaults to standard. */ truthfulnessMode?: TruthfulnessMode; } export interface PromptTemplateResult { /** The assembled system prompt. */ prompt: string; /** Which files were loaded. */ loadedFiles: string[]; } /** * Assemble a system prompt from multiple template files. * * Searches `searchDirs` in order for each template file. * First match wins — a file found in an earlier directory takes precedence. * Sections are assembled in the order defined in PROMPT_FILES. */ export function assembleSystemPrompt(config: PromptTemplateConfig): PromptTemplateResult { const level = config.contextLevel ?? 'normal'; const truthfulnessMode = config.truthfulnessMode ?? 'standard'; const includeAllTemplates = level !== 'minimal'; const includeExtraSections = level !== 'minimal'; const includeDebugSection = level === 'debug'; const includeTruthfulness = truthfulnessMode === 'strict' || truthfulnessMode === 'standard'; const sections: string[] = []; const loadedFiles: string[] = []; for (const { name, section } of PROMPT_FILES) { if (!includeAllTemplates && name !== 'SOUL.md') { continue; } for (const dir of config.searchDirs) { const filePath = resolve(dir, name); if (existsSync(filePath)) { const content = readFileSync(filePath, 'utf-8').trim(); if (content) { // SOUL.md is special — it's the base identity, no section header if (name === 'SOUL.md') { sections.push(content); } else { sections.push(`# ${section}\n\n${content}`); } loadedFiles.push(filePath); } break; // First match wins for this file } } } // Add extra sections if (includeExtraSections && config.extraSections) { for (const { name, content } of config.extraSections) { if (content.trim()) { sections.push(`# ${name}\n\n${content.trim()}`); } } } // Inject truthfulness guidance (for strict and standard modes) if (includeTruthfulness) { sections.push(getTruthfulnessGuidance(truthfulnessMode)); } // Inject current date/time as runtime context const now = new Date(); const dateStr = now.toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', }); const timeStr = now.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', timeZoneName: 'short', hour12: false, }); const runtimeContext = `# Runtime Context\n\nCurrent date: ${dateStr}\nCurrent time: ${timeStr}`; sections.push(runtimeContext); if (includeDebugSection) { const loadedFilesList = loadedFiles.length > 0 ? loadedFiles.map((filePath) => `- ${filePath}`).join('\n') : '- none'; const searchDirsList = config.searchDirs.length > 0 ? config.searchDirs.map((dir) => `- ${dir}`).join('\n') : '- none'; sections.push( `# Prompt Debug\n\nContext level: ${level}\n\nLoaded files:\n${loadedFilesList}\n\nDirectory resolution notes:\n${searchDirsList}\n- First match wins per template file.`, ); } // Fallback when no prompt template files were found. if (loadedFiles.length === 0) { const truthfulnessSection = includeTruthfulness ? `${getTruthfulnessGuidance(truthfulnessMode)}\n\n` : ''; return { prompt: `You are Flynn, a helpful personal AI assistant. Be direct, concise, and helpful. Use markdown when it improves readability.\n\n${truthfulnessSection}${runtimeContext}`, loadedFiles: [], }; } return { prompt: sections.join('\n\n'), loadedFiles, }; }