squads-cli 0.4.10 → 0.4.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +66 -2
- package/dist/cli.js +868 -241
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +569 -5
- package/dist/index.js +1030 -0
- package/dist/index.js.map +1 -1
- package/docker/docker-compose.engram.yml +55 -66
- package/package.json +1 -1
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/version.ts"],"sourcesContent":["import { createRequire } from 'module';\nconst require = createRequire(import.meta.url);\nconst pkg = require('../package.json');\nexport const version: string = pkg.version;\n"],"mappings":";AAAA,SAAS,qBAAqB;AAC9B,IAAMA,WAAU,cAAc,YAAY,GAAG;AAC7C,IAAM,MAAMA,SAAQ,iBAAiB;AAC9B,IAAM,UAAkB,IAAI;","names":["require"]}
|
|
1
|
+
{"version":3,"sources":["../src/version.ts","../src/lib/squad-parser.ts","../src/lib/condenser/tokens.ts","../src/lib/condenser/deduplication.ts","../src/lib/condenser/pruning.ts","../src/lib/condenser/summarizer.ts","../src/lib/condenser/index.ts"],"sourcesContent":["import { createRequire } from 'module';\nconst require = createRequire(import.meta.url);\nconst pkg = require('../package.json');\nexport const version: string = pkg.version;\n","import { readFileSync, existsSync, readdirSync, writeFileSync } from 'fs';\nimport { join, basename } from 'path';\nimport matter from 'gray-matter';\nimport { resolveMcpConfig, type McpResolution } from './mcp-config.js';\n\nexport type EffortLevel = 'high' | 'medium' | 'low';\n\n// Context schema for frontmatter\nexport interface SquadContext {\n mcp?: string[];\n skills?: string[];\n memory?: {\n load?: string[];\n };\n model?: {\n default?: string;\n expensive?: string;\n cheap?: string;\n };\n budget?: {\n daily?: number;\n weekly?: number;\n perExecution?: number;\n };\n}\n\n// Frontmatter schema\nexport interface SquadFrontmatter {\n name?: string;\n mission?: string;\n repo?: string;\n stack?: string;\n context?: SquadContext;\n effort?: EffortLevel;\n}\n\nexport interface Agent {\n name: string;\n role: string;\n trigger: string;\n status?: string;\n filePath?: string;\n squad?: string;\n effort?: EffortLevel;\n}\n\nexport interface Pipeline {\n name: string;\n agents: string[];\n}\n\nexport interface Goal {\n description: string;\n completed: boolean;\n progress?: string;\n metrics?: string[];\n}\n\nexport interface Squad {\n name: string;\n mission: string;\n agents: Agent[];\n pipelines: Pipeline[];\n triggers: {\n scheduled: string[];\n event: string[];\n manual: string[];\n };\n dependencies: string[];\n outputPath: string;\n goals: Goal[];\n effort?: EffortLevel; // Squad-level default effort\n context?: SquadContext; // Frontmatter context block\n repo?: string;\n stack?: string;\n}\n\n/**\n * Resolved execution context with paths and metadata.\n * Extends SquadContext with resolved paths for MCP, skills, and memory.\n */\nexport interface ExecutionContext extends SquadContext {\n /** Squad name this context belongs to */\n squadName: string;\n /** Resolved paths and metadata */\n resolved: {\n /** Path to MCP config file to use */\n mcpConfigPath: string;\n /** Source of MCP config resolution */\n mcpSource: 'user-override' | 'generated' | 'fallback';\n /** List of MCP servers in the config */\n mcpServers: string[];\n /** Resolved skill directory paths */\n skillPaths: string[];\n /** Resolved memory file paths */\n memoryPaths: string[];\n };\n}\n\nexport function findSquadsDir(): string | null {\n // Look for .agents/squads in current directory or parent directories\n let dir = process.cwd();\n\n for (let i = 0; i < 5; i++) {\n const squadsPath = join(dir, '.agents', 'squads');\n if (existsSync(squadsPath)) {\n return squadsPath;\n }\n const parent = join(dir, '..');\n if (parent === dir) break;\n dir = parent;\n }\n\n return null;\n}\n\nexport function findProjectRoot(): string | null {\n // Find the root of the squads project (where .agents/ lives)\n const squadsDir = findSquadsDir();\n if (!squadsDir) return null;\n // squadsDir is /path/to/.agents/squads, so go up 2 levels\n return join(squadsDir, '..', '..');\n}\n\nexport function hasLocalInfraConfig(): boolean {\n // Check if the project has a local .env file with infra config\n const projectRoot = findProjectRoot();\n if (!projectRoot) return false;\n\n const envPath = join(projectRoot, '.env');\n if (!existsSync(envPath)) return false;\n\n // Check if .env has any infra-related keys\n const content = readFileSync(envPath, 'utf-8');\n const infraKeys = ['LANGFUSE_', 'SQUADS_BRIDGE', 'SQUADS_POSTGRES', 'SQUADS_REDIS'];\n return infraKeys.some(key => content.includes(key));\n}\n\nexport function listSquads(squadsDir: string): string[] {\n const squads: string[] = [];\n\n const entries = readdirSync(squadsDir, { withFileTypes: true });\n for (const entry of entries) {\n if (entry.isDirectory() && !entry.name.startsWith('_')) {\n const squadFile = join(squadsDir, entry.name, 'SQUAD.md');\n if (existsSync(squadFile)) {\n squads.push(entry.name);\n }\n }\n }\n\n return squads;\n}\n\nexport function listAgents(squadsDir: string, squadName?: string): Agent[] {\n const agents: Agent[] = [];\n\n const dirs = squadName\n ? [squadName]\n : readdirSync(squadsDir, { withFileTypes: true })\n .filter(e => e.isDirectory() && !e.name.startsWith('_'))\n .map(e => e.name);\n\n for (const dir of dirs) {\n const squadPath = join(squadsDir, dir);\n if (!existsSync(squadPath)) continue;\n\n const files = readdirSync(squadPath);\n for (const file of files) {\n if (file.endsWith('.md') && file !== 'SQUAD.md') {\n const agentName = file.replace('.md', '');\n agents.push({\n name: agentName,\n role: `Agent in ${dir}`,\n trigger: 'manual',\n filePath: join(squadPath, file)\n });\n }\n }\n }\n\n return agents;\n}\n\nexport function parseSquadFile(filePath: string): Squad {\n const rawContent = readFileSync(filePath, 'utf-8');\n\n // Parse frontmatter with gray-matter\n const { data: frontmatter, content: bodyContent } = matter(rawContent);\n const fm = frontmatter as SquadFrontmatter;\n\n const lines = bodyContent.split('\\n');\n\n const squad: Squad = {\n name: fm.name || basename(filePath).replace('.md', ''),\n mission: fm.mission || '',\n agents: [],\n pipelines: [],\n triggers: { scheduled: [], event: [], manual: [] },\n dependencies: [],\n outputPath: '',\n goals: [],\n // Apply frontmatter fields\n effort: fm.effort,\n context: fm.context,\n repo: fm.repo,\n stack: fm.stack,\n };\n\n let currentSection = '';\n let inTable = false;\n let tableHeaders: string[] = [];\n\n for (const line of lines) {\n // Extract squad name from title\n if (line.startsWith('# Squad:')) {\n squad.name = line.replace('# Squad:', '').trim().toLowerCase();\n continue;\n }\n\n // Track sections\n if (line.startsWith('## ')) {\n currentSection = line.replace('## ', '').trim().toLowerCase();\n inTable = false;\n continue;\n }\n\n // Extract mission\n if (currentSection === 'mission' && line.trim() && !line.startsWith('#')) {\n if (!squad.mission) {\n squad.mission = line.trim();\n }\n }\n\n // Extract squad-level effort (e.g., \"effort: medium\" in Context section)\n const effortMatch = line.match(/^effort:\\s*(high|medium|low)/i);\n if (effortMatch && !squad.effort) {\n squad.effort = effortMatch[1].toLowerCase() as EffortLevel;\n }\n\n // Parse agent tables\n if (currentSection.includes('agent') || currentSection.includes('orchestrator') ||\n currentSection.includes('evaluator') || currentSection.includes('builder') ||\n currentSection.includes('priority')) {\n\n if (line.includes('|') && line.includes('Agent')) {\n inTable = true;\n tableHeaders = line.split('|').map(h => h.trim().toLowerCase());\n continue;\n }\n\n if (inTable && line.includes('|') && !line.includes('---')) {\n const cells = line.split('|').map(c => c.trim().replace(/`/g, ''));\n const agentIdx = tableHeaders.findIndex(h => h === 'agent');\n const roleIdx = tableHeaders.findIndex(h => h === 'role');\n const triggerIdx = tableHeaders.findIndex(h => h === 'trigger');\n const statusIdx = tableHeaders.findIndex(h => h === 'status');\n const effortIdx = tableHeaders.findIndex(h => h === 'effort');\n\n if (agentIdx >= 0 && cells[agentIdx]) {\n const effortValue = effortIdx >= 0 ? cells[effortIdx]?.toLowerCase() : undefined;\n const effort = ['high', 'medium', 'low'].includes(effortValue || '')\n ? effortValue as EffortLevel\n : undefined;\n\n squad.agents.push({\n name: cells[agentIdx],\n role: roleIdx >= 0 ? cells[roleIdx] : '',\n trigger: triggerIdx >= 0 ? cells[triggerIdx] : 'manual',\n status: statusIdx >= 0 ? cells[statusIdx] : 'active',\n effort\n });\n }\n }\n }\n\n // Parse pipelines (looking for patterns like: agent1 → agent2 → agent3)\n if (line.includes('→') && line.includes('`')) {\n const pipelineMatch = line.match(/`([^`]+)`\\s*→\\s*`([^`]+)`/g);\n if (pipelineMatch) {\n const agentNames = line.match(/`([^`]+)`/g)?.map(m => m.replace(/`/g, '')) || [];\n if (agentNames.length >= 2) {\n squad.pipelines.push({\n name: 'default',\n agents: agentNames\n });\n }\n }\n }\n\n // Also look for Pipeline: format\n if (line.toLowerCase().includes('pipeline:')) {\n const pipelineContent = line.split(':')[1];\n if (pipelineContent && pipelineContent.includes('→')) {\n const agentNames = pipelineContent.match(/`([^`]+)`/g)?.map(m => m.replace(/`/g, '')) || [];\n if (agentNames.length >= 2) {\n squad.pipelines.push({\n name: 'default',\n agents: agentNames\n });\n }\n }\n }\n\n // Extract output path\n if (line.toLowerCase().includes('primary') && line.includes('`')) {\n const match = line.match(/`([^`]+)`/);\n if (match) {\n squad.outputPath = match[1].replace(/\\/$/, '');\n }\n }\n\n // Parse goals (checkbox format: - [ ] or - [x])\n if (currentSection === 'goals') {\n const goalMatch = line.match(/^-\\s*\\[([ x])\\]\\s*(.+)$/);\n if (goalMatch) {\n const completed = goalMatch[1] === 'x';\n let description = goalMatch[2].trim();\n let progress: string | undefined;\n\n // Check for progress annotation\n const progressMatch = description.match(/\\(progress:\\s*([^)]+)\\)/i);\n if (progressMatch) {\n progress = progressMatch[1];\n description = description.replace(progressMatch[0], '').trim();\n }\n\n squad.goals.push({\n description,\n completed,\n progress\n });\n }\n }\n }\n\n return squad;\n}\n\nexport function loadSquad(squadName: string): Squad | null {\n const squadsDir = findSquadsDir();\n if (!squadsDir) return null;\n\n const squadFile = join(squadsDir, squadName, 'SQUAD.md');\n if (!existsSync(squadFile)) return null;\n\n return parseSquadFile(squadFile);\n}\n\nexport function loadAgentDefinition(agentPath: string): string {\n if (!existsSync(agentPath)) return '';\n return readFileSync(agentPath, 'utf-8');\n}\n\nexport function addGoalToSquad(squadName: string, goal: string): boolean {\n const squadsDir = findSquadsDir();\n if (!squadsDir) return false;\n\n const squadFile = join(squadsDir, squadName, 'SQUAD.md');\n if (!existsSync(squadFile)) return false;\n\n let content = readFileSync(squadFile, 'utf-8');\n\n // Check if Goals section exists\n if (!content.includes('## Goals')) {\n // Add Goals section before Dependencies or at end\n const insertPoint = content.indexOf('## Dependencies');\n if (insertPoint > 0) {\n content = content.slice(0, insertPoint) + `## Goals\\n\\n- [ ] ${goal}\\n\\n` + content.slice(insertPoint);\n } else {\n content += `\\n## Goals\\n\\n- [ ] ${goal}\\n`;\n }\n } else {\n // Add to existing Goals section\n const goalsIdx = content.indexOf('## Goals');\n const nextSectionIdx = content.indexOf('\\n## ', goalsIdx + 1);\n const endIdx = nextSectionIdx > 0 ? nextSectionIdx : content.length;\n\n // Find last goal line or section header\n const goalsSection = content.slice(goalsIdx, endIdx);\n const lastGoalMatch = goalsSection.match(/^-\\s*\\[[ x]\\].+$/gm);\n\n if (lastGoalMatch) {\n // Add after last goal\n const lastGoal = lastGoalMatch[lastGoalMatch.length - 1];\n const lastGoalIdx = content.lastIndexOf(lastGoal, endIdx);\n const insertPos = lastGoalIdx + lastGoal.length;\n content = content.slice(0, insertPos) + `\\n- [ ] ${goal}` + content.slice(insertPos);\n } else {\n // No goals yet, add after section header\n const headerEnd = goalsIdx + '## Goals'.length;\n content = content.slice(0, headerEnd) + `\\n\\n- [ ] ${goal}` + content.slice(headerEnd);\n }\n }\n\n writeFileSync(squadFile, content);\n return true;\n}\n\nexport function updateGoalInSquad(\n squadName: string,\n goalIndex: number,\n updates: { completed?: boolean; progress?: string }\n): boolean {\n const squadsDir = findSquadsDir();\n if (!squadsDir) return false;\n\n const squadFile = join(squadsDir, squadName, 'SQUAD.md');\n if (!existsSync(squadFile)) return false;\n\n const content = readFileSync(squadFile, 'utf-8');\n const lines = content.split('\\n');\n\n let currentSection = '';\n let goalCount = 0;\n\n for (let i = 0; i < lines.length; i++) {\n const line = lines[i];\n\n if (line.startsWith('## ')) {\n currentSection = line.replace('## ', '').trim().toLowerCase();\n continue;\n }\n\n if (currentSection === 'goals') {\n const goalMatch = line.match(/^-\\s*\\[([ x])\\]\\s*(.+)$/);\n if (goalMatch) {\n if (goalCount === goalIndex) {\n let newLine = '- [' + (updates.completed ? 'x' : ' ') + '] ' + goalMatch[2];\n\n // Handle progress update\n if (updates.progress !== undefined) {\n // Remove existing progress annotation\n newLine = newLine.replace(/\\s*\\(progress:\\s*[^)]+\\)/i, '');\n if (updates.progress) {\n newLine += ` (progress: ${updates.progress})`;\n }\n }\n\n lines[i] = newLine;\n writeFileSync(squadFile, lines.join('\\n'));\n return true;\n }\n goalCount++;\n }\n }\n }\n\n return false;\n}\n\n/**\n * Find the skills directory (.claude/skills)\n */\nfunction findSkillsDir(): string | null {\n const projectRoot = findProjectRoot();\n if (!projectRoot) return null;\n\n const skillsDir = join(projectRoot, '.claude', 'skills');\n return existsSync(skillsDir) ? skillsDir : null;\n}\n\n/**\n * Find the memory directory (.agents/memory)\n */\nfunction findMemoryDir(): string | null {\n const projectRoot = findProjectRoot();\n if (!projectRoot) return null;\n\n const memoryDir = join(projectRoot, '.agents', 'memory');\n return existsSync(memoryDir) ? memoryDir : null;\n}\n\n/**\n * Resolve a skill name to its directory path.\n */\nfunction resolveSkillPath(skillName: string): string | null {\n const skillsDir = findSkillsDir();\n if (!skillsDir) return null;\n\n const skillPath = join(skillsDir, skillName);\n return existsSync(skillPath) ? skillPath : null;\n}\n\n/**\n * Resolve memory glob patterns to actual file paths.\n */\nfunction resolveMemoryPaths(patterns: string[]): string[] {\n const memoryDir = findMemoryDir();\n if (!memoryDir) return [];\n\n const resolved: string[] = [];\n\n for (const pattern of patterns) {\n // Handle simple patterns like \"intelligence/*\" or \"research/*\"\n if (pattern.endsWith('/*')) {\n const subdir = pattern.slice(0, -2);\n const subdirPath = join(memoryDir, subdir);\n if (existsSync(subdirPath)) {\n // Add all .md files in the subdirectory\n try {\n const files = readdirSync(subdirPath);\n for (const file of files) {\n if (file.endsWith('.md')) {\n resolved.push(join(subdirPath, file));\n }\n }\n } catch {\n // Ignore read errors\n }\n }\n } else {\n // Direct path\n const fullPath = join(memoryDir, pattern);\n if (existsSync(fullPath)) {\n resolved.push(fullPath);\n }\n }\n }\n\n return resolved;\n}\n\n/**\n * Resolve execution context for a squad.\n *\n * Takes a Squad object and resolves all context references to actual paths:\n * - MCP config path (three-tier resolution)\n * - Skill directory paths\n * - Memory file paths\n *\n * @param squad - The squad to resolve context for\n * @param forceRegenerate - Force MCP config regeneration\n * @returns Resolved execution context with all paths\n */\nexport function resolveExecutionContext(\n squad: Squad,\n forceRegenerate = false\n): ExecutionContext {\n const ctx = squad.context || {};\n\n // Resolve MCP config\n const mcpResolution: McpResolution = resolveMcpConfig(\n squad.name,\n ctx.mcp,\n forceRegenerate\n );\n\n // Resolve skill paths\n const skillPaths: string[] = [];\n if (ctx.skills) {\n for (const skill of ctx.skills) {\n const path = resolveSkillPath(skill);\n if (path) {\n skillPaths.push(path);\n }\n }\n }\n\n // Resolve memory paths\n const memoryPaths = ctx.memory?.load\n ? resolveMemoryPaths(ctx.memory.load)\n : [];\n\n return {\n // Copy all SquadContext fields\n ...ctx,\n // Add squad name\n squadName: squad.name,\n // Add resolved paths\n resolved: {\n mcpConfigPath: mcpResolution.path,\n mcpSource: mcpResolution.source,\n mcpServers: mcpResolution.servers || [],\n skillPaths,\n memoryPaths,\n },\n };\n}\n","/**\n * Token estimation and tracking for context compression.\n *\n * Uses character-based heuristics for speed (no API calls needed).\n * ~4 characters per token is a reasonable approximation for English text.\n */\n\n// Token-to-character ratios by content type\nconst RATIOS = {\n english: 4.0, // Standard English text\n code: 3.5, // Code tends to have more tokens per char\n json: 3.0, // JSON has many punctuation tokens\n mixed: 3.75, // Default for mixed content\n} as const;\n\nexport type ContentType = keyof typeof RATIOS;\n\n/**\n * Estimate token count from text content.\n *\n * @param text - The text to estimate tokens for\n * @param type - Content type hint for better accuracy\n * @returns Estimated token count\n */\nexport function estimateTokens(text: string, type: ContentType = 'mixed'): number {\n if (!text) return 0;\n const ratio = RATIOS[type];\n return Math.ceil(text.length / ratio);\n}\n\n/**\n * Estimate tokens for a message object (handles different formats).\n */\nexport function estimateMessageTokens(message: {\n role?: string;\n content?: string | Array<{ type: string; text?: string }>;\n}): number {\n // Role overhead (~2-4 tokens)\n let tokens = 4;\n\n if (typeof message.content === 'string') {\n tokens += estimateTokens(message.content);\n } else if (Array.isArray(message.content)) {\n for (const part of message.content) {\n if (part.text) {\n tokens += estimateTokens(part.text);\n }\n // Tool results, images, etc. add overhead\n tokens += 10;\n }\n }\n\n return tokens;\n}\n\n/**\n * Model context limits (in tokens).\n * These are the input context windows, not output limits.\n */\nexport const MODEL_LIMITS: Record<string, number> = {\n // Anthropic models\n 'claude-opus-4-5-20251101': 200_000,\n 'claude-sonnet-4-20250514': 200_000,\n 'claude-3-5-haiku-20241022': 200_000,\n // Aliases\n opus: 200_000,\n sonnet: 200_000,\n haiku: 200_000,\n // Default fallback\n default: 200_000,\n};\n\n/**\n * Get the context limit for a model.\n */\nexport function getModelLimit(model: string): number {\n return MODEL_LIMITS[model] ?? MODEL_LIMITS.default;\n}\n\n/**\n * Token usage tracker for a session.\n */\nexport interface TokenTracker {\n /** Total tokens used so far */\n used: number;\n /** Model's context limit */\n limit: number;\n /** Usage as percentage (0-1) */\n percentage: number;\n /** Breakdown by category */\n breakdown: {\n system: number;\n user: number;\n assistant: number;\n tools: number;\n };\n}\n\n/**\n * Create a new token tracker for a session.\n *\n * @param model - Model name to determine context limit\n * @returns Fresh token tracker\n */\nexport function createTracker(model: string = 'default'): TokenTracker {\n return {\n used: 0,\n limit: getModelLimit(model),\n percentage: 0,\n breakdown: {\n system: 0,\n user: 0,\n assistant: 0,\n tools: 0,\n },\n };\n}\n\n/**\n * Update tracker with new content.\n *\n * @param tracker - Tracker to update (mutated in place)\n * @param content - Content to add\n * @param category - Category for breakdown tracking\n */\nexport function updateTracker(\n tracker: TokenTracker,\n content: string,\n category: keyof TokenTracker['breakdown'] = 'assistant'\n): void {\n const tokens = estimateTokens(content);\n tracker.used += tokens;\n tracker.breakdown[category] += tokens;\n tracker.percentage = tracker.used / tracker.limit;\n}\n\n/**\n * Update tracker from a message object.\n */\nexport function updateTrackerFromMessage(\n tracker: TokenTracker,\n message: { role?: string; content?: string | Array<{ type: string; text?: string }> }\n): void {\n const tokens = estimateMessageTokens(message);\n const category = mapRoleToCategory(message.role);\n tracker.used += tokens;\n tracker.breakdown[category] += tokens;\n tracker.percentage = tracker.used / tracker.limit;\n}\n\nfunction mapRoleToCategory(role?: string): keyof TokenTracker['breakdown'] {\n switch (role) {\n case 'system':\n return 'system';\n case 'user':\n return 'user';\n case 'assistant':\n return 'assistant';\n case 'tool':\n case 'tool_result':\n return 'tools';\n default:\n return 'assistant';\n }\n}\n\n/**\n * Check if compression is needed based on thresholds.\n */\nexport type CompressionLevel = 'none' | 'light' | 'medium' | 'heavy';\n\nexport interface ThresholdConfig {\n light: number; // Default: 0.70\n medium: number; // Default: 0.85\n heavy: number; // Default: 0.95\n}\n\nconst DEFAULT_THRESHOLDS: ThresholdConfig = {\n light: 0.7,\n medium: 0.85,\n heavy: 0.95,\n};\n\n/**\n * Determine what level of compression is needed.\n *\n * @param tracker - Current token tracker state\n * @param thresholds - Custom thresholds (optional)\n * @returns Compression level needed\n */\nexport function getCompressionLevel(\n tracker: TokenTracker,\n thresholds: ThresholdConfig = DEFAULT_THRESHOLDS\n): CompressionLevel {\n if (tracker.percentage >= thresholds.heavy) return 'heavy';\n if (tracker.percentage >= thresholds.medium) return 'medium';\n if (tracker.percentage >= thresholds.light) return 'light';\n return 'none';\n}\n\n/**\n * Format tracker status for display.\n */\nexport function formatTrackerStatus(tracker: TokenTracker): string {\n const pct = (tracker.percentage * 100).toFixed(1);\n const used = (tracker.used / 1000).toFixed(1);\n const limit = (tracker.limit / 1000).toFixed(0);\n const level = getCompressionLevel(tracker);\n\n const levelIndicator =\n level === 'none' ? '' : level === 'light' ? ' [!]' : level === 'medium' ? ' [!!]' : ' [!!!]';\n\n return `${used}K / ${limit}K tokens (${pct}%)${levelIndicator}`;\n}\n","/**\n * File deduplication for context compression.\n *\n * Tracks file reads across a conversation and replaces duplicate\n * reads with concise references to save tokens.\n *\n * Based on Cline's approach: keeping only the latest version of each\n * file prevents LLM confusion during edit operations.\n */\n\nimport { estimateTokens } from './tokens.js';\n\n/**\n * Record of a file read in the conversation.\n */\nexport interface FileReadRecord {\n /** File path that was read */\n path: string;\n /** Turn index where the read occurred */\n turnIndex: number;\n /** Estimated token count of the content */\n tokenCount: number;\n /** Hash of content for change detection */\n contentHash: string;\n}\n\n/**\n * Simple content hash for change detection.\n */\nfunction hashContent(content: string): string {\n // Simple hash - just use first 100 chars + length\n // Full crypto hash would be overkill here\n const prefix = content.slice(0, 100);\n return `${content.length}:${prefix}`;\n}\n\n/**\n * Tracks file reads across a conversation for deduplication.\n */\nexport class FileDeduplicator {\n /** Map of file path to all reads of that file */\n private reads: Map<string, FileReadRecord[]> = new Map();\n\n /** Current turn index */\n private currentTurn = 0;\n\n /**\n * Record a file read.\n *\n * @param path - File path that was read\n * @param content - File content that was read\n */\n trackRead(path: string, content: string): void {\n const record: FileReadRecord = {\n path,\n turnIndex: this.currentTurn,\n tokenCount: estimateTokens(content, 'code'),\n contentHash: hashContent(content),\n };\n\n const existing = this.reads.get(path) || [];\n existing.push(record);\n this.reads.set(path, existing);\n }\n\n /**\n * Advance to next turn.\n */\n nextTurn(): void {\n this.currentTurn++;\n }\n\n /**\n * Get current turn index.\n */\n getTurn(): number {\n return this.currentTurn;\n }\n\n /**\n * Check if a file has been read before.\n *\n * @param path - File path to check\n * @returns Previous read record if exists\n */\n getPreviousRead(path: string): FileReadRecord | undefined {\n const reads = this.reads.get(path);\n if (!reads || reads.length === 0) return undefined;\n return reads[reads.length - 1];\n }\n\n /**\n * Get all files that have been read multiple times.\n *\n * @returns Map of path to read count\n */\n getDuplicateReads(): Map<string, number> {\n const duplicates = new Map<string, number>();\n for (const [path, reads] of this.reads) {\n if (reads.length > 1) {\n duplicates.set(path, reads.length);\n }\n }\n return duplicates;\n }\n\n /**\n * Calculate potential token savings from deduplication.\n */\n getPotentialSavings(): number {\n let savings = 0;\n for (const reads of this.reads.values()) {\n if (reads.length > 1) {\n // Can save all but the most recent read\n for (let i = 0; i < reads.length - 1; i++) {\n savings += reads[i].tokenCount;\n }\n }\n }\n return savings;\n }\n\n /**\n * Generate a deduplication reference message.\n *\n * @param path - File path\n * @param previousTurn - Turn where file was previously read\n */\n static createReference(path: string, previousTurn: number): string {\n return `[File \"${path}\" was read at turn ${previousTurn}. Content unchanged.]`;\n }\n\n /**\n * Reset tracker state.\n */\n reset(): void {\n this.reads.clear();\n this.currentTurn = 0;\n }\n\n /**\n * Get statistics for debugging.\n */\n getStats(): {\n filesTracked: number;\n totalReads: number;\n duplicateReads: number;\n potentialSavings: number;\n } {\n let totalReads = 0;\n let duplicateReads = 0;\n for (const reads of this.reads.values()) {\n totalReads += reads.length;\n if (reads.length > 1) {\n duplicateReads += reads.length - 1;\n }\n }\n\n return {\n filesTracked: this.reads.size,\n totalReads,\n duplicateReads,\n potentialSavings: this.getPotentialSavings(),\n };\n }\n}\n\n/**\n * Message type for deduplication processing.\n */\nexport interface DeduplicatableMessage {\n role: string;\n content: string | Array<{ type: string; text?: string; tool_use_id?: string }>;\n}\n\n/**\n * Extract file paths from tool results.\n */\nexport function extractFileReads(message: DeduplicatableMessage): Array<{ path: string; content: string }> {\n const reads: Array<{ path: string; content: string }> = [];\n\n if (typeof message.content === 'string') {\n // Look for file read patterns in text\n const fileReadPattern = /(?:Reading|Read|Contents of) (?:file )?[`\"']?([^`\"'\\n]+)[`\"']?[\\s\\S]*?(?:```[\\w]*\\n([\\s\\S]*?)```|^(\\d+[→│].+)$)/gm;\n let match;\n while ((match = fileReadPattern.exec(message.content)) !== null) {\n const path = match[1];\n const content = match[2] || match[3] || '';\n if (path && content) {\n reads.push({ path, content });\n }\n }\n } else if (Array.isArray(message.content)) {\n for (const part of message.content) {\n if (part.type === 'tool_result' && part.text) {\n // Tool results often contain file contents\n // Look for common patterns from Read tool\n const lines = part.text.split('\\n');\n const firstLine = lines[0] || '';\n\n // Pattern: \"Reading /path/to/file\"\n if (firstLine.includes('→') || firstLine.match(/^\\s*\\d+[│|]/)) {\n // This looks like file content with line numbers\n // Try to extract path from context\n // (In practice, we'd get this from the tool input)\n }\n }\n }\n }\n\n return reads;\n}\n\n/**\n * Check if content represents a file that was previously read.\n *\n * @param deduplicator - Deduplicator instance\n * @param path - File path\n * @param content - Current content\n * @param savingsThreshold - Minimum savings ratio to deduplicate (default: 0.3 = 30%)\n * @returns Reference message if should deduplicate, null otherwise\n */\nexport function shouldDeduplicate(\n deduplicator: FileDeduplicator,\n path: string,\n content: string,\n savingsThreshold = 0.3\n): string | null {\n const previous = deduplicator.getPreviousRead(path);\n\n if (!previous) {\n // First read - track it\n deduplicator.trackRead(path, content);\n return null;\n }\n\n const currentHash = hashContent(content);\n const currentTokens = estimateTokens(content, 'code');\n\n // Check if content changed\n if (previous.contentHash !== currentHash) {\n // Content changed - keep the new version, track it\n deduplicator.trackRead(path, content);\n return null;\n }\n\n // Calculate savings\n const referenceTokens = estimateTokens(FileDeduplicator.createReference(path, previous.turnIndex));\n const savings = (currentTokens - referenceTokens) / currentTokens;\n\n if (savings < savingsThreshold) {\n // Not worth deduplicating (Cline uses 30% threshold)\n deduplicator.trackRead(path, content);\n return null;\n }\n\n // Track this read but return reference\n deduplicator.trackRead(path, content);\n return FileDeduplicator.createReference(path, previous.turnIndex);\n}\n","/**\n * Token-based pruning for context compression.\n *\n * Based on OpenCode's approach: protect recent tool outputs (40K tokens)\n * while pruning older outputs that exceed the threshold.\n *\n * Key insight: Recent context is critical for coherence. Older tool\n * outputs can be removed entirely without summarization.\n */\n\nimport { estimateTokens, estimateMessageTokens } from './tokens.js';\n\n/**\n * Configuration for token pruning.\n */\nexport interface PruneConfig {\n /** Tokens to protect from pruning (recent window). Default: 40000 */\n protectRecent: number;\n /** Minimum tokens that must be prunable before we prune. Default: 20000 */\n minimumPrunable: number;\n /** Tool types that should never be pruned */\n protectedTools: string[];\n}\n\nconst DEFAULT_CONFIG: PruneConfig = {\n protectRecent: 40_000,\n minimumPrunable: 20_000,\n protectedTools: ['skill', 'memory', 'goal'],\n};\n\n/**\n * Message structure for pruning.\n */\nexport interface PrunableMessage {\n role: string;\n content: string | Array<MessagePart>;\n /** Internal: marks message as prunable */\n _prunable?: boolean;\n /** Internal: token count for this message */\n _tokens?: number;\n}\n\nexport interface MessagePart {\n type: string;\n text?: string;\n tool_use_id?: string;\n name?: string;\n /** Internal: marks part as pruned */\n _pruned?: boolean;\n /** Internal: timestamp when pruned */\n _prunedAt?: number;\n}\n\n/**\n * Placeholder for pruned tool output.\n */\nexport function createPrunedPlaceholder(toolName: string, tokensSaved: number): string {\n return `[Tool output pruned: ${toolName} (~${Math.round(tokensSaved / 1000)}K tokens)]`;\n}\n\n/**\n * Token pruner for conversation context.\n */\nexport class TokenPruner {\n private config: PruneConfig;\n\n constructor(config: Partial<PruneConfig> = {}) {\n this.config = { ...DEFAULT_CONFIG, ...config };\n }\n\n /**\n * Prune messages to reduce token count.\n *\n * Strategy:\n * 1. Scan messages backward from newest to oldest\n * 2. Accumulate tokens for tool outputs\n * 3. Mark outputs beyond protection window for pruning\n * 4. Replace pruned outputs with placeholders\n *\n * @param messages - Messages to prune\n * @returns Pruned messages (new array, originals not mutated)\n */\n pruneMessages(messages: PrunableMessage[]): PrunableMessage[] {\n // First pass: calculate tokens for each message\n const annotated = messages.map((msg) => ({\n ...msg,\n _tokens: estimateMessageTokens(msg),\n }));\n\n // Calculate what's prunable\n const analysis = this.analyzePrunability(annotated);\n\n if (analysis.prunableTokens < this.config.minimumPrunable) {\n // Not enough to prune - return unchanged\n return messages;\n }\n\n // Second pass: prune from oldest to newest, stopping at protection window\n return this.applyPruning(annotated, analysis.protectionIndex);\n }\n\n /**\n * Analyze which messages can be pruned.\n */\n private analyzePrunability(messages: Array<PrunableMessage & { _tokens: number }>): {\n prunableTokens: number;\n protectedTokens: number;\n protectionIndex: number;\n } {\n let protectedTokens = 0;\n let protectionIndex = messages.length;\n\n // Scan backward to find protection boundary\n for (let i = messages.length - 1; i >= 0; i--) {\n const msg = messages[i];\n\n // Only tool results are prunable\n if (!this.isToolResult(msg)) {\n protectedTokens += msg._tokens;\n continue;\n }\n\n // Check if tool is protected type\n if (this.isProtectedTool(msg)) {\n protectedTokens += msg._tokens;\n continue;\n }\n\n // Add to protected until we hit threshold\n if (protectedTokens + msg._tokens <= this.config.protectRecent) {\n protectedTokens += msg._tokens;\n } else {\n // Found the boundary\n protectionIndex = i + 1;\n break;\n }\n }\n\n // Calculate prunable tokens (everything before protection index that's a tool result)\n let prunableTokens = 0;\n for (let i = 0; i < protectionIndex; i++) {\n const msg = messages[i];\n if (this.isToolResult(msg) && !this.isProtectedTool(msg)) {\n prunableTokens += msg._tokens;\n }\n }\n\n return { prunableTokens, protectedTokens, protectionIndex };\n }\n\n /**\n * Apply pruning to messages before the protection index.\n */\n private applyPruning(\n messages: Array<PrunableMessage & { _tokens: number }>,\n protectionIndex: number\n ): PrunableMessage[] {\n return messages.map((msg, i) => {\n // Messages at or after protection index are kept as-is\n if (i >= protectionIndex) {\n // Remove internal annotations\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n const { _tokens, _prunable, ...clean } = msg;\n return clean;\n }\n\n // Non-tool messages are kept\n if (!this.isToolResult(msg)) {\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n const { _tokens, _prunable, ...clean } = msg;\n return clean;\n }\n\n // Protected tools are kept\n if (this.isProtectedTool(msg)) {\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n const { _tokens, _prunable, ...clean } = msg;\n return clean;\n }\n\n // Prune this tool result\n return this.createPrunedMessage(msg);\n });\n }\n\n /**\n * Create a pruned version of a message.\n */\n private createPrunedMessage(msg: PrunableMessage & { _tokens: number }): PrunableMessage {\n const toolName = this.getToolName(msg);\n const placeholder = createPrunedPlaceholder(toolName, msg._tokens);\n\n if (typeof msg.content === 'string') {\n return {\n role: msg.role,\n content: placeholder,\n };\n }\n\n // For array content, replace tool result parts\n const content = msg.content.map((part) => {\n if (part.type === 'tool_result' && part.text) {\n return {\n ...part,\n text: placeholder,\n _pruned: true,\n _prunedAt: Date.now(),\n };\n }\n return part;\n });\n\n return {\n role: msg.role,\n content,\n };\n }\n\n /**\n * Check if a message is a tool result.\n */\n private isToolResult(msg: PrunableMessage): boolean {\n if (msg.role === 'tool') return true;\n\n if (Array.isArray(msg.content)) {\n return msg.content.some((part) => part.type === 'tool_result');\n }\n\n return false;\n }\n\n /**\n * Check if a tool is in the protected list.\n */\n private isProtectedTool(msg: PrunableMessage): boolean {\n const toolName = this.getToolName(msg);\n return this.config.protectedTools.includes(toolName.toLowerCase());\n }\n\n /**\n * Extract tool name from a message.\n */\n private getToolName(msg: PrunableMessage): string {\n if (Array.isArray(msg.content)) {\n for (const part of msg.content) {\n if (part.name) return part.name;\n }\n }\n\n // Try to extract from content\n if (typeof msg.content === 'string') {\n const match = msg.content.match(/Tool (?:output|result).*?:\\s*(\\w+)/i);\n if (match) return match[1];\n }\n\n return 'unknown';\n }\n\n /**\n * Get statistics about potential pruning.\n */\n getStats(messages: PrunableMessage[]): {\n totalTokens: number;\n prunableTokens: number;\n protectedTokens: number;\n savingsPercentage: number;\n } {\n const annotated = messages.map((msg) => ({\n ...msg,\n _tokens: estimateMessageTokens(msg),\n }));\n\n const totalTokens = annotated.reduce((sum, msg) => sum + msg._tokens, 0);\n const analysis = this.analyzePrunability(annotated);\n\n return {\n totalTokens,\n prunableTokens: analysis.prunableTokens,\n protectedTokens: analysis.protectedTokens,\n savingsPercentage: totalTokens > 0 ? (analysis.prunableTokens / totalTokens) * 100 : 0,\n };\n }\n}\n","/**\n * LLM-based summarization for heavy context compression.\n *\n * Based on OpenHands' Context Condenser approach:\n * - Keep first N events (initial context)\n * - Keep last M events (recent context)\n * - Summarize the middle section via LLM\n *\n * This is the \"last resort\" compression - only used when at 95%+ context.\n */\n\nimport Anthropic from '@anthropic-ai/sdk';\nimport { estimateTokens } from './tokens.js';\n\n/**\n * Configuration for LLM summarization.\n */\nexport interface SummaryConfig {\n /** Number of messages to preserve from start. Default: 4 */\n keepFirst: number;\n /** Number of messages to preserve from end. Default: 20 */\n keepLast: number;\n /** Model to use for summarization. Default: 'claude-3-5-haiku-20241022' */\n model: string;\n /** Maximum tokens for summary output. Default: 2000 */\n maxSummaryTokens: number;\n}\n\nconst DEFAULT_CONFIG: SummaryConfig = {\n keepFirst: 4,\n keepLast: 20,\n model: 'claude-3-5-haiku-20241022', // Cheap and fast\n maxSummaryTokens: 2000,\n};\n\n/**\n * Message structure for summarization.\n */\nexport interface SummarizableMessage {\n role: string;\n content: string | Array<{ type: string; text?: string }>;\n}\n\n/**\n * Summarization prompt template.\n */\nconst SUMMARIZATION_PROMPT = `You are summarizing a conversation between a user and an AI assistant to reduce context length while preserving essential information.\n\n<conversation_to_summarize>\n{{MIDDLE_CONTENT}}\n</conversation_to_summarize>\n\nCreate a concise summary that preserves:\n1. **User's goals** - What the user is trying to accomplish\n2. **Progress made** - Key actions taken and their outcomes\n3. **Current state** - What's done vs. what still needs to be done\n4. **Critical context** - File paths, error messages, decisions made\n5. **Blockers** - Any issues that need to be resolved\n\nFormat your summary as a structured note that the assistant can reference to continue the work.\n\nKeep the summary under {{MAX_TOKENS}} tokens. Focus on actionable information.`;\n\n/**\n * LLM-based conversation summarizer.\n */\nexport class ConversationSummarizer {\n private config: SummaryConfig;\n private client: Anthropic | null = null;\n\n constructor(config: Partial<SummaryConfig> = {}) {\n this.config = { ...DEFAULT_CONFIG, ...config };\n }\n\n /**\n * Get or create Anthropic client.\n */\n private getClient(): Anthropic {\n if (!this.client) {\n this.client = new Anthropic();\n }\n return this.client;\n }\n\n /**\n * Summarize messages to reduce token count.\n *\n * Strategy:\n * 1. Keep first N messages (system prompt, initial context)\n * 2. Keep last M messages (recent context, current task)\n * 3. Summarize everything in between\n *\n * @param messages - Messages to summarize\n * @returns Summarized messages\n */\n async summarize(messages: SummarizableMessage[]): Promise<SummarizableMessage[]> {\n if (messages.length <= this.config.keepFirst + this.config.keepLast) {\n // Not enough messages to summarize\n return messages;\n }\n\n const firstMessages = messages.slice(0, this.config.keepFirst);\n const middleMessages = messages.slice(this.config.keepFirst, -this.config.keepLast);\n const lastMessages = messages.slice(-this.config.keepLast);\n\n // Generate summary of middle section\n const summary = await this.generateSummary(middleMessages);\n\n // Create summary message\n const summaryMessage: SummarizableMessage = {\n role: 'user',\n content: `[Context Summary - ${middleMessages.length} messages condensed]\\n\\n${summary}`,\n };\n\n return [...firstMessages, summaryMessage, ...lastMessages];\n }\n\n /**\n * Generate a summary of the middle messages.\n */\n private async generateSummary(messages: SummarizableMessage[]): Promise<string> {\n const middleContent = this.formatMessagesForSummary(messages);\n\n const prompt = SUMMARIZATION_PROMPT.replace('{{MIDDLE_CONTENT}}', middleContent).replace(\n '{{MAX_TOKENS}}',\n String(this.config.maxSummaryTokens)\n );\n\n try {\n const client = this.getClient();\n const response = await client.messages.create({\n model: this.config.model,\n max_tokens: this.config.maxSummaryTokens,\n messages: [\n {\n role: 'user',\n content: prompt,\n },\n ],\n });\n\n // Extract text from response\n const textBlock = response.content.find((block) => block.type === 'text');\n if (textBlock && 'text' in textBlock) {\n return textBlock.text;\n }\n\n return '[Summary generation failed - no text in response]';\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n return `[Summary generation failed: ${message}]`;\n }\n }\n\n /**\n * Format messages for the summarization prompt.\n */\n private formatMessagesForSummary(messages: SummarizableMessage[]): string {\n return messages\n .map((msg, i) => {\n const role = msg.role.toUpperCase();\n const content = this.extractContent(msg);\n return `[${i + 1}] ${role}:\\n${content}`;\n })\n .join('\\n\\n---\\n\\n');\n }\n\n /**\n * Extract text content from a message.\n */\n private extractContent(msg: SummarizableMessage): string {\n if (typeof msg.content === 'string') {\n return this.truncateContent(msg.content);\n }\n\n const parts: string[] = [];\n for (const part of msg.content) {\n if (part.text) {\n parts.push(this.truncateContent(part.text));\n }\n }\n return parts.join('\\n');\n }\n\n /**\n * Truncate very long content for summary input.\n */\n private truncateContent(content: string, maxChars = 2000): string {\n if (content.length <= maxChars) return content;\n return content.slice(0, maxChars) + `\\n[...truncated ${content.length - maxChars} chars]`;\n }\n\n /**\n * Estimate the cost of summarization.\n *\n * @param messages - Messages that would be summarized\n * @returns Estimated cost in USD\n */\n estimateCost(messages: SummarizableMessage[]): number {\n if (messages.length <= this.config.keepFirst + this.config.keepLast) {\n return 0;\n }\n\n const middleMessages = messages.slice(this.config.keepFirst, -this.config.keepLast);\n const inputTokens = middleMessages.reduce((sum, msg) => {\n const content = typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content);\n return sum + estimateTokens(content);\n }, 0);\n\n // Haiku pricing: $0.25/MTok input, $1.25/MTok output\n const inputCost = (inputTokens / 1_000_000) * 0.25;\n const outputCost = (this.config.maxSummaryTokens / 1_000_000) * 1.25;\n\n return inputCost + outputCost;\n }\n\n /**\n * Get statistics about potential summarization.\n */\n getStats(messages: SummarizableMessage[]): {\n totalMessages: number;\n wouldKeep: number;\n wouldSummarize: number;\n estimatedCost: number;\n } {\n const wouldKeep = Math.min(messages.length, this.config.keepFirst + this.config.keepLast);\n const wouldSummarize = Math.max(0, messages.length - wouldKeep);\n\n return {\n totalMessages: messages.length,\n wouldKeep,\n wouldSummarize,\n estimatedCost: this.estimateCost(messages),\n };\n }\n}\n\n/**\n * Create a summary message without LLM call (for testing/fallback).\n */\nexport function createFallbackSummary(messages: SummarizableMessage[]): string {\n const actions: string[] = [];\n const files: Set<string> = new Set();\n const errors: string[] = [];\n\n for (const msg of messages) {\n const content = typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content);\n\n // Extract file paths\n const fileMatches = content.match(/(?:\\/[\\w.\\-/]+\\.[\\w]+)/g);\n if (fileMatches) {\n fileMatches.forEach((f) => files.add(f));\n }\n\n // Extract errors\n if (content.toLowerCase().includes('error')) {\n const errorMatch = content.match(/error[:\\s]+([^\\n]+)/i);\n if (errorMatch && errors.length < 3) {\n errors.push(errorMatch[1].slice(0, 100));\n }\n }\n\n // Extract key actions (from assistant messages)\n if (msg.role === 'assistant' && content.length < 500) {\n actions.push(content.slice(0, 100));\n }\n }\n\n const parts: string[] = ['## Conversation Summary (Fallback)', ''];\n\n if (files.size > 0) {\n parts.push('### Files Referenced');\n parts.push([...files].slice(0, 10).map((f) => `- ${f}`).join('\\n'));\n parts.push('');\n }\n\n if (actions.length > 0) {\n parts.push('### Key Actions');\n parts.push(actions.slice(0, 5).map((a) => `- ${a}`).join('\\n'));\n parts.push('');\n }\n\n if (errors.length > 0) {\n parts.push('### Errors Encountered');\n parts.push(errors.map((e) => `- ${e}`).join('\\n'));\n }\n\n return parts.join('\\n');\n}\n","/**\n * Context Condenser - Main Pipeline\n *\n * Coordinates the three compression strategies:\n * 1. Deduplication (70% threshold) - Replace duplicate file reads\n * 2. Pruning (85% threshold) - Remove old tool outputs\n * 3. Summarization (95% threshold) - LLM-based middle section summary\n *\n * Based on patterns from OpenCode, OpenHands, and Cline.\n */\n\nimport {\n TokenTracker,\n createTracker,\n updateTrackerFromMessage,\n getCompressionLevel,\n CompressionLevel,\n ThresholdConfig,\n formatTrackerStatus,\n} from './tokens.js';\nimport { FileDeduplicator } from './deduplication.js';\nimport { TokenPruner, PruneConfig, PrunableMessage } from './pruning.js';\nimport { ConversationSummarizer, SummaryConfig, SummarizableMessage } from './summarizer.js';\n\n// Re-export types\nexport { TokenTracker, CompressionLevel, ThresholdConfig };\nexport { FileDeduplicator } from './deduplication.js';\nexport { TokenPruner, PruneConfig } from './pruning.js';\nexport { ConversationSummarizer, SummaryConfig } from './summarizer.js';\nexport * from './tokens.js';\n\n/**\n * Configuration for the context condenser.\n */\nexport interface CondenserConfig {\n /** Whether context compression is enabled */\n enabled: boolean;\n /** Threshold for light compression (deduplication) */\n lightThreshold: number;\n /** Threshold for medium compression (pruning) */\n mediumThreshold: number;\n /** Threshold for heavy compression (summarization) */\n heavyThreshold: number;\n /** Model context limit */\n modelLimit: number;\n /** Model name for tracking */\n model: string;\n /** Pruning configuration */\n pruning: Partial<PruneConfig>;\n /** Summarization configuration */\n summarization: Partial<SummaryConfig>;\n}\n\nconst DEFAULT_CONFIG: CondenserConfig = {\n enabled: true,\n lightThreshold: 0.7,\n mediumThreshold: 0.85,\n heavyThreshold: 0.95,\n modelLimit: 200_000,\n model: 'claude-sonnet-4-20250514',\n pruning: {},\n summarization: {},\n};\n\n/**\n * Message type for the condenser pipeline.\n */\nexport interface CondenserMessage extends PrunableMessage, SummarizableMessage {\n role: string;\n content: string | Array<{ type: string; text?: string; tool_use_id?: string; name?: string }>;\n}\n\n/**\n * Result of a condense operation.\n */\nexport interface CondenserResult {\n /** Condensed messages */\n messages: CondenserMessage[];\n /** Compression level applied */\n level: CompressionLevel;\n /** Tokens before compression */\n tokensBefore: number;\n /** Tokens after compression */\n tokensAfter: number;\n /** Savings percentage */\n savingsPercentage: number;\n /** Duration in milliseconds */\n durationMs: number;\n}\n\n/**\n * Context Condenser - Main class.\n */\nexport class ContextCondenser {\n private config: CondenserConfig;\n private tracker: TokenTracker;\n private deduplicator: FileDeduplicator;\n private pruner: TokenPruner;\n private summarizer: ConversationSummarizer;\n\n /** Metrics for tracking */\n private metrics = {\n condensationCount: 0,\n tokensRecovered: 0,\n lastLevel: 'none' as CompressionLevel,\n };\n\n constructor(config: Partial<CondenserConfig> = {}) {\n this.config = { ...DEFAULT_CONFIG, ...config };\n this.tracker = createTracker(this.config.model);\n this.tracker.limit = this.config.modelLimit;\n this.deduplicator = new FileDeduplicator();\n this.pruner = new TokenPruner(this.config.pruning);\n this.summarizer = new ConversationSummarizer(this.config.summarization);\n }\n\n /**\n * Main entry point - condense messages if needed.\n *\n * @param messages - Current conversation messages\n * @returns Condensed messages and metadata\n */\n async condense(messages: CondenserMessage[]): Promise<CondenserResult> {\n const startTime = Date.now();\n\n if (!this.config.enabled) {\n return this.createResult(messages, messages, 'none', startTime);\n }\n\n // Update tracker with all messages\n this.updateTrackerFromMessages(messages);\n\n const level = getCompressionLevel(this.tracker, {\n light: this.config.lightThreshold,\n medium: this.config.mediumThreshold,\n heavy: this.config.heavyThreshold,\n });\n\n if (level === 'none') {\n return this.createResult(messages, messages, level, startTime);\n }\n\n let condensed: CondenserMessage[];\n\n switch (level) {\n case 'light':\n condensed = this.applyDeduplication(messages);\n break;\n case 'medium':\n condensed = this.applyPruning(messages);\n break;\n case 'heavy':\n condensed = await this.applySummarization(messages);\n break;\n }\n\n // Update metrics\n this.metrics.condensationCount++;\n this.metrics.lastLevel = level;\n\n return this.createResult(messages, condensed, level, startTime);\n }\n\n /**\n * Apply light compression (deduplication).\n */\n private applyDeduplication(messages: CondenserMessage[]): CondenserMessage[] {\n // For now, just return messages as-is\n // Full deduplication requires tracking reads across tool calls\n // which we'd integrate with the actual tool execution layer\n return messages;\n }\n\n /**\n * Apply medium compression (pruning).\n */\n private applyPruning(messages: CondenserMessage[]): CondenserMessage[] {\n return this.pruner.pruneMessages(messages) as CondenserMessage[];\n }\n\n /**\n * Apply heavy compression (summarization).\n */\n private async applySummarization(messages: CondenserMessage[]): Promise<CondenserMessage[]> {\n // First apply pruning\n const pruned = this.applyPruning(messages);\n\n // Then summarize if still needed\n const summarized = await this.summarizer.summarize(pruned);\n\n return summarized as CondenserMessage[];\n }\n\n /**\n * Update tracker from messages.\n */\n private updateTrackerFromMessages(messages: CondenserMessage[]): void {\n // Reset tracker\n this.tracker = createTracker(this.config.model);\n this.tracker.limit = this.config.modelLimit;\n\n // Add all messages\n for (const msg of messages) {\n updateTrackerFromMessage(this.tracker, msg);\n }\n }\n\n /**\n * Create result object.\n */\n private createResult(\n before: CondenserMessage[],\n after: CondenserMessage[],\n level: CompressionLevel,\n startTime: number\n ): CondenserResult {\n const tokensBefore = this.estimateTokens(before);\n const tokensAfter = this.estimateTokens(after);\n const savings = tokensBefore > 0 ? ((tokensBefore - tokensAfter) / tokensBefore) * 100 : 0;\n\n if (level !== 'none') {\n this.metrics.tokensRecovered += tokensBefore - tokensAfter;\n }\n\n return {\n messages: after,\n level,\n tokensBefore,\n tokensAfter,\n savingsPercentage: savings,\n durationMs: Date.now() - startTime,\n };\n }\n\n /**\n * Estimate tokens for messages.\n */\n private estimateTokens(messages: CondenserMessage[]): number {\n const tempTracker = createTracker(this.config.model);\n for (const msg of messages) {\n updateTrackerFromMessage(tempTracker, msg);\n }\n return tempTracker.used;\n }\n\n /**\n * Get current tracker status.\n */\n getStatus(): string {\n return formatTrackerStatus(this.tracker);\n }\n\n /**\n * Get tracker for external monitoring.\n */\n getTracker(): TokenTracker {\n return { ...this.tracker };\n }\n\n /**\n * Get metrics.\n */\n getMetrics(): typeof this.metrics {\n return { ...this.metrics };\n }\n\n /**\n * Check if compression is needed.\n */\n needsCompression(): CompressionLevel {\n return getCompressionLevel(this.tracker, {\n light: this.config.lightThreshold,\n medium: this.config.mediumThreshold,\n heavy: this.config.heavyThreshold,\n });\n }\n\n /**\n * Reset condenser state.\n */\n reset(): void {\n this.tracker = createTracker(this.config.model);\n this.tracker.limit = this.config.modelLimit;\n this.deduplicator.reset();\n this.metrics = {\n condensationCount: 0,\n tokensRecovered: 0,\n lastLevel: 'none',\n };\n }\n\n /**\n * Get file deduplicator for integration with tool layer.\n */\n getDeduplicator(): FileDeduplicator {\n return this.deduplicator;\n }\n}\n\n/**\n * Create a condenser with squad-specific configuration.\n */\nexport function createCondenser(squadConfig?: {\n condenser?: {\n enabled?: boolean;\n light_threshold?: number;\n medium_threshold?: number;\n heavy_threshold?: number;\n protect_recent?: number;\n };\n model?: {\n default?: string;\n };\n}): ContextCondenser {\n const condenserConfig = squadConfig?.condenser || {};\n const modelConfig = squadConfig?.model || {};\n\n return new ContextCondenser({\n enabled: condenserConfig.enabled ?? true,\n lightThreshold: condenserConfig.light_threshold ?? 0.7,\n mediumThreshold: condenserConfig.medium_threshold ?? 0.85,\n heavyThreshold: condenserConfig.heavy_threshold ?? 0.95,\n model: modelConfig.default ?? 'claude-sonnet-4-20250514',\n pruning: {\n protectRecent: condenserConfig.protect_recent ?? 40_000,\n },\n });\n}\n"],"mappings":"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","names":["require","DEFAULT_CONFIG","DEFAULT_CONFIG"]}
|
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
# Squads
|
|
2
|
-
# Full telemetry pipeline + Memory system
|
|
1
|
+
# Squads Full Stack - With Memory + Scheduler
|
|
2
|
+
# Full telemetry pipeline + Memory system + Scheduler
|
|
3
3
|
#
|
|
4
4
|
# Usage:
|
|
5
5
|
# cd docker && docker-compose -f docker-compose.engram.yml up -d
|
|
6
|
+
# With webhooks: docker-compose -f docker-compose.engram.yml --profile webhooks up -d
|
|
6
7
|
#
|
|
7
8
|
# Access:
|
|
8
9
|
# Langfuse UI: http://localhost:3100
|
|
9
|
-
# Neo4j Browser: http://localhost:7474
|
|
10
|
-
# Engram MCP: http://localhost:8080
|
|
11
10
|
# Mem0 API: http://localhost:8000
|
|
11
|
+
# Scheduler API: http://localhost:8090
|
|
12
12
|
# Postgres: localhost:5433
|
|
13
13
|
|
|
14
14
|
name: agents-squads
|
|
@@ -60,30 +60,6 @@ services:
|
|
|
60
60
|
networks:
|
|
61
61
|
- squads
|
|
62
62
|
|
|
63
|
-
# =============================================================================
|
|
64
|
-
# Neo4j - Knowledge graph for agent memory
|
|
65
|
-
# =============================================================================
|
|
66
|
-
neo4j:
|
|
67
|
-
image: neo4j:5-community
|
|
68
|
-
container_name: squads-neo4j
|
|
69
|
-
restart: unless-stopped
|
|
70
|
-
environment:
|
|
71
|
-
NEO4J_AUTH: neo4j/${NEO4J_PASSWORD:-squads_local_dev}
|
|
72
|
-
NEO4J_PLUGINS: '["apoc"]'
|
|
73
|
-
ports:
|
|
74
|
-
- "${NEO4J_HTTP_PORT:-7474}:7474"
|
|
75
|
-
- "${NEO4J_BOLT_PORT:-7687}:7687"
|
|
76
|
-
volumes:
|
|
77
|
-
- squads_neo4j_data:/data
|
|
78
|
-
healthcheck:
|
|
79
|
-
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:7474"]
|
|
80
|
-
interval: 10s
|
|
81
|
-
timeout: 5s
|
|
82
|
-
retries: 5
|
|
83
|
-
start_period: 30s
|
|
84
|
-
networks:
|
|
85
|
-
- squads
|
|
86
|
-
|
|
87
63
|
# =============================================================================
|
|
88
64
|
# Langfuse - LLM Observability (local instance)
|
|
89
65
|
# =============================================================================
|
|
@@ -199,10 +175,8 @@ services:
|
|
|
199
175
|
POSTGRES_PASSWORD: squads
|
|
200
176
|
POSTGRES_DB: engram
|
|
201
177
|
POSTGRES_COLLECTION_NAME: memories
|
|
202
|
-
# Neo4j
|
|
203
|
-
|
|
204
|
-
NEO4J_USERNAME: neo4j
|
|
205
|
-
NEO4J_PASSWORD: ${NEO4J_PASSWORD:-squads_local_dev}
|
|
178
|
+
# Neo4j (disabled by default - saves ~600MB RAM)
|
|
179
|
+
NEO4J_ENABLED: ${NEO4J_ENABLED:-false}
|
|
206
180
|
# LLM Provider: openai (fast) or ollama (local)
|
|
207
181
|
# Set LLM_PROVIDER=openai and OPENAI_API_KEY for fast embeddings
|
|
208
182
|
LLM_PROVIDER: ${LLM_PROVIDER:-openai}
|
|
@@ -223,8 +197,6 @@ services:
|
|
|
223
197
|
depends_on:
|
|
224
198
|
postgres:
|
|
225
199
|
condition: service_healthy
|
|
226
|
-
neo4j:
|
|
227
|
-
condition: service_healthy
|
|
228
200
|
healthcheck:
|
|
229
201
|
test: ["CMD-SHELL", "python -c \"import urllib.request; urllib.request.urlopen('http://localhost:8000/health')\" || exit 1"]
|
|
230
202
|
interval: 10s
|
|
@@ -235,47 +207,65 @@ services:
|
|
|
235
207
|
- squads
|
|
236
208
|
|
|
237
209
|
# =============================================================================
|
|
238
|
-
#
|
|
210
|
+
# Scheduler API - Trigger management and webhooks
|
|
239
211
|
# =============================================================================
|
|
240
|
-
|
|
212
|
+
scheduler-api:
|
|
241
213
|
build:
|
|
242
|
-
context: ../../
|
|
214
|
+
context: ../../hq/squads-scheduler
|
|
243
215
|
dockerfile: Dockerfile
|
|
244
|
-
container_name: squads-
|
|
216
|
+
container_name: squads-scheduler-api
|
|
245
217
|
restart: unless-stopped
|
|
218
|
+
command: uvicorn api:app --host 0.0.0.0 --port 8090
|
|
246
219
|
environment:
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
# Database (for auth)
|
|
250
|
-
POSTGRES_HOST: postgres
|
|
251
|
-
POSTGRES_PORT: 5432
|
|
252
|
-
POSTGRES_USER: squads
|
|
253
|
-
POSTGRES_PASSWORD: squads
|
|
254
|
-
POSTGRES_DB: engram
|
|
255
|
-
# MCP Server
|
|
256
|
-
MCP_HOST: 0.0.0.0
|
|
257
|
-
MCP_PORT: 8080
|
|
258
|
-
# Project ID Mode
|
|
259
|
-
PROJECT_ID_MODE: ${PROJECT_ID_MODE:-auto}
|
|
260
|
-
DEFAULT_USER_ID: ${DEFAULT_USER_ID:-claude_code_mcp}
|
|
261
|
-
# Chunking
|
|
262
|
-
CHUNK_MAX_SIZE: ${CHUNK_MAX_SIZE:-1000}
|
|
263
|
-
CHUNK_OVERLAP_SIZE: ${CHUNK_OVERLAP_SIZE:-150}
|
|
264
|
-
# Logging
|
|
265
|
-
LOG_LEVEL: ${LOG_LEVEL:-INFO}
|
|
220
|
+
DATABASE_URL: postgresql://squads:squads@postgres:5432/squads
|
|
221
|
+
STRIPE_WEBHOOK_SECRET: ${STRIPE_WEBHOOK_SECRET:-}
|
|
266
222
|
ports:
|
|
267
|
-
- "${
|
|
223
|
+
- "${SCHEDULER_API_PORT:-8090}:8090"
|
|
268
224
|
depends_on:
|
|
269
|
-
mem0:
|
|
270
|
-
condition: service_healthy
|
|
271
225
|
postgres:
|
|
272
226
|
condition: service_healthy
|
|
273
227
|
healthcheck:
|
|
274
|
-
test: ["CMD
|
|
275
|
-
interval:
|
|
276
|
-
timeout:
|
|
277
|
-
retries:
|
|
278
|
-
|
|
228
|
+
test: ["CMD", "curl", "-f", "http://localhost:8090/health"]
|
|
229
|
+
interval: 30s
|
|
230
|
+
timeout: 10s
|
|
231
|
+
retries: 3
|
|
232
|
+
networks:
|
|
233
|
+
- squads
|
|
234
|
+
|
|
235
|
+
# =============================================================================
|
|
236
|
+
# Scheduler Worker - Trigger evaluation and agent execution
|
|
237
|
+
# =============================================================================
|
|
238
|
+
scheduler-worker:
|
|
239
|
+
build:
|
|
240
|
+
context: ../../hq/squads-scheduler
|
|
241
|
+
dockerfile: Dockerfile
|
|
242
|
+
container_name: squads-scheduler-worker
|
|
243
|
+
restart: unless-stopped
|
|
244
|
+
command: python scheduler.py
|
|
245
|
+
environment:
|
|
246
|
+
DATABASE_URL: postgresql://squads:squads@postgres:5432/squads
|
|
247
|
+
HQ_PATH: /hq
|
|
248
|
+
SQUADS_CLI: squads
|
|
249
|
+
volumes:
|
|
250
|
+
- ../../hq:/hq:ro
|
|
251
|
+
depends_on:
|
|
252
|
+
- scheduler-api
|
|
253
|
+
networks:
|
|
254
|
+
- squads
|
|
255
|
+
|
|
256
|
+
# =============================================================================
|
|
257
|
+
# Ngrok - Tunnel for webhooks (dev/testing)
|
|
258
|
+
# =============================================================================
|
|
259
|
+
ngrok:
|
|
260
|
+
image: ngrok/ngrok:latest
|
|
261
|
+
container_name: squads-ngrok
|
|
262
|
+
restart: unless-stopped
|
|
263
|
+
profiles: ["webhooks"] # Only start with: --profile webhooks
|
|
264
|
+
command: http scheduler-api:8090 --log stdout
|
|
265
|
+
environment:
|
|
266
|
+
NGROK_AUTHTOKEN: ${NGROK_AUTHTOKEN:-}
|
|
267
|
+
depends_on:
|
|
268
|
+
- scheduler-api
|
|
279
269
|
networks:
|
|
280
270
|
- squads
|
|
281
271
|
|
|
@@ -286,4 +276,3 @@ networks:
|
|
|
286
276
|
volumes:
|
|
287
277
|
squads_postgres_data:
|
|
288
278
|
squads_redis_data:
|
|
289
|
-
squads_neo4j_data:
|