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:
+59
-15
@@ -1,7 +1,7 @@
|
||||
import { Lifecycle } from './lifecycle.js';
|
||||
import type { Config, ModelConfig } from '../config/index.js';
|
||||
import { AnthropicClient, OpenAIClient, OllamaClient, LlamaCppClient, ModelRouter } from '../models/index.js';
|
||||
import type { ModelClient } from '../models/index.js';
|
||||
import { AnthropicClient, OpenAIClient, OllamaClient, LlamaCppClient, ModelRouter, DEFAULT_RETRY_CONFIG } from '../models/index.js';
|
||||
import type { ModelClient, RetryConfig } from '../models/index.js';
|
||||
import { AgentOrchestrator, type DelegationConfig } from '../backends/index.js';
|
||||
import { SessionStore, SessionManager } from '../session/index.js';
|
||||
import { HookEngine } from '../hooks/index.js';
|
||||
@@ -14,9 +14,10 @@ import { CronScheduler } from '../automation/index.js';
|
||||
import type { InboundMessage, OutboundMessage } from '../channels/index.js';
|
||||
import { McpManager } from '../mcp/index.js';
|
||||
import { SkillRegistry, SkillInstaller, loadAllSkills } from '../skills/index.js';
|
||||
import { assembleSystemPrompt } from '../prompt/index.js';
|
||||
import { resolve } from 'path';
|
||||
import { homedir } from 'os';
|
||||
import { mkdirSync, readFileSync, existsSync } from 'fs';
|
||||
import { mkdirSync } from 'fs';
|
||||
|
||||
export interface DaemonContext {
|
||||
config: Config;
|
||||
@@ -34,21 +35,23 @@ export interface DaemonContext {
|
||||
skillInstaller: SkillInstaller;
|
||||
}
|
||||
|
||||
function loadSystemPrompt(): string {
|
||||
// Try to load SOUL.md from working directory first, then from project root
|
||||
const paths = [
|
||||
resolve(process.cwd(), 'SOUL.md'),
|
||||
resolve(import.meta.dirname, '../../SOUL.md'),
|
||||
function loadSystemPrompt(config: Config): string {
|
||||
const searchDirs = [
|
||||
process.cwd(),
|
||||
resolve(import.meta.dirname, '../..'),
|
||||
...(config.prompt.search_dirs ?? []),
|
||||
];
|
||||
|
||||
for (const soulPath of paths) {
|
||||
if (existsSync(soulPath)) {
|
||||
return readFileSync(soulPath, 'utf-8');
|
||||
}
|
||||
const result = assembleSystemPrompt({
|
||||
searchDirs,
|
||||
extraSections: config.prompt.extra_sections,
|
||||
});
|
||||
|
||||
if (result.loadedFiles.length > 0) {
|
||||
console.log(`Loaded prompt templates: ${result.loadedFiles.map(f => f.split('/').pop()).join(', ')}`);
|
||||
}
|
||||
|
||||
// Fallback if SOUL.md not found
|
||||
return 'You are Flynn, a helpful personal AI assistant. Be direct, concise, and helpful. Use markdown when it improves readability.';
|
||||
return result.prompt;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -125,12 +128,26 @@ function createModelRouter(config: Config): ModelRouter {
|
||||
console.log(`Model router: default=${models.default.provider}/${models.default.model}, ` +
|
||||
`fallback=[${models.fallback_chain.join(', ')}]`);
|
||||
|
||||
// Build retry config if enabled
|
||||
const retryConfig: RetryConfig | undefined = config.retry.enabled ? {
|
||||
maxRetries: config.retry.max_retries,
|
||||
initialDelayMs: config.retry.initial_delay_ms,
|
||||
backoffMultiplier: config.retry.backoff_multiplier,
|
||||
maxDelayMs: config.retry.max_delay_ms,
|
||||
nonRetryablePatterns: DEFAULT_RETRY_CONFIG.nonRetryablePatterns,
|
||||
} : undefined;
|
||||
|
||||
if (retryConfig) {
|
||||
console.log(`Retry policy: max_retries=${retryConfig.maxRetries}, initial_delay=${retryConfig.initialDelayMs}ms`);
|
||||
}
|
||||
|
||||
return new ModelRouter({
|
||||
default: defaultClient,
|
||||
fast: fastClient,
|
||||
complex: complexClient,
|
||||
local: localClient,
|
||||
fallbackChain,
|
||||
retryConfig,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -210,6 +227,33 @@ function createMessageRouter(deps: {
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (msg.metadata.command === 'usage') {
|
||||
const usage = agent.getUsage();
|
||||
const lines = [
|
||||
'**Token Usage**',
|
||||
'',
|
||||
`Primary: ${usage.primary.inputTokens.toLocaleString()} in / ${usage.primary.outputTokens.toLocaleString()} out (${usage.primary.calls} calls)`,
|
||||
];
|
||||
|
||||
const delegationEntries = Object.entries(usage.delegation);
|
||||
if (delegationEntries.length > 0) {
|
||||
lines.push('');
|
||||
lines.push('Delegation:');
|
||||
for (const [tier, stats] of delegationEntries) {
|
||||
lines.push(` ${tier}: ${stats.inputTokens.toLocaleString()} in / ${stats.outputTokens.toLocaleString()} out (${stats.calls} calls)`);
|
||||
}
|
||||
}
|
||||
|
||||
lines.push('');
|
||||
lines.push(`**Total:** ${usage.total.inputTokens.toLocaleString()} in / ${usage.total.outputTokens.toLocaleString()} out (${usage.total.calls} calls)`);
|
||||
|
||||
if (usage.total.estimatedCost > 0) {
|
||||
lines.push(`**Estimated cost:** $${usage.total.estimatedCost.toFixed(4)}`);
|
||||
}
|
||||
|
||||
await reply({ text: lines.join('\n'), replyTo: msg.id });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -331,7 +375,7 @@ export async function startDaemon(config: Config): Promise<DaemonContext> {
|
||||
const modelRouter = createModelRouter(config);
|
||||
|
||||
// Load system prompt and append skill instructions
|
||||
let systemPrompt = loadSystemPrompt();
|
||||
let systemPrompt = loadSystemPrompt(config);
|
||||
const skillAdditions = skillRegistry.getSystemPromptAdditions();
|
||||
if (skillAdditions) {
|
||||
systemPrompt = `${systemPrompt}\n\n# Available Skills\n\n${skillAdditions}`;
|
||||
|
||||
Reference in New Issue
Block a user