pack
This commit is contained in:
333
agent-system/agents/base-agent.ts
Normal file
333
agent-system/agents/base-agent.ts
Normal file
@@ -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<string, unknown>;
|
||||||
|
longTerm: Map<string, unknown>;
|
||||||
|
conversationHistory: Array<{
|
||||||
|
role: 'user' | 'assistant' | 'system';
|
||||||
|
content: string;
|
||||||
|
timestamp: Date;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AgentTool {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
execute: (params: unknown) => Promise<unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AgentConfig {
|
||||||
|
id?: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
systemPrompt: string;
|
||||||
|
tools?: AgentTool[];
|
||||||
|
maxTokens?: number;
|
||||||
|
contextConfig?: Partial<CompactionConfig>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AgentResponse {
|
||||||
|
content: string;
|
||||||
|
tokens: {
|
||||||
|
prompt: number;
|
||||||
|
completion: number;
|
||||||
|
total: number;
|
||||||
|
};
|
||||||
|
toolCalls?: Array<{
|
||||||
|
name: string;
|
||||||
|
params: unknown;
|
||||||
|
result: unknown;
|
||||||
|
}>;
|
||||||
|
metadata?: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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<string, AgentTool>;
|
||||||
|
protected memory: AgentMemory;
|
||||||
|
protected contextManager: ConversationContextManager;
|
||||||
|
protected tokenCounter: TokenCounter;
|
||||||
|
protected zai: Awaited<ReturnType<typeof ZAI.create>> | 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<void> {
|
||||||
|
if (this.initialized) return;
|
||||||
|
this.zai = await ZAI.create();
|
||||||
|
this.initialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process a user message
|
||||||
|
*/
|
||||||
|
async process(input: string, context?: string): Promise<AgentResponse> {
|
||||||
|
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<Array<{
|
||||||
|
name: string;
|
||||||
|
params: unknown;
|
||||||
|
result: unknown;
|
||||||
|
}>> {
|
||||||
|
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<AgentResponse>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SimpleAgent - A basic agent implementation
|
||||||
|
*/
|
||||||
|
export class SimpleAgent extends BaseAgent {
|
||||||
|
async act(input: string, context?: string): Promise<AgentResponse> {
|
||||||
|
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
|
||||||
|
});
|
||||||
|
}
|
||||||
232
agent-system/agents/task-agent.ts
Normal file
232
agent-system/agents/task-agent.ts
Normal file
@@ -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<string, string[]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
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<AgentResponse> {
|
||||||
|
// 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<TaskPlan> {
|
||||||
|
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<TaskResult> {
|
||||||
|
if (!this.currentPlan) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
steps: [],
|
||||||
|
output: null,
|
||||||
|
errors: ['No plan available']
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const errors: string[] = [];
|
||||||
|
const completedSteps = new Set<string>();
|
||||||
|
|
||||||
|
// 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<unknown> {
|
||||||
|
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
|
||||||
|
});
|
||||||
|
}
|
||||||
556
agent-system/core/context-manager.ts
Normal file
556
agent-system/core/context-manager.ts
Normal file
@@ -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<CompactionConfig> = {}) {
|
||||||
|
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<CompactionResult> {
|
||||||
|
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<CompactionResult> {
|
||||||
|
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<number>();
|
||||||
|
|
||||||
|
// 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<CompactionResult> {
|
||||||
|
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<number>();
|
||||||
|
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<CompactionConfig> = {}) {
|
||||||
|
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<ConversationTurn[]> {
|
||||||
|
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<CompactionResult> {
|
||||||
|
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();
|
||||||
532
agent-system/core/orchestrator.ts
Normal file
532
agent-system/core/orchestrator.ts
Normal file
@@ -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<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
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<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
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<void>;
|
||||||
|
|
||||||
|
interface TaskQueue {
|
||||||
|
pending: Task[];
|
||||||
|
running: Map<string, Task>;
|
||||||
|
completed: Task[];
|
||||||
|
failed: Task[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AgentOrchestrator - Central coordinator for multi-agent systems
|
||||||
|
*/
|
||||||
|
export class AgentOrchestrator {
|
||||||
|
private agents: Map<string, AgentState> = new Map();
|
||||||
|
private tasks: TaskQueue = {
|
||||||
|
pending: [],
|
||||||
|
running: new Map(),
|
||||||
|
completed: [],
|
||||||
|
failed: []
|
||||||
|
};
|
||||||
|
private eventHandlers: Map<string, EventHandler[]> = new Map();
|
||||||
|
private taskProcessors: Map<string, (task: Task) => Promise<unknown>> = new Map();
|
||||||
|
private running = false;
|
||||||
|
private processInterval?: ReturnType<typeof setInterval>;
|
||||||
|
|
||||||
|
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<string, unknown>;
|
||||||
|
} = {}
|
||||||
|
): 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<unknown>
|
||||||
|
): 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<void> {
|
||||||
|
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<void> {
|
||||||
|
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<never> {
|
||||||
|
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();
|
||||||
455
agent-system/core/subagent-spawner.ts
Normal file
455
agent-system/core/subagent-spawner.ts
Normal file
@@ -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<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SubagentPool {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
subagents: Map<string, SubagentInstance>;
|
||||||
|
createdAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SubagentInstance - A running subagent
|
||||||
|
*/
|
||||||
|
export class SubagentInstance {
|
||||||
|
id: string;
|
||||||
|
definition: SubagentDefinition;
|
||||||
|
orchestrator: AgentOrchestrator;
|
||||||
|
private zai: Awaited<ReturnType<typeof ZAI.create>> | 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<void> {
|
||||||
|
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<SubagentResult> {
|
||||||
|
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<string, SubagentInstance> = new Map();
|
||||||
|
private pools: Map<string, SubagentPool> = new Map();
|
||||||
|
private definitions: Map<SubagentType, SubagentDefinition> = 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<SubagentInstance> {
|
||||||
|
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<SubagentPool> {
|
||||||
|
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<SubagentResult> {
|
||||||
|
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<SubagentResult[]> {
|
||||||
|
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<SubagentResult> {
|
||||||
|
const spawner = new SubagentSpawner();
|
||||||
|
return spawner.executeWithSubagent(type, task, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default spawner instance
|
||||||
|
export const defaultSpawner = new SubagentSpawner();
|
||||||
332
agent-system/core/summarizer.ts
Normal file
332
agent-system/core/summarizer.ts
Normal file
@@ -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<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_OPTIONS: Required<SummarizerOptions> = {
|
||||||
|
maxSummaryTokens: 1000,
|
||||||
|
preserveRecentMessages: 3,
|
||||||
|
extractKeyPoints: true,
|
||||||
|
extractDecisions: true,
|
||||||
|
extractActionItems: true
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ConversationSummarizer - Creates intelligent summaries of conversations
|
||||||
|
*/
|
||||||
|
export class ConversationSummarizer {
|
||||||
|
private zai: Awaited<ReturnType<typeof ZAI.create>> | null = null;
|
||||||
|
private tokenCounter: TokenCounter;
|
||||||
|
private options: Required<SummarizerOptions>;
|
||||||
|
|
||||||
|
constructor(options: SummarizerOptions = {}) {
|
||||||
|
this.options = { ...DEFAULT_OPTIONS, ...options };
|
||||||
|
this.tokenCounter = new TokenCounter();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the summarizer (lazy load ZAI)
|
||||||
|
*/
|
||||||
|
private async init(): Promise<void> {
|
||||||
|
if (!this.zai) {
|
||||||
|
this.zai = await ZAI.create();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Summarize a conversation
|
||||||
|
*/
|
||||||
|
async summarize(
|
||||||
|
messages: ConversationTurn[],
|
||||||
|
options?: Partial<SummarizerOptions>
|
||||||
|
): Promise<SummaryResult> {
|
||||||
|
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<SummarizerOptions>
|
||||||
|
): 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<SummaryResult> {
|
||||||
|
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<Map<string, SummaryResult>> {
|
||||||
|
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<string, SummaryResult>();
|
||||||
|
|
||||||
|
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();
|
||||||
220
agent-system/core/token-counter.ts
Normal file
220
agent-system/core/token-counter.ts
Normal file
@@ -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;
|
||||||
|
}
|
||||||
179
agent-system/index.ts
Normal file
179
agent-system/index.ts
Normal file
@@ -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';
|
||||||
232
agent-system/storage/memory-store.ts
Normal file
232
agent-system/storage/memory-store.ts
Normal file
@@ -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<string, unknown>;
|
||||||
|
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<string, unknown>;
|
||||||
|
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();
|
||||||
309
agent-system/utils/helpers.ts
Normal file
309
agent-system/utils/helpers.ts
Normal file
@@ -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<T extends (...args: unknown[]) => unknown>(
|
||||||
|
fn: T,
|
||||||
|
delay: number
|
||||||
|
): (...args: Parameters<T>) => void {
|
||||||
|
let timeoutId: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
|
||||||
|
return (...args: Parameters<T>) => {
|
||||||
|
if (timeoutId) clearTimeout(timeoutId);
|
||||||
|
timeoutId = setTimeout(() => fn(...args), delay);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Throttle a function
|
||||||
|
*/
|
||||||
|
export function throttle<T extends (...args: unknown[]) => unknown>(
|
||||||
|
fn: T,
|
||||||
|
limit: number
|
||||||
|
): (...args: Parameters<T>) => void {
|
||||||
|
let inThrottle = false;
|
||||||
|
|
||||||
|
return (...args: Parameters<T>) => {
|
||||||
|
if (!inThrottle) {
|
||||||
|
fn(...args);
|
||||||
|
inThrottle = true;
|
||||||
|
setTimeout(() => { inThrottle = false; }, limit);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retry a function with exponential backoff
|
||||||
|
*/
|
||||||
|
export async function retry<T>(
|
||||||
|
fn: () => Promise<T>,
|
||||||
|
options: {
|
||||||
|
maxAttempts?: number;
|
||||||
|
initialDelay?: number;
|
||||||
|
maxDelay?: number;
|
||||||
|
backoffFactor?: number;
|
||||||
|
} = {}
|
||||||
|
): Promise<T> {
|
||||||
|
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<void> {
|
||||||
|
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<T>(obj: T): T {
|
||||||
|
return JSON.parse(JSON.stringify(obj));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deep merge objects
|
||||||
|
*/
|
||||||
|
export function deepMerge<T extends Record<string, unknown>>(
|
||||||
|
target: T,
|
||||||
|
...sources: Partial<T>[]
|
||||||
|
): 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<string, unknown>, source[key] as Record<string, unknown>);
|
||||||
|
} 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<string, unknown> {
|
||||||
|
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<T>(
|
||||||
|
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<string, { value: T; expiry: number }>();
|
||||||
|
|
||||||
|
// 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<T>(
|
||||||
|
...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<T>(
|
||||||
|
...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<T>(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<T, K extends string | number | symbol>(
|
||||||
|
array: T[],
|
||||||
|
keyFn: (item: T) => K
|
||||||
|
): Record<K, T[]> {
|
||||||
|
return array.reduce((acc, item) => {
|
||||||
|
const key = keyFn(item);
|
||||||
|
if (!acc[key]) acc[key] = [];
|
||||||
|
acc[key].push(item);
|
||||||
|
return acc;
|
||||||
|
}, {} as Record<K, T[]>);
|
||||||
|
}
|
||||||
BIN
db/custom.db
Normal file
BIN
db/custom.db
Normal file
Binary file not shown.
142
src/app/api/agent-system/route.ts
Normal file
142
src/app/api/agent-system/route.ts
Normal file
@@ -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()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
751
src/app/page.tsx
751
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<TokenResult | null>(null);
|
||||||
|
|
||||||
|
// Context Compaction State
|
||||||
|
const [conversationMessages, setConversationMessages] = useState<Array<{ role: string; content: string }>>([
|
||||||
|
{ 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<BudgetInfo | null>(null);
|
||||||
|
const [compactionResult, setCompactionResult] = useState<CompactionResult | null>(null);
|
||||||
|
|
||||||
|
// Orchestrator State
|
||||||
|
const [orchestratorStats, setOrchestratorStats] = useState<OrchestratorStats | null>(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<SubagentResult | null>(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 (
|
return (
|
||||||
<div style={{
|
<div className="min-h-screen bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900">
|
||||||
display: 'flex',
|
<div className="container mx-auto p-6">
|
||||||
flexDirection: 'column',
|
{/* Header */}
|
||||||
alignItems: 'center',
|
<div className="mb-8">
|
||||||
justifyContent: 'center',
|
<h1 className="text-4xl font-bold text-white mb-2 flex items-center gap-3">
|
||||||
minHeight: '100vh',
|
<Cpu className="w-10 h-10 text-emerald-400" />
|
||||||
gap: '2rem',
|
Agent System
|
||||||
padding: '1rem'
|
</h1>
|
||||||
}}>
|
<p className="text-slate-400 text-lg">
|
||||||
<div style={{
|
Complete implementation of context compaction, agent orchestration, and subagent spawning
|
||||||
position: 'relative',
|
</p>
|
||||||
width: '6rem',
|
</div>
|
||||||
height: '6rem'
|
|
||||||
}}>
|
{/* Stats Overview */}
|
||||||
<img
|
{orchestratorStats && (
|
||||||
src="/logo.svg"
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
|
||||||
alt="Z.ai Logo"
|
<Card className="bg-slate-800/50 border-slate-700">
|
||||||
style={{
|
<CardContent className="p-4">
|
||||||
width: '100%',
|
<div className="flex items-center gap-3">
|
||||||
height: '100%',
|
<Users className="w-8 h-8 text-blue-400" />
|
||||||
objectFit: 'contain'
|
<div>
|
||||||
}}
|
<p className="text-slate-400 text-sm">Agents</p>
|
||||||
/>
|
<p className="text-2xl font-bold text-white">
|
||||||
|
{orchestratorStats.agents.total}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
<Card className="bg-slate-800/50 border-slate-700">
|
||||||
|
<CardContent className="p-4">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Activity className="w-8 h-8 text-amber-400" />
|
||||||
|
<div>
|
||||||
|
<p className="text-slate-400 text-sm">Working</p>
|
||||||
|
<p className="text-2xl font-bold text-white">
|
||||||
|
{orchestratorStats.agents.working}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
<Card className="bg-slate-800/50 border-slate-700">
|
||||||
|
<CardContent className="p-4">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Zap className="w-8 h-8 text-emerald-400" />
|
||||||
|
<div>
|
||||||
|
<p className="text-slate-400 text-sm">Tasks Running</p>
|
||||||
|
<p className="text-2xl font-bold text-white">
|
||||||
|
{orchestratorStats.tasks.running}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
<Card className="bg-slate-800/50 border-slate-700">
|
||||||
|
<CardContent className="p-4">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<BarChart3 className="w-8 h-8 text-purple-400" />
|
||||||
|
<div>
|
||||||
|
<p className="text-slate-400 text-sm">Completed</p>
|
||||||
|
<p className="text-2xl font-bold text-white">
|
||||||
|
{orchestratorStats.tasks.completed}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Main Tabs */}
|
||||||
|
<Tabs defaultValue="tokens" className="space-y-6">
|
||||||
|
<TabsList className="bg-slate-800/50 border border-slate-700">
|
||||||
|
<TabsTrigger value="tokens" className="data-[state=active]:bg-emerald-600">
|
||||||
|
<MessageSquare className="w-4 h-4 mr-2" />
|
||||||
|
Token Counter
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="compaction" className="data-[state=active]:bg-emerald-600">
|
||||||
|
<Layers className="w-4 h-4 mr-2" />
|
||||||
|
Context Compaction
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="orchestrator" className="data-[state=active]:bg-emerald-600">
|
||||||
|
<Users className="w-4 h-4 mr-2" />
|
||||||
|
Orchestrator
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="subagents" className="data-[state=active]:bg-emerald-600">
|
||||||
|
<Cpu className="w-4 h-4 mr-2" />
|
||||||
|
Subagents
|
||||||
|
</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
{/* Token Counter Tab */}
|
||||||
|
<TabsContent value="tokens">
|
||||||
|
<Card className="bg-slate-800/50 border-slate-700">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-white">Token Counter</CardTitle>
|
||||||
|
<CardDescription className="text-slate-400">
|
||||||
|
Estimate token counts for text using character-based approximation
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<Label className="text-slate-300">Input Text</Label>
|
||||||
|
<Textarea
|
||||||
|
value={tokenInput}
|
||||||
|
onChange={(e) => setTokenInput(e.target.value)}
|
||||||
|
placeholder="Enter text to count tokens..."
|
||||||
|
className="bg-slate-900 border-slate-600 text-white mt-2 min-h-32"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Button onClick={handleCountTokens} className="bg-emerald-600 hover:bg-emerald-700">
|
||||||
|
Count Tokens
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{tokenResult && (
|
||||||
|
<div className="grid grid-cols-3 gap-4 mt-4">
|
||||||
|
<Card className="bg-slate-900/50 border-slate-600">
|
||||||
|
<CardContent className="p-4 text-center">
|
||||||
|
<p className="text-3xl font-bold text-emerald-400">{tokenResult.tokens}</p>
|
||||||
|
<p className="text-slate-400 text-sm">Tokens</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
<Card className="bg-slate-900/50 border-slate-600">
|
||||||
|
<CardContent className="p-4 text-center">
|
||||||
|
<p className="text-3xl font-bold text-blue-400">{tokenResult.characters}</p>
|
||||||
|
<p className="text-slate-400 text-sm">Characters</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
<Card className="bg-slate-900/50 border-slate-600">
|
||||||
|
<CardContent className="p-4 text-center">
|
||||||
|
<p className="text-3xl font-bold text-purple-400">{tokenResult.words}</p>
|
||||||
|
<p className="text-slate-400 text-sm">Words</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
{/* Context Compaction Tab */}
|
||||||
|
<TabsContent value="compaction">
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
|
<Card className="bg-slate-800/50 border-slate-700">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-white">Conversation</CardTitle>
|
||||||
|
<CardDescription className="text-slate-400">
|
||||||
|
Messages in the context window
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<ScrollArea className="h-64 pr-4">
|
||||||
|
{conversationMessages.map((msg, idx) => (
|
||||||
|
<div key={idx} className={`p-3 rounded-lg mb-2 ${
|
||||||
|
msg.role === 'user'
|
||||||
|
? 'bg-blue-900/30 border border-blue-800'
|
||||||
|
: 'bg-emerald-900/30 border border-emerald-800'
|
||||||
|
}`}>
|
||||||
|
<Badge variant={msg.role === 'user' ? 'default' : 'secondary'} className="mb-1">
|
||||||
|
{msg.role}
|
||||||
|
</Badge>
|
||||||
|
<p className="text-slate-300 text-sm">{msg.content}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</ScrollArea>
|
||||||
|
|
||||||
|
<Separator className="bg-slate-700" />
|
||||||
|
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<select
|
||||||
|
value={newMessage.role}
|
||||||
|
onChange={(e) => setNewMessage({ ...newMessage, role: e.target.value })}
|
||||||
|
className="bg-slate-900 border border-slate-600 text-white rounded px-2"
|
||||||
|
>
|
||||||
|
<option value="user">User</option>
|
||||||
|
<option value="assistant">Assistant</option>
|
||||||
|
</select>
|
||||||
|
<Input
|
||||||
|
value={newMessage.content}
|
||||||
|
onChange={(e) => setNewMessage({ ...newMessage, content: e.target.value })}
|
||||||
|
placeholder="Add message..."
|
||||||
|
className="bg-slate-900 border-slate-600 text-white flex-1"
|
||||||
|
/>
|
||||||
|
<Button onClick={handleAddMessage} size="icon" className="bg-emerald-600">
|
||||||
|
<Plus className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="bg-slate-800/50 border-slate-700">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-white">Compaction Controls</CardTitle>
|
||||||
|
<CardDescription className="text-slate-400">
|
||||||
|
Manage context size with intelligent compaction
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button onClick={handleCheckCompaction} variant="outline" className="border-slate-600 text-slate-300">
|
||||||
|
<BarChart3 className="w-4 h-4 mr-2" />
|
||||||
|
Check Status
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleCompactContext}
|
||||||
|
className="bg-emerald-600 hover:bg-emerald-700"
|
||||||
|
disabled={isProcessing}
|
||||||
|
>
|
||||||
|
{isProcessing ? (
|
||||||
|
<RefreshCw className="w-4 h-4 mr-2 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Layers className="w-4 h-4 mr-2" />
|
||||||
|
)}
|
||||||
|
Compact Context
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{budget && (
|
||||||
|
<Card className="bg-slate-900/50 border-slate-600">
|
||||||
|
<CardContent className="p-4">
|
||||||
|
<p className="text-slate-400 text-sm mb-2">Token Budget</p>
|
||||||
|
<Progress value={budget.percentageUsed} className="mb-2" />
|
||||||
|
<div className="flex justify-between text-sm">
|
||||||
|
<span className="text-slate-400">
|
||||||
|
Used: <span className="text-white">{budget.used.toLocaleString()}</span>
|
||||||
|
</span>
|
||||||
|
<span className="text-slate-400">
|
||||||
|
Remaining: <span className="text-emerald-400">{budget.remaining.toLocaleString()}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{compactionResult && (
|
||||||
|
<Card className="bg-slate-900/50 border-slate-600">
|
||||||
|
<CardContent className="p-4 space-y-3">
|
||||||
|
<p className="text-emerald-400 font-semibold">Compaction Complete</p>
|
||||||
|
<div className="grid grid-cols-2 gap-2 text-sm">
|
||||||
|
<div>
|
||||||
|
<span className="text-slate-400">Original:</span>
|
||||||
|
<span className="text-white ml-1">{compactionResult.originalTokenCount}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-slate-400">New:</span>
|
||||||
|
<span className="text-emerald-400 ml-1">{compactionResult.newTokenCount}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-slate-400">Saved:</span>
|
||||||
|
<span className="text-blue-400 ml-1">{compactionResult.tokensSaved}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-slate-400">Ratio:</span>
|
||||||
|
<span className="text-purple-400 ml-1">
|
||||||
|
{(compactionResult.compressionRatio * 100).toFixed(1)}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Badge variant="outline" className="border-slate-600">
|
||||||
|
Strategy: {compactionResult.strategy}
|
||||||
|
</Badge>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
{/* Orchestrator Tab */}
|
||||||
|
<TabsContent value="orchestrator">
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
|
<Card className="bg-slate-800/50 border-slate-700">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-white">Register Agent</CardTitle>
|
||||||
|
<CardDescription className="text-slate-400">
|
||||||
|
Add a new agent to the orchestrator
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<Label className="text-slate-300">Name</Label>
|
||||||
|
<Input
|
||||||
|
value={agentConfig.name}
|
||||||
|
onChange={(e) => setAgentConfig({ ...agentConfig, name: e.target.value })}
|
||||||
|
className="bg-slate-900 border-slate-600 text-white mt-1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label className="text-slate-300">Type</Label>
|
||||||
|
<Input
|
||||||
|
value={agentConfig.type}
|
||||||
|
onChange={(e) => setAgentConfig({ ...agentConfig, type: e.target.value })}
|
||||||
|
className="bg-slate-900 border-slate-600 text-white mt-1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label className="text-slate-300">Capabilities (comma-separated)</Label>
|
||||||
|
<Input
|
||||||
|
value={agentConfig.capabilities}
|
||||||
|
onChange={(e) => setAgentConfig({ ...agentConfig, capabilities: e.target.value })}
|
||||||
|
className="bg-slate-900 border-slate-600 text-white mt-1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<Label className="text-slate-300">Max Concurrent Tasks</Label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
value={agentConfig.maxConcurrentTasks}
|
||||||
|
onChange={(e) => setAgentConfig({ ...agentConfig, maxConcurrentTasks: parseInt(e.target.value) })}
|
||||||
|
className="bg-slate-900 border-slate-600 text-white mt-1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label className="text-slate-300">Timeout (ms)</Label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
value={agentConfig.timeout}
|
||||||
|
onChange={(e) => setAgentConfig({ ...agentConfig, timeout: parseInt(e.target.value) })}
|
||||||
|
className="bg-slate-900 border-slate-600 text-white mt-1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button onClick={handleRegisterAgent} className="w-full bg-emerald-600 hover:bg-emerald-700">
|
||||||
|
<Plus className="w-4 h-4 mr-2" />
|
||||||
|
Register Agent
|
||||||
|
</Button>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="bg-slate-800/50 border-slate-700">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-white">Orchestrator Stats</CardTitle>
|
||||||
|
<CardDescription className="text-slate-400">
|
||||||
|
Current state of the orchestration system
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
{orchestratorStats && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<p className="text-slate-400 text-sm mb-2">Agents</p>
|
||||||
|
<div className="flex gap-4">
|
||||||
|
<Badge variant="outline" className="border-slate-600">
|
||||||
|
Total: {orchestratorStats.agents.total}
|
||||||
|
</Badge>
|
||||||
|
<Badge variant="outline" className="border-emerald-600 text-emerald-400">
|
||||||
|
Idle: {orchestratorStats.agents.idle}
|
||||||
|
</Badge>
|
||||||
|
<Badge variant="outline" className="border-amber-600 text-amber-400">
|
||||||
|
Working: {orchestratorStats.agents.working}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Separator className="bg-slate-700" />
|
||||||
|
<div>
|
||||||
|
<p className="text-slate-400 text-sm mb-2">Tasks</p>
|
||||||
|
<div className="flex gap-2 flex-wrap">
|
||||||
|
<Badge variant="outline" className="border-slate-600">
|
||||||
|
Pending: {orchestratorStats.tasks.pending}
|
||||||
|
</Badge>
|
||||||
|
<Badge variant="outline" className="border-blue-600 text-blue-400">
|
||||||
|
Running: {orchestratorStats.tasks.running}
|
||||||
|
</Badge>
|
||||||
|
<Badge variant="outline" className="border-emerald-600 text-emerald-400">
|
||||||
|
Completed: {orchestratorStats.tasks.completed}
|
||||||
|
</Badge>
|
||||||
|
<Badge variant="outline" className="border-red-600 text-red-400">
|
||||||
|
Failed: {orchestratorStats.tasks.failed}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
{/* Subagents Tab */}
|
||||||
|
<TabsContent value="subagents">
|
||||||
|
<Card className="bg-slate-800/50 border-slate-700">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-white">Spawn Subagent</CardTitle>
|
||||||
|
<CardDescription className="text-slate-400">
|
||||||
|
Execute tasks using specialized subagents
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
<div>
|
||||||
|
<Label className="text-slate-300">Agent Type</Label>
|
||||||
|
<select
|
||||||
|
value={subagentType}
|
||||||
|
onChange={(e) => setSubagentType(e.target.value)}
|
||||||
|
className="w-full bg-slate-900 border border-slate-600 text-white rounded px-3 py-2 mt-1"
|
||||||
|
>
|
||||||
|
<option value="explorer">Explorer</option>
|
||||||
|
<option value="researcher">Researcher</option>
|
||||||
|
<option value="coder">Coder</option>
|
||||||
|
<option value="reviewer">Reviewer</option>
|
||||||
|
<option value="planner">Planner</option>
|
||||||
|
<option value="executor">Executor</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className="md:col-span-2">
|
||||||
|
<Label className="text-slate-300">Task</Label>
|
||||||
|
<Input
|
||||||
|
value={subagentTask}
|
||||||
|
onChange={(e) => setSubagentTask(e.target.value)}
|
||||||
|
placeholder="Enter task for the subagent..."
|
||||||
|
className="bg-slate-900 border-slate-600 text-white mt-1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label className="text-slate-300">Context (Optional)</Label>
|
||||||
|
<Textarea
|
||||||
|
value={subagentContext}
|
||||||
|
onChange={(e) => setSubagentContext(e.target.value)}
|
||||||
|
placeholder="Additional context for the task..."
|
||||||
|
className="bg-slate-900 border-slate-600 text-white mt-1 min-h-20"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
onClick={handleExecuteSubagent}
|
||||||
|
className="w-full bg-emerald-600 hover:bg-emerald-700"
|
||||||
|
disabled={isProcessing || !subagentTask.trim()}
|
||||||
|
>
|
||||||
|
{isProcessing ? (
|
||||||
|
<RefreshCw className="w-4 h-4 mr-2 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Send className="w-4 h-4 mr-2" />
|
||||||
|
)}
|
||||||
|
Execute Task
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{subagentResult && (
|
||||||
|
<Card className="bg-slate-900/50 border-slate-600 mt-4">
|
||||||
|
<CardContent className="p-4">
|
||||||
|
<div className="flex items-center gap-2 mb-3">
|
||||||
|
{subagentResult.success ? (
|
||||||
|
<Badge className="bg-emerald-600">Success</Badge>
|
||||||
|
) : (
|
||||||
|
<Badge className="bg-red-600">Failed</Badge>
|
||||||
|
)}
|
||||||
|
<span className="text-slate-400 text-sm">
|
||||||
|
Duration: {subagentResult.duration}ms
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<ScrollArea className="h-48">
|
||||||
|
<pre className="text-slate-300 text-sm whitespace-pre-wrap">
|
||||||
|
{typeof subagentResult.output === 'string'
|
||||||
|
? subagentResult.output
|
||||||
|
: JSON.stringify(subagentResult.output, null, 2)}
|
||||||
|
</pre>
|
||||||
|
</ScrollArea>
|
||||||
|
{subagentResult.error && (
|
||||||
|
<p className="text-red-400 text-sm mt-2">{subagentResult.error}</p>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<footer className="mt-8 text-center text-slate-500 text-sm">
|
||||||
|
<p>Agent System v1.0.0 — Open Source Implementation</p>
|
||||||
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ES2017",
|
"target": "ES2017",
|
||||||
"lib": ["dom", "dom.iterable", "esnext"],
|
"lib": [
|
||||||
|
"dom",
|
||||||
|
"dom.iterable",
|
||||||
|
"esnext"
|
||||||
|
],
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
@@ -12,7 +16,7 @@
|
|||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"jsx": "preserve",
|
"jsx": "react-jsx",
|
||||||
"incremental": true,
|
"incremental": true,
|
||||||
"plugins": [
|
"plugins": [
|
||||||
{
|
{
|
||||||
@@ -20,9 +24,19 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["./src/*"]
|
"@/*": [
|
||||||
|
"./src/*"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
"include": [
|
||||||
"exclude": ["node_modules"]
|
"next-env.d.ts",
|
||||||
|
"**/*.ts",
|
||||||
|
"**/*.tsx",
|
||||||
|
".next/types/**/*.ts",
|
||||||
|
".next/dev/types/**/*.ts"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"node_modules"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user