language-operator 0.1.61 → 0.1.63

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/commands/persona.md +9 -0
  3. data/.claude/commands/task.md +46 -1
  4. data/.rubocop.yml +13 -0
  5. data/.rubocop_custom/use_ux_helper.rb +44 -0
  6. data/CHANGELOG.md +8 -0
  7. data/Gemfile.lock +12 -1
  8. data/Makefile +26 -7
  9. data/Makefile.common +50 -0
  10. data/bin/aictl +8 -1
  11. data/components/agent/Gemfile +1 -1
  12. data/components/agent/bin/langop-agent +7 -0
  13. data/docs/README.md +58 -0
  14. data/docs/{dsl/best-practices.md → best-practices.md} +4 -4
  15. data/docs/cli-reference.md +274 -0
  16. data/docs/{dsl/constraints.md → constraints.md} +5 -5
  17. data/docs/how-agents-work.md +156 -0
  18. data/docs/installation.md +218 -0
  19. data/docs/quickstart.md +299 -0
  20. data/docs/understanding-generated-code.md +265 -0
  21. data/docs/using-tools.md +457 -0
  22. data/docs/webhooks.md +509 -0
  23. data/examples/ux_helpers_demo.rb +296 -0
  24. data/lib/language_operator/agent/base.rb +11 -1
  25. data/lib/language_operator/agent/executor.rb +23 -6
  26. data/lib/language_operator/agent/safety/safe_executor.rb +41 -39
  27. data/lib/language_operator/agent/task_executor.rb +344 -63
  28. data/lib/language_operator/agent/web_server.rb +110 -14
  29. data/lib/language_operator/agent/webhook_authenticator.rb +39 -5
  30. data/lib/language_operator/agent.rb +88 -2
  31. data/lib/language_operator/cli/base_command.rb +17 -11
  32. data/lib/language_operator/cli/command_loader.rb +72 -0
  33. data/lib/language_operator/cli/commands/agent/base.rb +837 -0
  34. data/lib/language_operator/cli/commands/agent/code_operations.rb +102 -0
  35. data/lib/language_operator/cli/commands/agent/helpers/cluster_llm_client.rb +116 -0
  36. data/lib/language_operator/cli/commands/agent/helpers/code_parser.rb +115 -0
  37. data/lib/language_operator/cli/commands/agent/helpers/synthesis_watcher.rb +96 -0
  38. data/lib/language_operator/cli/commands/agent/learning.rb +289 -0
  39. data/lib/language_operator/cli/commands/agent/lifecycle.rb +102 -0
  40. data/lib/language_operator/cli/commands/agent/logs.rb +125 -0
  41. data/lib/language_operator/cli/commands/agent/workspace.rb +327 -0
  42. data/lib/language_operator/cli/commands/cluster.rb +129 -84
  43. data/lib/language_operator/cli/commands/install.rb +1 -1
  44. data/lib/language_operator/cli/commands/model/base.rb +215 -0
  45. data/lib/language_operator/cli/commands/model/test.rb +165 -0
  46. data/lib/language_operator/cli/commands/persona.rb +16 -34
  47. data/lib/language_operator/cli/commands/quickstart.rb +3 -2
  48. data/lib/language_operator/cli/commands/status.rb +40 -67
  49. data/lib/language_operator/cli/commands/system/base.rb +44 -0
  50. data/lib/language_operator/cli/commands/system/exec.rb +147 -0
  51. data/lib/language_operator/cli/commands/system/helpers/llm_synthesis.rb +183 -0
  52. data/lib/language_operator/cli/commands/system/helpers/pod_manager.rb +212 -0
  53. data/lib/language_operator/cli/commands/system/helpers/template_loader.rb +57 -0
  54. data/lib/language_operator/cli/commands/system/helpers/template_validator.rb +174 -0
  55. data/lib/language_operator/cli/commands/system/schema.rb +92 -0
  56. data/lib/language_operator/cli/commands/system/synthesis_template.rb +151 -0
  57. data/lib/language_operator/cli/commands/system/synthesize.rb +224 -0
  58. data/lib/language_operator/cli/commands/system/validate_template.rb +130 -0
  59. data/lib/language_operator/cli/commands/tool/base.rb +271 -0
  60. data/lib/language_operator/cli/commands/tool/install.rb +255 -0
  61. data/lib/language_operator/cli/commands/tool/search.rb +69 -0
  62. data/lib/language_operator/cli/commands/tool/test.rb +115 -0
  63. data/lib/language_operator/cli/commands/use.rb +29 -6
  64. data/lib/language_operator/cli/errors/handler.rb +20 -17
  65. data/lib/language_operator/cli/errors/suggestions.rb +3 -5
  66. data/lib/language_operator/cli/errors/thor_errors.rb +55 -0
  67. data/lib/language_operator/cli/formatters/code_formatter.rb +4 -11
  68. data/lib/language_operator/cli/formatters/log_formatter.rb +8 -15
  69. data/lib/language_operator/cli/formatters/progress_formatter.rb +6 -8
  70. data/lib/language_operator/cli/formatters/status_formatter.rb +26 -7
  71. data/lib/language_operator/cli/formatters/table_formatter.rb +47 -36
  72. data/lib/language_operator/cli/formatters/value_formatter.rb +75 -0
  73. data/lib/language_operator/cli/helpers/cluster_context.rb +5 -3
  74. data/lib/language_operator/cli/helpers/kubeconfig_validator.rb +2 -1
  75. data/lib/language_operator/cli/helpers/label_utils.rb +97 -0
  76. data/lib/language_operator/{ux/concerns/provider_helpers.rb → cli/helpers/provider_helper.rb} +10 -29
  77. data/lib/language_operator/cli/helpers/schedule_builder.rb +21 -1
  78. data/lib/language_operator/cli/helpers/user_prompts.rb +19 -11
  79. data/lib/language_operator/cli/helpers/ux_helper.rb +538 -0
  80. data/lib/language_operator/{ux/concerns/input_validation.rb → cli/helpers/validation_helper.rb} +13 -66
  81. data/lib/language_operator/cli/main.rb +50 -40
  82. data/lib/language_operator/cli/templates/tools/generic.yaml +3 -0
  83. data/lib/language_operator/cli/wizards/agent_wizard.rb +12 -20
  84. data/lib/language_operator/cli/wizards/model_wizard.rb +271 -0
  85. data/lib/language_operator/cli/wizards/quickstart_wizard.rb +8 -34
  86. data/lib/language_operator/client/base.rb +28 -0
  87. data/lib/language_operator/client/config.rb +4 -1
  88. data/lib/language_operator/client/mcp_connector.rb +1 -1
  89. data/lib/language_operator/config/cluster_config.rb +3 -2
  90. data/lib/language_operator/config.rb +38 -11
  91. data/lib/language_operator/constants/kubernetes_labels.rb +80 -0
  92. data/lib/language_operator/constants.rb +13 -0
  93. data/lib/language_operator/dsl/http.rb +127 -10
  94. data/lib/language_operator/dsl.rb +153 -6
  95. data/lib/language_operator/errors.rb +50 -0
  96. data/lib/language_operator/kubernetes/client.rb +11 -6
  97. data/lib/language_operator/kubernetes/resource_builder.rb +58 -84
  98. data/lib/language_operator/templates/schema/agent_dsl_openapi.yaml +1 -1
  99. data/lib/language_operator/templates/schema/agent_dsl_schema.json +1 -1
  100. data/lib/language_operator/type_coercion.rb +118 -34
  101. data/lib/language_operator/utils/secure_path.rb +74 -0
  102. data/lib/language_operator/utils.rb +7 -0
  103. data/lib/language_operator/validators.rb +54 -2
  104. data/lib/language_operator/version.rb +1 -1
  105. data/synth/001/Makefile +10 -2
  106. data/synth/001/agent.rb +16 -15
  107. data/synth/001/output.log +27 -10
  108. data/synth/002/Makefile +10 -2
  109. data/synth/003/Makefile +1 -1
  110. data/synth/003/README.md +205 -133
  111. data/synth/003/agent.optimized.rb +66 -0
  112. data/synth/003/agent.synthesized.rb +41 -0
  113. metadata +111 -35
  114. data/docs/dsl/agent-reference.md +0 -604
  115. data/docs/dsl/mcp-integration.md +0 -1177
  116. data/docs/dsl/webhooks.md +0 -932
  117. data/docs/dsl/workflows.md +0 -744
  118. data/lib/language_operator/cli/commands/agent.rb +0 -1712
  119. data/lib/language_operator/cli/commands/model.rb +0 -366
  120. data/lib/language_operator/cli/commands/system.rb +0 -1259
  121. data/lib/language_operator/cli/commands/tool.rb +0 -654
  122. data/lib/language_operator/cli/formatters/optimization_formatter.rb +0 -226
  123. data/lib/language_operator/cli/helpers/pastel_helper.rb +0 -24
  124. data/lib/language_operator/learning/adapters/base_adapter.rb +0 -149
  125. data/lib/language_operator/learning/adapters/jaeger_adapter.rb +0 -221
  126. data/lib/language_operator/learning/adapters/signoz_adapter.rb +0 -435
  127. data/lib/language_operator/learning/adapters/tempo_adapter.rb +0 -239
  128. data/lib/language_operator/learning/optimizer.rb +0 -319
  129. data/lib/language_operator/learning/pattern_detector.rb +0 -260
  130. data/lib/language_operator/learning/task_synthesizer.rb +0 -288
  131. data/lib/language_operator/learning/trace_analyzer.rb +0 -285
  132. data/lib/language_operator/templates/task_synthesis.tmpl +0 -98
  133. data/lib/language_operator/ux/base.rb +0 -81
  134. data/lib/language_operator/ux/concerns/README.md +0 -155
  135. data/lib/language_operator/ux/concerns/headings.rb +0 -90
  136. data/lib/language_operator/ux/create_agent.rb +0 -255
  137. data/lib/language_operator/ux/create_model.rb +0 -267
  138. data/lib/language_operator/ux/quickstart.rb +0 -594
  139. data/synth/003/agent.rb +0 -41
  140. data/synth/003/output.log +0 -68
  141. /data/docs/{architecture/agent-runtime.md → agent-internals.md} +0 -0
  142. /data/docs/{dsl/chat-endpoints.md → chat-endpoints.md} +0 -0
  143. /data/docs/{dsl/SCHEMA_VERSION.md → schema-versioning.md} +0 -0
