| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| import { Context } from '@deepseek-ai/cordis' |
| import z from '@deepseek-ai/schemastery' |
| import { |
| CompactionEngine, |
| type CompactionResult, |
| type CompactionTrigger, |
| } from '@deepseek-ai/dsh-compaction' |
| import type { Session } from '@deepseek-ai/dsh-session' |
|
|
| |
| export interface AgentFrameCompactionConfig { |
| |
| semantic: boolean |
| |
| retainRatio: number |
| |
| physical: boolean |
| |
| bytesPerToken: number |
| |
| auto: boolean |
| } |
|
|
| const DEFAULT_CONFIG: AgentFrameCompactionConfig = { |
| semantic: true, |
| retainRatio: 0.2, |
| physical: true, |
| bytesPerToken: 7776, |
| auto: true, |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| export class AgentFrameCompactionEngine extends CompactionEngine { |
| static inject = ['llm', 'sessions'] |
|
|
| static Config: z<AgentFrameCompactionConfig> = z.object({ |
| semantic: z.boolean().default(true), |
| retainRatio: z.number().min(0.05).max(0.9).default(0.2), |
| physical: z.boolean().default(true), |
| bytesPerToken: z.number().default(7776), |
| auto: z.boolean().default(true), |
| }) |
|
|
| readonly config: AgentFrameCompactionConfig |
|
|
| constructor(ctx: Context, config: Partial<AgentFrameCompactionConfig> = {}) { |
| super(ctx) |
| this.config = { ...DEFAULT_CONFIG, ...config } |
| if (this.config.auto) this._registerAutomaticCompaction() |
| } |
|
|
| private _registerAutomaticCompaction(): void { |
| const { ctx } = this |
| |
| ctx.on('compaction/pressure' as any, async (agent: any, signal: AbortSignal) => { |
| try { |
| await this.compactIfNeeded(agent, 'pressure', signal) |
| } catch (e) { |
| ctx.logger.warn('[agentframe] auto compaction failed:', e) |
| } |
| }) |
| } |
|
|
| async compactIfNeeded( |
| agent: any, |
| _trigger: CompactionTrigger, |
| signal: AbortSignal, |
| ): Promise<CompactionResult | null> { |
| const session: Session = agent.session |
| const span = this._selectSpan(session) |
| if (!span) return null |
| return this.compactRegion(span.start, span.end, agent, signal) |
| } |
|
|
| async compactNow( |
| agent: any, |
| signal: AbortSignal, |
| _sourceCommandId?: any, |
| ): Promise<CompactionResult | null> { |
| const session: Session = agent.session |
| const span = this._selectSpan(session) |
| if (!span) return null |
| return this.compactRegion(span.start, span.end, agent, signal) |
| } |
|
|
| async compactRegion( |
| start: number, |
| end: number, |
| agent: any, |
| _signal?: AbortSignal, |
| ): Promise<CompactionResult> { |
| const session: Session = agent.session |
| const compactionId = `agentframe-${Date.now()}-${Math.floor(Math.random() * 1e6)}` |
|
|
| |
| await this._append(session, { |
| type: 'compaction/start', |
| compactionId, |
| provider: 'agentframe', |
| }) |
|
|
| |
| const summary = this._condense(session, start, end) |
|
|
| |
| await this._append(session, { |
| type: 'user/message', |
| content: [ |
| { |
| type: 'text', |
| text: |
| '[agentframe-compaction]\n' + |
| 'The following is an automatically condensed checkpoint of an earlier ' + |
| 'conversation span. Treat it as established background:\n\n' + |
| summary + |
| '\n\nContinue the task directly from the messages that follow.', |
| }, |
| ], |
| surfaceOp: { op: 'replace', start, end }, |
| source: { compactionId }, |
| }) |
|
|
| |
| await this._append(session, { |
| type: 'compaction/end', |
| compactionId, |
| provider: 'agentframe', |
| }) |
|
|
| return { |
| compactionId, |
| summary, |
| shadowed: [start, end], |
| seqs: [start, end], |
| tokens: { input: 0, output: 0 }, |
| provider: 'agentframe', |
| model: 'semantic+physical', |
| } as unknown as CompactionResult |
| } |
|
|
| |
| private async _append(session: Session, event: Record<string, unknown>): Promise<void> { |
| const s = session as any |
| if (typeof s.append === 'function') { |
| |
| try { |
| await s.append(event, {}) |
| } catch { |
| await s.append(event) |
| } |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| private _condense(session: Session, start: number, end: number): string { |
| const events = this._surfaceEvents(session, start, end) |
| const keep: string[] = [] |
| for (const ev of events) { |
| const text = this._eventText(ev) |
| if (!text) continue |
| if (/`|\.(ts|py|js|json|md|sh)\b|npm |pnpm |git |error|fix|decide|因为|所以|方案|决定/.test(text)) { |
| keep.push(text.slice(0, 400)) |
| } else if (keep.length < 40 && text.length > 20) { |
| keep.push(text.slice(0, 200)) |
| } |
| } |
| const cap = Math.max(8, Math.floor(keep.length * this.config.retainRatio * 5)) |
| return keep.slice(0, cap).join('\n') |
| } |
|
|
| private _surfaceEvents(session: Session, start: number, end: number): any[] { |
| try { |
| const log = (session as any).log ?? [] |
| return log.filter((e: any) => e.seq >= start && e.seq <= end) |
| } catch { |
| return [] |
| } |
| } |
|
|
| private _eventText(ev: any): string { |
| if (typeof ev?.content === 'string') return ev.content |
| if (Array.isArray(ev?.content)) { |
| return ev.content |
| .map((b: any) => (typeof b === 'string' ? b : b?.text ?? '')) |
| .join(' ') |
| } |
| return '' |
| } |
|
|
| |
| private _selectSpan(session: Session): { start: number; end: number } | null { |
| try { |
| const log = (session as any).log ?? [] |
| const surface = log.filter((e: any) => e.seq !== undefined) |
| if (surface.length < 8) return null |
| const end = surface[surface.length - 1].seq |
| const idx = Math.max(0, surface.length - 1 - Math.floor(surface.length * (1 - this.config.retainRatio))) |
| const start = surface[idx].seq |
| return start < end ? { start, end } : null |
| } catch { |
| return null |
| } |
| } |
| } |
|
|
| export default AgentFrameCompactionEngine |
|
|