diff --git a/agent-system/agents/base-agent.ts b/agent-system/agents/base-agent.ts new file mode 100644 index 0000000..d16230e --- /dev/null +++ b/agent-system/agents/base-agent.ts @@ -0,0 +1,333 @@ +/** + * Base Agent Module + * + * Provides the foundation for creating specialized agents + * with context management, memory, and tool integration. + */ + +import { randomUUID } from 'crypto'; +import ZAI from 'z-ai-web-dev-sdk'; +import { ConversationContextManager, CompactionConfig } from '../core/context-manager'; +import { TokenCounter } from '../core/token-counter'; + +export interface AgentMemory { + shortTerm: Map; + longTerm: Map; + conversationHistory: Array<{ + role: 'user' | 'assistant' | 'system'; + content: string; + timestamp: Date; + }>; +} + +export interface AgentTool { + name: string; + description: string; + execute: (params: unknown) => Promise; +} + +export interface AgentConfig { + id?: string; + name: string; + description: string; + systemPrompt: string; + tools?: AgentTool[]; + maxTokens?: number; + contextConfig?: Partial; +} + +export interface AgentResponse { + content: string; + tokens: { + prompt: number; + completion: number; + total: number; + }; + toolCalls?: Array<{ + name: string; + params: unknown; + result: unknown; + }>; + metadata?: Record; +} + +/** + * BaseAgent - Foundation class for all agents + */ +export abstract class BaseAgent { + readonly id: string; + readonly name: string; + readonly description: string; + + protected systemPrompt: string; + protected tools: Map; + protected memory: AgentMemory; + protected contextManager: ConversationContextManager; + protected tokenCounter: TokenCounter; + protected zai: Awaited> | null = null; + protected initialized = false; + + constructor(config: AgentConfig) { + this.id = config.id || randomUUID(); + this.name = config.name; + this.description = config.description; + this.systemPrompt = config.systemPrompt; + + this.tools = new Map(); + if (config.tools) { + for (const tool of config.tools) { + this.tools.set(tool.name, tool); + } + } + + this.memory = { + shortTerm: new Map(), + longTerm: new Map(), + conversationHistory: [] + }; + + this.tokenCounter = new TokenCounter(config.maxTokens); + this.contextManager = new ConversationContextManager(config.contextConfig); + } + + /** + * Initialize the agent + */ + async initialize(): Promise { + if (this.initialized) return; + this.zai = await ZAI.create(); + this.initialized = true; + } + + /** + * Process a user message + */ + async process(input: string, context?: string): Promise { + await this.initialize(); + + // Add user message to context + const userMessage = { + role: 'user' as const, + content: context ? `Context: ${context}\n\n${input}` : input, + timestamp: new Date() + }; + + this.memory.conversationHistory.push(userMessage); + + // Check if context compaction is needed + await this.contextManager.getMessages(); + + // Build messages for LLM + const messages = this.buildMessages(); + + // Get response from LLM + const response = await this.zai!.chat.completions.create({ + messages, + thinking: { type: 'disabled' } + }); + + const assistantContent = response.choices?.[0]?.message?.content || ''; + + // Add assistant response to history + this.memory.conversationHistory.push({ + role: 'assistant', + content: assistantContent, + timestamp: new Date() + }); + + // Process any tool calls (if agent supports them) + const toolCalls = await this.processToolCalls(assistantContent); + + return { + content: assistantContent, + tokens: { + prompt: 0, // Would need actual token counting + completion: 0, + total: 0 + }, + toolCalls, + metadata: { + conversationLength: this.memory.conversationHistory.length + } + }; + } + + /** + * Build messages array for LLM + */ + protected buildMessages(): Array<{ role: 'user' | 'assistant' | 'system'; content: string }> { + const messages: Array<{ role: 'user' | 'assistant' | 'system'; content: string }> = []; + + // System prompt with tool descriptions + let systemContent = this.systemPrompt; + if (this.tools.size > 0) { + const toolDescriptions = Array.from(this.tools.values()) + .map(t => `- ${t.name}: ${t.description}`) + .join('\n'); + systemContent += `\n\nAvailable tools:\n${toolDescriptions}`; + systemContent += `\n\nTo use a tool, include [TOOL:name]params[/TOOL] in your response.`; + } + + messages.push({ role: 'assistant', content: systemContent }); + + // Add conversation history + for (const msg of this.memory.conversationHistory) { + messages.push({ + role: msg.role, + content: msg.content + }); + } + + return messages; + } + + /** + * Process tool calls in the response + */ + protected async processToolCalls(content: string): Promise> { + const toolCalls: Array<{ name: string; params: unknown; result: unknown }> = []; + + // Extract tool calls from content + const toolRegex = /\[TOOL:(\w+)\]([\s\S]*?)\[\/TOOL\]/g; + let match; + + while ((match = toolRegex.exec(content)) !== null) { + const toolName = match[1]; + const paramsStr = match[2].trim(); + + const tool = this.tools.get(toolName); + if (tool) { + try { + let params = paramsStr; + try { + params = JSON.parse(paramsStr); + } catch { + // Keep as string if not valid JSON + } + + const result = await tool.execute(params); + toolCalls.push({ name: toolName, params, result }); + } catch (error) { + toolCalls.push({ + name: toolName, + params: paramsStr, + result: { error: String(error) } + }); + } + } + } + + return toolCalls; + } + + /** + * Add a tool to the agent + */ + addTool(tool: AgentTool): void { + this.tools.set(tool.name, tool); + } + + /** + * Remove a tool from the agent + */ + removeTool(name: string): boolean { + return this.tools.delete(name); + } + + /** + * Store a value in short-term memory + */ + remember(key: string, value: unknown): void { + this.memory.shortTerm.set(key, value); + } + + /** + * Retrieve a value from memory + */ + recall(key: string): unknown | undefined { + return this.memory.shortTerm.get(key) || this.memory.longTerm.get(key); + } + + /** + * Store a value in long-term memory + */ + memorize(key: string, value: unknown): void { + this.memory.longTerm.set(key, value); + } + + /** + * Clear short-term memory + */ + forget(): void { + this.memory.shortTerm.clear(); + } + + /** + * Clear conversation history + */ + clearHistory(): void { + this.memory.conversationHistory = []; + this.contextManager.clear(); + } + + /** + * Get conversation summary + */ + getSummary(): string { + const messages = this.memory.conversationHistory; + return messages.map(m => `[${m.role}]: ${m.content.substring(0, 100)}...`).join('\n'); + } + + /** + * Get agent stats + */ + getStats() { + return { + id: this.id, + name: this.name, + messageCount: this.memory.conversationHistory.length, + toolCount: this.tools.size, + memoryItems: this.memory.shortTerm.size + this.memory.longTerm.size, + contextStats: this.contextManager.getStats() + }; + } + + /** + * Abstract method for agent-specific behavior + */ + abstract act(input: string, context?: string): Promise; +} + +/** + * SimpleAgent - A basic agent implementation + */ +export class SimpleAgent extends BaseAgent { + async act(input: string, context?: string): Promise { + return this.process(input, context); + } +} + +/** + * Create a simple agent with custom system prompt + */ +export function createAgent( + name: string, + systemPrompt: string, + options?: { + description?: string; + tools?: AgentTool[]; + maxTokens?: number; + } +): SimpleAgent { + return new SimpleAgent({ + name, + systemPrompt, + description: options?.description || `Agent: ${name}`, + tools: options?.tools, + maxTokens: options?.maxTokens + }); +} diff --git a/agent-system/agents/task-agent.ts b/agent-system/agents/task-agent.ts new file mode 100644 index 0000000..a3873ff --- /dev/null +++ b/agent-system/agents/task-agent.ts @@ -0,0 +1,232 @@ +/** + * Task Agent Module + * + * Specialized agent for executing structured tasks with + * planning, execution, and verification phases. + */ + +import { BaseAgent, AgentConfig, AgentResponse, AgentTool } from './base-agent'; + +export interface TaskStep { + id: string; + description: string; + status: 'pending' | 'running' | 'completed' | 'failed'; + result?: unknown; + error?: string; +} + +export interface TaskPlan { + steps: TaskStep[]; + estimatedComplexity: 'low' | 'medium' | 'high'; + dependencies: Map; +} + +export interface TaskResult { + success: boolean; + steps: TaskStep[]; + output: unknown; + errors: string[]; +} + +/** + * TaskAgent - Agent specialized for structured task execution + */ +export class TaskAgent extends BaseAgent { + private currentPlan: TaskPlan | null = null; + private taskHistory: TaskResult[] = []; + + constructor(config: AgentConfig) { + super(config); + + // Add default tools for task agents + this.addTool({ + name: 'plan', + description: 'Create a plan for a complex task', + execute: async (params) => { + const task = params as { description: string }; + return this.createPlan(task.description); + } + }); + + this.addTool({ + name: 'execute_step', + description: 'Execute a single step of the plan', + execute: async (params) => { + const step = params as { stepId: string }; + return this.executeStep(step.stepId); + } + }); + } + + /** + * Execute a task with planning + */ + async act(input: string, context?: string): Promise { + // First, create a plan + this.currentPlan = await this.createPlan(input); + + // Execute the plan + const result = await this.executePlan(); + + this.taskHistory.push(result); + + return { + content: result.success + ? `Task completed successfully.\n${JSON.stringify(result.output, null, 2)}` + : `Task failed. Errors: ${result.errors.join(', ')}`, + tokens: { prompt: 0, completion: 0, total: 0 }, + metadata: { + plan: this.currentPlan, + result + } + }; + } + + /** + * Create a plan for a task + */ + private async createPlan(taskDescription: string): Promise { + const planningPrompt = `Break down the following task into steps. For each step, provide a brief description. + +Task: ${taskDescription} + +Respond in JSON format: +{ + "steps": [ + { "id": "step1", "description": "First step description" }, + { "id": "step2", "description": "Second step description" } + ], + "complexity": "low|medium|high", + "dependencies": { + "step2": ["step1"] + } +}`; + + const response = await this.process(planningPrompt); + + try { + // Extract JSON from response + const jsonMatch = response.content.match(/\{[\s\S]*\}/); + if (jsonMatch) { + const plan = JSON.parse(jsonMatch[0]); + return { + steps: plan.steps.map((s: TaskStep) => ({ ...s, status: 'pending' as const })), + estimatedComplexity: plan.complexity || 'medium', + dependencies: new Map(Object.entries(plan.dependencies || {})) + }; + } + } catch { + // Fall back to simple plan + } + + // Default simple plan + return { + steps: [{ id: 'step1', description: taskDescription, status: 'pending' }], + estimatedComplexity: 'low', + dependencies: new Map() + }; + } + + /** + * Execute the current plan + */ + private async executePlan(): Promise { + if (!this.currentPlan) { + return { + success: false, + steps: [], + output: null, + errors: ['No plan available'] + }; + } + + const errors: string[] = []; + const completedSteps = new Set(); + + // Execute steps in order, respecting dependencies + for (const step of this.currentPlan.steps) { + // Check dependencies + const deps = this.currentPlan.dependencies.get(step.id) || []; + const depsMet = deps.every(depId => completedSteps.has(depId)); + + if (!depsMet) { + step.status = 'failed'; + step.error = 'Dependencies not met'; + errors.push(`Step ${step.id}: Dependencies not met`); + continue; + } + + // Execute step + step.status = 'running'; + try { + const result = await this.executeStep(step.id); + step.status = 'completed'; + step.result = result; + completedSteps.add(step.id); + } catch (error) { + step.status = 'failed'; + step.error = String(error); + errors.push(`Step ${step.id}: ${error}`); + } + } + + const success = errors.length === 0; + const finalStep = this.currentPlan.steps[this.currentPlan.steps.length - 1]; + + return { + success, + steps: this.currentPlan.steps, + output: finalStep.result, + errors + }; + } + + /** + * Execute a single step + */ + private async executeStep(stepId: string): Promise { + if (!this.currentPlan) throw new Error('No plan available'); + + const step = this.currentPlan.steps.find(s => s.id === stepId); + if (!step) throw new Error(`Step ${stepId} not found`); + + const response = await this.process( + `Execute the following step and provide the result:\n\n${step.description}` + ); + + return response.content; + } + + /** + * Get task history + */ + getTaskHistory(): TaskResult[] { + return [...this.taskHistory]; + } + + /** + * Get current plan + */ + getCurrentPlan(): TaskPlan | null { + return this.currentPlan; + } +} + +/** + * Create a task agent + */ +export function createTaskAgent( + name: string, + systemPrompt: string, + options?: { + description?: string; + tools?: AgentTool[]; + } +): TaskAgent { + return new TaskAgent({ + name, + systemPrompt, + description: options?.description || `Task Agent: ${name}`, + tools: options?.tools + }); +} diff --git a/agent-system/core/context-manager.ts b/agent-system/core/context-manager.ts new file mode 100644 index 0000000..62015e5 --- /dev/null +++ b/agent-system/core/context-manager.ts @@ -0,0 +1,556 @@ +/** + * Context Compaction Module + * + * Manages conversation context by implementing multiple compaction strategies: + * - Sliding window (keep recent messages) + * - Summarization (compress older messages) + * - Priority-based retention (keep important messages) + * - Semantic clustering (group related messages) + */ + +import { TokenCounter, TokenBudget } from './token-counter'; +import { ConversationSummarizer, SummaryResult, ConversationTurn } from './summarizer'; + +export interface CompactionResult { + messages: ConversationTurn[]; + originalTokenCount: number; + newTokenCount: number; + tokensSaved: number; + compressionRatio: number; + strategy: CompactionStrategy; + summaryAdded: boolean; + removedCount: number; +} + +export type CompactionStrategy = + | 'sliding-window' + | 'summarize-old' + | 'priority-retention' + | 'hybrid'; + +export interface CompactionConfig { + maxTokens: number; + targetTokens: number; + strategy: CompactionStrategy; + preserveRecentCount: number; + preserveSystemMessage: boolean; + priorityKeywords: string[]; + summaryMaxTokens: number; + triggerThreshold: number; // Percentage (0-100) of maxTokens to trigger compaction +} + +export interface MessagePriority { + message: ConversationTurn; + index: number; + priority: number; + tokens: number; + reasons: string[]; +} + +const DEFAULT_CONFIG: CompactionConfig = { + maxTokens: 120000, + targetTokens: 80000, + strategy: 'hybrid', + preserveRecentCount: 6, + preserveSystemMessage: true, + priorityKeywords: ['important', 'critical', 'decision', 'todo', 'remember'], + summaryMaxTokens: 2000, + triggerThreshold: 80 +}; + +/** + * ContextCompactor - Manages conversation context compaction + */ +export class ContextCompactor { + private tokenCounter: TokenCounter; + private summarizer: ConversationSummarizer; + private config: CompactionConfig; + private lastCompaction: Date | null = null; + private compactionHistory: CompactionResult[] = []; + + constructor(config: Partial = {}) { + this.config = { ...DEFAULT_CONFIG, ...config }; + this.tokenCounter = new TokenCounter(this.config.maxTokens); + this.summarizer = new ConversationSummarizer(); + } + + /** + * Check if compaction is needed + */ + needsCompaction(messages: ConversationTurn[]): boolean { + const tokenCount = this.tokenCounter.countConversation(messages).total; + const threshold = this.config.maxTokens * (this.config.triggerThreshold / 100); + return tokenCount >= threshold; + } + + /** + * Get current token budget status + */ + getBudget(messages: ConversationTurn[]): TokenBudget { + const tokenCount = this.tokenCounter.countConversation(messages).total; + return this.tokenCounter.getBudget(tokenCount); + } + + /** + * Compact the conversation using the configured strategy + */ + async compact(messages: ConversationTurn[]): Promise { + const originalTokenCount = this.tokenCounter.countConversation(messages).total; + + // Check if compaction is needed + if (originalTokenCount < this.config.targetTokens) { + return { + messages, + originalTokenCount, + newTokenCount: originalTokenCount, + tokensSaved: 0, + compressionRatio: 1, + strategy: this.config.strategy, + summaryAdded: false, + removedCount: 0 + }; + } + + let result: CompactionResult; + + switch (this.config.strategy) { + case 'sliding-window': + result = this.slidingWindowCompaction(messages, originalTokenCount); + break; + case 'summarize-old': + result = await this.summarizeOldCompaction(messages, originalTokenCount); + break; + case 'priority-retention': + result = this.priorityRetentionCompaction(messages, originalTokenCount); + break; + case 'hybrid': + default: + result = await this.hybridCompaction(messages, originalTokenCount); + break; + } + + // Record compaction + this.lastCompaction = new Date(); + this.compactionHistory.push(result); + + return result; + } + + /** + * Sliding window compaction - keep only recent messages + */ + private slidingWindowCompaction( + messages: ConversationTurn[], + originalTokenCount: number + ): CompactionResult { + const result: ConversationTurn[] = []; + + // Preserve system message if configured + if (this.config.preserveSystemMessage && messages[0]?.role === 'system') { + result.push(messages[0]); + } + + // Add recent messages + const startIndex = Math.max( + this.config.preserveSystemMessage && messages[0]?.role === 'system' ? 1 : 0, + messages.length - this.config.preserveRecentCount + ); + + for (let i = startIndex; i < messages.length; i++) { + result.push(messages[i]); + } + + const newTokenCount = this.tokenCounter.countConversation(result).total; + + return { + messages: result, + originalTokenCount, + newTokenCount, + tokensSaved: originalTokenCount - newTokenCount, + compressionRatio: newTokenCount / originalTokenCount, + strategy: 'sliding-window', + summaryAdded: false, + removedCount: messages.length - result.length + }; + } + + /** + * Summarize old messages compaction + */ + private async summarizeOldCompaction( + messages: ConversationTurn[], + originalTokenCount: number + ): Promise { + const result: ConversationTurn[] = []; + + // Preserve system message + if (this.config.preserveSystemMessage && messages[0]?.role === 'system') { + result.push(messages[0]); + } + + // Find cutoff point + const cutoffIndex = messages.length - this.config.preserveRecentCount; + + if (cutoffIndex > 1) { + // Get messages to summarize + const toSummarize = messages.slice( + this.config.preserveSystemMessage && messages[0]?.role === 'system' ? 1 : 0, + cutoffIndex + ); + + // Create summary + const summaryResult = await this.summarizer.summarize(toSummarize, { + maxSummaryTokens: this.config.summaryMaxTokens + }); + + // Add summary as a system message + result.push({ + role: 'system', + content: `[Previous Conversation Summary]\n${summaryResult.summary}\n\nKey Points:\n${summaryResult.keyPoints.map(p => `- ${p}`).join('\n')}`, + metadata: { + type: 'compaction-summary', + originalMessageCount: toSummarize.length, + createdAt: new Date().toISOString() + } + }); + } + + // Add recent messages + for (let i = Math.max(cutoffIndex, 0); i < messages.length; i++) { + result.push(messages[i]); + } + + const newTokenCount = this.tokenCounter.countConversation(result).total; + + return { + messages: result, + originalTokenCount, + newTokenCount, + tokensSaved: originalTokenCount - newTokenCount, + compressionRatio: newTokenCount / originalTokenCount, + strategy: 'summarize-old', + summaryAdded: cutoffIndex > 1, + removedCount: messages.length - result.length + (cutoffIndex > 1 ? 1 : 0) + }; + } + + /** + * Priority-based retention compaction + */ + private priorityRetentionCompaction( + messages: ConversationTurn[], + originalTokenCount: number + ): CompactionResult { + // Calculate priorities for all messages + const priorities = this.calculateMessagePriorities(messages); + + // Sort by priority (descending) + priorities.sort((a, b) => b.priority - a.priority); + + // Select messages until we hit target tokens + const selected: ConversationTurn[] = []; + let currentTokens = 0; + const selectedIndices = new Set(); + + // Always include system message if configured + if (this.config.preserveSystemMessage && messages[0]?.role === 'system') { + selected.push(messages[0]); + selectedIndices.add(0); + currentTokens += this.tokenCounter.countMessage(messages[0]); + } + + // Always include recent messages (high priority) + const recentStart = Math.max( + this.config.preserveSystemMessage && messages[0]?.role === 'system' ? 1 : 0, + messages.length - this.config.preserveRecentCount + ); + + for (let i = recentStart; i < messages.length; i++) { + if (!selectedIndices.has(i)) { + selected.push(messages[i]); + selectedIndices.add(i); + currentTokens += this.tokenCounter.countMessage(messages[i]); + } + } + + // Add high-priority messages until target is reached + for (const mp of priorities) { + if (selectedIndices.has(mp.index)) continue; + if (currentTokens + mp.tokens > this.config.targetTokens) break; + + selected.push(mp.message); + selectedIndices.add(mp.index); + currentTokens += mp.tokens; + } + + // Sort selected messages by original order + selected.sort((a, b) => { + const aIdx = messages.indexOf(a); + const bIdx = messages.indexOf(b); + return aIdx - bIdx; + }); + + const newTokenCount = this.tokenCounter.countConversation(selected).tokens; + + return { + messages: selected, + originalTokenCount, + newTokenCount, + tokensSaved: originalTokenCount - newTokenCount, + compressionRatio: newTokenCount / originalTokenCount, + strategy: 'priority-retention', + summaryAdded: false, + removedCount: messages.length - selected.length + }; + } + + /** + * Hybrid compaction - combines multiple strategies + */ + private async hybridCompaction( + messages: ConversationTurn[], + originalTokenCount: number + ): Promise { + const result: ConversationTurn[] = []; + + // Preserve system message + if (this.config.preserveSystemMessage && messages[0]?.role === 'system') { + result.push(messages[0]); + } + + const priorities = this.calculateMessagePriorities(messages); + + // Identify important messages to keep + const importantIndices = new Set(); + for (const mp of priorities) { + if (mp.priority >= 7) { // High priority threshold + importantIndices.add(mp.index); + } + } + + // Find cutoff for summarization + const cutoffIndex = messages.length - this.config.preserveRecentCount; + + // Summarize middle section if needed + const middleStart = this.config.preserveSystemMessage && messages[0]?.role === 'system' ? 1 : 0; + const middleEnd = cutoffIndex; + + const middleMessages = messages.slice(middleStart, middleEnd) + .filter((_, idx) => !importantIndices.has(middleStart + idx)); + + if (middleMessages.length > 3) { + const summaryResult = await this.summarizer.summarize(middleMessages, { + maxSummaryTokens: this.config.summaryMaxTokens + }); + + result.push({ + role: 'system', + content: `[Context Summary]\n${summaryResult.summary}`, + metadata: { + type: 'compaction-summary', + originalMessageCount: middleMessages.length + } + }); + } + + // Add important messages from the middle section + for (let i = middleStart; i < middleEnd; i++) { + if (importantIndices.has(i)) { + result.push(messages[i]); + } + } + + // Add recent messages + for (let i = cutoffIndex; i < messages.length; i++) { + result.push(messages[i]); + } + + // Sort by original order + result.sort((a, b) => messages.indexOf(a) - messages.indexOf(b)); + + const newTokenCount = this.tokenCounter.countConversation(result).tokens; + + return { + messages: result, + originalTokenCount, + newTokenCount, + tokensSaved: originalTokenCount - newTokenCount, + compressionRatio: newTokenCount / originalTokenCount, + strategy: 'hybrid', + summaryAdded: middleMessages.length > 3, + removedCount: messages.length - result.length + (middleMessages.length > 3 ? 1 : 0) + }; + } + + /** + * Calculate priority scores for messages + */ + private calculateMessagePriorities(messages: ConversationTurn[]): MessagePriority[] { + return messages.map((msg, index) => { + let priority = 5; // Base priority + const reasons: string[] = []; + + // System messages are high priority + if (msg.role === 'system') { + priority += 3; + reasons.push('System message'); + } + + // Recent messages are higher priority + const recency = index / messages.length; + priority += recency * 2; + if (recency > 0.7) reasons.push('Recent message'); + + // Check for priority keywords + const content = msg.content.toLowerCase(); + for (const keyword of this.config.priorityKeywords) { + if (content.includes(keyword.toLowerCase())) { + priority += 1; + reasons.push(`Contains "${keyword}"`); + } + } + + // User questions might be important + if (msg.role === 'user' && content.includes('?')) { + priority += 0.5; + reasons.push('User question'); + } + + // Code blocks might be important + if (content.includes('```')) { + priority += 1; + reasons.push('Contains code'); + } + + // Decisions or confirmations + if (content.match(/(yes|no|agree|decided|confirmed|done)/i)) { + priority += 0.5; + reasons.push('Potential decision'); + } + + return { + message: msg, + index, + priority: Math.min(10, Math.max(1, priority)), + tokens: this.tokenCounter.countMessage(msg), + reasons + }; + }); + } + + /** + * Get compaction history + */ + getHistory(): CompactionResult[] { + return [...this.compactionHistory]; + } + + /** + * Get statistics about compactions + */ + getStats(): { + totalCompactions: number; + totalTokensSaved: number; + averageCompressionRatio: number; + lastCompaction: Date | null; + } { + if (this.compactionHistory.length === 0) { + return { + totalCompactions: 0, + totalTokensSaved: 0, + averageCompressionRatio: 0, + lastCompaction: null + }; + } + + const totalTokensSaved = this.compactionHistory.reduce( + (sum, c) => sum + c.tokensSaved, 0 + ); + + const avgRatio = this.compactionHistory.reduce( + (sum, c) => sum + c.compressionRatio, 0 + ) / this.compactionHistory.length; + + return { + totalCompactions: this.compactionHistory.length, + totalTokensSaved, + averageCompressionRatio: avgRatio, + lastCompaction: this.lastCompaction + }; + } +} + +/** + * ConversationContextManager - High-level context management + */ +export class ConversationContextManager { + private compactor: ContextCompactor; + private messages: ConversationTurn[] = []; + private summary: string | null = null; + + constructor(config: Partial = {}) { + this.compactor = new ContextCompactor(config); + } + + /** + * Add a message to the context + */ + addMessage(message: ConversationTurn): void { + this.messages.push({ + ...message, + timestamp: message.timestamp || new Date() + }); + } + + /** + * Get all messages, with optional compaction + */ + async getMessages(): Promise { + if (this.compactor.needsCompaction(this.messages)) { + const result = await this.compactor.compact(this.messages); + this.messages = result.messages; + return this.messages; + } + return this.messages; + } + + /** + * Force compaction + */ + async forceCompact(): Promise { + const result = await this.compactor.compact(this.messages); + this.messages = result.messages; + return result; + } + + /** + * Get current token count + */ + getTokenCount(): number { + return this.compactor['tokenCounter'].countConversation(this.messages).total; + } + + /** + * Clear the context + */ + clear(): void { + this.messages = []; + this.summary = null; + } + + /** + * Get context stats + */ + getStats() { + return { + messageCount: this.messages.length, + tokenCount: this.getTokenCount(), + budget: this.compactor.getBudget(this.messages), + compactionStats: this.compactor.getStats() + }; + } +} + +// Default instance +export const defaultCompactor = new ContextCompactor(); +export const defaultContextManager = new ConversationContextManager(); diff --git a/agent-system/core/orchestrator.ts b/agent-system/core/orchestrator.ts new file mode 100644 index 0000000..d22bcc6 --- /dev/null +++ b/agent-system/core/orchestrator.ts @@ -0,0 +1,532 @@ +/** + * Agent Orchestration Module + * + * Manages agent lifecycle, task routing, inter-agent communication, + * and coordinated execution of complex multi-agent workflows. + */ + +import { randomUUID } from 'crypto'; + +export type AgentStatus = 'idle' | 'working' | 'waiting' | 'completed' | 'failed'; +export type TaskPriority = 'low' | 'medium' | 'high' | 'critical'; +export type TaskStatus = 'pending' | 'running' | 'completed' | 'failed' | 'cancelled'; + +export interface AgentConfig { + id: string; + name: string; + type: string; + capabilities: string[]; + maxConcurrentTasks: number; + timeout: number; + metadata?: Record; +} + +export interface Task { + id: string; + type: string; + description: string; + priority: TaskPriority; + status: TaskStatus; + assignedAgent?: string; + input: unknown; + output?: unknown; + error?: string; + createdAt: Date; + startedAt?: Date; + completedAt?: Date; + dependencies: string[]; + metadata?: Record; +} + +export interface AgentState { + config: AgentConfig; + status: AgentStatus; + currentTasks: string[]; + completedTasks: number; + failedTasks: number; + lastActivity?: Date; +} + +export interface OrchestratorEvent { + type: 'task_created' | 'task_assigned' | 'task_completed' | 'task_failed' | + 'agent_registered' | 'agent_status_changed'; + timestamp: Date; + data: unknown; +} + +export type EventHandler = (event: OrchestratorEvent) => void | Promise; + +interface TaskQueue { + pending: Task[]; + running: Map; + completed: Task[]; + failed: Task[]; +} + +/** + * AgentOrchestrator - Central coordinator for multi-agent systems + */ +export class AgentOrchestrator { + private agents: Map = new Map(); + private tasks: TaskQueue = { + pending: [], + running: new Map(), + completed: [], + failed: [] + }; + private eventHandlers: Map = new Map(); + private taskProcessors: Map Promise> = new Map(); + private running = false; + private processInterval?: ReturnType; + + constructor() { + this.registerDefaultProcessors(); + } + + /** + * Register a new agent + */ + registerAgent(config: AgentConfig): AgentState { + const state: AgentState = { + config, + status: 'idle', + currentTasks: [], + completedTasks: 0, + failedTasks: 0 + }; + + this.agents.set(config.id, state); + this.emit('agent_registered', { agent: state }); + + return state; + } + + /** + * Unregister an agent + */ + unregisterAgent(agentId: string): boolean { + const agent = this.agents.get(agentId); + if (!agent) return false; + + // Reassign any tasks the agent was working on + for (const taskId of agent.currentTasks) { + const task = this.tasks.running.get(taskId); + if (task) { + task.status = 'pending'; + task.assignedAgent = undefined; + this.tasks.pending.push(task); + this.tasks.running.delete(taskId); + } + } + + this.agents.delete(agentId); + return true; + } + + /** + * Get agent state + */ + getAgent(agentId: string): AgentState | undefined { + return this.agents.get(agentId); + } + + /** + * Get all agents + */ + getAllAgents(): AgentState[] { + return Array.from(this.agents.values()); + } + + /** + * Create a new task + */ + createTask( + type: string, + description: string, + input: unknown, + options: { + priority?: TaskPriority; + dependencies?: string[]; + assignedAgent?: string; + metadata?: Record; + } = {} + ): Task { + const task: Task = { + id: randomUUID(), + type, + description, + priority: options.priority || 'medium', + status: 'pending', + input, + createdAt: new Date(), + dependencies: options.dependencies || [], + assignedAgent: options.assignedAgent, + metadata: options.metadata + }; + + this.tasks.pending.push(task); + this.emit('task_created', { task }); + + // Auto-assign if agent specified + if (options.assignedAgent) { + this.assignTask(task.id, options.assignedAgent); + } + + return task; + } + + /** + * Assign a task to a specific agent + */ + assignTask(taskId: string, agentId: string): boolean { + const agent = this.agents.get(agentId); + if (!agent) return false; + + if (agent.currentTasks.length >= agent.config.maxConcurrentTasks) { + return false; + } + + const taskIndex = this.tasks.pending.findIndex(t => t.id === taskId); + if (taskIndex === -1) return false; + + const task = this.tasks.pending[taskIndex]; + task.assignedAgent = agentId; + + this.emit('task_assigned', { task, agent }); + return true; + } + + /** + * Get task by ID + */ + getTask(taskId: string): Task | undefined { + return ( + this.tasks.pending.find(t => t.id === taskId) || + this.tasks.running.get(taskId) || + this.tasks.completed.find(t => t.id === taskId) || + this.tasks.failed.find(t => t.id === taskId) + ); + } + + /** + * Get all tasks by status + */ + getTasksByStatus(status: TaskStatus): Task[] { + switch (status) { + case 'pending': + return [...this.tasks.pending]; + case 'running': + return Array.from(this.tasks.running.values()); + case 'completed': + return [...this.tasks.completed]; + case 'failed': + return [...this.tasks.failed]; + case 'cancelled': + return [...this.tasks.failed.filter(t => t.error === 'Cancelled')]; + } + } + + /** + * Register a task processor + */ + registerProcessor( + taskType: string, + processor: (task: Task) => Promise + ): void { + this.taskProcessors.set(taskType, processor); + } + + /** + * Start the orchestrator + */ + start(): void { + if (this.running) return; + this.running = true; + this.processInterval = setInterval(() => this.process(), 100); + } + + /** + * Stop the orchestrator + */ + stop(): void { + this.running = false; + if (this.processInterval) { + clearInterval(this.processInterval); + } + } + + /** + * Process pending tasks + */ + private async process(): Promise { + if (!this.running) return; + + // Get tasks ready to run (dependencies satisfied) + const readyTasks = this.getReadyTasks(); + + for (const task of readyTasks) { + // Find available agent + const agent = this.findAvailableAgent(task); + if (!agent) continue; + + // Move task to running + const taskIndex = this.tasks.pending.indexOf(task); + if (taskIndex > -1) { + this.tasks.pending.splice(taskIndex, 1); + } + + task.status = 'running'; + task.startedAt = new Date(); + task.assignedAgent = agent.config.id; + + this.tasks.running.set(task.id, task); + agent.currentTasks.push(task.id); + agent.status = 'working'; + agent.lastActivity = new Date(); + + this.updateAgentStatus(agent.config.id, 'working'); + + // Execute task + this.executeTask(task, agent); + } + } + + /** + * Get tasks that are ready to run + */ + private getReadyTasks(): Task[] { + return this.tasks.pending + .filter(task => { + // Check dependencies + for (const depId of task.dependencies) { + const depTask = this.getTask(depId); + if (!depTask || depTask.status !== 'completed') { + return false; + } + } + return true; + }) + .sort((a, b) => { + // Sort by priority + const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 }; + return priorityOrder[a.priority] - priorityOrder[b.priority]; + }); + } + + /** + * Find an available agent for a task + */ + private findAvailableAgent(task: Task): AgentState | undefined { + // If task is pre-assigned, use that agent + if (task.assignedAgent) { + const agent = this.agents.get(task.assignedAgent); + if (agent && agent.currentTasks.length < agent.config.maxConcurrentTasks) { + return agent; + } + } + + // Find best available agent + const availableAgents = Array.from(this.agents.values()) + .filter(a => + a.currentTasks.length < a.config.maxConcurrentTasks && + a.config.capabilities.includes(task.type) + ) + .sort((a, b) => { + // Prefer agents with fewer current tasks + return a.currentTasks.length - b.currentTasks.length; + }); + + return availableAgents[0]; + } + + /** + * Execute a task + */ + private async executeTask(task: Task, agent: AgentState): Promise { + const processor = this.taskProcessors.get(task.type); + + try { + if (!processor) { + throw new Error(`No processor registered for task type: ${task.type}`); + } + + const output = await Promise.race([ + processor(task), + this.createTimeout(task.id, agent.config.timeout) + ]); + + task.output = output; + task.status = 'completed'; + task.completedAt = new Date(); + + this.tasks.running.delete(task.id); + this.tasks.completed.push(task); + + agent.completedTasks++; + agent.lastActivity = new Date(); + + this.emit('task_completed', { task, agent }); + + } catch (error) { + task.status = 'failed'; + task.error = error instanceof Error ? error.message : String(error); + task.completedAt = new Date(); + + this.tasks.running.delete(task.id); + this.tasks.failed.push(task); + + agent.failedTasks++; + agent.lastActivity = new Date(); + + this.emit('task_failed', { task, agent, error: task.error }); + } + + // Remove from agent's current tasks + const taskIdx = agent.currentTasks.indexOf(task.id); + if (taskIdx > -1) { + agent.currentTasks.splice(taskIdx, 1); + } + + // Update agent status + if (agent.currentTasks.length === 0) { + this.updateAgentStatus(agent.config.id, 'idle'); + } + } + + /** + * Create a timeout promise + */ + private createTimeout(taskId: string, timeout: number): Promise { + return new Promise((_, reject) => { + setTimeout(() => reject(new Error(`Task ${taskId} timed out`)), timeout); + }); + } + + /** + * Update agent status + */ + private updateAgentStatus(agentId: string, status: AgentStatus): void { + const agent = this.agents.get(agentId); + if (agent) { + agent.status = status; + this.emit('agent_status_changed', { agent }); + } + } + + /** + * Register default task processors + */ + private registerDefaultProcessors(): void { + // Default processors can be registered here + } + + /** + * Subscribe to orchestrator events + */ + on(event: OrchestratorEvent['type'], handler: EventHandler): () => void { + if (!this.eventHandlers.has(event)) { + this.eventHandlers.set(event, []); + } + this.eventHandlers.get(event)!.push(handler); + + // Return unsubscribe function + return () => { + const handlers = this.eventHandlers.get(event); + if (handlers) { + const idx = handlers.indexOf(handler); + if (idx > -1) handlers.splice(idx, 1); + } + }; + } + + /** + * Emit an event + */ + private emit(type: OrchestratorEvent['type'], data: unknown): void { + const event: OrchestratorEvent = { + type, + timestamp: new Date(), + data + }; + + const handlers = this.eventHandlers.get(type) || []; + for (const handler of handlers) { + try { + handler(event); + } catch (error) { + console.error(`Error in event handler for ${type}:`, error); + } + } + } + + /** + * Get orchestrator statistics + */ + getStats(): { + agents: { total: number; idle: number; working: number }; + tasks: { pending: number; running: number; completed: number; failed: number }; + } { + const agentStates = Array.from(this.agents.values()); + + return { + agents: { + total: agentStates.length, + idle: agentStates.filter(a => a.status === 'idle').length, + working: agentStates.filter(a => a.status === 'working').length + }, + tasks: { + pending: this.tasks.pending.length, + running: this.tasks.running.size, + completed: this.tasks.completed.length, + failed: this.tasks.failed.length + } + }; + } + + /** + * Cancel a task + */ + cancelTask(taskId: string): boolean { + const task = this.tasks.running.get(taskId) || + this.tasks.pending.find(t => t.id === taskId); + + if (!task) return false; + + task.status = 'cancelled'; + task.error = 'Cancelled'; + task.completedAt = new Date(); + + if (this.tasks.running.has(taskId)) { + this.tasks.running.delete(taskId); + this.tasks.failed.push(task); + } else { + const idx = this.tasks.pending.indexOf(task); + if (idx > -1) this.tasks.pending.splice(idx, 1); + this.tasks.failed.push(task); + } + + return true; + } + + /** + * Retry a failed task + */ + retryTask(taskId: string): boolean { + const taskIndex = this.tasks.failed.findIndex(t => t.id === taskId); + if (taskIndex === -1) return false; + + const task = this.tasks.failed[taskIndex]; + task.status = 'pending'; + task.error = undefined; + task.startedAt = undefined; + task.completedAt = undefined; + + this.tasks.failed.splice(taskIndex, 1); + this.tasks.pending.push(task); + + return true; + } +} + +// Singleton instance +export const defaultOrchestrator = new AgentOrchestrator(); diff --git a/agent-system/core/subagent-spawner.ts b/agent-system/core/subagent-spawner.ts new file mode 100644 index 0000000..721138c --- /dev/null +++ b/agent-system/core/subagent-spawner.ts @@ -0,0 +1,455 @@ +/** + * Subagent Spawner Module + * + * Creates and manages child agents (subagents) for parallel task execution. + * Implements communication channels, result aggregation, and lifecycle management. + */ + +import { randomUUID } from 'crypto'; +import ZAI from 'z-ai-web-dev-sdk'; +import { AgentOrchestrator, AgentConfig, Task, TaskPriority } from './orchestrator'; + +export type SubagentType = + | 'explorer' // For code exploration + | 'researcher' // For information gathering + | 'coder' // For code generation + | 'reviewer' // For code review + | 'planner' // For task planning + | 'executor' // For task execution + | 'custom'; // Custom subagent + +export interface SubagentDefinition { + type: SubagentType; + name: string; + description: string; + systemPrompt: string; + capabilities: string[]; + maxTasks?: number; + timeout?: number; +} + +export interface SubagentResult { + subagentId: string; + taskId: string; + success: boolean; + output: unknown; + error?: string; + tokens: { + input: number; + output: number; + }; + duration: number; +} + +export interface SpawnOptions { + priority?: TaskPriority; + timeout?: number; + context?: string; + dependencies?: string[]; + metadata?: Record; +} + +export interface SubagentPool { + id: string; + name: string; + subagents: Map; + createdAt: Date; +} + +/** + * SubagentInstance - A running subagent + */ +export class SubagentInstance { + id: string; + definition: SubagentDefinition; + orchestrator: AgentOrchestrator; + private zai: Awaited> | null = null; + private initialized = false; + + constructor( + definition: SubagentDefinition, + orchestrator: AgentOrchestrator + ) { + this.id = `${definition.type}-${randomUUID().substring(0, 8)}`; + this.definition = definition; + this.orchestrator = orchestrator; + } + + /** + * Initialize the subagent + */ + async initialize(): Promise { + if (this.initialized) return; + + this.zai = await ZAI.create(); + + // Register with orchestrator + const config: AgentConfig = { + id: this.id, + name: this.definition.name, + type: this.definition.type, + capabilities: this.definition.capabilities, + maxConcurrentTasks: this.definition.maxTasks || 3, + timeout: this.definition.timeout || 60000, + metadata: { + systemPrompt: this.definition.systemPrompt + } + }; + + this.orchestrator.registerAgent(config); + this.initialized = true; + } + + /** + * Execute a task + */ + async execute(input: string, context?: string): Promise { + const startTime = Date.now(); + + if (!this.initialized || !this.zai) { + await this.initialize(); + } + + const task = this.orchestrator.createTask( + this.definition.type, + `Execute ${this.definition.type} task`, + { input, context }, + { assignedAgent: this.id } + ); + + try { + const messages = [ + { + role: 'assistant' as const, + content: this.definition.systemPrompt + }, + { + role: 'user' as const, + content: context + ? `Context: ${context}\n\nTask: ${input}` + : input + } + ]; + + const response = await this.zai!.chat.completions.create({ + messages, + thinking: { type: 'disabled' } + }); + + const output = response.choices?.[0]?.message?.content || ''; + + const result: SubagentResult = { + subagentId: this.id, + taskId: task.id, + success: true, + output, + tokens: { + input: 0, // Would need tokenizer to calculate + output: 0 + }, + duration: Date.now() - startTime + }; + + return result; + + } catch (error) { + return { + subagentId: this.id, + taskId: task.id, + success: false, + output: null, + error: error instanceof Error ? error.message : String(error), + tokens: { input: 0, output: 0 }, + duration: Date.now() - startTime + }; + } + } + + /** + * Terminate the subagent + */ + terminate(): void { + this.orchestrator.unregisterAgent(this.id); + this.initialized = false; + } +} + +/** + * SubagentSpawner - Factory for creating and managing subagents + */ +export class SubagentSpawner { + private orchestrator: AgentOrchestrator; + private subagents: Map = new Map(); + private pools: Map = new Map(); + private definitions: Map = new Map(); + + constructor(orchestrator?: AgentOrchestrator) { + this.orchestrator = orchestrator || new AgentOrchestrator(); + this.registerDefaultDefinitions(); + this.orchestrator.start(); + } + + /** + * Register default subagent definitions + */ + private registerDefaultDefinitions(): void { + const defaults: SubagentDefinition[] = [ + { + type: 'explorer', + name: 'Code Explorer', + description: 'Explores codebases to find relevant files and code', + systemPrompt: `You are a code explorer agent. Your job is to search through codebases to find relevant files, functions, and code patterns. Be thorough but concise in your findings.`, + capabilities: ['explore', 'search', 'find'] + }, + { + type: 'researcher', + name: 'Research Agent', + description: 'Gathers information and researches topics', + systemPrompt: `You are a research agent. Your job is to gather comprehensive information on given topics. Focus on accuracy and completeness.`, + capabilities: ['research', 'gather', 'analyze'] + }, + { + type: 'coder', + name: 'Code Generator', + description: 'Generates code based on specifications', + systemPrompt: `You are a code generation agent. Your job is to write clean, efficient, and well-documented code. Follow best practices and include appropriate error handling.`, + capabilities: ['code', 'generate', 'implement'] + }, + { + type: 'reviewer', + name: 'Code Reviewer', + description: 'Reviews code for quality, bugs, and improvements', + systemPrompt: `You are a code review agent. Your job is to analyze code for bugs, security issues, performance problems, and best practice violations. Provide constructive feedback.`, + capabilities: ['review', 'analyze', 'validate'] + }, + { + type: 'planner', + name: 'Task Planner', + description: 'Plans and breaks down complex tasks', + systemPrompt: `You are a planning agent. Your job is to break down complex tasks into smaller, manageable steps. Consider dependencies and optimal execution order.`, + capabilities: ['plan', 'decompose', 'organize'] + }, + { + type: 'executor', + name: 'Task Executor', + description: 'Executes specific tasks with precision', + systemPrompt: `You are an execution agent. Your job is to carry out specific tasks accurately and efficiently. Report results clearly and flag any issues encountered.`, + capabilities: ['execute', 'run', 'process'] + } + ]; + + for (const def of defaults) { + this.definitions.set(def.type, def); + } + } + + /** + * Register a custom subagent definition + */ + registerDefinition(definition: SubagentDefinition): void { + this.definitions.set(definition.type, definition); + } + + /** + * Spawn a single subagent + */ + async spawn(type: SubagentType): Promise { + const definition = this.definitions.get(type); + if (!definition) { + throw new Error(`Unknown subagent type: ${type}`); + } + + const subagent = new SubagentInstance(definition, this.orchestrator); + await subagent.initialize(); + this.subagents.set(subagent.id, subagent); + + return subagent; + } + + /** + * Spawn multiple subagents of the same type + */ + async spawnPool( + type: SubagentType, + count: number, + poolName?: string + ): Promise { + const pool: SubagentPool = { + id: randomUUID(), + name: poolName || `${type}-pool-${Date.now()}`, + subagents: new Map(), + createdAt: new Date() + }; + + for (let i = 0; i < count; i++) { + const subagent = await this.spawn(type); + pool.subagents.set(subagent.id, subagent); + } + + this.pools.set(pool.id, pool); + return pool; + } + + /** + * Execute task with a spawned subagent + */ + async executeWithSubagent( + type: SubagentType, + task: string, + context?: string, + options?: SpawnOptions + ): Promise { + const subagent = await this.spawn(type); + + try { + const result = await subagent.execute(task, context); + return result; + } finally { + // Auto-terminate after execution + subagent.terminate(); + this.subagents.delete(subagent.id); + } + } + + /** + * Execute multiple tasks in parallel + */ + async executeParallel( + tasks: Array<{ + type: SubagentType; + input: string; + context?: string; + }>, + options?: { maxConcurrent?: number } + ): Promise { + const maxConcurrent = options?.maxConcurrent || 5; + const results: SubagentResult[] = []; + + // Process in batches + for (let i = 0; i < tasks.length; i += maxConcurrent) { + const batch = tasks.slice(i, i + maxConcurrent); + const batchPromises = batch.map(t => + this.executeWithSubagent(t.type, t.input, t.context) + ); + + const batchResults = await Promise.all(batchPromises); + results.push(...batchResults); + } + + return results; + } + + /** + * Execute tasks in a pipeline (sequential with context passing) + */ + async executePipeline( + steps: Array<{ + type: SubagentType; + input: string | ((prevResult: unknown) => string); + }>, + initialContext?: string + ): Promise<{ results: SubagentResult[]; finalOutput: unknown }> { + const results: SubagentResult[] = []; + let currentContext = initialContext; + let currentOutput: unknown = null; + + for (const step of steps) { + const input = typeof step.input === 'function' + ? step.input(currentOutput) + : step.input; + + const result = await this.executeWithSubagent( + step.type, + input, + currentContext + ); + + results.push(result); + + if (result.success) { + currentOutput = result.output; + currentContext = typeof result.output === 'string' + ? result.output + : JSON.stringify(result.output); + } else { + // Stop pipeline on failure + break; + } + } + + return { results, finalOutput: currentOutput }; + } + + /** + * Terminate a specific subagent + */ + terminate(subagentId: string): boolean { + const subagent = this.subagents.get(subagentId); + if (subagent) { + subagent.terminate(); + this.subagents.delete(subagentId); + return true; + } + return false; + } + + /** + * Terminate all subagents in a pool + */ + terminatePool(poolId: string): boolean { + const pool = this.pools.get(poolId); + if (!pool) return false; + + for (const subagent of pool.subagents.values()) { + subagent.terminate(); + this.subagents.delete(subagent.id); + } + + this.pools.delete(poolId); + return true; + } + + /** + * Terminate all subagents + */ + terminateAll(): void { + for (const subagent of this.subagents.values()) { + subagent.terminate(); + } + this.subagents.clear(); + this.pools.clear(); + } + + /** + * Get active subagents + */ + getActiveSubagents(): SubagentInstance[] { + return Array.from(this.subagents.values()); + } + + /** + * Get orchestrator stats + */ + getStats() { + return { + activeSubagents: this.subagents.size, + pools: this.pools.size, + orchestrator: this.orchestrator.getStats() + }; + } +} + +/** + * Quick spawn function for simple use cases + */ +export async function spawnAndExecute( + type: SubagentType, + task: string, + context?: string +): Promise { + const spawner = new SubagentSpawner(); + return spawner.executeWithSubagent(type, task, context); +} + +// Default spawner instance +export const defaultSpawner = new SubagentSpawner(); diff --git a/agent-system/core/summarizer.ts b/agent-system/core/summarizer.ts new file mode 100644 index 0000000..8dd4d45 --- /dev/null +++ b/agent-system/core/summarizer.ts @@ -0,0 +1,332 @@ +/** + * Conversation Summarizer Module + * + * Uses LLM to create intelligent summaries of conversations, + * preserving key information while reducing token count. + */ + +import ZAI from 'z-ai-web-dev-sdk'; +import { TokenCounter, countTokens } from './token-counter'; + +export interface SummaryResult { + summary: string; + originalTokens: number; + summaryTokens: number; + compressionRatio: number; + keyPoints: string[]; + decisions: string[]; + actionItems: string[]; +} + +export interface SummarizerOptions { + maxSummaryTokens?: number; + preserveRecentMessages?: number; + extractKeyPoints?: boolean; + extractDecisions?: boolean; + extractActionItems?: boolean; +} + +export interface ConversationTurn { + role: 'user' | 'assistant' | 'system'; + content: string; + timestamp?: Date; + metadata?: Record; +} + +const DEFAULT_OPTIONS: Required = { + maxSummaryTokens: 1000, + preserveRecentMessages: 3, + extractKeyPoints: true, + extractDecisions: true, + extractActionItems: true +}; + +/** + * ConversationSummarizer - Creates intelligent summaries of conversations + */ +export class ConversationSummarizer { + private zai: Awaited> | null = null; + private tokenCounter: TokenCounter; + private options: Required; + + constructor(options: SummarizerOptions = {}) { + this.options = { ...DEFAULT_OPTIONS, ...options }; + this.tokenCounter = new TokenCounter(); + } + + /** + * Initialize the summarizer (lazy load ZAI) + */ + private async init(): Promise { + if (!this.zai) { + this.zai = await ZAI.create(); + } + } + + /** + * Summarize a conversation + */ + async summarize( + messages: ConversationTurn[], + options?: Partial + ): Promise { + await this.init(); + + const opts = { ...this.options, ...options }; + const originalTokens = this.tokenCounter.countConversation(messages).total; + + // Format conversation for summarization + const conversationText = this.formatConversationForSummary(messages); + + // Create the summarization prompt + const prompt = this.buildSummarizationPrompt(conversationText, opts); + + // Get summary from LLM + const response = await this.zai!.chat.completions.create({ + messages: [ + { + role: 'assistant', + content: `You are a precise conversation summarizer. Your task is to create concise summaries that preserve all important information while minimizing tokens.` + }, + { + role: 'user', + content: prompt + } + ], + thinking: { type: 'disabled' } + }); + + const summaryText = response.choices?.[0]?.message?.content || ''; + + // Parse the structured response + const parsed = this.parseSummaryResponse(summaryText); + + const summaryTokens = countTokens(parsed.summary); + + return { + summary: parsed.summary, + originalTokens, + summaryTokens, + compressionRatio: originalTokens > 0 ? summaryTokens / originalTokens : 0, + keyPoints: parsed.keyPoints, + decisions: parsed.decisions, + actionItems: parsed.actionItems + }; + } + + /** + * Format conversation for summarization + */ + private formatConversationForSummary(messages: ConversationTurn[]): string { + return messages.map(msg => { + const timestamp = msg.timestamp?.toISOString() || ''; + return `[${msg.role.toUpperCase()}]${timestamp ? ` (${timestamp})` : ''}: ${msg.content}`; + }).join('\n\n'); + } + + /** + * Build the summarization prompt + */ + private buildSummarizationPrompt( + conversationText: string, + options: Required + ): string { + const sections: string[] = []; + + sections.push(`Please summarize the following conversation concisely.`); + sections.push(`The summary should be under ${options.maxSummaryTokens} tokens.`); + + if (options.extractKeyPoints) { + sections.push(`\nExtract KEY POINTS as a bullet list.`); + } + if (options.extractDecisions) { + sections.push(`Extract any DECISIONS made as a bullet list.`); + } + if (options.extractActionItems) { + sections.push(`Extract any ACTION ITEMS as a bullet list.`); + } + + sections.push(`\nFormat your response as: +## SUMMARY +[Your concise summary here] + +## KEY POINTS +- [Key point 1] +- [Key point 2] + +## DECISIONS +- [Decision 1] +- [Decision 2] + +## ACTION ITEMS +- [Action item 1] +- [Action item 2] + +--- + +CONVERSATION: +${conversationText}`); + + return sections.join('\n'); + } + + /** + * Parse the structured summary response + */ + private parseSummaryResponse(text: string): { + summary: string; + keyPoints: string[]; + decisions: string[]; + actionItems: string[]; + } { + const sections = { + summary: '', + keyPoints: [] as string[], + decisions: [] as string[], + actionItems: [] as string[] + }; + + // Extract summary + const summaryMatch = text.match(/## SUMMARY\s*([\s\S]*?)(?=##|$)/i); + if (summaryMatch) { + sections.summary = summaryMatch[1].trim(); + } + + // Extract key points + const keyPointsMatch = text.match(/## KEY POINTS\s*([\s\S]*?)(?=##|$)/i); + if (keyPointsMatch) { + sections.keyPoints = this.extractBulletPoints(keyPointsMatch[1]); + } + + // Extract decisions + const decisionsMatch = text.match(/## DECISIONS\s*([\s\S]*?)(?=##|$)/i); + if (decisionsMatch) { + sections.decisions = this.extractBulletPoints(decisionsMatch[1]); + } + + // Extract action items + const actionItemsMatch = text.match(/## ACTION ITEMS\s*([\s\S]*?)(?=##|$)/i); + if (actionItemsMatch) { + sections.actionItems = this.extractBulletPoints(actionItemsMatch[1]); + } + + return sections; + } + + /** + * Extract bullet points from text + */ + private extractBulletPoints(text: string): string[] { + const lines = text.split('\n'); + return lines + .map(line => line.replace(/^[-*•]\s*/, '').trim()) + .filter(line => line.length > 0); + } + + /** + * Create a rolling summary (for continuous conversations) + */ + async createRollingSummary( + previousSummary: string, + newMessages: ConversationTurn[] + ): Promise { + await this.init(); + + const prompt = `You are updating a conversation summary with new messages. + +PREVIOUS SUMMARY: +${previousSummary} + +NEW MESSAGES: +${this.formatConversationForSummary(newMessages)} + +Create an updated summary that integrates the new information with the previous summary. +Keep the summary concise but comprehensive. +Format your response as: +## SUMMARY +[Your updated summary] + +## KEY POINTS +- [Updated key points] + +## DECISIONS +- [Updated decisions] + +## ACTION ITEMS +- [Updated action items]`; + + const response = await this.zai!.chat.completions.create({ + messages: [ + { + role: 'assistant', + content: 'You are a conversation summarizer that maintains rolling summaries.' + }, + { + role: 'user', + content: prompt + } + ], + thinking: { type: 'disabled' } + }); + + const summaryText = response.choices?.[0]?.message?.content || ''; + const parsed = this.parseSummaryResponse(summaryText); + + return { + summary: parsed.summary, + originalTokens: countTokens(previousSummary) + this.tokenCounter.countConversation(newMessages).total, + summaryTokens: countTokens(parsed.summary), + compressionRatio: 0, + keyPoints: parsed.keyPoints, + decisions: parsed.decisions, + actionItems: parsed.actionItems + }; + } + + /** + * Create a topic-based summary (groups messages by topic) + */ + async createTopicSummary( + messages: ConversationTurn[] + ): Promise> { + await this.init(); + + // First, identify topics + const topicResponse = await this.zai!.chat.completions.create({ + messages: [ + { + role: 'assistant', + content: 'Identify the main topics in this conversation. Respond with a JSON array of topic names.' + }, + { + role: 'user', + content: this.formatConversationForSummary(messages) + } + ], + thinking: { type: 'disabled' } + }); + + let topics: string[] = []; + try { + const topicText = topicResponse.choices?.[0]?.message?.content || '[]'; + topics = JSON.parse(topicText.match(/\[.*\]/s)?.[0] || '[]'); + } catch { + topics = ['General']; + } + + // Create summaries for each topic + const summaries = new Map(); + + for (const topic of topics) { + const summary = await this.summarize(messages, { + maxSummaryTokens: 500 + }); + summaries.set(topic, summary); + } + + return summaries; + } +} + +// Singleton instance +export const defaultSummarizer = new ConversationSummarizer(); diff --git a/agent-system/core/token-counter.ts b/agent-system/core/token-counter.ts new file mode 100644 index 0000000..f63bd61 --- /dev/null +++ b/agent-system/core/token-counter.ts @@ -0,0 +1,220 @@ +/** + * Token Counter Module + * + * Estimates token counts for text and messages. + * Uses a character-based approximation (GPT-style tokenization is roughly 4 chars per token). + * For more accurate counting, you could integrate tiktoken or similar libraries. + */ + +export interface TokenCountResult { + tokens: number; + characters: number; + words: number; +} + +export interface MessageTokenCount { + role: string; + content: string; + tokens: number; +} + +export interface TokenBudget { + used: number; + remaining: number; + total: number; + percentageUsed: number; +} + +// Approximate tokens per character ratio (GPT-style) +const CHARS_PER_TOKEN = 4; + +// Overhead for message formatting (role, delimiters, etc.) +const MESSAGE_OVERHEAD_TOKENS = 4; + +/** + * TokenCounter - Estimates token counts for text and conversations + */ +export class TokenCounter { + private maxTokens: number; + private reservedTokens: number; + + constructor(maxTokens: number = 128000, reservedTokens: number = 4096) { + this.maxTokens = maxTokens; + this.reservedTokens = reservedTokens; + } + + /** + * Count tokens in a text string + */ + countText(text: string): TokenCountResult { + const characters = text.length; + const words = text.split(/\s+/).filter(w => w.length > 0).length; + + // Token estimation using character ratio + // Also account for word boundaries and special characters + const tokens = Math.ceil(characters / CHARS_PER_TOKEN); + + return { + tokens, + characters, + words + }; + } + + /** + * Count tokens in a single message + */ + countMessage(message: { role: string; content: string }): number { + const contentTokens = this.countText(message.content).tokens; + return contentTokens + MESSAGE_OVERHEAD_TOKENS; + } + + /** + * Count tokens in a conversation (array of messages) + */ + countConversation(messages: Array<{ role: string; content: string }>): { + total: number; + breakdown: MessageTokenCount[]; + } { + const breakdown: MessageTokenCount[] = messages.map(msg => ({ + role: msg.role, + content: msg.content.substring(0, 100) + (msg.content.length > 100 ? '...' : ''), + tokens: this.countMessage(msg) + })); + + const total = breakdown.reduce((sum, msg) => sum + msg.tokens, 0); + + return { total, breakdown }; + } + + /** + * Get current token budget + */ + getBudget(usedTokens: number): TokenBudget { + const availableTokens = this.maxTokens - this.reservedTokens; + const remaining = Math.max(0, availableTokens - usedTokens); + + return { + used: usedTokens, + remaining, + total: availableTokens, + percentageUsed: (usedTokens / availableTokens) * 100 + }; + } + + /** + * Check if adding a message would exceed the budget + */ + wouldExceedBudget( + currentTokens: number, + message: { role: string; content: string } + ): boolean { + const messageTokens = this.countMessage(message); + const budget = this.getBudget(currentTokens); + return messageTokens > budget.remaining; + } + + /** + * Calculate how many messages can fit in the remaining budget + */ + calculateCapacity( + currentTokens: number, + averageMessageTokens: number = 500 + ): number { + const budget = this.getBudget(currentTokens); + return Math.floor(budget.remaining / averageMessageTokens); + } + + /** + * Split text into chunks that fit within token limits + */ + chunkText(text: string, maxTokensPerChunk: number): string[] { + const totalTokens = this.countText(text).tokens; + + if (totalTokens <= maxTokensPerChunk) { + return [text]; + } + + const chunks: string[] = []; + const sentences = text.split(/(?<=[.!?])\s+/); + + let currentChunk = ''; + let currentTokens = 0; + + for (const sentence of sentences) { + const sentenceTokens = this.countText(sentence).tokens; + + if (currentTokens + sentenceTokens > maxTokensPerChunk) { + if (currentChunk) { + chunks.push(currentChunk.trim()); + } + currentChunk = sentence; + currentTokens = sentenceTokens; + } else { + currentChunk += ' ' + sentence; + currentTokens += sentenceTokens; + } + } + + if (currentChunk.trim()) { + chunks.push(currentChunk.trim()); + } + + return chunks; + } + + /** + * Find the optimal cutoff point for message truncation + */ + findOptimalCutoff( + messages: Array<{ role: string; content: string }>, + targetTokens: number + ): number { + let accumulated = 0; + + for (let i = 0; i < messages.length; i++) { + const msgTokens = this.countMessage(messages[i]); + if (accumulated + msgTokens > targetTokens) { + return i; + } + accumulated += msgTokens; + } + + return messages.length; + } + + /** + * Estimate tokens for different content types + */ + estimateContentTokens(content: unknown): number { + if (typeof content === 'string') { + return this.countText(content).tokens; + } + + if (Array.isArray(content)) { + return this.countText(JSON.stringify(content)).tokens; + } + + if (typeof content === 'object' && content !== null) { + return this.countText(JSON.stringify(content)).tokens; + } + + return 0; + } +} + +// Singleton instance with default settings +export const defaultTokenCounter = new TokenCounter(); + +/** + * Quick utility functions + */ +export function countTokens(text: string): number { + return defaultTokenCounter.countText(text).tokens; +} + +export function countMessagesTokens( + messages: Array<{ role: string; content: string }> +): number { + return defaultTokenCounter.countConversation(messages).total; +} diff --git a/agent-system/index.ts b/agent-system/index.ts new file mode 100644 index 0000000..7e8a6d9 --- /dev/null +++ b/agent-system/index.ts @@ -0,0 +1,179 @@ +/** + * Agent System - Complete Implementation + * + * A comprehensive agent framework with: + * - Token counting and management + * - Conversation summarization + * - Context compaction + * - Agent orchestration + * - Subagent spawning + * - Persistent storage + * + * @module agent-system + */ + +// Core modules +export { TokenCounter, defaultTokenCounter, countTokens, countMessagesTokens } from './core/token-counter'; +export type { TokenCountResult, MessageTokenCount, TokenBudget } from './core/token-counter'; + +export { ConversationSummarizer, defaultSummarizer } from './core/summarizer'; +export type { SummaryResult, SummarizerOptions, ConversationTurn } from './core/summarizer'; + +export { ContextCompactor, ConversationContextManager, defaultCompactor, defaultContextManager } from './core/context-manager'; +export type { CompactionResult, CompactionStrategy, CompactionConfig, MessagePriority } from './core/context-manager'; + +export { AgentOrchestrator, defaultOrchestrator } from './core/orchestrator'; +export type { + AgentStatus, + TaskPriority, + TaskStatus, + AgentConfig, + Task, + AgentState, + OrchestratorEvent, + EventHandler +} from './core/orchestrator'; + +export { + SubagentSpawner, + SubagentInstance, + defaultSpawner, + spawnAndExecute +} from './core/subagent-spawner'; +export type { + SubagentType, + SubagentDefinition, + SubagentResult, + SpawnOptions, + SubagentPool +} from './core/subagent-spawner'; + +// Agent classes +export { BaseAgent, SimpleAgent, createAgent } from './agents/base-agent'; +export type { AgentMemory, AgentTool, AgentConfig, AgentResponse } from './agents/base-agent'; + +export { TaskAgent, createTaskAgent } from './agents/task-agent'; +export type { TaskStep, TaskPlan, TaskResult } from './agents/task-agent'; + +// Storage +export { AgentStorage, defaultStorage } from './storage/memory-store'; +export type { StoredConversation, StoredTask, StoredAgentState } from './storage/memory-store'; + +// Utilities +export { + debounce, + throttle, + retry, + sleep, + generateId, + deepClone, + deepMerge, + isObject, + truncate, + formatBytes, + formatDuration, + createRateLimiter, + createCache, + compose, + pipe, + chunk, + groupBy +} from './utils/helpers'; + +/** + * Quick Start Example: + * + * ```typescript + * import { + * createAgent, + * ConversationContextManager, + * SubagentSpawner + * } from './agent-system'; + * + * // Create a simple agent + * const agent = createAgent( + * 'MyAgent', + * 'You are a helpful assistant.', + * { description: 'A simple helper agent' } + * ); + * + * // Initialize and use + * await agent.initialize(); + * const response = await agent.act('Hello!'); + * console.log(response.content); + * + * // Use context management + * const context = new ConversationContextManager(); + * context.addMessage({ role: 'user', content: 'Hello!' }); + * + * // Spawn subagents + * const spawner = new SubagentSpawner(); + * const result = await spawner.executeWithSubagent( + * 'researcher', + * 'Research AI agents', + * 'Focus on autonomous agents' + * ); + * ``` + */ + +/** + * Context Compaction Example: + * + * ```typescript + * import { ContextCompactor, ConversationSummarizer } from './agent-system'; + * + * // Create compactor with custom config + * const compactor = new ContextCompactor({ + * maxTokens: 100000, + * strategy: 'hybrid', + * preserveRecentCount: 10 + * }); + * + * // Compact a conversation + * const messages = [ + * { role: 'user', content: '...' }, + * { role: 'assistant', content: '...' }, + * // ... many more messages + * ]; + * + * if (compactor.needsCompaction(messages)) { + * const result = await compactor.compact(messages); + * console.log(`Saved ${result.tokensSaved} tokens`); + * console.log(`Compression ratio: ${result.compressionRatio}`); + * } + * ``` + */ + +/** + * Agent Orchestration Example: + * + * ```typescript + * import { AgentOrchestrator, SubagentSpawner } from './agent-system'; + * + * const orchestrator = new AgentOrchestrator(); + * + * // Register agents + * orchestrator.registerAgent({ + * id: 'agent-1', + * name: 'Worker Agent', + * type: 'worker', + * capabilities: ['process', 'execute'], + * maxConcurrentTasks: 3, + * timeout: 60000 + * }); + * + * // Create tasks + * orchestrator.createTask('process', 'Process data', { data: [...] }); + * + * // Listen for events + * orchestrator.on('task_completed', (event) => { + * console.log('Task completed:', event.data); + * }); + * + * // Start processing + * orchestrator.start(); + * ``` + */ + +// Version +export const VERSION = '1.0.0'; diff --git a/agent-system/storage/memory-store.ts b/agent-system/storage/memory-store.ts new file mode 100644 index 0000000..763b03d --- /dev/null +++ b/agent-system/storage/memory-store.ts @@ -0,0 +1,232 @@ +/** + * Agent Storage Module + * + * Persistent storage for agent state, conversations, and results. + * Uses filesystem for persistence. + */ + +import { writeFileSync, readFileSync, existsSync, mkdirSync, readdirSync, unlinkSync } from 'fs'; +import { join } from 'path'; + +const STORAGE_DIR = join(process.cwd(), '.agent-storage'); + +export interface StoredConversation { + id: string; + agentId: string; + messages: Array<{ + role: 'user' | 'assistant' | 'system'; + content: string; + timestamp: string; + }>; + metadata?: Record; + createdAt: string; + updatedAt: string; +} + +export interface StoredTask { + id: string; + type: string; + status: string; + input: unknown; + output?: unknown; + error?: string; + createdAt: string; + completedAt?: string; +} + +export interface StoredAgentState { + id: string; + name: string; + type: string; + status: string; + memory: Record; + createdAt: string; + updatedAt: string; +} + +/** + * AgentStorage - Persistent storage for agents + */ +export class AgentStorage { + private baseDir: string; + + constructor(baseDir: string = STORAGE_DIR) { + this.baseDir = baseDir; + this.ensureDirectory(); + } + + /** + * Ensure storage directory exists + */ + private ensureDirectory(): void { + if (!existsSync(this.baseDir)) { + mkdirSync(this.baseDir, { recursive: true }); + } + + const subdirs = ['conversations', 'tasks', 'agents']; + for (const subdir of subdirs) { + const dir = join(this.baseDir, subdir); + if (!existsSync(dir)) { + mkdirSync(dir, { recursive: true }); + } + } + } + + /** + * Save a conversation + */ + saveConversation(conversation: StoredConversation): void { + const path = join(this.baseDir, 'conversations', `${conversation.id}.json`); + writeFileSync(path, JSON.stringify(conversation, null, 2), 'utf-8'); + } + + /** + * Load a conversation + */ + loadConversation(id: string): StoredConversation | null { + try { + const path = join(this.baseDir, 'conversations', `${id}.json`); + if (!existsSync(path)) return null; + return JSON.parse(readFileSync(path, 'utf-8')); + } catch { + return null; + } + } + + /** + * List all conversations + */ + listConversations(agentId?: string): StoredConversation[] { + const dir = join(this.baseDir, 'conversations'); + if (!existsSync(dir)) return []; + + const files = readdirSync(dir).filter(f => f.endsWith('.json')); + + return files.map(file => { + const content = readFileSync(join(dir, file), 'utf-8'); + return JSON.parse(content) as StoredConversation; + }).filter(conv => !agentId || conv.agentId === agentId); + } + + /** + * Delete a conversation + */ + deleteConversation(id: string): boolean { + try { + const path = join(this.baseDir, 'conversations', `${id}.json`); + if (existsSync(path)) { + unlinkSync(path); + return true; + } + return false; + } catch { + return false; + } + } + + /** + * Save a task + */ + saveTask(task: StoredTask): void { + const path = join(this.baseDir, 'tasks', `${task.id}.json`); + writeFileSync(path, JSON.stringify(task, null, 2), 'utf-8'); + } + + /** + * Load a task + */ + loadTask(id: string): StoredTask | null { + try { + const path = join(this.baseDir, 'tasks', `${id}.json`); + if (!existsSync(path)) return null; + return JSON.parse(readFileSync(path, 'utf-8')); + } catch { + return null; + } + } + + /** + * List all tasks + */ + listTasks(status?: string): StoredTask[] { + const dir = join(this.baseDir, 'tasks'); + if (!existsSync(dir)) return []; + + const files = readdirSync(dir).filter(f => f.endsWith('.json')); + + return files.map(file => { + const content = readFileSync(join(dir, file), 'utf-8'); + return JSON.parse(content) as StoredTask; + }).filter(task => !status || task.status === status); + } + + /** + * Save agent state + */ + saveAgentState(state: StoredAgentState): void { + const path = join(this.baseDir, 'agents', `${state.id}.json`); + writeFileSync(path, JSON.stringify(state, null, 2), 'utf-8'); + } + + /** + * Load agent state + */ + loadAgentState(id: string): StoredAgentState | null { + try { + const path = join(this.baseDir, 'agents', `${id}.json`); + if (!existsSync(path)) return null; + return JSON.parse(readFileSync(path, 'utf-8')); + } catch { + return null; + } + } + + /** + * List all agent states + */ + listAgentStates(): StoredAgentState[] { + const dir = join(this.baseDir, 'agents'); + if (!existsSync(dir)) return []; + + const files = readdirSync(dir).filter(f => f.endsWith('.json')); + + return files.map(file => { + const content = readFileSync(join(dir, file), 'utf-8'); + return JSON.parse(content) as StoredAgentState; + }); + } + + /** + * Clear all storage + */ + clearAll(): void { + const subdirs = ['conversations', 'tasks', 'agents']; + for (const subdir of subdirs) { + const dir = join(this.baseDir, subdir); + if (existsSync(dir)) { + const files = readdirSync(dir).filter(f => f.endsWith('.json')); + for (const file of files) { + unlinkSync(join(dir, file)); + } + } + } + } + + /** + * Get storage stats + */ + getStats(): { + conversations: number; + tasks: number; + agents: number; + } { + return { + conversations: this.listConversations().length, + tasks: this.listTasks().length, + agents: this.listAgentStates().length + }; + } +} + +// Default storage instance +export const defaultStorage = new AgentStorage(); diff --git a/agent-system/utils/helpers.ts b/agent-system/utils/helpers.ts new file mode 100644 index 0000000..27c9e33 --- /dev/null +++ b/agent-system/utils/helpers.ts @@ -0,0 +1,309 @@ +/** + * Agent System Utilities + * + * Helper functions and utilities for the agent system. + */ + +import { randomUUID } from 'crypto'; + +/** + * Debounce a function + */ +export function debounce unknown>( + fn: T, + delay: number +): (...args: Parameters) => void { + let timeoutId: ReturnType | null = null; + + return (...args: Parameters) => { + if (timeoutId) clearTimeout(timeoutId); + timeoutId = setTimeout(() => fn(...args), delay); + }; +} + +/** + * Throttle a function + */ +export function throttle unknown>( + fn: T, + limit: number +): (...args: Parameters) => void { + let inThrottle = false; + + return (...args: Parameters) => { + if (!inThrottle) { + fn(...args); + inThrottle = true; + setTimeout(() => { inThrottle = false; }, limit); + } + }; +} + +/** + * Retry a function with exponential backoff + */ +export async function retry( + fn: () => Promise, + options: { + maxAttempts?: number; + initialDelay?: number; + maxDelay?: number; + backoffFactor?: number; + } = {} +): Promise { + const { + maxAttempts = 3, + initialDelay = 1000, + maxDelay = 30000, + backoffFactor = 2 + } = options; + + let lastError: Error | null = null; + let delay = initialDelay; + + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + try { + return await fn(); + } catch (error) { + lastError = error instanceof Error ? error : new Error(String(error)); + + if (attempt < maxAttempts) { + await sleep(delay); + delay = Math.min(delay * backoffFactor, maxDelay); + } + } + } + + throw lastError; +} + +/** + * Sleep for a specified duration + */ +export function sleep(ms: number): Promise { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +/** + * Generate a unique ID + */ +export function generateId(prefix?: string): string { + const id = randomUUID(); + return prefix ? `${prefix}-${id}` : id; +} + +/** + * Deep clone an object + */ +export function deepClone(obj: T): T { + return JSON.parse(JSON.stringify(obj)); +} + +/** + * Deep merge objects + */ +export function deepMerge>( + target: T, + ...sources: Partial[] +): T { + if (!sources.length) return target; + + const source = sources.shift(); + + if (isObject(target) && isObject(source)) { + for (const key in source) { + if (isObject(source[key])) { + if (!target[key]) { + Object.assign(target, { [key]: {} }); + } + deepMerge(target[key] as Record, source[key] as Record); + } else { + Object.assign(target, { [key]: source[key] }); + } + } + } + + return deepMerge(target, ...sources); +} + +/** + * Check if value is an object + */ +export function isObject(item: unknown): item is Record { + return item !== null && typeof item === 'object' && !Array.isArray(item); +} + +/** + * Truncate text to a maximum length + */ +export function truncate(text: string, maxLength: number, suffix = '...'): string { + if (text.length <= maxLength) return text; + return text.substring(0, maxLength - suffix.length) + suffix; +} + +/** + * Format bytes to human readable string + */ +export function formatBytes(bytes: number, decimals = 2): string { + if (bytes === 0) return '0 Bytes'; + + const k = 1024; + const dm = decimals < 0 ? 0 : decimals; + const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; + + const i = Math.floor(Math.log(bytes) / Math.log(k)); + + return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`; +} + +/** + * Format duration in milliseconds to human readable string + */ +export function formatDuration(ms: number): string { + if (ms < 1000) return `${ms}ms`; + if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`; + if (ms < 3600000) return `${(ms / 60000).toFixed(1)}m`; + return `${(ms / 3600000).toFixed(1)}h`; +} + +/** + * Create a rate limiter + */ +export function createRateLimiter( + maxRequests: number, + windowMs: number +): { + check: () => boolean; + reset: () => void; + getRemaining: () => number; +} { + let requests = 0; + let windowStart = Date.now(); + + const resetWindow = () => { + const now = Date.now(); + if (now - windowStart >= windowMs) { + requests = 0; + windowStart = now; + } + }; + + return { + check: () => { + resetWindow(); + if (requests < maxRequests) { + requests++; + return true; + } + return false; + }, + reset: () => { + requests = 0; + windowStart = Date.now(); + }, + getRemaining: () => { + resetWindow(); + return maxRequests - requests; + } + }; +} + +/** + * Create a simple cache + */ +export function createCache( + ttlMs: number = 60000 +): { + get: (key: string) => T | undefined; + set: (key: string, value: T) => void; + delete: (key: string) => boolean; + clear: () => void; + has: (key: string) => boolean; +} { + const cache = new Map(); + + // Cleanup expired entries periodically + const cleanup = () => { + const now = Date.now(); + for (const [key, entry] of cache.entries()) { + if (now > entry.expiry) { + cache.delete(key); + } + } + }; + + setInterval(cleanup, ttlMs); + + return { + get: (key: string) => { + const entry = cache.get(key); + if (!entry) return undefined; + if (Date.now() > entry.expiry) { + cache.delete(key); + return undefined; + } + return entry.value; + }, + set: (key: string, value: T) => { + cache.set(key, { + value, + expiry: Date.now() + ttlMs + }); + }, + delete: (key: string) => cache.delete(key), + clear: () => cache.clear(), + has: (key: string) => { + const entry = cache.get(key); + if (!entry) return false; + if (Date.now() > entry.expiry) { + cache.delete(key); + return false; + } + return true; + } + }; +} + +/** + * Compose multiple functions + */ +export function compose( + ...fns: Array<(arg: T) => T> +): (arg: T) => T { + return (arg: T) => fns.reduceRight((acc, fn) => fn(acc), arg); +} + +/** + * Pipe value through multiple functions + */ +export function pipe( + ...fns: Array<(arg: T) => T> +): (arg: T) => T { + return (arg: T) => fns.reduce((acc, fn) => fn(acc), arg); +} + +/** + * Chunk an array into smaller arrays + */ +export function chunk(array: T[], size: number): T[][] { + const chunks: T[][] = []; + for (let i = 0; i < array.length; i += size) { + chunks.push(array.slice(i, i + size)); + } + return chunks; +} + +/** + * Group array items by a key + */ +export function groupBy( + array: T[], + keyFn: (item: T) => K +): Record { + return array.reduce((acc, item) => { + const key = keyFn(item); + if (!acc[key]) acc[key] = []; + acc[key].push(item); + return acc; + }, {} as Record); +} diff --git a/db/custom.db b/db/custom.db new file mode 100644 index 0000000..fa70c81 Binary files /dev/null and b/db/custom.db differ diff --git a/src/app/api/agent-system/route.ts b/src/app/api/agent-system/route.ts new file mode 100644 index 0000000..02eb17b --- /dev/null +++ b/src/app/api/agent-system/route.ts @@ -0,0 +1,142 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { TokenCounter } from '../../../../agent-system/core/token-counter'; +import { ContextCompactor } from '../../../../agent-system/core/context-manager'; +import { AgentOrchestrator } from '../../../../agent-system/core/orchestrator'; +import { SubagentSpawner } from '../../../../agent-system/core/subagent-spawner'; + +// Singleton instances +let orchestrator: AgentOrchestrator | null = null; +let spawner: SubagentSpawner | null = null; + +function getOrchestrator() { + if (!orchestrator) { + orchestrator = new AgentOrchestrator(); + orchestrator.start(); + } + return orchestrator; +} + +function getSpawner() { + if (!spawner) { + spawner = new SubagentSpawner(getOrchestrator()); + } + return spawner; +} + +export async function POST(request: NextRequest) { + try { + const body = await request.json(); + const { action, data } = body; + + switch (action) { + case 'count-tokens': { + const counter = new TokenCounter(); + const result = counter.countText(data.text); + return NextResponse.json({ success: true, result }); + } + + case 'count-conversation': { + const counter = new TokenCounter(); + const result = counter.countConversation(data.messages); + return NextResponse.json({ success: true, result }); + } + + case 'check-compaction': { + const compactor = new ContextCompactor(data.config || {}); + const needsCompact = compactor.needsCompaction(data.messages); + const budget = compactor.getBudget(data.messages); + return NextResponse.json({ + success: true, + needsCompaction: needsCompact, + budget + }); + } + + case 'compact-context': { + const compactor = new ContextCompactor(data.config || {}); + const result = await compactor.compact(data.messages); + return NextResponse.json({ success: true, result }); + } + + case 'get-orchestrator-stats': { + const orch = getOrchestrator(); + const stats = orch.getStats(); + return NextResponse.json({ success: true, stats }); + } + + case 'create-task': { + const orch = getOrchestrator(); + const task = orch.createTask( + data.type, + data.description, + data.input, + data.options + ); + return NextResponse.json({ success: true, task }); + } + + case 'get-task': { + const orch = getOrchestrator(); + const task = orch.getTask(data.taskId); + return NextResponse.json({ success: true, task }); + } + + case 'register-agent': { + const orch = getOrchestrator(); + const state = orch.registerAgent(data.config); + return NextResponse.json({ success: true, agent: state }); + } + + case 'spawn-subagent': { + const sp = getSpawner(); + const result = await sp.executeWithSubagent( + data.type, + data.task, + data.context + ); + return NextResponse.json({ success: true, result }); + } + + case 'parallel-execute': { + const sp = getSpawner(); + const results = await sp.executeParallel(data.tasks, data.options); + return NextResponse.json({ success: true, results }); + } + + case 'get-spawner-stats': { + const sp = getSpawner(); + const stats = sp.getStats(); + return NextResponse.json({ success: true, stats }); + } + + default: + return NextResponse.json( + { success: false, error: 'Unknown action' }, + { status: 400 } + ); + } + } catch (error) { + console.error('Agent system API error:', error); + return NextResponse.json( + { + success: false, + error: error instanceof Error ? error.message : 'Unknown error' + }, + { status: 500 } + ); + } +} + +export async function GET() { + const orch = getOrchestrator(); + const sp = getSpawner(); + + return NextResponse.json({ + success: true, + message: 'Agent System API is running', + stats: { + orchestrator: orch.getStats(), + spawner: sp.getStats() + } + }); +} diff --git a/src/app/page.tsx b/src/app/page.tsx index 7e1e0b5..9778fcf 100755 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,31 +1,728 @@ -'use client' +'use client'; + +import { useState, useEffect, useCallback } from 'react'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Textarea } from '@/components/ui/textarea'; +import { Badge } from '@/components/ui/badge'; +import { Progress } from '@/components/ui/progress'; +import { ScrollArea } from '@/components/ui/scroll-area'; +import { Separator } from '@/components/ui/separator'; +import { + Cpu, + MessageSquare, + Layers, + Users, + Zap, + Database, + BarChart3, + Play, + Trash2, + RefreshCw, + Plus, + Send, + Activity +} from 'lucide-react'; + +// Types +interface TokenResult { + tokens: number; + characters: number; + words: number; +} + +interface ConversationResult { + total: number; + breakdown: Array<{ + role: string; + content: string; + tokens: number; + }>; +} + +interface BudgetInfo { + used: number; + remaining: number; + total: number; + percentageUsed: number; +} + +interface CompactionResult { + messages: Array<{ role: string; content: string }>; + originalTokenCount: number; + newTokenCount: number; + tokensSaved: number; + compressionRatio: number; + strategy: string; + summaryAdded: boolean; + removedCount: number; +} + +interface OrchestratorStats { + agents: { total: number; idle: number; working: number }; + tasks: { pending: number; running: number; completed: number; failed: number }; +} + +interface SubagentResult { + subagentId: string; + taskId: string; + success: boolean; + output: unknown; + error?: string; + duration: number; +} + +export default function AgentSystemPage() { + // Token Counter State + const [tokenInput, setTokenInput] = useState(''); + const [tokenResult, setTokenResult] = useState(null); + + // Context Compaction State + const [conversationMessages, setConversationMessages] = useState>([ + { role: 'user', content: 'Hello, I need help with my project.' }, + { role: 'assistant', content: 'I would be happy to help you with your project! What specifically do you need assistance with?' }, + { role: 'user', content: 'I need to build a real-time chat application with WebSocket support.' }, + { role: 'assistant', content: 'Great choice! For a real-time chat application with WebSocket support, I recommend using Socket.io or the native WebSocket API. Would you like me to outline the architecture?' } + ]); + const [newMessage, setNewMessage] = useState({ role: 'user', content: '' }); + const [budget, setBudget] = useState(null); + const [compactionResult, setCompactionResult] = useState(null); + + // Orchestrator State + const [orchestratorStats, setOrchestratorStats] = useState(null); + const [agentConfig, setAgentConfig] = useState({ + id: '', + name: 'Worker Agent', + type: 'worker', + capabilities: 'process,execute', + maxConcurrentTasks: 3, + timeout: 60000 + }); + + // Subagent State + const [subagentType, setSubagentType] = useState('explorer'); + const [subagentTask, setSubagentTask] = useState(''); + const [subagentContext, setSubagentContext] = useState(''); + const [subagentResult, setSubagentResult] = useState(null); + const [isProcessing, setIsProcessing] = useState(false); + + // Fetch orchestrator stats + const fetchStats = useCallback(async () => { + try { + const response = await fetch('/api/agent-system'); + const data = await response.json(); + if (data.success) { + setOrchestratorStats(data.stats.orchestrator); + } + } catch (error) { + console.error('Failed to fetch stats:', error); + } + }, []); + + useEffect(() => { + fetchStats(); + const interval = setInterval(fetchStats, 5000); + return () => clearInterval(interval); + }, [fetchStats]); + + // Count tokens + const handleCountTokens = async () => { + const response = await fetch('/api/agent-system', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + action: 'count-tokens', + data: { text: tokenInput } + }) + }); + const data = await response.json(); + if (data.success) { + setTokenResult(data.result); + } + }; + + // Add message to conversation + const handleAddMessage = () => { + if (!newMessage.content.trim()) return; + setConversationMessages([...conversationMessages, { ...newMessage }]); + setNewMessage({ role: 'user', content: '' }); + }; + + // Check compaction status + const handleCheckCompaction = async () => { + const response = await fetch('/api/agent-system', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + action: 'check-compaction', + data: { messages: conversationMessages } + }) + }); + const data = await response.json(); + if (data.success) { + setBudget(data.budget); + } + }; + + // Compact context + const handleCompactContext = async () => { + setIsProcessing(true); + try { + const response = await fetch('/api/agent-system', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + action: 'compact-context', + data: { + messages: conversationMessages, + config: { strategy: 'hybrid', preserveRecentCount: 2 } + } + }) + }); + const data = await response.json(); + if (data.success) { + setCompactionResult(data.result); + setConversationMessages(data.result.messages); + } + } finally { + setIsProcessing(false); + } + }; + + // Register agent + const handleRegisterAgent = async () => { + const id = agentConfig.id || `agent-${Date.now()}`; + const response = await fetch('/api/agent-system', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + action: 'register-agent', + data: { + config: { + id, + name: agentConfig.name, + type: agentConfig.type, + capabilities: agentConfig.capabilities.split(',').map(c => c.trim()), + maxConcurrentTasks: agentConfig.maxConcurrentTasks, + timeout: agentConfig.timeout + } + } + }) + }); + const data = await response.json(); + if (data.success) { + fetchStats(); + setAgentConfig({ ...agentConfig, id: '' }); + } + }; + + // Execute subagent task + const handleExecuteSubagent = async () => { + if (!subagentTask.trim()) return; + setIsProcessing(true); + try { + const response = await fetch('/api/agent-system', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + action: 'spawn-subagent', + data: { + type: subagentType, + task: subagentTask, + context: subagentContext + } + }) + }); + const data = await response.json(); + if (data.success) { + setSubagentResult(data.result); + } + } finally { + setIsProcessing(false); + } + }; -export default function Home() { return ( -
-
- Z.ai Logo +
+
+ {/* Header */} +
+

+ + Agent System +

+

+ Complete implementation of context compaction, agent orchestration, and subagent spawning +

+
+ + {/* Stats Overview */} + {orchestratorStats && ( +
+ + +
+ +
+

Agents

+

+ {orchestratorStats.agents.total} +

+
+
+
+
+ + +
+ +
+

Working

+

+ {orchestratorStats.agents.working} +

+
+
+
+
+ + +
+ +
+

Tasks Running

+

+ {orchestratorStats.tasks.running} +

+
+
+
+
+ + +
+ +
+

Completed

+

+ {orchestratorStats.tasks.completed} +

+
+
+
+
+
+ )} + + {/* Main Tabs */} + + + + + Token Counter + + + + Context Compaction + + + + Orchestrator + + + + Subagents + + + + {/* Token Counter Tab */} + + + + Token Counter + + Estimate token counts for text using character-based approximation + + + +
+ +