UNPKG

aiwg

Version:

Deployment tool and support utility for AI context. Copies agents, skills, commands, rules, and behaviors into the paths each AI platform reads (Claude Code, Codex, Copilot, Cursor, Warp, OpenClaw, and 6 more) so one source of truth works across 10 platfo

174 lines 6.02 kB
/** * Context Budget — configurable context/generation token split * * Manages token budgets for context windows with a default 70/30 split * (70% context, 30% generation). Supports priority scoring based on * recency, similarity, and @-mention presence. * * @module metrics/context-budget * @issue #144 */ import { promises as fs } from 'fs'; import path from 'path'; import { estimateTokens } from './token-counter.js'; // ============================================================================ // Constants // ============================================================================ const DEFAULT_CONFIG = { totalTokens: 200000, contextFraction: 0.70, generationFraction: 0.30, warningThreshold: 0.85, hardLimitThreshold: 0.95, }; const CONFIG_PATH = '.aiwg/config/context-budget.json'; // ============================================================================ // ContextBudgetManager // ============================================================================ export class ContextBudgetManager { config; items; projectPath; constructor(projectPath, config) { this.projectPath = projectPath; this.config = { ...DEFAULT_CONFIG, ...config }; this.items = []; // Validate fractions sum to 1 const total = this.config.contextFraction + this.config.generationFraction; if (Math.abs(total - 1.0) > 0.001) { throw new Error(`Context and generation fractions must sum to 1.0, got ${total}`); } } get contextBudget() { return Math.floor(this.config.totalTokens * this.config.contextFraction); } get generationBudget() { return Math.floor(this.config.totalTokens * this.config.generationFraction); } addItem(id, content, sourceType, similarity) { const tokens = estimateTokens(content); const priority = calculatePriority(sourceType, similarity); const item = { id, content, tokens, priority, source: { type: sourceType, addedAt: new Date().toISOString(), similarity, }, }; this.items.push(item); return item; } removeItem(id) { const index = this.items.findIndex((item) => item.id === id); if (index === -1) return false; this.items.splice(index, 1); return true; } getStatus() { const contextUsed = this.items.reduce((sum, item) => sum + item.tokens, 0); const contextBudget = this.contextBudget; const usageFraction = contextBudget > 0 ? contextUsed / contextBudget : 0; let level; if (usageFraction > 1.0) { level = 'exceeded'; } else if (usageFraction >= this.config.hardLimitThreshold) { level = 'critical'; } else if (usageFraction >= this.config.warningThreshold) { level = 'warning'; } else { level = 'ok'; } return { contextBudget, generationBudget: this.generationBudget, contextUsed, contextRemaining: Math.max(0, contextBudget - contextUsed), usageFraction: Math.round(usageFraction * 10000) / 10000, level, itemCount: this.items.length, }; } wouldExceed(content) { const tokens = estimateTokens(content); const status = this.getStatus(); return (status.contextUsed + tokens) > this.contextBudget; } degrade() { const targetTokens = Math.floor(this.contextBudget * this.config.warningThreshold); const sorted = [...this.items].sort((a, b) => a.priority - b.priority); let currentTokens = this.items.reduce((sum, item) => sum + item.tokens, 0); const dropped = []; const droppedIds = new Set(); for (const item of sorted) { if (currentTokens <= targetTokens) break; if (item.source.type === 'system') continue; dropped.push(item); droppedIds.add(item.id); currentTokens -= item.tokens; } this.items = this.items.filter((item) => !droppedIds.has(item.id)); const tokensFreed = dropped.reduce((sum, item) => sum + item.tokens, 0); return { kept: [...this.items], dropped, tokensFreed, }; } getItems() { return [...this.items].sort((a, b) => b.priority - a.priority); } async loadConfig() { try { const configPath = path.join(this.projectPath, CONFIG_PATH); const content = await fs.readFile(configPath, 'utf-8'); const loaded = JSON.parse(content); this.config = { ...DEFAULT_CONFIG, ...loaded }; } catch { // Use defaults } } async saveConfig() { const configPath = path.join(this.projectPath, CONFIG_PATH); await fs.mkdir(path.dirname(configPath), { recursive: true }); await fs.writeFile(configPath, JSON.stringify(this.config, null, 2), 'utf-8'); } getConfig() { return { ...this.config }; } } // ============================================================================ // Priority Scoring // ============================================================================ export function calculatePriority(sourceType, similarity) { let base; switch (sourceType) { case 'system': base = 1.0; break; case 'at-mention': base = 0.9; break; case 'user': base = 0.7; break; case 'auto': base = 0.5; break; default: base = 0.3; } const similarityBonus = similarity ? similarity * 0.1 : 0; return Math.min(1.0, base + similarityBonus); } //# sourceMappingURL=context-budget.js.map