pack
This commit is contained in:
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;
|
||||
}
|
||||
Reference in New Issue
Block a user