UNPKG

@astreus-ai/astreus

Version:

AI Agent Framework with Chat Management

1,016 lines (891 loc) 38.4 kB
import { v4 as uuidv4 } from "uuid"; import { TaskConfig, TaskInstance, TaskStatus, TaskResult, } from "../types/task"; import { Plugin } from "../types/plugin"; import { PluginManager } from "../plugin"; import { createDatabase, DatabaseInstance } from "../database"; import { MemoryInstance, MemoryEntry, ProviderModel, ProviderMessage, CompletionOptions } from "../types"; import { logger } from "../utils"; import { IntentRecognizer } from "../utils/intent"; /** * Task class implementing the TaskInstance interface */ export class Task implements TaskInstance { public id: string; public config: TaskConfig; public status: TaskStatus; public result?: TaskResult; public retries: number; public plugins: Plugin[]; public createdAt: Date; public startedAt?: Date; public completedAt?: Date; public agentId?: string; public sessionId?: string; public contextId?: string; public memory?: MemoryInstance; public database?: DatabaseInstance; public isCancelled: boolean = false; // Add a tracking variable to prevent duplicate logging private toolSelectionLogged: boolean = false; constructor(config: TaskConfig, memory?: MemoryInstance, model?: ProviderModel, database?: DatabaseInstance) { this.id = config.id || uuidv4(); this.config = { ...config, model: config.model || model // Use config.model if provided, otherwise use the model parameter }; this.status = "pending"; this.retries = 0; this.plugins = []; this.createdAt = new Date(); this.agentId = config.agentId; this.sessionId = config.sessionId; this.memory = memory; this.database = database; // Set context ID based on session ID if (this.sessionId) { this.contextId = this.sessionId; } // Load the plugins required for this task (but don't wait in constructor) this.loadPlugins(this.config.model).catch(error => { logger.error(`Error loading plugins for task ${this.id}:`, error); }); } /** * Initialize plugins for the task * This tries three methods in order: * 1. Use LLM-selected tools if model is provided * 2. Use keyword matching based on task name * 3. Fall back to all agent plugins */ public async loadPlugins(model?: ProviderModel): Promise<void> { // Get all available tools const allTools = PluginManager.getAll(); // Method 1: Get plugins from config first (explicitly provided in task config) if (this.config.plugins && this.config.plugins.length > 0) { const configPlugins = this.config.plugins .map(pluginName => { // Check if it's a tool instance already if (typeof pluginName === 'object' && 'name' in pluginName) { return pluginName as Plugin; } // Otherwise look it up by name return PluginManager.get(pluginName as string); }) .filter(Boolean) as Plugin[]; if (configPlugins.length > 0) { if (!this.toolSelectionLogged) { logger.task(this.id, `Using ${configPlugins.length} plugins from task config`, this.config.name); this.toolSelectionLogged = true; } this.plugins = configPlugins; return; } } // Method 2: Use LLM to select appropriate tools if model is provided if (model) { try { logger.task(this.id, "Using LLM to select tools for task", this.config.name); // Request tools with IntentRecognizer const selectedTools = await IntentRecognizer.recognizeIntent( this.config.name, this.config.description || this.config.name, allTools, model ); if (selectedTools.length > 0) { if (!this.toolSelectionLogged) { logger.task(this.id, 'LLM selected these tools for the task', selectedTools.map(t => t.name).join(', ')); this.toolSelectionLogged = true; } this.plugins = selectedTools; // Save plugins to config to avoid duplicate selection this.config.plugins = selectedTools.map(t => t.name); return; } else { logger.warn(`LLM did not select any tools for task "${this.config.name}", falling back to default`); } } catch (error) { logger.error(`Error using LLM for tool selection: ${error}`); // Fall back to default method } } logger.warn(`No plugins selected for task "${this.config.name}"`); } /** * Execute the task using configured plugins */ async execute(input?: any): Promise<TaskResult> { // Database operations are handled in createTask static method // Add debug logging - Task execution start logger.debug(`Task ${this.id} (${this.config.name}) execution started with input:`, JSON.stringify(input || this.config.input)); // Record task execution start in memory await this.addTaskMemoryEntry(`Task execution started: ${this.config.name}`, "task_event"); // Use provided input or fallback to config input const taskInput: Record<string, unknown> = input || this.config.input; try { // Make sure plugins are loaded before starting the task if (!this.plugins || this.plugins.length === 0) { await this.loadPlugins(this.config.model); } // Skip execution if task is cancelled if (this.isCancelled) { throw new Error("Task was cancelled"); } // Update status only after plugins are loaded this.status = "running"; this.startedAt = new Date(); // Get task context from previous tasks in the same session let taskContext = {}; if (this.sessionId) { taskContext = await this.getTaskContext(this.sessionId); } // Merge task input with the accumulated context const enrichedInput: Record<string, unknown> = { ...taskInput, _context: taskContext, }; // Log the enriched input at debug level logger.debug(`Task ${this.id} enriched input:`, JSON.stringify(enrichedInput)); // Execute plugins with the input let currentOutput: Record<string, unknown> = enrichedInput; // Ensure plugins array is valid before proceeding if (!this.plugins || !Array.isArray(this.plugins)) { this.plugins = []; } logger.debug(`Task ${this.id} has ${this.plugins.length} plugins to execute`); // Record available plugins in memory if (this.plugins.length > 0) { await this.addTaskMemoryEntry( `Available plugins: ${this.plugins.map(p => p.name).join(", ")}`, "task_event" ); } // Use the plugins as they are let prioritizedPlugins = [...this.plugins]; // Check if we have plugins to execute if (prioritizedPlugins.length === 0) { logger.warn(`Task ${this.id} has no plugins to execute, the task may not complete as expected`); } // Use model to execute the task with the plugins if available const model = this.config.model; if (model && prioritizedPlugins.length > 0) { logger.debug(`Task ${this.id} using model ${model.name} to execute with plugins`); // Record model usage in memory await this.addTaskMemoryEntry( `Using model ${model.name} to execute task with plugins`, "task_event" ); // Format tools for the model - convert Astreus tool format to provider tool format const formattedTools = prioritizedPlugins.map(tool => { // Convert parameters to proper format for provider let formattedParameters = {}; if (tool.parameters && Array.isArray(tool.parameters)) { // Convert array of parameter objects to JSON Schema properties const properties: Record<string, any> = {}; const required: string[] = []; tool.parameters.forEach(param => { if (param.name && param.type) { properties[param.name] = { type: param.type, description: param.description || `Parameter ${param.name}` }; if (param.required) { required.push(param.name); } } }); // Create proper JSON Schema formattedParameters = { type: "object", properties: properties }; // Add required array if there are required parameters if (required.length > 0) { formattedParameters = { ...formattedParameters, required: required }; } } else if (tool.parameters) { // Use parameters as-is if not an array formattedParameters = tool.parameters; } return { name: tool.name, description: tool.description || "", parameters: formattedParameters }; }); // Create messages for the model const messages: ProviderMessage[] = [ { role: "user", content: `Complete this task: ${this.config.name} ${this.config.description ? `Description: ${this.config.description}` : ''} Input: ${JSON.stringify(enrichedInput)}` } ]; // Let the model generate a response using the tools try { // Create the system message const systemMessage = `You are an AI assistant that can use tools to complete tasks. Complete this task: ${this.config.name} ${this.config.description ? `Description: ${this.config.description}` : ''} Available tools: ${prioritizedPlugins.map(tool => `- ${tool.name}: ${tool.description || 'No description provided'}`).join('\n')} Use the available tools to fulfill this task effectively. When a tool should be used, call it with appropriate parameters.`; // Log formatted tools for debugging logger.debug(`Task ${this.id} using ${formattedTools.length} tools with model ${model.name}`, { toolNames: formattedTools.map(t => t.name).join(', ') }); // If the tools have parameters with improper formats, log them for debugging const toolsWithPotentialIssues = formattedTools.filter(tool => { const params = tool.parameters; return !params || (typeof params === 'object' && Object.keys(params).length === 0); }); if (toolsWithPotentialIssues.length > 0) { logger.warn(`Task ${this.id} has tools with potential parameter format issues:`, toolsWithPotentialIssues.map(t => t.name).join(', ')); } // Call the model with messages and completion options const completionOptions: CompletionOptions = { tools: formattedTools, toolCalling: true, systemMessage: systemMessage }; logger.debug(`Task ${this.id} calling model with ${messages.length} messages and ${formattedTools.length} tools`); const completion = await model.complete(messages, completionOptions); // Check if completion is a structured object with tool calls if (typeof completion === 'object' && completion.tool_calls && Array.isArray(completion.tool_calls)) { logger.debug(`Task ${this.id} received structured tool calls response`); const toolCalls = completion.tool_calls; logger.info(`Task ${this.id} response includes ${toolCalls.length} tool calls:`); // Log tool calls and execute them const toolResults = []; for (let i = 0; i < toolCalls.length; i++) { const call = toolCalls[i]; if (call.type === 'function' && call.name) { logger.info(`Tool call ${i + 1}: ${call.name} with arguments: ${JSON.stringify(call.arguments)}`); // Record tool call in memory await this.addTaskMemoryEntry( `Tool call: ${call.name} with arguments: ${JSON.stringify(call.arguments)}`, "task_tool" ); // Find the tool to execute const tool = prioritizedPlugins.find(t => t.name === call.name); if (tool && typeof tool.execute === 'function') { try { logger.info(`Executing tool ${call.name}...`); const result = await tool.execute({ ...enrichedInput, ...call.arguments }); toolResults.push({ name: call.name, arguments: call.arguments, result }); logger.info(`Tool ${call.name} executed successfully`); // Record successful tool execution in memory await this.addTaskMemoryEntry( `Tool ${call.name} executed successfully`, "task_result", { toolName: call.name, success: true } ); } catch (error) { logger.error(`Error executing tool ${call.name}:`, error); toolResults.push({ name: call.name, arguments: call.arguments, error: error instanceof Error ? error.message : `Error: ${error}` }); // Record tool execution error in memory await this.addTaskMemoryEntry( `Tool ${call.name} execution failed: ${error instanceof Error ? error.message : error}`, "task_result", { toolName: call.name, success: false } ); } } else { logger.warn(`Tool ${call.name} not found or has no execute method`); toolResults.push({ name: call.name, arguments: call.arguments, error: `Tool ${call.name} not found or has no execute method` }); // Record tool not found in memory await this.addTaskMemoryEntry( `Tool ${call.name} not found or has no execute method`, "task_result", { toolName: call.name, success: false } ); } } } // Generate a response based on tool results if (toolResults.length > 0) { try { logger.info(`Generating response based on ${toolResults.length} tool results`); const resultsMessage: ProviderMessage = { role: "system", content: `The following tools were called based on the user's request: ${toolResults.map(tr => `Tool: ${tr.name} Arguments: ${JSON.stringify(tr.arguments)} Result: ${tr.error ? 'ERROR: ' + tr.error : JSON.stringify(tr.result)}`).join('\n\n')} Please analyze these results and generate a helpful, coherent response to the user that summarizes what was done and the outcome. Do not mention the technical details of the tool calls - just provide a natural, conversational response about what was accomplished.` }; // Call the model again with the results const summaryResponse = await model.complete([ ...messages, resultsMessage ]); // Use the summary response as content const summaryContent = typeof summaryResponse === 'string' ? summaryResponse : summaryResponse.content; // Add both the summary and the raw results currentOutput = { ...enrichedInput, content: summaryContent, summary: summaryContent, tool_calls: toolCalls, tool_results: toolResults, tools_used: prioritizedPlugins.map(t => t.name) }; } catch (error) { logger.error(`Error generating summary from tool results:`, error); // Fall back to standard output without summary currentOutput = { ...enrichedInput, content: completion.content || '', tool_calls: toolCalls, tool_results: toolResults, tools_used: prioritizedPlugins.map(t => t.name) }; } } else { // No tools were successfully executed currentOutput = { ...enrichedInput, content: completion.content || '', tool_calls: toolCalls, tool_results: toolResults, tools_used: prioritizedPlugins.map(t => t.name) }; } } // Handle text-based tool calls (for older models) else if (typeof completion === 'string' && completion.includes('Tool Call:')) { logger.debug(`Task ${this.id} model response received, length: ${completion.length} chars (text format)`); // Extract and log tool calls for better debugging const toolCallMatches = completion.match(/Tool Call: ([^\n]+)[\s\S]*?Arguments: ({[\s\S]*?})(?=\n\n|\n?$)/g); if (toolCallMatches && toolCallMatches.length > 0) { logger.info(`Task ${this.id} response includes ${toolCallMatches.length} tool calls (text format):`); // Execute each tool call const toolResults = []; const parsedCalls = []; for (let i = 0; i < toolCallMatches.length; i++) { const match = toolCallMatches[i]; const toolName = match.match(/Tool Call: ([^\n]+)/)?.[1]; const argsMatch = match.match(/Arguments: (.*?)(?=\n\n|\n?$)/s); const argsString = argsMatch ? argsMatch[1] : '{}'; logger.info(`Tool call ${i + 1}: ${toolName} with arguments: ${argsString}`); if (toolName) { // Parse arguments let args = {}; try { args = JSON.parse(argsString); } catch (err) { logger.warn(`Failed to parse arguments for tool ${toolName}: ${err}`); } parsedCalls.push({ name: toolName, arguments: args }); // Find and execute the tool const tool = prioritizedPlugins.find(t => t.name === toolName); if (tool && typeof tool.execute === 'function') { try { logger.info(`Executing tool ${toolName}...`); const result = await tool.execute({ ...enrichedInput, ...args }); toolResults.push({ name: toolName, arguments: args, result }); logger.info(`Tool ${toolName} executed successfully`); } catch (error) { logger.error(`Error executing tool ${toolName}:`, error); toolResults.push({ name: toolName, arguments: args, error: error instanceof Error ? error.message : `Error: ${error}` }); } } else { logger.warn(`Tool ${toolName} not found or has no execute method`); toolResults.push({ name: toolName, arguments: args, error: `Tool ${toolName} not found or has no execute method` }); } } } // Generate a response based on tool results if (toolResults.length > 0) { try { logger.info(`Generating response based on ${toolResults.length} tool results`); const resultsMessage: ProviderMessage = { role: "system", content: `The following tools were called based on the user's request: ${toolResults.map(tr => `Tool: ${tr.name} Arguments: ${JSON.stringify(tr.arguments)} Result: ${tr.error ? 'ERROR: ' + tr.error : JSON.stringify(tr.result)}`).join('\n\n')} Please analyze these results and generate a helpful, coherent response to the user that summarizes what was done and the outcome. Do not mention the technical details of the tool calls - just provide a natural, conversational response about what was accomplished.` }; // Call the model again with the results const summaryResponse = await model.complete([ ...messages, resultsMessage ]); // Use the summary response const summaryContent = typeof summaryResponse === 'string' ? summaryResponse : summaryResponse.content; // Include both the summary and the raw results currentOutput = { ...enrichedInput, content: summaryContent, summary: summaryContent, result: completion, tool_calls: parsedCalls, tool_results: toolResults, tools_used: prioritizedPlugins.map(t => t.name) }; } catch (error) { logger.error(`Error generating summary from tool results:`, error); // Fall back to standard output without summary currentOutput = { ...enrichedInput, result: completion, tool_calls: parsedCalls, tool_results: toolResults, tools_used: prioritizedPlugins.map(t => t.name) }; } } else { // No tools were successfully executed currentOutput = { ...enrichedInput, result: completion, tool_calls: parsedCalls, tool_results: toolResults, tools_used: prioritizedPlugins.map(t => t.name) }; } } else { logger.info(`Task ${this.id} response appears to include tool calls but format could not be parsed`); // Just pass through the response currentOutput = { ...enrichedInput, result: completion, tools_used: prioritizedPlugins.map(t => t.name) }; } } else { // No tool calls detected logger.debug(`Task ${this.id} model response received, length: ${typeof completion === 'string' ? completion.length : JSON.stringify(completion).length} chars`); logger.warn(`Task ${this.id} response does not include tool calls, the model may not be using the tools properly`); // Use the response as-is currentOutput = { ...enrichedInput, result: completion, tools_used: prioritizedPlugins.map(t => t.name) }; } logger.debug(`Model execution completed for task ${this.id}`); } catch (error) { logger.error(`Error executing task with model: ${error}`); throw error; } } else { // No model or no plugins, execute plugins sequentially logger.debug(`Task ${this.id} using sequential plugin execution (${prioritizedPlugins.length} plugins)`); let pluginOutput: unknown = null; // Execute each plugin in sequence for (const plugin of prioritizedPlugins) { if (this.isCancelled) { throw new Error("Task was cancelled during execution"); } if (!plugin) { logger.warn("Encountered null or undefined plugin, skipping"); continue; } if (plugin.execute) { const pluginName = plugin.name || "unnamed_plugin"; logger.debug(`Executing plugin '${pluginName}' with input:`, JSON.stringify(currentOutput)); try { // Execute the plugin with the current input/output chain pluginOutput = await plugin.execute(currentOutput); logger.debug(`Plugin '${pluginName}' returned:`, JSON.stringify(pluginOutput)); // Update currentOutput with plugin's output, not the entire input if (pluginOutput !== undefined && pluginOutput !== null) { // If plugin returns a value, use it as the new current output currentOutput = pluginOutput as Record<string, unknown>; logger.debug(`Updated currentOutput with plugin result:`, JSON.stringify(currentOutput)); } else { logger.debug(`Plugin returned undefined/null, keeping previous output`); } // If plugin returns undefined/null, keep the previous output } catch (error) { logger.error(`Error executing plugin '${pluginName}':`, error); throw error; } } else { const pluginName = plugin.name || "unnamed_plugin"; logger.warn( `Plugin '${pluginName}' does not have an execute method` ); } } } // The final output is the result from execution const finalOutput = currentOutput; // Only show the final output at debug level logger.debug(`Task ${this.id} final output:`, JSON.stringify(finalOutput)); // Strip _context from the output if it got added by the enriched input if (finalOutput && typeof finalOutput === 'object' && '_context' in finalOutput) { const { _context, ...strippedOutput } = finalOutput; // Use _context to prevent unused variable warning logger.debug(`Context data size: ${JSON.stringify(_context).length} bytes`); // Only replace if there are other properties beyond _context if (Object.keys(strippedOutput).length > 0) { currentOutput = strippedOutput as Record<string, unknown>; logger.debug(`Stripped _context, new output:`, JSON.stringify(currentOutput)); } else { logger.debug(`After stripping _context, no other properties found, keeping original`); } } // Update the context with this task's output if (this.sessionId) { taskContext = { ...taskContext, [this.config.name]: currentOutput, lastTaskOutput: currentOutput, }; // Store updated context in memory system await this.saveTaskContext(this.sessionId, taskContext); } // Set result and update status this.result = { success: true, output: currentOutput, context: taskContext, }; logger.debug(`Task ${this.id} completed successfully`); this.completedAt = new Date(); if (this.result.success) { this.status = "completed"; logger.task(this.id, `Task ${this.status}`, this.config.name); // Record task completion in memory await this.addTaskMemoryEntry( `Task completed successfully: ${this.config.name}`, "task_event", { output: JSON.stringify(currentOutput).slice(0, 500) } // Limit size of stored output ); } else { this.status = "failed"; logger.task(this.id, `Task ${this.status}`, this.config.name); // Record task failure in memory await this.addTaskMemoryEntry( `Task failed: ${this.config.name}`, "task_event" ); } } catch (error) { // Handle error this.result = { success: false, error: error as Error, }; logger.error(`Task ${this.id} failed with error:`, error); // Record task error in memory await this.addTaskMemoryEntry( `Task error: ${error instanceof Error ? error.message : error}`, "task_event", { errorType: error instanceof Error ? error.name : 'Unknown' } ); // Retry if max retries not exceeded if (this.retries < (this.config.maxRetries || 0)) { this.retries++; this.status = "pending"; logger.info( `Retrying task '${this.id}', attempt ${this.retries}/${this.config.maxRetries}` ); // Record retry in memory await this.addTaskMemoryEntry( `Retrying task (attempt ${this.retries}/${this.config.maxRetries})`, "task_event" ); // Database state is managed by createTask static method return this.execute(taskInput); } else { this.status = "failed"; this.completedAt = new Date(); } } return this.result as TaskResult; } /** * Get task context from memory using session ID */ public async getTaskContext(sessionId: string): Promise<Record<string, unknown>> { try { // Use the memory system if available if (this.memory) { // Get the task context memories from session const memories = await this.memory.getBySession(sessionId); // Find the most recent task context memory const contextMemories = memories.filter( (m) => m.role === "task_context" ); if (contextMemories.length > 0) { // Sort by timestamp descending and get most recent contextMemories.sort( (a, b) => { // Ensure timestamps are Date objects or convert them const aTime = a.timestamp instanceof Date ? a.timestamp.getTime() : new Date(a.timestamp).getTime(); const bTime = b.timestamp instanceof Date ? b.timestamp.getTime() : new Date(b.timestamp).getTime(); return bTime - aTime; } ); // Extract the context data from memory content try { return JSON.parse(contextMemories[0].content); } catch (parseError) { logger.error( `Error parsing task context data for session ${sessionId}:`, parseError ); return {}; } } } return {}; } catch (error) { logger.error( `Error retrieving task context for session ${sessionId}:`, error ); return {}; } } /** * Save task context to memory system */ public async saveTaskContext( sessionId: string, contextData: Record<string, unknown> ): Promise<void> { try { // Only save if memory system is available if (this.memory) { // Serialize the context data const serializedData = JSON.stringify(contextData); // Store in memory with role=task_context await this.memory.add({ agentId: this.agentId || "system", sessionId: sessionId, userId: "", role: "task_context", content: serializedData, metadata: { taskId: this.id, taskName: this.config.name, contextType: "task_execution_context", }, }); } else { logger.warn( `Memory system not available for task ${this.id}, skipping context save` ); } } catch (error) { logger.error( `Error saving task context for session ${sessionId}:`, error ); } } /** * Cancel the task execution */ cancel(): void { this.isCancelled = true; if (this.status === "pending" || this.status === "running") { this.status = "failed"; this.result = { success: false, error: new Error("Task was cancelled"), }; } } /** * Set the memory instance for this task */ setMemory(memory: MemoryInstance): void { this.memory = memory; } /** * Get memory entries specific to this task * @returns Promise that resolves to an array of task-specific memory entries */ public async getTaskMemory(): Promise<MemoryEntry[]> { if (!this.memory || !this.sessionId) { return []; } try { // Get all memories for this session const memories = await this.memory.getBySession(this.sessionId); // Filter for memories related to this task return memories.filter(memory => memory.metadata && typeof memory.metadata === 'object' && 'taskId' in memory.metadata && memory.metadata.taskId === this.id ); } catch (error) { logger.error(`Error getting task memory for task ${this.id}:`, error); return []; } } /** * Add a memory entry for this task * @param content Content of the memory entry * @param role Role for the memory entry (task_event, task_tool, task_result) * @param additionalMetadata Additional metadata to include * @returns Promise that resolves when the memory is added */ public async addTaskMemoryEntry( content: string, role: "task_event" | "task_tool" | "task_result" = "task_event", additionalMetadata: Record<string, unknown> = {} ): Promise<void> { if (!this.memory || !this.sessionId) { return; } try { await this.memory.add({ agentId: this.agentId || "system", sessionId: this.sessionId, userId: "", role: role, content: content, metadata: { taskId: this.id, taskName: this.config.name, taskStatus: this.status, ...additionalMetadata } }); logger.debug(`Added task memory entry for task ${this.id}: ${role}`); } catch (error) { logger.error(`Error adding task memory for task ${this.id}:`, error); } } /** * Create a new task asynchronously */ static async createTask( config: TaskConfig, memory?: MemoryInstance, model?: ProviderModel, database?: DatabaseInstance ): Promise<TaskInstance> { // Create a new task instance const task = new Task(config, memory, model, database); // Save task to database if database is available if (task.database) { try { // Ensure tasks table exists await task.database.ensureTable('tasks', (table) => { table.string("id").primary(); table.string("name").notNullable(); table.text("description").notNullable(); table.string("status").notNullable().index(); table.integer("retries").defaultTo(0); table.json("plugins").nullable(); table.json("input").nullable(); table.json("dependencies").nullable(); table.json("result").nullable(); table.timestamp("createdAt").defaultTo(task.database!.knex.fn.now()); table.timestamp("startedAt").nullable(); table.timestamp("completedAt").nullable(); table.string("agentId").nullable().index(); table.string("sessionId").nullable().index(); table.string("contextId").nullable().index(); }); // Use default tasks table name - can be customized via database config if needed const tasksTable = task.database.getTable('tasks'); // Prepare task data for database const taskData = { id: task.id, name: task.config.name, description: task.config.description, status: task.status, retries: task.retries, plugins: JSON.stringify(task.config.plugins || []), input: task.config.input ? JSON.stringify(task.config.input) : null, dependencies: task.config.dependencies ? JSON.stringify(task.config.dependencies) : null, result: task.result ? JSON.stringify(task.result) : null, createdAt: task.createdAt, startedAt: task.startedAt || null, completedAt: task.completedAt || null, agentId: task.agentId || null, sessionId: task.sessionId || null, contextId: task.contextId || null, }; // Check if task already exists const existingTask = await tasksTable.findOne({ id: task.id }); if (existingTask) { // Update existing task await tasksTable.update({ id: task.id }, taskData); logger.debug(`Task ${task.id}: Updated in database`); } else { // Insert new task await tasksTable.insert(taskData); logger.debug(`Task ${task.id}: Saved to database`); } // Task state is now stored in database } catch (error) { logger.error(`Task ${task.id}: Error saving to database:`, error); // Don't throw error to avoid breaking task creation } } return task; } /** * Create a new task synchronously */ static createTaskSync( config: TaskConfig, memory?: MemoryInstance, model?: ProviderModel, database?: DatabaseInstance ): TaskInstance { // Create and return a new task instance without waiting for save return new Task(config, memory, model, database); } }