UNPKG

@just-every/task

Version:
1,097 lines (1,093 loc) 49.8 kB
/** * Task Engine - Simplified Version * * Task implementation for LLM orchestration. * Provides meta-cognition and thought delays on top of ensemble. * Model rotation is handled by ensemble automatically. */ import { getThoughtDelay, runThoughtDelayWithController, } from './thought_utils.js'; import { ensembleRequest, createToolFunction, cloneAgent, waitWhilePaused, } from '@just-every/ensemble'; import { Metamemory } from '../metamemory/index.js'; import { spawnMetaThought } from '../metacognition/index.js'; import { v4 as uuidv4 } from 'uuid'; // WeakMap to store message arrays for active tasks const activeTaskMessages = new WeakMap(); // Map to track cleanup functions for generators const generatorCleanup = new WeakMap(); // WeakMap to store task local state for active tasks const activeTaskLocalStates = new WeakMap(); // WeakMap to store task IDs for active tasks const activeTaskIds = new WeakMap(); // WeakMap to track completed tasks const completedTasks = new WeakMap(); /** * Get Task control tools */ export function getTaskTools() { return [ createToolFunction((result) => { //console.log('[Task] Task completed:', result); // Return the result so it can be captured in the tool_done event return result; }, 'Report that the task has completed successfully', { result: { type: 'string', description: 'A few paragraphs describing the result. Be thorough and comprehensive.', }, }, undefined, 'task_complete', false), createToolFunction((error) => { console.error('[Task] Task failed:', error); // Return the error so it can be captured in the tool_done event return error; }, 'Report that you were not able to complete the task', { error: { type: 'string', description: 'Describe the error that occurred in a few sentences', }, }, undefined, 'task_fatal_error', false), ]; } /** * Process meta memory with timer-based triggering */ async function processMetaMemory(metamemory, messages, metaState, taskLocalState, asyncEventQueue, pendingAsyncOps, abortSignal) { if (!metamemory || !taskLocalState.memory?.enabled) { return; } // Check if processing is stuck if (taskLocalState.memory?.processing) { const STUCK_THRESHOLD = 2 * 60 * 1000; // 2 minutes const processingTime = taskLocalState.memory.lastProcessingStartTime ? Date.now() - taskLocalState.memory.lastProcessingStartTime : 0; if (processingTime > STUCK_THRESHOLD) { console.warn(`[Task] Meta memory processing appears stuck (running for ${Math.round(processingTime / 1000)}s), forcibly resetting`); taskLocalState.memory.processing = false; taskLocalState.memory.lastProcessingStartTime = undefined; } else { return; // Still processing within reasonable time } } const unprocessedMessages = metaState.unprocessedMemoryMessages; if (unprocessedMessages.length === 0) { return; } // Mark as processing BEFORE clearing unprocessed messages to prevent race condition taskLocalState.memory.processing = true; taskLocalState.memory.lastProcessingStartTime = Date.now(); console.log(`[Task] Processing ${unprocessedMessages.length} messages for meta memory`); // Clear the unprocessed messages metaState.unprocessedMemoryMessages = []; metaState.lastMemoryProcessTime = Date.now(); // Emit metamemory tagging started event const startEventData = { messageCount: messages.length, unprocessedCount: unprocessedMessages.length, }; if (taskLocalState.memory.state) { // Convert Maps to objects for JSON serialization startEventData.state = { topicTags: Object.fromEntries(taskLocalState.memory.state.topicTags), taggedMessages: Object.fromEntries(taskLocalState.memory.state.taggedMessages), topicCompaction: taskLocalState.memory.state.topicCompaction ? Object.fromEntries(taskLocalState.memory.state.topicCompaction) : undefined, lastProcessedIndex: taskLocalState.memory.state.lastProcessedIndex, }; } const metaStartEvent = { type: 'metamemory_event', operation: 'tagging_start', eventId: uuidv4(), data: startEventData, timestamp: Date.now(), }; asyncEventQueue.push(metaStartEvent); // Fire and forget - process in background with timeout const processingStart = Date.now(); const MEMORY_TIMEOUT = 3 * 60 * 1000; // 3 minutes const memoryPromise = metamemory.processMessages([...messages]); const timeoutPromise = new Promise((_, reject) => { const timeoutId = setTimeout(() => reject(new Error('Metamemory processing timeout after 3 minutes')), MEMORY_TIMEOUT); // Listen for abort signal if (abortSignal) { abortSignal.addEventListener('abort', () => { clearTimeout(timeoutId); reject(new Error('Metamemory processing aborted')); }); } }); const memoryOp = Promise.race([memoryPromise, timeoutPromise]) .then(async (result) => { const processingTime = Math.round((Date.now() - processingStart) / 1000); if (taskLocalState?.memory) { taskLocalState.memory.state = metamemory.getState(); console.log(`[Task] Metamemory background processing completed in ${processingTime}s`); // Check for compaction after processing try { await metamemory.checkCompact([...messages]); } catch (error) { console.error('[Task] Error checking compaction:', error); } // Create processing complete event const stateForSerialization = { topicTags: Object.fromEntries(taskLocalState.memory.state.topicTags), taggedMessages: Object.fromEntries(taskLocalState.memory.state.taggedMessages), topicCompaction: taskLocalState.memory.state.topicCompaction ? Object.fromEntries(taskLocalState.memory.state.topicCompaction) : undefined, lastProcessedIndex: taskLocalState.memory.state.lastProcessedIndex, }; const completeEvent = { type: 'metamemory_event', operation: 'tagging_complete', eventId: metaStartEvent.eventId, data: { messageCount: messages.length, processingTime: processingTime * 1000, state: stateForSerialization, ...result, }, timestamp: Date.now(), }; asyncEventQueue.push(completeEvent); } }) .catch((error) => { const isTimeout = error.message?.includes('timeout'); const isAborted = error.message?.includes('aborted'); // Only log errors that aren't due to normal task completion if (!isAborted) { console.error(`[Task] ${isTimeout ? 'Timeout' : 'Error'} in metamemory background processing:`, error); } }) .finally(() => { if (taskLocalState?.memory) { taskLocalState.memory.processing = false; taskLocalState.memory.lastProcessingStartTime = undefined; } pendingAsyncOps.delete(memoryOp); }); // Track this operation pendingAsyncOps.add(memoryOp); } /** * Process meta cognition with timer-based triggering */ async function processMetaCognition(agent, messages, metaState, taskLocalState, startTime, asyncEventQueue, pendingAsyncOps, abortSignal) { // Check if processing is stuck if (taskLocalState.cognition?.processing) { const STUCK_THRESHOLD = 2 * 60 * 1000; // 2 minutes const processingTime = taskLocalState.cognition.lastProcessingStartTime ? Date.now() - taskLocalState.cognition.lastProcessingStartTime : 0; if (processingTime > STUCK_THRESHOLD) { console.warn(`[Task] Meta cognition processing appears stuck (running for ${Math.round(processingTime / 1000)}s), forcibly resetting`); taskLocalState.cognition.processing = false; taskLocalState.cognition.lastProcessingStartTime = undefined; } else { return; // Still processing within reasonable time } } if (metaState.unprocessedCognitionMessages.length === 0) { return; } console.log(`[Task] Processing meta cognition with ${metaState.unprocessedCognitionMessages.length} unprocessed messages`); // Clear the unprocessed messages metaState.unprocessedCognitionMessages = []; metaState.lastCognitionProcessTime = Date.now(); // Note: messagesSinceLastCognition is reset immediately when triggering to prevent race conditions // Emit metacognition start event const serializedCognitionState = taskLocalState?.cognition ? { ...taskLocalState.cognition, disabledModels: taskLocalState.cognition.disabledModels ? Array.from(taskLocalState.cognition.disabledModels) : undefined, } : undefined; const metaStartEvent = { type: 'metacognition_event', operation: 'analysis_start', eventId: uuidv4(), data: { requestCount: taskLocalState.requestCount, state: serializedCognitionState, }, timestamp: Date.now(), }; asyncEventQueue.push(metaStartEvent); // Mark as processing to prevent concurrent runs if (taskLocalState.cognition) { taskLocalState.cognition.processing = true; taskLocalState.cognition.lastProcessingStartTime = Date.now(); } // Fire and forget - process in background with timeout const processingStart = Date.now(); const COGNITION_TIMEOUT = 3 * 60 * 1000; // 3 minutes const cognitionPromise = spawnMetaThought(agent, messages, new Date(startTime), taskLocalState.requestCount || 0, taskLocalState); const timeoutPromise = new Promise((_, reject) => { const timeoutId = setTimeout(() => reject(new Error('Meta-cognition processing timeout after 3 minutes')), COGNITION_TIMEOUT); // Listen for abort signal if (abortSignal) { abortSignal.addEventListener('abort', () => { clearTimeout(timeoutId); reject(new Error('Meta-cognition processing aborted')); }); } }); const cognitionOp = Promise.race([cognitionPromise, timeoutPromise]) .then((result) => { const processingTime = Math.round((Date.now() - processingStart) / 1000); // Emit metacognition complete event const serializedCompleteCognitionState = taskLocalState?.cognition ? { ...taskLocalState.cognition, disabledModels: taskLocalState.cognition.disabledModels ? Array.from(taskLocalState.cognition.disabledModels) : undefined, } : undefined; const metaCompleteEvent = { type: 'metacognition_event', operation: 'analysis_complete', eventId: metaStartEvent.eventId, data: { requestCount: taskLocalState?.requestCount || 0, processingTime: processingTime * 1000, ...result, state: serializedCompleteCognitionState, }, timestamp: Date.now(), }; asyncEventQueue.push(metaCompleteEvent); console.log(`[Task] Meta-cognition background processing completed in ${processingTime}s`); }) .catch((error) => { const isTimeout = error.message?.includes('timeout'); const isAborted = error.message?.includes('aborted'); // Only log errors that aren't due to normal task completion if (!isAborted) { console.error(`[Task] ${isTimeout ? 'Timeout' : 'Error'} in meta-cognition background processing:`, error); } }) .finally(() => { if (taskLocalState?.cognition) { taskLocalState.cognition.processing = false; taskLocalState.cognition.lastProcessingStartTime = undefined; } pendingAsyncOps.delete(cognitionOp); }); // Track this operation pendingAsyncOps.add(cognitionOp); } /** * Resume a task from a previous state * * @param agent - The agent to use * @param finalState - The final state from a previous task * @param newContent - Optional new content to add to the conversation * @returns AsyncGenerator that yields events * * @example * ```typescript * // First task * let finalState; * for await (const event of runTask(agent, 'Start analysis')) { * if (event.type === 'task_complete') { * finalState = event.finalState; * } * } * * // Resume with additional instructions * for await (const event of resumeTask(agent, finalState, 'Continue with security analysis')) { * // ... * } * ``` */ export function resumeTask(agent, finalState, newContent) { // If new content provided, add it to messages const messages = finalState.messages || []; if (newContent && messages) { messages.push({ type: 'message', role: 'user', content: newContent, id: uuidv4(), }); // Update the finalState with the new messages finalState.messages = messages; // Resume with the full state, using a placeholder content since we already added the message return runTask(agent, 'Resume task', finalState); } // Resume with the full state without adding new content return runTask(agent, 'Continue with the task', finalState); } /** * Run Mind with automatic everything * * @param agent - The agent from ensemble * @param content - The task/prompt to execute * @param initialState - Optional initial state for the task * @returns AsyncGenerator that yields all ProviderStreamEvents and TaskEvents * * @example * ```typescript * import { Agent } from '@just-every/ensemble'; * import { runTask } from '@just-every/task'; * * const agent = new Agent({ * name: 'MyAgent', * modelClass: 'reasoning' * }); * * for await (const event of runTask(agent, 'Analyze this code')) { * console.log(event); * } * * // With initial state * const state = { metaFrequency: '10', thoughtDelay: '2' }; * for await (const event of runTask(agent, 'Complex task', state)) { * console.log(event); * } * * // Handle task completion with state * for await (const event of runTask(agent, 'Task')) { * if (event.type === 'task_complete') { * console.log('Result:', event.result); * console.log('Final state:', event.finalState); * } * } * ``` */ export function runTask(agent, content, taskLocalState) { // Basic validation if (!agent || typeof agent !== 'object') { throw new Error('Agent must be a valid Agent instance'); } if (!content || typeof content !== 'string' || content.trim().length === 0) { throw new Error('Content must be a non-empty string'); } // Use provided messages or create new ones const messages = taskLocalState?.messages ? [...taskLocalState.messages] : [ { type: 'message', role: 'user', content, id: uuidv4(), }, ]; // Create wrapper to handle cleanup async function* taskGenerator() { const startTime = Date.now(); const taskId = uuidv4(); let taskStartEmitted = false; let taskCompleted = false; // Add Task tools to the agent const taskTools = taskLocalState?.runIndefinitely ? [] : getTaskTools(); // Clone agent to get AgentDefinition and add Task tools const agentDef = cloneAgent(agent); agentDef.tools = [...taskTools, ...(agent.tools || [])]; if (!taskLocalState?.runIndefinitely) { // Build initial messages with tool guidance const toolGuidance = 'You must complete tasks by using the provided tools. When you have finished a task, you MUST call the task_complete() tool with a comprehensive result. If you cannot complete the task, you MUST call the task_fatal_error() tool with an explanation. Do not just provide a final answer without using these tools.'; // Check if agent instructions already contain the exact tool guidance if (!agentDef.instructions?.includes(toolGuidance)) { agentDef.instructions = agentDef.instructions ? `${agentDef.instructions}\n\n${toolGuidance}` : toolGuidance; } } // If resuming with existing messages, check if we already have system instructions if (taskLocalState?.messages && taskLocalState.messages.length > 0) { // Look for any system message in the history const hasSystemMessage = taskLocalState.messages.some((msg) => { if (msg.type === 'message' && msg.role === 'system' && agent.instructions) { // Check if content is a string and contains instructions const content = msg.content; if (typeof content === 'string') { return content.includes(agent.instructions); } } return false; }); if (hasSystemMessage) { // Clear instructions from agent since they're already in the message history // This prevents duplicate system messages agentDef.instructions = undefined; console.log('[Task] Cleared agent instructions to prevent duplicates when resuming'); console.log('[Task] Agent after clearing instructions:', { name: agentDef.name, hasTools: !!(agentDef.tools && agentDef.tools.length > 0), toolCount: agentDef.tools?.length || 0, hasInstructions: !!agentDef.instructions, }); } } // Track completion state let isComplete = false; taskLocalState = taskLocalState || {}; taskLocalState.requestCount = taskLocalState?.requestCount || 0; // Always create a new AbortController - it cannot be serialized/restored taskLocalState.delayAbortController = new AbortController(); taskLocalState.cognition = taskLocalState?.cognition || {}; taskLocalState.cognition.enabled = taskLocalState?.cognition?.enabled !== undefined ? taskLocalState.cognition.enabled : true; taskLocalState.cognition.frequency = taskLocalState?.cognition?.frequency || 10; taskLocalState.cognition.thoughtDelay = taskLocalState?.cognition?.thoughtDelay || getThoughtDelay(); // Reconstruct Set from array if needed (Sets don't serialize properly) taskLocalState.cognition.disabledModels = taskLocalState.cognition .disabledModels ? new Set(Array.isArray(taskLocalState.cognition.disabledModels) ? taskLocalState.cognition.disabledModels : taskLocalState.cognition.disabledModels) : new Set(); taskLocalState.cognition.modelScores = taskLocalState.cognition.modelScores || {}; // Check for stuck cognition processing before we do anything else if (taskLocalState.cognition.processing && taskLocalState.cognition.lastProcessingStartTime) { const STUCK_THRESHOLD = 2 * 60 * 1000; // 2 minutes const processingTime = Date.now() - taskLocalState.cognition.lastProcessingStartTime; if (processingTime > STUCK_THRESHOLD) { console.warn(`[Task] Meta cognition processing appears stuck (running for ${Math.round(processingTime / 1000)}s), forcibly resetting`); taskLocalState.cognition.processing = false; taskLocalState.cognition.lastProcessingStartTime = undefined; } } taskLocalState.memory = taskLocalState?.memory || {}; taskLocalState.memory.enabled = taskLocalState?.memory?.enabled !== undefined ? taskLocalState.memory.enabled : true; // Reconstruct Maps from objects if needed (Maps don't serialize properly) if (taskLocalState.memory.state) { const state = taskLocalState.memory.state; taskLocalState.memory.state = { topicTags: state.topicTags instanceof Map ? state.topicTags : new Map(Object.entries(state.topicTags || {})), taggedMessages: state.taggedMessages instanceof Map ? state.taggedMessages : new Map(Object.entries(state.taggedMessages || {})), lastProcessedIndex: state.lastProcessedIndex || 0, topicCompaction: state.topicCompaction ? state.topicCompaction instanceof Map ? state.topicCompaction : new Map(Object.entries(state.topicCompaction)) : undefined, }; } else { taskLocalState.memory.state = { topicTags: new Map(), taggedMessages: new Map(), lastProcessedIndex: 0, }; } // Check for stuck memory processing before we do anything else if (taskLocalState.memory.processing && taskLocalState.memory.lastProcessingStartTime) { const STUCK_THRESHOLD = 2 * 60 * 1000; // 2 minutes const processingTime = Date.now() - taskLocalState.memory.lastProcessingStartTime; if (processingTime > STUCK_THRESHOLD) { console.warn(`[Task] Meta memory processing appears stuck (running for ${Math.round(processingTime / 1000)}s), forcibly resetting`); taskLocalState.memory.processing = false; taskLocalState.memory.lastProcessingStartTime = undefined; } } // Initialize metamemory if enabled let metamemory; if (taskLocalState.memory.enabled) { metamemory = new Metamemory({ agent, }); // Restore previous state if available if (taskLocalState.memory.state) { metamemory.restoreState(taskLocalState.memory.state); console.log('[Task] Restored metamemory state with', taskLocalState.memory.state.topicTags?.size || 0, 'topics and', taskLocalState.memory.state.taggedMessages?.size || 0, 'tagged messages'); } } // Queue for events generated asynchronously const asyncEventQueue = []; // Track pending async operations const pendingAsyncOps = new Set(); // Create abort controller for meta processing operations const metaProcessingAbortController = new AbortController(); // Initialize meta processing state const metaState = { unprocessedMemoryMessages: [], unprocessedCognitionMessages: [], lastMemoryProcessTime: Date.now(), lastCognitionProcessTime: Date.now(), messagesSinceLastCognition: 0, }; // Initialize timers const metaTimers = { memoryTimer: null, // Not used - kept for backwards compatibility cognitionTimer: null, memoryDebounceTimer: null, cognitionDebounceTimer: null, }; // Helper function to bump memory timer const bumpMetaMemoryTimer = () => { // Check if we should process immediately due to batch size if (metaState.unprocessedMemoryMessages.length >= 10) { // Clear any existing timer to prevent duplicate processing if (metaTimers.memoryDebounceTimer) { clearTimeout(metaTimers.memoryDebounceTimer); metaTimers.memoryDebounceTimer = null; } // Process immediately if not already processing if (taskLocalState && !taskLocalState.memory?.processing) { processMetaMemory(metamemory, messages, metaState, taskLocalState, asyncEventQueue, pendingAsyncOps, metaProcessingAbortController.signal); } return; } // Clear existing debounce timer if (metaTimers.memoryDebounceTimer) { clearTimeout(metaTimers.memoryDebounceTimer); } // Set new debounce timer for 1 second metaTimers.memoryDebounceTimer = setTimeout(() => { // Check again if not already processing if (taskLocalState && !taskLocalState.memory?.processing) { processMetaMemory(metamemory, messages, metaState, taskLocalState, asyncEventQueue, pendingAsyncOps, metaProcessingAbortController.signal); } metaTimers.memoryDebounceTimer = null; }, 1000); }; // Helper function to bump cognition timer const bumpMetaCognitionTimer = () => { // Skip if cognition is disabled if (!taskLocalState?.cognition?.enabled) { return; } // Increment message counter metaState.messagesSinceLastCognition++; // Check if we should process based on message count if (metaState.messagesSinceLastCognition >= (taskLocalState?.cognition?.frequency || 10)) { // Reset counter immediately to prevent multiple triggers metaState.messagesSinceLastCognition = 0; // Clear any existing debounce timer if (metaTimers.cognitionDebounceTimer) { clearTimeout(metaTimers.cognitionDebounceTimer); metaTimers.cognitionDebounceTimer = null; } // Set a 1-second debounce before processing metaTimers.cognitionDebounceTimer = setTimeout(() => { if (taskLocalState) { processMetaCognition(agent, messages, metaState, taskLocalState, startTime, asyncEventQueue, pendingAsyncOps, metaProcessingAbortController.signal); } }, 1000); } }; // Set up periodic timer for cognition only // (Memory doesn't need periodic timer since every message is processed after 1s debounce) metaTimers.cognitionTimer = setInterval(() => { if (messages.length > 0 && taskLocalState && taskLocalState.cognition?.enabled) { processMetaCognition(agent, messages, metaState, taskLocalState, startTime, asyncEventQueue, pendingAsyncOps, metaProcessingAbortController.signal); } }, 180000); // Every 3 minutes try { //console.log(`[Task] Starting execution for agent: ${agent.name}`); // Emit task_start event if (!taskLocalState?.runIndefinitely) { const taskStartEvent = { type: 'task_start', task_id: taskId, finalState: { ...taskLocalState, }, }; yield taskStartEvent; taskStartEmitted = true; } // Error tracking for detecting unrecoverable failures let consecutiveSameErrors = 0; let lastErrorMessage = ''; let totalErrors = 0; const MAX_CONSECUTIVE_SAME_ERRORS = taskLocalState.errorHandling?.maxConsecutiveInitErrors ?? 3; const MAX_TOTAL_ERRORS = taskLocalState.errorHandling?.maxTotalErrors ?? 10; while (!isComplete) { // Track current error message for reporting let currentErrorMessage = ''; // Wait if ensemble is paused (before any processing) await waitWhilePaused(); // Apply thought delay if (taskLocalState.requestCount > 1) { const delay = taskLocalState.cognition.thoughtDelay; if (delay > 0) { try { console.log(`[Task] Applying thought delay of ${delay} seconds`); await runThoughtDelayWithController(taskLocalState.delayAbortController, delay); } catch (error) { console.error('[Task] Error during thought delay:', error); // Continue execution even if thought delay fails } } } // Increment task-local request counter for meta-cognition taskLocalState.requestCount++; console.log(`[Task] Request count: ${taskLocalState.requestCount}, Meta frequency: ${taskLocalState.cognition.frequency}`); // Check for any async events to yield first while (asyncEventQueue.length > 0) { const asyncEvent = asyncEventQueue.shift(); yield asyncEvent; } // Compact messages before ensemble request if metamemory is enabled let messagesToProcess = messages; if (metamemory && taskLocalState.memory.enabled) { try { messagesToProcess = metamemory.compact([...messages]); } catch (error) { console.error('[Task] Error compacting messages:', error); messagesToProcess = messages; // Fall back to original messages } } // Run ensemble request and yield all events console.log(`[Task] Starting ensemble request with ${messagesToProcess.length} messages`); for await (const event of ensembleRequest(messagesToProcess, agentDef)) { // Track errors BEFORE yielding so we can stop the task if (event.type === 'error') { currentErrorMessage = event.error || event.message || 'Unknown error'; totalErrors++; // Check if it's the same error repeating if (currentErrorMessage === lastErrorMessage) { consecutiveSameErrors++; } else { consecutiveSameErrors = 1; lastErrorMessage = currentErrorMessage; } // Check if we should stop immediately if (consecutiveSameErrors >= MAX_CONSECUTIVE_SAME_ERRORS || totalErrors >= MAX_TOTAL_ERRORS) { // Break out of the for-await loop to trigger error handling below break; } } // Reset error counts on actual progress (successful response or tool completion) if (event.type === 'response_output' || (event.type === 'tool_done' && !event.error)) { consecutiveSameErrors = 0; // Reset - agent made progress lastErrorMessage = ''; // Clear last error } // Check for any async events that were queued while (asyncEventQueue.length > 0) { const asyncEvent = asyncEventQueue.shift(); yield asyncEvent; } // Yield the event to the caller yield event; // Handle tool calls if (event.type === 'tool_done' && 'result' in event) { const toolEvent = event; const toolName = toolEvent.tool_call?.function?.name; if (!taskLocalState?.runIndefinitely && (toolName === 'task_complete' || toolName === 'task_fatal_error')) { isComplete = true; taskCompleted = true; // Emit task_complete or task_fatal_error event with final state const completeEvent = { type: toolName, task_id: taskId, result: toolEvent.result?.output || '', finalState: { ...taskLocalState, }, }; yield completeEvent; } } else if (event.type === 'response_output') { const responseEvent = event; if (responseEvent.message) { if (!responseEvent.message.id) { responseEvent.message.id = uuidv4(); } messages.push(responseEvent.message); // Add to unprocessed queues metaState.unprocessedMemoryMessages.push(responseEvent.message); metaState.unprocessedCognitionMessages.push(responseEvent.message); // Bump timers if (metamemory && taskLocalState.memory.enabled) { bumpMetaMemoryTimer(); } if (taskLocalState.cognition?.enabled) { bumpMetaCognitionTimer(); } } } } // Check for any async events that were queued during processing while (asyncEventQueue.length > 0) { const asyncEvent = asyncEventQueue.shift(); yield asyncEvent; } // After the for-await loop, check error patterns if (consecutiveSameErrors >= MAX_CONSECUTIVE_SAME_ERRORS) { console.error(`[Task] Same error repeated ${consecutiveSameErrors} times - stopping task`); isComplete = true; taskCompleted = true; // Emit task_fatal_error event const fatalEvent = { type: 'task_fatal_error', task_id: taskId, result: `Task terminated: Same error repeated ${consecutiveSameErrors} times. Error: ${lastErrorMessage}`, finalState: { ...taskLocalState, }, }; yield fatalEvent; } else if (totalErrors >= MAX_TOTAL_ERRORS) { console.error(`[Task] Too many errors (${totalErrors}/${MAX_TOTAL_ERRORS}) - stopping task`); isComplete = true; taskCompleted = true; // Emit task_fatal_error event const fatalEvent = { type: 'task_fatal_error', task_id: taskId, result: `Task terminated: Too many errors (${totalErrors}). Last error: ${currentErrorMessage}`, finalState: { ...taskLocalState, }, }; yield fatalEvent; } } // Wait for all pending async operations to complete if (pendingAsyncOps.size > 0) { console.log(`[Task] Waiting for ${pendingAsyncOps.size} async operations to complete...`); // Set a maximum wait time for all async operations const MAX_WAIT_TIME = 5 * 60 * 1000; // 5 minutes const allOpsPromise = Promise.all(Array.from(pendingAsyncOps)); const waitTimeoutPromise = new Promise((resolve) => setTimeout(() => { console.warn('[Task] Timeout waiting for async operations, continuing...'); resolve(); }, MAX_WAIT_TIME)); await Promise.race([allOpsPromise, waitTimeoutPromise]); // Yield any remaining async events while (asyncEventQueue.length > 0) { const asyncEvent = asyncEventQueue.shift(); yield asyncEvent; } } } catch (error) { console.error('[Task] Error running agent:', error); // If task_start was emitted but no completion event, emit task_fatal_error if (taskStartEmitted && !taskCompleted && !taskLocalState?.runIndefinitely) { const errorMessage = error instanceof Error ? error.message : String(error); const errorEvent = { type: 'task_fatal_error', task_id: taskId, result: `Task failed with error: ${errorMessage}`, finalState: { ...taskLocalState, }, }; yield errorEvent; taskCompleted = true; } // Yield an error event const errorMessage = error instanceof Error ? error.message : String(error); yield { type: 'error', error: new Error(`Agent execution failed: ${errorMessage}`), }; } finally { // Abort any ongoing meta processing operations metaProcessingAbortController.abort(); // Clean up all timers if (metaTimers.cognitionTimer) { clearInterval(metaTimers.cognitionTimer); } if (metaTimers.memoryDebounceTimer) { clearTimeout(metaTimers.memoryDebounceTimer); } if (metaTimers.cognitionDebounceTimer) { clearTimeout(metaTimers.cognitionDebounceTimer); } // Yield any remaining async events while (asyncEventQueue.length > 0) { const asyncEvent = asyncEventQueue.shift(); yield asyncEvent; } // Ensure task completion event is always emitted if task_start was emitted if (taskStartEmitted && !taskCompleted && !taskLocalState?.runIndefinitely) { const errorEvent = { type: 'task_fatal_error', task_id: taskId, result: 'Task ended without explicit completion', finalState: { ...taskLocalState, }, }; yield errorEvent; } } } // Create the generator const generator = taskGenerator(); // Store the messages array in the WeakMap activeTaskMessages.set(generator, messages); // Set up cleanup function that will clean up the wrapped generator let cleanupRef = null; const cleanup = () => { if (cleanupRef) { activeTaskMessages.delete(cleanupRef); activeTaskLocalStates.delete(cleanupRef); activeTaskIds.delete(cleanupRef); completedTasks.delete(cleanupRef); generatorCleanup.delete(cleanupRef); } }; // Store initial references for the generator activeTaskLocalStates.set(generator, taskLocalState || {}); activeTaskIds.set(generator, 'pending'); // Create a wrapper that ensures cleanup // We need to store a reference to the wrapped generator for the closure let wrapperRef = null; const wrappedGenerator = (async function* () { let taskIdCaptured = false; try { for await (const event of generator) { // Capture task ID from first event with task_id if (!taskIdCaptured && 'task_id' in event && event.task_id) { const taskId = event.task_id; if (wrapperRef) { activeTaskIds.set(wrapperRef, taskId); activeTaskIds.delete(generator); } taskIdCaptured = true; } // Update task local state on each event if ('finalState' in event && event.finalState) { const finalState = event.finalState; if (wrapperRef) { activeTaskLocalStates.set(wrapperRef, finalState); activeTaskLocalStates.delete(generator); } } // Mark task as completed when we see completion events if (event.type === 'task_complete' || event.type === 'task_fatal_error') { if (wrapperRef) { completedTasks.set(wrapperRef, true); } } yield event; } } finally { cleanup(); } })(); // Set the reference now that it's created wrapperRef = wrappedGenerator; cleanupRef = wrappedGenerator; // IMPORTANT: Set cleanupRef so cleanup function works // Transfer the mapping to the wrapped generator activeTaskMessages.set(wrappedGenerator, messages); activeTaskMessages.delete(generator); if (taskLocalState) { activeTaskLocalStates.set(wrappedGenerator, taskLocalState); } activeTaskLocalStates.delete(generator); generatorCleanup.set(wrappedGenerator, cleanup); generatorCleanup.delete(generator); return wrappedGenerator; } /** * Internal function to add a message to a messages array * Used by both addMessageToTask and metacognition's inject_thought */ export function internalAddMessage(messages, message, source = 'external') { // Validate the message if (!message || typeof message !== 'object') { throw new Error('Message must be a valid message object'); } if (!message.type || message.type !== 'message') { throw new Error('Message must have type "message"'); } if (!message.role || !['system', 'user', 'assistant', 'developer'].includes(message.role)) { throw new Error('Message must have a valid role: system, user, assistant, or developer'); } if (!message.content || typeof message.content !== 'string') { throw new Error('Message must have string content'); } // Add ID if not present if (!message.id) { message.id = uuidv4(); } // Add the message messages.push(message); console.log(`[Task] ${source === 'metacognition' ? 'Metacognition' : 'External'} message added with role: ${message.role}`); } /** * Add a message to an active task's message stream * * @param taskGenerator - The generator returned by runTask * @param message - The message to inject * * @example * ```typescript * const task = runTask(agent, 'Analyze this code'); * * // Inject a message while task is running * addMessageToTask(task, { * type: 'message', * role: 'developer', * content: 'Focus on performance issues' * }); * ``` */ export function addMessageToTask(taskGenerator, message) { // Validate inputs if (!taskGenerator) { throw new Error('Task generator is required'); } // Get the messages array for this task const messages = activeTaskMessages.get(taskGenerator); if (!messages) { throw new Error('Task not found or already completed. Messages can only be added to active tasks.'); } // Use the internal function internalAddMessage(messages, message, 'external'); } /** * Get the current status of an active task * * @param taskGenerator - The generator returned by runTask * @param agent - The agent to use for generating the summary * @returns Promise<TaskStatusEvent> - The status of the task * * @example * ```typescript * const task = runTask(agent, 'Analyze this code'); * * // Get status while task is running * const status = await taskStatus(task, agent); * console.log(status.data.summary); * ``` */ export async function taskStatus(taskGenerator, agent, prompt = `Please provide a 2-3 sentence summary of the progress of this task. You do not need to explain what the task is, only progress performed. Provide at least a one sentence overview of overall task history. Also at least one sentence focusing on the recent/current state.`) { // Validate inputs if (!taskGenerator) { throw new Error('Task generator is required'); } // Check if task is marked as completed if (completedTasks.get(taskGenerator)) { throw new Error('Task not found or already completed. Status can only be retrieved for active tasks.'); } // Get the messages array for this task const messages = activeTaskMessages.get(taskGenerator); if (!messages) { throw new Error('Task not found or already completed. Status can only be retrieved for active tasks.'); } // Get task ID const taskId = activeTaskIds.get(taskGenerator) || 'unknown'; // Get basic info const totalMessages = messages.length; // Get last message timestamp let lastMessageTimestamp = Date.now(); // Default to now if (messages.length > 0) { const lastMessage = messages[messages.length - 1]; // Try to get timestamp from message, or use current time lastMessageTimestamp = lastMessage.timestamp || Date.now(); } // Build context for the summary - just use recent messages const RECENT_MESSAGE_COUNT = 20; const recentMessages = messages.slice(-RECENT_MESSAGE_COUNT); // Build simple context const contextParts = []; contextParts.push(`## Recent ${Math.min(recentMessages.length, RECENT_MESSAGE_COUNT)} Messages:`); recentMessages.forEach((msg) => { const msgObj = msg; const truncatedContent = msgObj.content ? (msgObj.content.length > 200 ? msgObj.content.substring(0, 200) + '...' : msgObj.content) : ''; contextParts.push(`${msgObj.role}: ${truncatedContent}`); }); // Create a simple prompt for the summary model const summaryPrompt = `You are an expert at summarizing task progress. Your job is to provide a concise summary of the current state of the task based on the recent messages. Please review the list of messages below which have been output from a currently running task and summarize the current state. WARNING: Do not make assumptions! Summarize only what is present in the messages. Total messages: ${totalMessages} Current timestamp: ${Date.now()} Last message timestamp: ${lastMessageTimestamp} ${prompt}`; // Use ensemble to generate the summary const summaryMessages = [ { type: 'message', role: 'developer', content: summaryPrompt, }, { type: 'message', role: 'user', content: contextParts.join('\n\n'), } ]; // Clone agent and set model class for summary const summaryAgent = cloneAgent(agent); summaryAgent.modelClass = 'summary'; // Cast to any to bypass type checking for now // ensembleRequest returns an AsyncGenerator, we need to consume it const responseGenerator = ensembleRequest(summaryMessages, summaryAgent); // Extract the summary from the response let summary = 'Unable to generate summary'; try { // Consume the generator to get the response for await (const event of responseGenerator) { // When message is complete, extract the text from the message if (event.type === 'message_complete') { const completeEvent = event; // The content is directly on the event, not nested in message if (completeEvent.content) { summary = completeEvent.content; } break; // Message is complete, we can stop } } } catch (error) { console.error('[TaskStatus] Error generating summary:', error); // Keep default "Unable to generate summary" message } // Build the status event const statusEvent = { type: 'task_status', task_id: taskId, messageCount: totalMessages, currentTimestamp: Date.now(), lastMessageTimestamp: lastMessageTimestamp, summary: summary, }; return statusEvent; } //# sourceMappingURL=engine.js.map