@@ -25,6 +25,9 @@ module LanguageOperator
25
25
  class TaskTimeoutError < TaskExecutionError
26
26
  end
27
27
 
28
+ class TaskNetworkError < TaskExecutionError
29
+ end
30
+
28
31
  # Task Executor for DSL v1 organic functions
29
32
  #
30
33
  # Executes both neural (LLM-based) and symbolic (code-based) tasks.
@@ -74,6 +77,11 @@ module LanguageOperator
74
77
  @agent = agent
75
78
  @tasks = tasks
76
79
  @config = default_config.merge(config)
80
+
81
+ # Pre-cache task lookup and timeout information for performance
82
+ @task_cache = build_task_cache
83
+ @task_timeouts = build_timeout_cache
84
+
77
85
  logger.debug('TaskExecutor initialized',
78
86
  task_count: @tasks.size,
79
87
  timeout_symbolic: @config[:timeout_symbolic],
@@ -98,27 +106,35 @@ module LanguageOperator
98
106
  def execute_task(task_name, inputs: {}, timeout: nil, max_retries: nil)
99
107
  execution_start = Time.now
100
108
  max_retries ||= @config[:max_retries]
109
+
110
+ # Reset JSON parsing retry flag for this task
111
+ @parsing_retry_attempted = false
101
112
 
102
113
  with_span('task_executor.execute_task', attributes: {
103
114
  'task.name' => task_name.to_s,
104
115
  'task.inputs' => inputs.keys.map(&:to_s).join(','),
105
116
  'task.max_retries' => max_retries
106
117
  }) do
107
- # Find task definition
108
- task = @tasks[task_name.to_sym]
109
- raise ArgumentError, "Task not found: #{task_name}. Available tasks: #{@tasks.keys.join(', ')}" unless task
118
+ # Fast task lookup using pre-built cache
119
+ task_name_sym = task_name.to_sym
120
+ task_info = @task_cache[task_name_sym]
121
+ raise ArgumentError, "Task not found: #{task_name}. Available tasks: #{@tasks.keys.join(', ')}" unless task_info
110
122
 
111
- task_type = determine_task_type(task)
123
+ task = task_info[:definition]
124
+ task_type = task_info[:type]
112
125
 
113
- # Determine timeout based on task type if not explicitly provided
114
- timeout ||= task_timeout_for_type(task)
126
+ # Use cached timeout if not explicitly provided
127
+ timeout ||= @task_timeouts[task_name_sym]
115
128
 
116
- logger.info('Executing task',
117
- task: task_name,
118
- type: task_type,
119
- timeout: timeout,
120
- max_retries: max_retries,
121
- inputs: summarize_values(inputs))
129
+ # Optimize logging - only log if debug level enabled or log_executions is true
130
+ if logger.logger.level <= ::Logger::DEBUG || @config[:log_executions]
131
+ logger.info('Executing task',
132
+ task: task_name,
133
+ type: task_type,
134
+ timeout: timeout,
135
+ max_retries: max_retries,
136
+ inputs: summarize_values(inputs))
137
+ end
122
138
 
123
139
  # Add timeout to span attributes after it's determined
124
140
  OpenTelemetry::Trace.current_span&.set_attribute('task.timeout', timeout)
@@ -190,10 +206,41 @@ module LanguageOperator
190
206
  logger.info('Parsing neural task response',
191
207
  task: task.name)
192
208
 
193
- # Parse response within child span
209
+ # Parse response within child span with retry logic
194
210
  parsed_outputs = tracer.in_span('task_executor.parse_response') do |parse_span|
195
211
  record_parse_metadata(response_text, parse_span)
196
- parse_neural_response(response_text, task)
212
+
213
+ begin
214
+ parse_neural_response(response_text, task)
215
+ rescue RuntimeError => e
216
+ # If parsing fails and this is a JSON parsing error, try one more time with clarified prompt
217
+ raise e unless e.message.include?('returned invalid JSON') && !@parsing_retry_attempted
218
+
219
+ @parsing_retry_attempted = true
220
+
221
+ logger.warn('JSON parsing failed, retrying with clarified prompt',
222
+ task: task.name,
223
+ original_error: e.message,
224
+ response_preview: response_text[0..300])
225
+
226
+ # Build retry prompt with clearer instructions
227
+ retry_prompt = build_parsing_retry_prompt(task, validated_inputs, response_text, e.message)
228
+
229
+ logger.info('Retrying LLM call with clarified prompt',
230
+ task: task.name,
231
+ retry_prompt_length: retry_prompt.length)
232
+
233
+ # Retry LLM call
234
+ retry_response = @agent.send_message(retry_prompt)
235
+ retry_response_text = retry_response.is_a?(String) ? retry_response : retry_response.content
236
+
237
+ logger.info('Parsing retry response',
238
+ task: task.name,
239
+ retry_response_length: retry_response_text.length)
240
+
241
+ # Try parsing the retry response
242
+ parse_neural_response(retry_response_text, task)
243
+ end
197
244
  end
198
245
 
199
246
  logger.info('Response parsed successfully',
@@ -293,13 +340,19 @@ module LanguageOperator
293
340
  def execute_parallel(tasks, in_threads: 4)
294
341
  require 'parallel'
295
342
 
343
+ # Capture current OpenTelemetry context before parallel execution
344
+ current_context = OpenTelemetry::Context.current
345
+
296
346
  logger.info('Executing tasks in parallel', count: tasks.size, threads: in_threads)
297
347
 
298
348
  results = Parallel.map(tasks, in_threads: in_threads) do |task_spec|
299
- task_name = task_spec[:name]
300
- task_inputs = task_spec[:inputs] || {}
349
+ # Restore OpenTelemetry context in worker thread
350
+ OpenTelemetry::Context.with_current(current_context) do
351
+ task_name = task_spec[:name]
352
+ task_inputs = task_spec[:inputs] || {}
301
353
 
302
- execute_task(task_name, inputs: task_inputs)
354
+ execute_task(task_name, inputs: task_inputs)
355
+ end
303
356
  end
304
357
 
305
358
  logger.info('Parallel execution complete', results_count: results.size)
@@ -319,22 +372,41 @@ module LanguageOperator
319
372
  end
320
373
 
321
374
  # Summarize hash values for logging (truncate long strings)
375
+ # Optimized for performance with lazy computation
322
376
  #
323
377
  # @param hash [Hash] Hash to summarize
324
378
  # @return [Hash] Summarized hash with truncated values
325
379
  def summarize_values(hash)
326
380
  return {} unless hash.is_a?(Hash)
327
381
 
328
- hash.transform_values do |v|
329
- case v
330
- when String
331
- v.length > 100 ? "#{v[0..97]}... (#{v.length} chars)" : v
332
- when Array
333
- v.length > 5 ? "#{v.first(3).inspect}... (#{v.length} items)" : v.inspect
334
- else
335
- v.inspect
336
- end
382
+ # OPTIMIZE: only create new hash if values need summarization
383
+ needs_summarization = false
384
+ result = {}
385
+
386
+ hash.each do |key, v|
387
+ summarized_value = case v
388
+ when String
389
+ if v.length > 100
390
+ needs_summarization = true
391
+ "#{v[0, 97]}... (#{v.length} chars)"
392
+ else
393
+ v
394
+ end
395
+ when Array
396
+ if v.length > 5
397
+ needs_summarization = true
398
+ "#{v.first(3).inspect}... (#{v.length} items)"
399
+ else
400
+ v.inspect
401
+ end
402
+ else
403
+ v.inspect
404
+ end
405
+ result[key] = summarized_value
337
406
  end
407
+
408
+ # Return original if no summarization was needed (rare optimization)
409
+ needs_summarization ? result : hash
338
410
  end
339
411
 
340
412
  # Build prompt for neural task execution
@@ -371,6 +443,63 @@ module LanguageOperator
371
443
  prompt
372
444
  end
373
445
 
446
+ # Build retry prompt when JSON parsing fails
447
+ #
448
+ # @param task [TaskDefinition] The task definition
449
+ # @param inputs [Hash] Validated input parameters
450
+ # @param failed_response [String] The previous response that failed to parse
451
+ # @param error_message [String] The parsing error message
452
+ # @return [String] Prompt for LLM retry
453
+ def build_parsing_retry_prompt(task, inputs, failed_response, error_message)
454
+ prompt = "# Task: #{task.name} (RETRY - JSON Parsing Failed)\n\n"
455
+ prompt += "## Instructions\n#{task.instructions_text}\n\n"
456
+
457
+ if inputs.any?
458
+ prompt += "## Inputs\n"
459
+ inputs.each do |key, value|
460
+ prompt += "- #{key}: #{value.inspect}\n"
461
+ end
462
+ prompt += "\n"
463
+ end
464
+
465
+ prompt += "## Previous Response (Failed to Parse)\n"
466
+ prompt += "Your previous response caused a parsing error: #{error_message}\n"
467
+ prompt += "Previous response preview:\n```\n#{failed_response[0..500]}#{'...' if failed_response.length > 500}\n```\n\n"
468
+
469
+ prompt += "## Output Schema (CRITICAL)\n"
470
+ prompt += "You MUST return valid JSON with exactly these fields:\n"
471
+ task.outputs_schema.each do |key, type|
472
+ prompt += "- #{key} (#{type})\n"
473
+ end
474
+ prompt += "\n"
475
+
476
+ prompt += "## Response Format (CRITICAL)\n"
477
+ prompt += "IMPORTANT: Your response must be ONLY valid JSON. No other text.\n"
478
+ prompt += "Do NOT use [THINK] tags or any other text.\n"
479
+ prompt += "Do NOT include code blocks like ```json.\n"
480
+ prompt += "Return ONLY the JSON object, nothing else.\n"
481
+ prompt += "The JSON must match the output schema exactly.\n\n"
482
+
483
+ prompt += "Example correct format:\n"
484
+ prompt += "{\n"
485
+ task.outputs_schema.each_with_index do |(key, type), index|
486
+ value_example = case type
487
+ when 'string' then '"example"'
488
+ when 'integer' then '42'
489
+ when 'number' then '3.14'
490
+ when 'boolean' then 'true'
491
+ when 'array' then '[]'
492
+ when 'hash' then '{}'
493
+ else '"value"'
494
+ end
495
+ comma = index < task.outputs_schema.length - 1 ? ',' : ''
496
+ prompt += " \"#{key}\": #{value_example}#{comma}\n"
497
+ end
498
+ prompt += "}\n"
499
+
500
+ prompt
501
+ end
502
+
374
503
  # Parse LLM response to extract output values
375
504
  #
376
505
  # @param response_text [String] LLM response
@@ -390,13 +519,30 @@ module LanguageOperator
390
519
  end
391
520
 
392
521
  # Strip thinking tags that some models add (e.g., [THINK]...[/THINK] or unclosed [THINK]...)
393
- # First try to strip matched pairs, then strip any remaining unclosed [THINK] content
522
+ # First try to strip matched pairs, then strip unclosed [THINK] only if there's JSON after it
394
523
  logger.debug('Parsing neural response', task: task.name, response_length: response_text.length, response_start: response_text[0..100])
395
524
 
396
525
  cleaned_text = response_text.gsub(%r{\[THINK\].*?\[/THINK\]}m, '')
397
- .gsub(/\[THINK\].*?(?=\{|$)/m, '')
526
+ .gsub(/\[THINK\].*?(?=\{)/m, '')
398
527
  .strip
399
528
 
529
+ # If cleaned text is empty or still contains unclosed [THINK], try more aggressive cleaning
530
+ if cleaned_text.empty? || cleaned_text.start_with?('[THINK]')
531
+ # Strip everything from [THINK] to end if no [/THINK] found
532
+ cleaned_text = response_text.gsub(/\[THINK\].*$/m, '').strip
533
+
534
+ # If still no JSON found, extract everything after the last [THINK] block
535
+ if cleaned_text.empty? && response_text.include?('{')
536
+ last_think = response_text.rindex('[THINK]')
537
+ if last_think
538
+ after_think = response_text[last_think..]
539
+ # Find first JSON-like structure after [THINK]
540
+ json_start = after_think.index('{')
541
+ cleaned_text = after_think[json_start..] if json_start
542
+ end
543
+ end
544
+ end
545
+
400
546
  logger.debug('After stripping THINK tags', cleaned_length: cleaned_text.length, cleaned_start: cleaned_text[0..100])
401
547
 
402
548
  # Try to extract JSON from response
@@ -405,9 +551,17 @@ module LanguageOperator
405
551
  json_text = if json_match
406
552
  json_match[1]
407
553
  else
408
- # Try to find raw JSON object
554
+ # Try to find raw JSON object - be more aggressive about finding JSON
409
555
  json_object_match = cleaned_text.match(/\{.*\}/m)
410
- json_object_match ? json_object_match[0] : cleaned_text
556
+ if json_object_match
557
+ json_object_match[0]
558
+ elsif cleaned_text.include?('{')
559
+ # Extract from first { to end of string (handles incomplete responses)
560
+ json_start = cleaned_text.index('{')
561
+ cleaned_text[json_start..]
562
+ else
563
+ cleaned_text
564
+ end
411
565
  end
412
566
 
413
567
  logger.debug('Extracted JSON text', json_length: json_text.length, json_start: json_text[0..100])
@@ -425,13 +579,23 @@ module LanguageOperator
425
579
  raise "Neural task '#{task.name}' returned invalid JSON: #{e.message}"
426
580
  end
427
581
 
428
- # Recursively convert all hash keys to symbols
582
+ # Recursively convert all hash keys to symbols (optimized for performance)
429
583
  def deep_symbolize_keys(obj)
430
584
  case obj
431
585
  when Hash
432
- obj.transform_keys(&:to_sym).transform_values { |v| deep_symbolize_keys(v) }
586
+ # OPTIMIZE: pre-allocate hash with correct size and avoid double iteration
587
+ result = {}
588
+ obj.each do |key, value|
589
+ result[key.to_sym] = deep_symbolize_keys(value)
590
+ end
591
+ result
433
592
  when Array
434
- obj.map { |item| deep_symbolize_keys(item) }
593
+ # OPTIMIZE: pre-allocate array with correct size
594
+ result = Array.new(obj.size)
595
+ obj.each_with_index do |item, index|
596
+ result[index] = deep_symbolize_keys(item)
597
+ end
598
+ result
435
599
  else
436
600
  obj
437
601
  end
@@ -538,7 +702,11 @@ module LanguageOperator
538
702
  raise create_appropriate_error(task_name, last_error)
539
703
  end
540
704
 
541
- # Execute a single attempt of a task with timeout
705
+ # Execute a single attempt of a task with timeout and error context preservation
706
+ #
707
+ # This method implements timeout handling that preserves original error context
708
+ # while maintaining error precedence hierarchy. Timeout errors always take
709
+ # precedence over any nested errors (including network errors).
542
710
  #
543
711
  # @param task [TaskDefinition] The task definition
544
712
  # @param task_name [Symbol] Name of the task
@@ -551,29 +719,63 @@ module LanguageOperator
551
719
  attempt_start = Time.now
552
720
 
553
721
  result = if timeout.positive?
554
- Timeout.timeout(timeout) do
555
- execute_task_implementation(task, inputs)
556
- end
722
+ execute_with_timeout(task, task_name, inputs, timeout)
557
723
  else
558
724
  execute_task_implementation(task, inputs)
559
725
  end
560
726
 
561
727
  execution_time = Time.now - attempt_start
562
- logger.info('Task completed',
563
- task: task_name,
564
- attempt: attempt + 1,
565
- execution_time: execution_time.round(3),
566
- outputs: summarize_values(result))
728
+
729
+ # Optimize logging - only log if debug level enabled or log_executions is true
730
+ if logger.logger.level <= ::Logger::DEBUG || @config[:log_executions]
731
+ logger.info('Task completed',
732
+ task: task_name,
733
+ attempt: attempt + 1,
734
+ execution_time: execution_time.round(3),
735
+ outputs: summarize_values(result))
736
+ end
567
737
 
568
738
  result
739
+ end
740
+
741
+ # Execute task with timeout wrapper that preserves error context
742
+ #
743
+ # This method ensures that timeout errors always take precedence over
744
+ # any nested errors (e.g., network errors), solving the race condition
745
+ # between timeout detection and error classification.
746
+ #
747
+ # @param task [TaskDefinition] The task definition
748
+ # @param task_name [Symbol] Name of the task
749
+ # @param inputs [Hash] Input parameters
750
+ # @param timeout [Numeric] Timeout in seconds
751
+ # @return [Hash] Task outputs
752
+ # @raise [TaskTimeoutError] If execution times out (always takes precedence)
753
+ def execute_with_timeout(task, task_name, inputs, timeout)
754
+ attempt_start = Time.now
755
+
756
+ Timeout.timeout(timeout) do
757
+ execute_task_implementation(task, inputs)
758
+ end
569
759
  rescue Timeout::Error => e
760
+ # Timeout always wins - this solves the race condition
570
761
  execution_time = Time.now - attempt_start
762
+
571
763
  logger.warn('Task execution timed out',
572
764
  task: task_name,
573
- attempt: attempt + 1,
574
765
  timeout: timeout,
575
- execution_time: execution_time.round(3))
576
- raise TaskTimeoutError.new(task_name, "timed out after #{timeout}s", e)
766
+ execution_time: execution_time.round(3),
767
+ timeout_precedence: 'timeout error takes precedence over any nested errors')
768
+
769
+ # Always wrap as TaskTimeoutError, preserving original timeout context
770
+ raise TaskTimeoutError.new(task_name, "timed out after #{timeout}s (execution_time: #{execution_time.round(3)}s)", e)
771
+ rescue *RETRYABLE_ERRORS => e
772
+ # Network errors that escape timeout handling (very rare)
773
+ # These occur outside the timeout window, so they're genuine network errors
774
+ logger.debug('Network error outside timeout window',
775
+ task: task_name,
776
+ error: e.class.name,
777
+ message: e.message)
778
+ raise TaskNetworkError.new(task_name, "network error: #{e.message}", e)
577
779
  end
578
780
 
579
781
  # Execute the actual task implementation (neural or symbolic)
@@ -613,27 +815,51 @@ module LanguageOperator
613
815
  # @param error [Exception] The error that occurred
614
816
  # @return [Boolean] Whether the error should be retried
615
817
  def retryable_error?(error)
616
- RETRYABLE_ERRORS.any? { |error_class| error.is_a?(error_class) }
818
+ case error
819
+ when TaskNetworkError
820
+ # Network errors wrapped in TaskNetworkError are retryable
821
+ true
822
+ when TaskTimeoutError, TaskValidationError
823
+ # Timeout and validation errors are never retryable
824
+ false
825
+ when TaskExecutionError
826
+ # Check the original error for retryability
827
+ error.original_error ? retryable_error?(error.original_error) : false
828
+ when RuntimeError
829
+ # JSON parsing errors from neural tasks are retryable
830
+ error.message.include?('returned invalid JSON')
831
+ else
832
+ # Check against the standard retryable error list
833
+ RETRYABLE_ERRORS.any? { |error_class| error.is_a?(error_class) }
834
+ end
617
835
  end
618
836
 
619
- # Categorize error for logging and operator integration
837
+ # Categorize error for logging and operator integration with precedence hierarchy
838
+ #
839
+ # Error precedence (highest to lowest):
840
+ # 1. Timeout errors (always win, even if wrapping network errors)
841
+ # 2. Validation errors (argument/input validation failures)
842
+ # 3. Network errors (connection, socket, DNS issues)
843
+ # 4. Execution errors (general task execution failures)
844
+ # 5. System errors (unexpected/unknown errors)
620
845
  #
621
846
  # @param error [Exception] The error that occurred
622
847
  # @return [Symbol] Error category
623
848
  def categorize_error(error)
624
- case error
625
- when ArgumentError, TaskValidationError
626
- :validation
627
- when Timeout::Error, TaskTimeoutError
628
- :timeout
629
- when TaskExecutionError
630
- # Check the original error for categorization
631
- error.original_error ? categorize_error(error.original_error) : :execution
632
- when *RETRYABLE_ERRORS
633
- :network
634
- else
635
- :execution
636
- end
849
+ # Precedence Level 1: Timeout errors always win
850
+ return :timeout if error.is_a?(Timeout::Error) || error.is_a?(TaskTimeoutError)
851
+
852
+ # Precedence Level 2: Validation errors
853
+ return :validation if error.is_a?(ArgumentError) || error.is_a?(TaskValidationError)
854
+
855
+ # For wrapped errors, check original error but preserve timeout precedence
856
+ return categorize_error(error.original_error) if error.is_a?(TaskExecutionError) && error.original_error
857
+
858
+ # Precedence Level 3: Network errors
859
+ return :network if error.is_a?(TaskNetworkError) || RETRYABLE_ERRORS.any? { |err_class| error.is_a?(err_class) }
860
+
861
+ # Precedence Level 4: General execution errors
862
+ :execution
637
863
  end
638
864
 
639
865
  # Calculate retry delay with exponential backoff
@@ -645,19 +871,24 @@ module LanguageOperator
645
871
  [delay, @config[:retry_delay_max]].min
646
872
  end
647
873
 
648
- # Create appropriate error type based on original error
874
+ # Create appropriate error type based on original error with precedence hierarchy
649
875
  #
650
876
  # @param task_name [Symbol] Name of the task
651
877
  # @param original_error [Exception] The original error
652
878
  # @return [TaskExecutionError] Appropriate error type
653
879
  def create_appropriate_error(task_name, original_error)
654
880
  case original_error
655
- when TaskTimeoutError
881
+ when TaskTimeoutError, TaskValidationError, TaskNetworkError
882
+ # Already wrapped in appropriate type
656
883
  original_error
657
884
  when Timeout::Error
658
- TaskTimeoutError.new(task_name, 'timed out', original_error)
885
+ # Always wrap timeout errors, preserving original context
886
+ TaskTimeoutError.new(task_name, "timed out after timeout (original: #{original_error.message})", original_error)
659
887
  when ArgumentError
660
888
  TaskValidationError.new(task_name, original_error.message, original_error)
889
+ when *RETRYABLE_ERRORS
890
+ # Wrap network errors for clear categorization
891
+ TaskNetworkError.new(task_name, "network error: #{original_error.message}", original_error)
661
892
  else
662
893
  TaskExecutionError.new(task_name, original_error.message, original_error)
663
894
  end
@@ -683,6 +914,56 @@ module LanguageOperator
683
914
  retryable: retryable_error?(error),
684
915
  backtrace: error.backtrace&.first(5))
685
916
  end
917
+
918
+ # Build task lookup cache for O(1) task resolution
919
+ #
920
+ # Pre-computes task metadata to avoid repeated type determinations
921
+ # and provide fast hash-based lookup instead of linear search.
922
+ #
923
+ # @return [Hash] Cache mapping task names to metadata
924
+ def build_task_cache
925
+ cache = {}
926
+ @tasks.each do |name, task|
927
+ # Guard against test doubles that don't respond to task methods
928
+ cache[name] = if task.respond_to?(:neural?) && task.respond_to?(:symbolic?)
929
+ {
930
+ definition: task,
931
+ type: determine_task_type(task),
932
+ neural: task.neural?,
933
+ symbolic: task.symbolic?
934
+ }
935
+ else
936
+ # Fallback for test doubles or invalid task objects
937
+ {
938
+ definition: task,
939
+ type: 'unknown',
940
+ neural: false,
941
+ symbolic: false
942
+ }
943
+ end
944
+ end
945
+ cache
946
+ end
947
+
948
+ # Build timeout cache for O(1) timeout resolution
949
+ #
950
+ # Pre-computes timeouts for all tasks to avoid repeated calculations
951
+ # during task execution hot path.
952
+ #
953
+ # @return [Hash] Cache mapping task names to timeout values
954
+ def build_timeout_cache
955
+ cache = {}
956
+ @tasks.each do |name, task|
957
+ # Guard against test doubles that don't respond to task methods
958
+ cache[name] = if task.respond_to?(:neural?) && task.respond_to?(:symbolic?)
959
+ task_timeout_for_type(task)
960
+ else
961
+ # Fallback timeout for test doubles or invalid task objects
962
+ @config[:timeout_symbolic]
963
+ end
964
+ end
965
+ cache
966
+ end
686
967
  end
687
968
  end
688
969
  end