90ce622080
Add runtime truthfulness modes and autonomy-level tool gating with audit metadata for overrides/denials. Wire policy through prompt assembly, tool execution context, and daemon/gateway agent paths; update tests and planning state for Phase 3 PR #2 completion.
134 lines
4.6 KiB
TypeScript
134 lines
4.6 KiB
TypeScript
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,
|
|
};
|
|
}
|