feat: add multi-model delegation (Phase 0) and context compaction (Phase 1)

Phase 0 — Multi-Model Delegation:
- AgentOrchestrator wraps NativeAgent with delegate() for stateless
  single-turn calls to any model tier (fast/default/complex/local)
- DelegationConfig maps task types (compaction, classification, etc.)
  to model tiers
- Delegation prompts for compaction, memory extraction, classification,
  and tool summarisation
- Per-tier usage tracking for cost visibility
- Config schema: agents.delegation and agents.primary_tier

Phase 1 — Context Compaction:
- Token estimation (char/4 heuristic) with context window lookup
- shouldCompact() threshold check against context window percentage
- compactHistory() splits old/recent messages, delegates summary to
  fast tier, returns CompactionResult
- Automatic compaction in AgentOrchestrator.process() when configured
- Force-compact via orchestrator.compact() with session persistence
- Session.replaceHistory() with atomic SQLite transaction
- /compact TUI command with feedback on compacted token counts
- Config schema: compaction.enabled, threshold_pct, keep_turns,
  summary_max_tokens

Tests: 385 passing across 50 files (22 new tests in 2 new test files)
This commit is contained in:
William Valentin
2026-02-06 13:17:02 -08:00
parent f7cc87a4bb
commit 306e11bd2e
22 changed files with 1562 additions and 12 deletions
+46 -9
View File
@@ -2,7 +2,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 { NativeAgent } from '../backends/index.js';
import { AgentOrchestrator, type DelegationConfig } from '../backends/index.js';
import { SessionStore, SessionManager } from '../session/index.js';
import { HookEngine } from '../hooks/index.js';
import { ToolRegistry, ToolExecutor, allBuiltinTools } from '../tools/index.js';
@@ -134,7 +134,8 @@ function createModelRouter(config: Config): ModelRouter {
/**
* Create the unified message handler for the channel registry.
* Each channel+sender pair gets its own NativeAgent backed by a persistent session.
* Each channel+sender pair gets its own AgentOrchestrator backed by a persistent session.
* The orchestrator wraps a NativeAgent and adds delegation to different model tiers.
*/
function createMessageRouter(deps: {
sessionManager: SessionManager;
@@ -142,21 +143,39 @@ function createMessageRouter(deps: {
systemPrompt: string;
toolRegistry: ToolRegistry;
toolExecutor: ToolExecutor;
config: Config;
}) {
// Cache agents by session ID to avoid recreating on every message
const agents = new Map<string, NativeAgent>();
const agents = new Map<string, AgentOrchestrator>();
function getOrCreateAgent(channel: string, senderId: string): NativeAgent {
function getOrCreateAgent(channel: string, senderId: string): AgentOrchestrator {
const sessionId = `${channel}:${senderId}`;
let agent = agents.get(sessionId);
if (!agent) {
const session = deps.sessionManager.getSession(channel, senderId);
agent = new NativeAgent({
modelClient: deps.modelRouter,
const delegationConfig: DelegationConfig = {
compaction: deps.config.agents.delegation.compaction ?? 'fast',
memory_extraction: deps.config.agents.delegation.memory_extraction ?? 'fast',
classification: deps.config.agents.delegation.classification ?? 'fast',
tool_summarisation: deps.config.agents.delegation.tool_summarisation ?? 'fast',
complex_reasoning: deps.config.agents.delegation.complex_reasoning ?? 'complex',
};
agent = new AgentOrchestrator({
modelRouter: deps.modelRouter,
systemPrompt: deps.systemPrompt,
session,
toolRegistry: deps.toolRegistry,
toolExecutor: deps.toolExecutor,
primaryTier: deps.config.agents.primary_tier ?? 'default',
delegation: delegationConfig,
maxDelegationDepth: deps.config.agents.max_delegation_depth ?? 3,
compaction: deps.config.compaction.enabled ? {
thresholdPct: deps.config.compaction.threshold_pct,
keepTurns: deps.config.compaction.keep_turns,
summaryMaxTokens: deps.config.compaction.summary_max_tokens,
} : undefined,
modelName: deps.config.models.default.model,
contextWindow: deps.config.models.default.context_window,
});
agents.set(sessionId, agent);
}
@@ -167,9 +186,26 @@ function createMessageRouter(deps: {
const agent = getOrCreateAgent(msg.channel, msg.senderId);
// Handle special commands
if (msg.metadata?.isCommand && msg.metadata.command === 'reset') {
agent.reset();
return;
if (msg.metadata?.isCommand) {
if (msg.metadata.command === 'reset') {
agent.reset();
return;
}
if (msg.metadata.command === 'compact') {
const result = await agent.compact();
if (result && result.compactedCount > 0) {
await reply({
text: `Compacted ${result.compactedCount} messages: ${result.tokensBefore}${result.tokensAfter} tokens`,
replyTo: msg.id,
});
} else {
await reply({
text: 'Nothing to compact.',
replyTo: msg.id,
});
}
return;
}
}
try {
@@ -284,6 +320,7 @@ export async function startDaemon(config: Config): Promise<DaemonContext> {
systemPrompt,
toolRegistry,
toolExecutor,
config,
}));
// Register Telegram adapter