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:
+46
-9
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user