feat: add P2 features — retry policy, prompt templating, usage tracking, tech debt cleanup

- Extract shared splitMessage() into channels/utils.ts (dedup 4 adapters)
- Add Slack user name resolution with caching (users.info API)
- Add withRetry() with exponential backoff + jitter, isRetryable() filter
- Wire retry config into ModelRouter.chat() (non-streaming only)
- Add assembleSystemPrompt() multi-file template system (SOUL/AGENTS/IDENTITY/USER/TOOLS.md)
- Add usage tracking accumulators in NativeAgent + AgentOrchestrator
- Add estimateCost() with per-model pricing table
- Add /usage TUI command with full usage report formatting
- Add retrySchema and promptSchema to config schema

Tests: 569 passing, typecheck clean
This commit is contained in:
William Valentin
2026-02-06 15:12:35 -08:00
parent de68deb1b2
commit 4316dbd3be
24 changed files with 902 additions and 143 deletions
+78
View File
@@ -0,0 +1,78 @@
import { readFileSync, existsSync } from 'fs';
import { resolve } from 'path';
/** 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 }>;
}
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 sections: string[] = [];
const loadedFiles: string[] = [];
for (const { name, section } of PROMPT_FILES) {
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 (config.extraSections) {
for (const { name, content } of config.extraSections) {
if (content.trim()) {
sections.push(`# ${name}\n\n${content.trim()}`);
}
}
}
// Fallback if nothing was loaded
if (sections.length === 0) {
return {
prompt: 'You are Flynn, a helpful personal AI assistant. Be direct, concise, and helpful. Use markdown when it improves readability.',
loadedFiles: [],
};
}
return {
prompt: sections.join('\n\n'),
loadedFiles,
};
}