Class: LanguageOperator::Agent::TaskExecutor
- Inherits:
-
Object
- Object
- LanguageOperator::Agent::TaskExecutor
- Includes:
- Instrumentation, Instrumentation::TaskTracer, Loggable
- Defined in:
- lib/language_operator/agent/task_executor.rb
Overview
Task Executor for DSL v1 organic functions
Executes both neural (LLM-based) and symbolic (code-based) tasks.
Provides the execute_task method that MainDefinition blocks use
to invoke tasks transparently regardless of implementation type.
Constant Summary collapse
- RETRYABLE_ERRORS =
Error types that should be retried
[ Timeout::Error, Errno::ECONNREFUSED, Errno::ECONNRESET, Errno::ETIMEDOUT, SocketError ].freeze
- ERROR_CATEGORIES =
Error categories for logging and operator integration
{ validation: 'VALIDATION', execution: 'EXECUTION', timeout: 'TIMEOUT', network: 'NETWORK', system: 'SYSTEM' }.freeze
Constants included from Instrumentation::TaskTracer
Instrumentation::TaskTracer::MAX_CAPTURED_LENGTH
Instance Attribute Summary collapse
-
#agent ⇒ Object
readonly
Returns the value of attribute agent.
-
#config ⇒ Object
readonly
Returns the value of attribute config.
-
#tasks ⇒ Object
readonly
Returns the value of attribute tasks.
Instance Method Summary collapse
-
#execute_llm(prompt) ⇒ String
Helper method for symbolic tasks to call LLM directly.
-
#execute_neural(task, inputs) ⇒ Hash
Execute a neural task (instructions-based, LLM-driven).
-
#execute_parallel(tasks, in_threads: 4) ⇒ Array
Execute multiple tasks in parallel.
-
#execute_task(task_name, inputs: {}, timeout: nil, max_retries: nil) ⇒ Hash
Execute a task by name with given inputs.
-
#execute_tool(tool_name, params = {}) ⇒ Object
Helper method for symbolic tasks to execute tools.
-
#initialize(agent, tasks = {}, config = {}) ⇒ TaskExecutor
constructor
Initialize the task executor.
Methods included from Loggable
Constructor Details
#initialize(agent, tasks = {}, config = {}) ⇒ TaskExecutor
Initialize the task executor
76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 |
# File 'lib/language_operator/agent/task_executor.rb', line 76 def initialize(agent, tasks = {}, config = {}) @agent = agent @tasks = tasks @config = default_config.merge(config) # Pre-cache task lookup and timeout information for performance @task_cache = build_task_cache @task_timeouts = build_timeout_cache logger.debug('TaskExecutor initialized', task_count: @tasks.size, timeout_symbolic: @config[:timeout_symbolic], timeout_neural: @config[:timeout_neural], timeout_hybrid: @config[:timeout_hybrid], max_retries: @config[:max_retries]) end |
Instance Attribute Details
#agent ⇒ Object (readonly)
Returns the value of attribute agent.
69 70 71 |
# File 'lib/language_operator/agent/task_executor.rb', line 69 def agent @agent end |
#config ⇒ Object (readonly)
Returns the value of attribute config.
69 70 71 |
# File 'lib/language_operator/agent/task_executor.rb', line 69 def config @config end |
#tasks ⇒ Object (readonly)
Returns the value of attribute tasks.
69 70 71 |
# File 'lib/language_operator/agent/task_executor.rb', line 69 def tasks @tasks end |
Instance Method Details
#execute_llm(prompt) ⇒ String
Helper method for symbolic tasks to call LLM directly
319 320 321 322 |
# File 'lib/language_operator/agent/task_executor.rb', line 319 def execute_llm(prompt) response = @agent.(prompt) response.is_a?(String) ? response : response.content end |
#execute_neural(task, inputs) ⇒ Hash
Execute a neural task (instructions-based, LLM-driven)
168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 |
# File 'lib/language_operator/agent/task_executor.rb', line 168 def execute_neural(task, inputs) # Validate inputs first validated_inputs = task.validate_inputs(inputs) logger.debug('Executing neural task', task: task.name, instructions: task.instructions_text, inputs: validated_inputs) # Build prompt for LLM prompt = build_neural_prompt(task, validated_inputs) logger.info('Sending prompt to LLM', task: task.name, prompt_length: prompt.length, available_tools: @agent.respond_to?(:tools) ? @agent.tools.map(&:name) : 'N/A') # Execute LLM call within traced span outputs = tracer.in_span('gen_ai.chat', attributes: neural_task_attributes(task, prompt, validated_inputs)) do |span| # Call LLM with full tool access logger.debug('Calling LLM with prompt', task: task.name, prompt_preview: prompt[0..200]) response = @agent.(prompt) # Check for tool calls and log details has_tool_calls = response.respond_to?(:tool_calls) && response.tool_calls&.any? tool_call_count = has_tool_calls ? response.tool_calls.length : 0 logger.info('LLM response received, extracting content', task: task.name, response_class: response.class.name, has_tool_calls: has_tool_calls, tool_call_count: tool_call_count) response_text = response.is_a?(String) ? response : response.content logger.info('Neural task response received', task: task.name, response_length: response_text.length) # Record token usage and response metadata record_token_usage(response, span) # Record tool calls if available record_tool_calls(response, span) logger.info('Parsing neural task response', task: task.name) # Parse response within child span with retry logic parsed_outputs = tracer.in_span('task_executor.parse_response') do |parse_span| (response_text, parse_span) begin parse_neural_response(response_text, task) rescue RuntimeError => e # If parsing fails and this is a JSON parsing error, try one more time with clarified prompt raise e unless e..include?('returned invalid JSON') && !@parsing_retry_attempted @parsing_retry_attempted = true logger.warn('JSON parsing failed, retrying with clarified prompt', task: task.name, original_error: e., response_preview: response_text[0..300]) # Build retry prompt with clearer instructions retry_prompt = build_parsing_retry_prompt(task, validated_inputs, response_text, e.) logger.info('Retrying LLM call with clarified prompt', task: task.name, retry_prompt_length: retry_prompt.length) # Retry LLM call retry_response = @agent.(retry_prompt) retry_response_text = retry_response.is_a?(String) ? retry_response : retry_response.content logger.info('Parsing retry response', task: task.name, retry_response_length: retry_response_text.length) # Try parsing the retry response parse_neural_response(retry_response_text, task) end end logger.info('Response parsed successfully', task: task.name, output_keys: parsed_outputs.keys) # Record output metadata (parsed_outputs, span) parsed_outputs end logger.info('Validating task outputs', task: task.name) # Validate outputs against schema task.validate_outputs(outputs) end |
#execute_parallel(tasks, in_threads: 4) ⇒ Array
Execute multiple tasks in parallel
Provides explicit parallelism for task execution. Users specify which tasks should run in parallel, and this method handles the concurrent execution.
347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 |
# File 'lib/language_operator/agent/task_executor.rb', line 347 def execute_parallel(tasks, in_threads: 4) require 'parallel' # Capture current OpenTelemetry context before parallel execution current_context = OpenTelemetry::Context.current logger.info('Executing tasks in parallel', count: tasks.size, threads: in_threads) results = Parallel.map(tasks, in_threads: in_threads) do |task_spec| # Restore OpenTelemetry context in worker thread OpenTelemetry::Context.with_current(current_context) do task_name = task_spec[:name] task_inputs = task_spec[:inputs] || {} execute_task(task_name, inputs: task_inputs) end end logger.info('Parallel execution complete', results_count: results.size) results rescue Parallel::DeadWorker => e logger.error('Parallel execution failed - worker died', error: e.) raise "Parallel task execution failed: #{e.}" rescue StandardError => e logger.error('Parallel execution failed', error: e.class.name, message: e.) raise end |
#execute_task(task_name, inputs: {}, timeout: nil, max_retries: nil) ⇒ Hash
Execute a task by name with given inputs
This is the main entry point called from MainDefinition blocks. Routes to neural or symbolic execution based on task implementation. Includes timeout, retry logic, and comprehensive error handling.
106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 |
# File 'lib/language_operator/agent/task_executor.rb', line 106 def execute_task(task_name, inputs: {}, timeout: nil, max_retries: nil) execution_start = Time.now max_retries ||= @config[:max_retries] # Reset JSON parsing retry flag for this task @parsing_retry_attempted = false with_span('task_executor.execute_task', attributes: build_task_execution_attributes(task_name, inputs, max_retries)) do # Fast task lookup using pre-built cache task_name_sym = task_name.to_sym task_info = @task_cache[task_name_sym] raise ArgumentError, "Task not found: #{task_name}. Available tasks: #{@tasks.keys.join(', ')}" unless task_info task = task_info[:definition] task_type = task_info[:type] # Use cached timeout if not explicitly provided timeout ||= @task_timeouts[task_name_sym] # Optimize logging - only log if debug level enabled or log_executions is true if logger.logger.level <= ::Logger::DEBUG || @config[:log_executions] logger.info('Executing task', task: task_name, type: task_type, timeout: timeout, max_retries: max_retries, inputs: summarize_values(inputs)) end # Add timeout to span attributes after it's determined OpenTelemetry::Trace.current_span&.set_attribute('task.timeout', timeout) # Execute with retry logic result = execute_with_retry(task, task_name, inputs, timeout, max_retries, execution_start) # Add task outputs to span for learning system (if enabled) current_span = OpenTelemetry::Trace.current_span current_span&.set_attribute('task.outputs', result.to_json) if current_span && capture_enabled?(:outputs) result end rescue ArgumentError => e # Validation errors should not be retried - re-raise immediately log_task_error(task_name, e, :validation, execution_start) raise TaskValidationError.new(task_name, e., e) rescue TaskValidationError => e # TaskValidationError from validate_inputs should be logged as :validation log_task_error(task_name, e, :validation, execution_start) raise e rescue StandardError => e # Catch any unexpected errors that escaped retry logic log_task_error(task_name, e, :system, execution_start) raise create_appropriate_error(task_name, e) end |
#execute_tool(tool_name, params = {}) ⇒ Object
Helper method for symbolic tasks to execute tools
Executes an MCP tool directly through the agent's MCP clients.
277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 |
# File 'lib/language_operator/agent/task_executor.rb', line 277 def execute_tool(tool_name, params = {}) tool_name_str = tool_name.to_s logger.info('Tool call initiated by symbolic task', tool: tool_name_str, params: summarize_values(params)) # Find the tool across all MCP clients tool = @agent.tools.find { |t| t.name == tool_name_str } raise ArgumentError, "Tool '#{tool_name_str}' not found" unless tool # Execute the tool (it's a Proc/lambda wrapped by RubyLLM) result = tool.call(**params) # Extract text from MCP Content objects text_result = if result.is_a?(RubyLLM::MCP::Content) result.text elsif result.respond_to?(:map) && result.first.is_a?(RubyLLM::MCP::Content) result.map(&:text).join else result end logger.debug('Tool call completed', tool: tool_name_str, result_preview: text_result.is_a?(String) ? text_result[0..200] : text_result.class.name) # Try to parse JSON response if it looks like JSON if text_result.is_a?(String) && (text_result.strip.start_with?('{') || text_result.strip.start_with?('[')) JSON.parse(text_result, symbolize_names: true) else text_result end rescue JSON::ParserError # Not JSON, return as-is text_result end |