feat(agent): add context window management with token-aware compaction (#14)
* feat(agent): add context window guard to prevent token overflow Implement token-aware context management that validates context window size on agent initialization and provides intelligent message compaction based on actual token usage rather than simple message count. Key changes: - Add context-window module with guard, token estimation, and types - Support both "count" (legacy) and "tokens" (new default) compaction modes - Warn when context window < 32K tokens, block when < 16K tokens - Trigger compaction at 80% utilization, target 50% after compaction Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat(agent): add summary-based compaction using LLM Implement intelligent compaction that uses LLM to generate summaries of older messages instead of simply truncating them. This preserves important context like key decisions, TODOs, and technical details. Key changes: - Add summarization.ts with compactMessagesWithSummary functions - Support chunked summarization for very large histories - Add "summary" compaction mode alongside "count" and "tokens" - Auto-resolve API key from environment based on provider - Graceful fallback to "tokens" mode if model/apiKey unavailable Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
3024e89071
commit
67cd46a072
11 changed files with 1116 additions and 16 deletions
|
|
@ -5,12 +5,45 @@ import { createAgentOutput } from "./output.js";
|
|||
import { resolveModel, resolveTools } from "./tools.js";
|
||||
import { SessionManager } from "./session/session-manager.js";
|
||||
import { ProfileManager } from "./profile/index.js";
|
||||
import {
|
||||
checkContextWindow,
|
||||
DEFAULT_CONTEXT_TOKENS,
|
||||
type ContextWindowGuardResult,
|
||||
} from "./context-window/index.js";
|
||||
|
||||
/**
|
||||
* 根据 provider 获取 API Key
|
||||
*/
|
||||
function resolveApiKey(provider: string): string | undefined {
|
||||
const providerEnvMap: Record<string, string> = {
|
||||
openai: "OPENAI_API_KEY",
|
||||
anthropic: "ANTHROPIC_API_KEY",
|
||||
google: "GOOGLE_API_KEY",
|
||||
"google-genai": "GOOGLE_API_KEY",
|
||||
kimi: "MOONSHOT_API_KEY",
|
||||
"kimi-coding": "MOONSHOT_API_KEY",
|
||||
deepseek: "DEEPSEEK_API_KEY",
|
||||
groq: "GROQ_API_KEY",
|
||||
mistral: "MISTRAL_API_KEY",
|
||||
together: "TOGETHER_API_KEY",
|
||||
};
|
||||
|
||||
const envVar = providerEnvMap[provider];
|
||||
if (envVar) {
|
||||
return process.env[envVar];
|
||||
}
|
||||
|
||||
// 尝试通用格式: PROVIDER_API_KEY
|
||||
const normalizedProvider = provider.toUpperCase().replace(/-/g, "_");
|
||||
return process.env[`${normalizedProvider}_API_KEY`];
|
||||
}
|
||||
|
||||
export class Agent {
|
||||
private readonly agent: PiAgentCore;
|
||||
private readonly output;
|
||||
private readonly session: SessionManager;
|
||||
private readonly profile?: ProfileManager;
|
||||
private readonly contextWindowGuard: ContextWindowGuardResult;
|
||||
|
||||
/** 当前会话 ID */
|
||||
readonly sessionId: string;
|
||||
|
|
@ -23,34 +56,87 @@ export class Agent {
|
|||
this.agent = new PiAgentCore();
|
||||
|
||||
// 加载 Agent Profile(如果指定了 profileId)
|
||||
let systemPrompt: string | undefined;
|
||||
if (options.profileId) {
|
||||
this.profile = new ProfileManager({
|
||||
profileId: options.profileId,
|
||||
baseDir: options.profileBaseDir,
|
||||
});
|
||||
const systemPrompt = this.profile.buildSystemPrompt();
|
||||
systemPrompt = this.profile.buildSystemPrompt();
|
||||
if (systemPrompt) {
|
||||
this.agent.setSystemPrompt(systemPrompt);
|
||||
}
|
||||
} else if (options.systemPrompt) {
|
||||
// 直接使用传入的 systemPrompt
|
||||
systemPrompt = options.systemPrompt;
|
||||
this.agent.setSystemPrompt(options.systemPrompt);
|
||||
}
|
||||
|
||||
this.sessionId = options.sessionId ?? uuidv7();
|
||||
this.session = new SessionManager({ sessionId: this.sessionId });
|
||||
const storedMeta = this.session.getMeta();
|
||||
if (!options.thinkingLevel && storedMeta?.thinkingLevel) {
|
||||
this.agent.setThinkingLevel(storedMeta.thinkingLevel as any);
|
||||
} else if (options.thinkingLevel) {
|
||||
this.agent.setThinkingLevel(options.thinkingLevel);
|
||||
}
|
||||
|
||||
// 解析 model(用于获取 context window)
|
||||
const storedMeta = (() => {
|
||||
// 临时创建 session 获取 meta,避免循环依赖
|
||||
const tempSession = new SessionManager({ sessionId: this.sessionId });
|
||||
return tempSession.getMeta();
|
||||
})();
|
||||
|
||||
const model = options.provider && options.model ? resolveModel(options) : resolveModel({
|
||||
...options,
|
||||
provider: storedMeta?.provider,
|
||||
model: storedMeta?.model,
|
||||
});
|
||||
|
||||
// === Context Window Guard ===
|
||||
this.contextWindowGuard = checkContextWindow({
|
||||
modelContextWindow: model.contextWindow,
|
||||
configContextTokens: options.contextWindowTokens,
|
||||
defaultTokens: DEFAULT_CONTEXT_TOKENS,
|
||||
});
|
||||
|
||||
// 警告:context window 较小
|
||||
if (this.contextWindowGuard.shouldWarn) {
|
||||
stderr.write(
|
||||
`[Context Window Guard] WARNING: Low context window: ${this.contextWindowGuard.tokens} tokens (source: ${this.contextWindowGuard.source})\n`,
|
||||
);
|
||||
}
|
||||
|
||||
// 阻止:context window 太小
|
||||
if (this.contextWindowGuard.shouldBlock) {
|
||||
throw new Error(
|
||||
`[Context Window Guard] Context window too small: ${this.contextWindowGuard.tokens} tokens. ` +
|
||||
`Minimum required: 16,000 tokens. Please use a model with a larger context window.`,
|
||||
);
|
||||
}
|
||||
|
||||
// 确定 compaction 模式
|
||||
const compactionMode = options.compactionMode ?? "tokens"; // 默认使用 token 模式
|
||||
|
||||
// 获取 API Key(用于 summary 模式)
|
||||
const apiKey = compactionMode === "summary" ? resolveApiKey(model.provider) : undefined;
|
||||
|
||||
// 创建 SessionManager(带 context window 配置)
|
||||
this.session = new SessionManager({
|
||||
sessionId: this.sessionId,
|
||||
compactionMode,
|
||||
// Token 模式参数
|
||||
contextWindowTokens: this.contextWindowGuard.tokens,
|
||||
systemPrompt,
|
||||
reserveTokens: options.reserveTokens,
|
||||
targetRatio: options.compactionTargetRatio,
|
||||
minKeepMessages: options.minKeepMessages,
|
||||
// Summary 模式参数
|
||||
model: compactionMode === "summary" ? model : undefined,
|
||||
apiKey,
|
||||
customInstructions: options.summaryInstructions,
|
||||
});
|
||||
|
||||
if (!options.thinkingLevel && storedMeta?.thinkingLevel) {
|
||||
this.agent.setThinkingLevel(storedMeta.thinkingLevel as any);
|
||||
} else if (options.thinkingLevel) {
|
||||
this.agent.setThinkingLevel(options.thinkingLevel);
|
||||
}
|
||||
|
||||
this.agent.setModel(model);
|
||||
this.agent.setTools(resolveTools(options));
|
||||
|
||||
|
|
@ -63,6 +149,7 @@ export class Agent {
|
|||
provider: this.agent.state.model?.provider,
|
||||
model: this.agent.state.model?.id,
|
||||
thinkingLevel: this.agent.state.thinkingLevel,
|
||||
contextWindowTokens: this.contextWindowGuard.tokens,
|
||||
});
|
||||
|
||||
this.agent.subscribe((event: AgentEvent) => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue