refactor(agent): 将 Summary Model 改造为内置 Sub Agent

- 扩展 AgentMode 类型添加 'internal' 模式
- 新增 summary agent preset (claude-3-5-haiku)
- AgentRegistry 添加 getInternal/listInternalAgents 方法
- CompressionManager 添加 setSummaryModelFromAgentConfig
- Agent 构造函数改用 Registry 配置初始化 Summary 模型
- 清理旧的 SummaryConfig 配置系统
- UI AgentsPanel 分离显示 System/Preset/Custom agents
- UI AgentEditor 为 internal agent 显示简化编辑界面
This commit is contained in:
2025-12-14 22:12:36 +08:00
parent e97daaa0eb
commit c307cd3a7c
20 changed files with 339 additions and 594 deletions
+3 -1
View File
@@ -5,6 +5,7 @@ import { codeReviewerAgent } from './code-reviewer.js';
import { buildAgent } from './build.js';
import { planAgent } from './plan.js';
import { visionAgent } from './vision.js';
import { summaryAgent } from './summary.js';
/**
* 预设 Agent 集合
@@ -16,6 +17,7 @@ export const presetAgents: Record<string, Omit<AgentInfo, 'name'>> = {
build: buildAgent,
plan: planAgent,
vision: visionAgent,
summary: summaryAgent,
};
/**
@@ -32,4 +34,4 @@ export function isPresetAgent(name: string): boolean {
return name in presetAgents;
}
export { generalAgent, exploreAgent, codeReviewerAgent, buildAgent, planAgent, visionAgent };
export { generalAgent, exploreAgent, codeReviewerAgent, buildAgent, planAgent, visionAgent, summaryAgent };
@@ -0,0 +1,20 @@
import type { AgentInfo } from '../types.js';
/**
* Summary Agent
* 内部 Agent,用于对话压缩时生成摘要
* 推荐使用成本较低的模型
*/
export const summaryAgent: Omit<AgentInfo, 'name'> = {
description: '对话压缩摘要生成(内部使用)',
mode: 'internal',
model: {
provider: 'anthropic',
model: 'claude-3-5-haiku-20241022',
},
tools: {
enabled: [], // 无工具,纯文本生成
noTask: true,
},
maxSteps: 1,
};
+22 -2
View File
@@ -61,16 +61,36 @@ export class AgentRegistry {
/**
* 列出可作为子 Agent 的 Agent(供 Task 工具使用)
* 排除 primary 和 internal 模式
*/
listSubagents(): AgentInfo[] {
return this.list().filter((a) => a.mode !== 'primary');
return this.list().filter((a) => a.mode !== 'primary' && a.mode !== 'internal');
}
/**
* 列出可作为主交互 Agent 的 Agent(供 /agent 命令使用)
* 排除 internal 模式
*/
listPrimaryAgents(): AgentInfo[] {
return this.list().filter((a) => a.mode !== 'subagent');
return this.list().filter((a) => a.mode !== 'subagent' && a.mode !== 'internal');
}
/**
* 获取内部 Agentinternal 模式)
*/
getInternal(name: string): AgentInfo | undefined {
const agent = this.get(name);
if (agent?.mode === 'internal') {
return agent;
}
return undefined;
}
/**
* 列出所有内部 Agent
*/
listInternalAgents(): AgentInfo[] {
return this.list().filter((a) => a.mode === 'internal');
}
/**
+2 -1
View File
@@ -9,8 +9,9 @@ export type { PermissionAction, PermissionRule };
* - primary: 主 Agent,用户直接使用
* - subagent: 子 Agent,由 Task 工具调用
* - all: 两种方式都可以
* - internal: 内部使用,不对外暴露(如 Summary Agent
*/
export type AgentMode = 'primary' | 'subagent' | 'all';
export type AgentMode = 'primary' | 'subagent' | 'all' | 'internal';
/**
* Agent Bash 权限配置
+28
View File
@@ -9,6 +9,8 @@ import {
CompressionStatus,
DEFAULT_COMPRESSION_CONFIG,
} from './types.js';
import type { AgentModelConfig } from '../agent/types.js';
import { getProviderRegistry } from '../provider/index.js';
/**
* 压缩管理器
@@ -41,6 +43,32 @@ export class CompressionManager {
this.summaryModel = model;
}
/**
* 从 Agent 配置设置摘要模型
* @param config Agent 的模型配置
* @param apiKey API Key
* @param baseUrl 可选的 Base URL
*/
setSummaryModelFromAgentConfig(
config: AgentModelConfig,
apiKey: string,
baseUrl?: string
): void {
const registry = getProviderRegistry();
const provider = config.provider || 'anthropic';
const modelName = config.model || 'claude-3-5-haiku-20241022';
try {
const getModel = registry.getModelFactory(provider, {
apiKey,
baseUrl,
});
this.summaryModel = getModel(modelName);
} catch (error) {
console.warn('[CompressionManager] Failed to create summary model:', error);
}
}
/**
* 获取用于摘要生成的模型
* 优先使用专用摘要模型,无则使用主模型
+33 -10
View File
@@ -18,8 +18,8 @@ import {
} from '../context/index.js';
import type { AgentInfo, ImageData } from '../agent/types.js';
import { agentRegistry, AgentExecutor } from '../agent/index.js';
import { loadVisionConfig, loadSummaryConfig } from '../utils/config.js';
import { getProviderRegistry } from '../provider/index.js';
import { loadVisionConfig } from '../utils/config.js';
import { getProviderRegistry, resolveApiKey } from '../provider/index.js';
import { getHookManager } from '../hooks/index.js';
import { getGitManager } from '../git/index.js';
@@ -62,15 +62,38 @@ export class Agent {
// 设置主模型(作为摘要模型的后备)
this.compressionManager.setModel(this.getModel(config.model));
// 加载摘要模型配置(可选,用于降低压缩成本)
const summaryConfig = loadSummaryConfig();
if (summaryConfig) {
const summaryModelFactory = providerRegistry.getModelFactory(summaryConfig.provider, {
apiKey: summaryConfig.apiKey,
baseUrl: summaryConfig.baseUrl,
});
this.compressionManager.setSummaryModel(summaryModelFactory(summaryConfig.model));
// 从 Agent Registry 加载 Summary Agent 配置
this.initSummaryModel(config, providerRegistry);
}
/**
* 从 Agent Registry 初始化 Summary 模型
*/
private initSummaryModel(
config: AgentConfig,
providerRegistry: ReturnType<typeof getProviderRegistry>
): void {
// 获取 Summary Agentinternal 模式)
const summaryAgentInfo = agentRegistry.getInternal('summary');
if (!summaryAgentInfo?.model) {
return;
}
const modelConfig = summaryAgentInfo.model;
// 确定 provider(默认使用主配置的 provider
const provider = modelConfig.provider || config.provider;
// 从 ProviderRegistry 获取 API Key
const providerConfig = providerRegistry.getConfig(provider);
const apiKey = resolveApiKey(providerConfig) || config.apiKey;
if (!apiKey) {
return;
}
// 设置 Summary 模型
const baseUrl = providerConfig?.baseUrl;
this.compressionManager.setSummaryModelFromAgentConfig(modelConfig, apiKey, baseUrl);
}
/**
+2 -2
View File
@@ -1,7 +1,7 @@
export { Agent } from './core/agent.js';
export { toolRegistry, todoManager, initTaskContext, updateTaskDescription, updateSkillDescription } from './tools/index.js';
export { loadConfig, saveConfig, getConfig, loadVisionConfig, loadSummaryConfig } from './utils/config.js';
export type { VisionConfig, SummaryConfig } from './utils/config.js';
export { loadConfig, saveConfig, getConfig, loadVisionConfig } from './utils/config.js';
export type { VisionConfig } from './utils/config.js';
// Context compression
export {
-64
View File
@@ -19,11 +19,6 @@ interface StoredConfig {
visionModel?: string;
/** Vision 专用的 Base URL(用于 OpenAI 兼容的 Vision 服务) */
visionBaseUrl?: string;
// Summary 配置(用于对话压缩摘要生成)
summaryProvider?: ProviderType;
summaryModel?: string;
/** Summary 专用的 Base URL(用于 OpenAI 兼容的 Summary 服务) */
summaryBaseUrl?: string;
}
// Vision 配置接口
@@ -35,15 +30,6 @@ export interface VisionConfig {
baseUrl?: string;
}
// Summary 配置接口(用于对话压缩摘要生成)
export interface SummaryConfig {
provider: ProviderType;
apiKey: string;
model: string;
/** 自定义 Base URL(用于 OpenAI 兼容的 Summary 服务) */
baseUrl?: string;
}
// 默认模型配置
const DEFAULT_MODELS: Record<ProviderType, string> = {
anthropic: 'claude-sonnet-4-20250514',
@@ -58,13 +44,6 @@ const DEFAULT_VISION_MODELS: Record<ProviderType, string> = {
openai: 'gpt-4o',
};
// 默认 Summary 模型(推荐使用成本较低的模型)
const DEFAULT_SUMMARY_MODELS: Record<ProviderType, string> = {
anthropic: 'claude-3-5-haiku-20241022',
deepseek: 'deepseek-chat',
openai: 'gpt-4o-mini',
};
// 默认系统提示词
const DEFAULT_SYSTEM_PROMPT = `你是一个运行在终端中的 AI 编程助手。你可以帮助用户:
- 读取和写入文件
@@ -167,49 +146,6 @@ export function loadVisionConfig(): VisionConfig | null {
};
}
/**
* 加载 Summary 配置
* Summary 用于对话压缩时生成摘要,推荐使用成本较低的小模型
* 通过 ProviderRegistry 获取 API Key
*/
export function loadSummaryConfig(): SummaryConfig | null {
// 从配置文件读取
const storedConfig = getConfig();
// 如果没有任何 summary 相关配置,返回 null(使用主模型)
const hasSummaryConfig = storedConfig.summaryProvider || storedConfig.summaryModel;
if (!hasSummaryConfig) {
return null;
}
// 确定 summary provider(默认使用主配置的 provider
const mainProvider = storedConfig.provider || 'anthropic';
const finalProvider = storedConfig.summaryProvider || mainProvider;
// 通过 ProviderRegistry 获取 API Key
const providerConfig = providerRegistry.getConfig(finalProvider);
const finalApiKey = resolveApiKey(providerConfig);
// 如果没有 API Key,返回 null
if (!finalApiKey) {
return null;
}
// 确定模型
const finalModel = storedConfig.summaryModel || DEFAULT_SUMMARY_MODELS[finalProvider];
// 确定 baseUrl
const finalBaseUrl = storedConfig.summaryBaseUrl || providerConfig?.baseUrl;
return {
provider: finalProvider,
apiKey: finalApiKey,
model: finalModel,
baseUrl: finalBaseUrl,
};
}
// 保存配置
export function saveConfig(config: Partial<StoredConfig>): void {
// 确保目录存在