language-operator 0.1.61 → 0.1.62

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 +346 -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],
@@ -104,21 +112,26 @@ module LanguageOperator
104
112
  'task.inputs' => inputs.keys.map(&:to_s).join(','),
105
113
  'task.max_retries' => max_retries
106
114
  }) 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
115
+ # Fast task lookup using pre-built cache
116
+ task_name_sym = task_name.to_sym
117
+ task_info = @task_cache[task_name_sym]
118
+ raise ArgumentError, "Task not found: #{task_name}. Available tasks: #{@tasks.keys.join(', ')}" unless task_info
110
119
 
111
- task_type = determine_task_type(task)
120
+ task = task_info[:definition]
121
+ task_type = task_info[:type]
112
122
 
113
- # Determine timeout based on task type if not explicitly provided
114
- timeout ||= task_timeout_for_type(task)
123
+ # Use cached timeout if not explicitly provided
124
+ timeout ||= @task_timeouts[task_name_sym]
115
125
 
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))
126
+ # Optimize logging - only log if debug level enabled or log_executions is true
127
+ if logger.logger.level <= ::Logger::DEBUG || @config[:log_executions]
128
+ logger.info('Executing task',
129
+ task: task_name,
130
+ type: task_type,
131
+ timeout: timeout,
132
+ max_retries: max_retries,
133
+ inputs: summarize_values(inputs))
134
+ end
122
135
 
123
136
  # Add timeout to span attributes after it's determined
124
137
  OpenTelemetry::Trace.current_span&.set_attribute('task.timeout', timeout)
@@ -190,10 +203,46 @@ module LanguageOperator
190
203
  logger.info('Parsing neural task response',
191
204
  task: task.name)
192
205
 
193
- # Parse response within child span
206
+ # Parse response within child span with retry logic
194
207
  parsed_outputs = tracer.in_span('task_executor.parse_response') do |parse_span|
195
208
  record_parse_metadata(response_text, parse_span)
196
- parse_neural_response(response_text, task)
209
+
210
+ begin
211
+ parse_neural_response(response_text, task)
212
+ rescue RuntimeError => e
213
+ # If parsing fails and this is a JSON parsing error, try one more time with clarified prompt
214
+ raise e unless e.message.include?('returned invalid JSON') && !defined?(@parsing_retry_attempted)
215
+
216
+ @parsing_retry_attempted = true
217
+
218
+ logger.warn('JSON parsing failed, retrying with clarified prompt',
219
+ task: task.name,
220
+ original_error: e.message,
221
+ response_preview: response_text[0..300])
222
+
223
+ # Build retry prompt with clearer instructions
224
+ retry_prompt = build_parsing_retry_prompt(task, validated_inputs, response_text, e.message)
225
+
226
+ logger.info('Retrying LLM call with clarified prompt',
227
+ task: task.name,
228
+ retry_prompt_length: retry_prompt.length)
229
+
230
+ # Retry LLM call
231
+ retry_response = @agent.send_message(retry_prompt)
232
+ retry_response_text = retry_response.is_a?(String) ? retry_response : retry_response.content
233
+
234
+ logger.info('Parsing retry response',
235
+ task: task.name,
236
+ retry_response_length: retry_response_text.length)
237
+
238
+ # Try parsing the retry response
239
+ parse_neural_response(retry_response_text, task)
240
+
241
+ # Re-raise original error if not a JSON parsing error or already retried
242
+ ensure
243
+ # Reset retry flag for next task
244
+ @parsing_retry_attempted = nil
245
+ end
197
246
  end
198
247
 
199
248
  logger.info('Response parsed successfully',
@@ -293,13 +342,19 @@ module LanguageOperator
293
342
  def execute_parallel(tasks, in_threads: 4)
294
343
  require 'parallel'
295
344
 
345
+ # Capture current OpenTelemetry context before parallel execution
346
+ current_context = OpenTelemetry::Context.current
347
+
296
348
  logger.info('Executing tasks in parallel', count: tasks.size, threads: in_threads)
297
349
 
298
350
  results = Parallel.map(tasks, in_threads: in_threads) do |task_spec|
299
- task_name = task_spec[:name]
300
- task_inputs = task_spec[:inputs] || {}
351
+ # Restore OpenTelemetry context in worker thread
352
+ OpenTelemetry::Context.with_current(current_context) do
353
+ task_name = task_spec[:name]
354
+ task_inputs = task_spec[:inputs] || {}
301
355
 
302
- execute_task(task_name, inputs: task_inputs)
356
+ execute_task(task_name, inputs: task_inputs)
357
+ end
303
358
  end
304
359
 
305
360
  logger.info('Parallel execution complete', results_count: results.size)
@@ -319,22 +374,41 @@ module LanguageOperator
319
374
  end
320
375
 
321
376
  # Summarize hash values for logging (truncate long strings)
377
+ # Optimized for performance with lazy computation
322
378
  #
323
379
  # @param hash [Hash] Hash to summarize
324
380
  # @return [Hash] Summarized hash with truncated values
325
381
  def summarize_values(hash)
326
382
  return {} unless hash.is_a?(Hash)
327
383
 
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
384
+ # OPTIMIZE: only create new hash if values need summarization
385
+ needs_summarization = false
386
+ result = {}
387
+
388
+ hash.each do |key, v|
389
+ summarized_value = case v
390
+ when String
391
+ if v.length > 100
392
+ needs_summarization = true
393
+ "#{v[0, 97]}... (#{v.length} chars)"
394
+ else
395
+ v
396
+ end
397
+ when Array
398
+ if v.length > 5
399
+ needs_summarization = true
400
+ "#{v.first(3).inspect}... (#{v.length} items)"
401
+ else
402
+ v.inspect
403
+ end
404
+ else
405
+ v.inspect
406
+ end
407
+ result[key] = summarized_value
337
408
  end
409
+
410
+ # Return original if no summarization was needed (rare optimization)
411
+ needs_summarization ? result : hash
338
412
  end
339
413
 
340
414
  # Build prompt for neural task execution
@@ -371,6 +445,63 @@ module LanguageOperator
371
445
  prompt
372
446
  end
373
447
 
448
+ # Build retry prompt when JSON parsing fails
449
+ #
450
+ # @param task [TaskDefinition] The task definition
451
+ # @param inputs [Hash] Validated input parameters
452
+ # @param failed_response [String] The previous response that failed to parse
453
+ # @param error_message [String] The parsing error message
454
+ # @return [String] Prompt for LLM retry
455
+ def build_parsing_retry_prompt(task, inputs, failed_response, error_message)
456
+ prompt = "# Task: #{task.name} (RETRY - JSON Parsing Failed)\n\n"
457
+ prompt += "## Instructions\n#{task.instructions_text}\n\n"
458
+
459
+ if inputs.any?
460
+ prompt += "## Inputs\n"
461
+ inputs.each do |key, value|
462
+ prompt += "- #{key}: #{value.inspect}\n"
463
+ end
464
+ prompt += "\n"
465
+ end
466
+
467
+ prompt += "## Previous Response (Failed to Parse)\n"
468
+ prompt += "Your previous response caused a parsing error: #{error_message}\n"
469
+ prompt += "Previous response preview:\n```\n#{failed_response[0..500]}#{'...' if failed_response.length > 500}\n```\n\n"
470
+
471
+ prompt += "## Output Schema (CRITICAL)\n"
472
+ prompt += "You MUST return valid JSON with exactly these fields:\n"
473
+ task.outputs_schema.each do |key, type|
474
+ prompt += "- #{key} (#{type})\n"
475
+ end
476
+ prompt += "\n"
477
+
478
+ prompt += "## Response Format (CRITICAL)\n"
479
+ prompt += "IMPORTANT: Your response must be ONLY valid JSON. No other text.\n"
480
+ prompt += "Do NOT use [THINK] tags or any other text.\n"
481
+ prompt += "Do NOT include code blocks like ```json.\n"
482
+ prompt += "Return ONLY the JSON object, nothing else.\n"
483
+ prompt += "The JSON must match the output schema exactly.\n\n"
484
+
485
+ prompt += "Example correct format:\n"
486
+ prompt += "{\n"
487
+ task.outputs_schema.each_with_index do |(key, type), index|
488
+ value_example = case type
489
+ when 'string' then '"example"'
490
+ when 'integer' then '42'
491
+ when 'number' then '3.14'
492
+ when 'boolean' then 'true'
493
+ when 'array' then '[]'
494
+ when 'hash' then '{}'
495
+ else '"value"'
496
+ end
497
+ comma = index < task.outputs_schema.length - 1 ? ',' : ''
498
+ prompt += " \"#{key}\": #{value_example}#{comma}\n"
499
+ end
500
+ prompt += "}\n"
501
+
502
+ prompt
503
+ end
504
+
374
505
  # Parse LLM response to extract output values
375
506
  #
376
507
  # @param response_text [String] LLM response
@@ -390,13 +521,30 @@ module LanguageOperator
390
521
  end
391
522
 
392
523
  # 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
524
+ # First try to strip matched pairs, then strip unclosed [THINK] only if there's JSON after it
394
525
  logger.debug('Parsing neural response', task: task.name, response_length: response_text.length, response_start: response_text[0..100])
395
526
 
396
527
  cleaned_text = response_text.gsub(%r{\[THINK\].*?\[/THINK\]}m, '')
397
- .gsub(/\[THINK\].*?(?=\{|$)/m, '')
528
+ .gsub(/\[THINK\].*?(?=\{)/m, '')
398
529
  .strip
399
530
 
531
+ # If cleaned text is empty or still contains unclosed [THINK], try more aggressive cleaning
532
+ if cleaned_text.empty? || cleaned_text.start_with?('[THINK]')
533
+ # Strip everything from [THINK] to end if no [/THINK] found
534
+ cleaned_text = response_text.gsub(/\[THINK\].*$/m, '').strip
535
+
536
+ # If still no JSON found, extract everything after the last [THINK] block
537
+ if cleaned_text.empty? && response_text.include?('{')
538
+ last_think = response_text.rindex('[THINK]')
539
+ if last_think
540
+ after_think = response_text[last_think..]
541
+ # Find first JSON-like structure after [THINK]
542
+ json_start = after_think.index('{')
543
+ cleaned_text = after_think[json_start..] if json_start
544
+ end
545
+ end
546
+ end
547
+
400
548
  logger.debug('After stripping THINK tags', cleaned_length: cleaned_text.length, cleaned_start: cleaned_text[0..100])
401
549
 
402
550
  # Try to extract JSON from response
@@ -405,9 +553,17 @@ module LanguageOperator
405
553
  json_text = if json_match
406
554
  json_match[1]
407
555
  else
408
- # Try to find raw JSON object
556
+ # Try to find raw JSON object - be more aggressive about finding JSON
409
557
  json_object_match = cleaned_text.match(/\{.*\}/m)
410
- json_object_match ? json_object_match[0] : cleaned_text
558
+ if json_object_match
559
+ json_object_match[0]
560
+ elsif cleaned_text.include?('{')
561
+ # Extract from first { to end of string (handles incomplete responses)
562
+ json_start = cleaned_text.index('{')
563
+ cleaned_text[json_start..]
564
+ else
565
+ cleaned_text
566
+ end
411
567
  end
412
568
 
413
569
  logger.debug('Extracted JSON text', json_length: json_text.length, json_start: json_text[0..100])
@@ -425,13 +581,23 @@ module LanguageOperator
425
581
  raise "Neural task '#{task.name}' returned invalid JSON: #{e.message}"
426
582
  end
427
583
 
428
- # Recursively convert all hash keys to symbols
584
+ # Recursively convert all hash keys to symbols (optimized for performance)
429
585
  def deep_symbolize_keys(obj)
430
586
  case obj
431
587
  when Hash
432
- obj.transform_keys(&:to_sym).transform_values { |v| deep_symbolize_keys(v) }
588
+ # OPTIMIZE: pre-allocate hash with correct size and avoid double iteration
589
+ result = {}
590
+ obj.each do |key, value|
591
+ result[key.to_sym] = deep_symbolize_keys(value)
592
+ end
593
+ result
433
594
  when Array
434
- obj.map { |item| deep_symbolize_keys(item) }
595
+ # OPTIMIZE: pre-allocate array with correct size
596
+ result = Array.new(obj.size)
597
+ obj.each_with_index do |item, index|
598
+ result[index] = deep_symbolize_keys(item)
599
+ end
600
+ result
435
601
  else
436
602
  obj
437
603
  end
@@ -538,7 +704,11 @@ module LanguageOperator
538
704
  raise create_appropriate_error(task_name, last_error)
539
705
  end
540
706
 
541
- # Execute a single attempt of a task with timeout
707
+ # Execute a single attempt of a task with timeout and error context preservation
708
+ #
709
+ # This method implements timeout handling that preserves original error context
710
+ # while maintaining error precedence hierarchy. Timeout errors always take
711
+ # precedence over any nested errors (including network errors).
542
712
  #
543
713
  # @param task [TaskDefinition] The task definition
544
714
  # @param task_name [Symbol] Name of the task
@@ -551,29 +721,63 @@ module LanguageOperator
551
721
  attempt_start = Time.now
552
722
 
553
723
  result = if timeout.positive?
554
- Timeout.timeout(timeout) do
555
- execute_task_implementation(task, inputs)
556
- end
724
+ execute_with_timeout(task, task_name, inputs, timeout)
557
725
  else
558
726
  execute_task_implementation(task, inputs)
559
727
  end
560
728
 
561
729
  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))
730
+
731
+ # Optimize logging - only log if debug level enabled or log_executions is true
732
+ if logger.logger.level <= ::Logger::DEBUG || @config[:log_executions]
733
+ logger.info('Task completed',
734
+ task: task_name,
735
+ attempt: attempt + 1,
736
+ execution_time: execution_time.round(3),
737
+ outputs: summarize_values(result))
738
+ end
567
739
 
568
740
  result
741
+ end
742
+
743
+ # Execute task with timeout wrapper that preserves error context
744
+ #
745
+ # This method ensures that timeout errors always take precedence over
746
+ # any nested errors (e.g., network errors), solving the race condition
747
+ # between timeout detection and error classification.
748
+ #
749
+ # @param task [TaskDefinition] The task definition
750
+ # @param task_name [Symbol] Name of the task
751
+ # @param inputs [Hash] Input parameters
752
+ # @param timeout [Numeric] Timeout in seconds
753
+ # @return [Hash] Task outputs
754
+ # @raise [TaskTimeoutError] If execution times out (always takes precedence)
755
+ def execute_with_timeout(task, task_name, inputs, timeout)
756
+ attempt_start = Time.now
757
+
758
+ Timeout.timeout(timeout) do
759
+ execute_task_implementation(task, inputs)
760
+ end
569
761
  rescue Timeout::Error => e
762
+ # Timeout always wins - this solves the race condition
570
763
  execution_time = Time.now - attempt_start
764
+
571
765
  logger.warn('Task execution timed out',
572
766
  task: task_name,
573
- attempt: attempt + 1,
574
767
  timeout: timeout,
575
- execution_time: execution_time.round(3))
576
- raise TaskTimeoutError.new(task_name, "timed out after #{timeout}s", e)
768
+ execution_time: execution_time.round(3),
769
+ timeout_precedence: 'timeout error takes precedence over any nested errors')
770
+
771
+ # Always wrap as TaskTimeoutError, preserving original timeout context
772
+ raise TaskTimeoutError.new(task_name, "timed out after #{timeout}s (execution_time: #{execution_time.round(3)}s)", e)
773
+ rescue *RETRYABLE_ERRORS => e
774
+ # Network errors that escape timeout handling (very rare)
775
+ # These occur outside the timeout window, so they're genuine network errors
776
+ logger.debug('Network error outside timeout window',
777
+ task: task_name,
778
+ error: e.class.name,
779
+ message: e.message)
780
+ raise TaskNetworkError.new(task_name, "network error: #{e.message}", e)
577
781
  end
578
782
 
579
783
  # Execute the actual task implementation (neural or symbolic)
@@ -613,27 +817,51 @@ module LanguageOperator
613
817
  # @param error [Exception] The error that occurred
614
818
  # @return [Boolean] Whether the error should be retried
615
819
  def retryable_error?(error)
616
- RETRYABLE_ERRORS.any? { |error_class| error.is_a?(error_class) }
820
+ case error
821
+ when TaskNetworkError
822
+ # Network errors wrapped in TaskNetworkError are retryable
823
+ true
824
+ when TaskTimeoutError, TaskValidationError
825
+ # Timeout and validation errors are never retryable
826
+ false
827
+ when TaskExecutionError
828
+ # Check the original error for retryability
829
+ error.original_error ? retryable_error?(error.original_error) : false
830
+ when RuntimeError
831
+ # JSON parsing errors from neural tasks are retryable
832
+ error.message.include?('returned invalid JSON')
833
+ else
834
+ # Check against the standard retryable error list
835
+ RETRYABLE_ERRORS.any? { |error_class| error.is_a?(error_class) }
836
+ end
617
837
  end
618
838
 
619
- # Categorize error for logging and operator integration
839
+ # Categorize error for logging and operator integration with precedence hierarchy
840
+ #
841
+ # Error precedence (highest to lowest):
842
+ # 1. Timeout errors (always win, even if wrapping network errors)
843
+ # 2. Validation errors (argument/input validation failures)
844
+ # 3. Network errors (connection, socket, DNS issues)
845
+ # 4. Execution errors (general task execution failures)
846
+ # 5. System errors (unexpected/unknown errors)
620
847
  #
621
848
  # @param error [Exception] The error that occurred
622
849
  # @return [Symbol] Error category
623
850
  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
851
+ # Precedence Level 1: Timeout errors always win
852
+ return :timeout if error.is_a?(Timeout::Error) || error.is_a?(TaskTimeoutError)
853
+
854
+ # Precedence Level 2: Validation errors
855
+ return :validation if error.is_a?(ArgumentError) || error.is_a?(TaskValidationError)
856
+
857
+ # For wrapped errors, check original error but preserve timeout precedence
858
+ return categorize_error(error.original_error) if error.is_a?(TaskExecutionError) && error.original_error
859
+
860
+ # Precedence Level 3: Network errors
861
+ return :network if error.is_a?(TaskNetworkError) || RETRYABLE_ERRORS.any? { |err_class| error.is_a?(err_class) }
862
+
863
+ # Precedence Level 4: General execution errors
864
+ :execution
637
865
  end
638
866
 
639
867
  # Calculate retry delay with exponential backoff
@@ -645,19 +873,24 @@ module LanguageOperator
645
873
  [delay, @config[:retry_delay_max]].min
646
874
  end
647
875
 
648
- # Create appropriate error type based on original error
876
+ # Create appropriate error type based on original error with precedence hierarchy
649
877
  #
650
878
  # @param task_name [Symbol] Name of the task
651
879
  # @param original_error [Exception] The original error
652
880
  # @return [TaskExecutionError] Appropriate error type
653
881
  def create_appropriate_error(task_name, original_error)
654
882
  case original_error
655
- when TaskTimeoutError
883
+ when TaskTimeoutError, TaskValidationError, TaskNetworkError
884
+ # Already wrapped in appropriate type
656
885
  original_error
657
886
  when Timeout::Error
658
- TaskTimeoutError.new(task_name, 'timed out', original_error)
887
+ # Always wrap timeout errors, preserving original context
888
+ TaskTimeoutError.new(task_name, "timed out after timeout (original: #{original_error.message})", original_error)
659
889
  when ArgumentError
660
890
  TaskValidationError.new(task_name, original_error.message, original_error)
891
+ when *RETRYABLE_ERRORS
892
+ # Wrap network errors for clear categorization
893
+ TaskNetworkError.new(task_name, "network error: #{original_error.message}", original_error)
661
894
  else
662
895
  TaskExecutionError.new(task_name, original_error.message, original_error)
663
896
  end
@@ -683,6 +916,56 @@ module LanguageOperator
683
916
  retryable: retryable_error?(error),
684
917
  backtrace: error.backtrace&.first(5))
685
918
  end
919
+
920
+ # Build task lookup cache for O(1) task resolution
921
+ #
922
+ # Pre-computes task metadata to avoid repeated type determinations
923
+ # and provide fast hash-based lookup instead of linear search.
924
+ #
925
+ # @return [Hash] Cache mapping task names to metadata
926
+ def build_task_cache
927
+ cache = {}
928
+ @tasks.each do |name, task|
929
+ # Guard against test doubles that don't respond to task methods
930
+ cache[name] = if task.respond_to?(:neural?) && task.respond_to?(:symbolic?)
931
+ {
932
+ definition: task,
933
+ type: determine_task_type(task),
934
+ neural: task.neural?,
935
+ symbolic: task.symbolic?
936
+ }
937
+ else
938
+ # Fallback for test doubles or invalid task objects
939
+ {
940
+ definition: task,
941
+ type: 'unknown',
942
+ neural: false,
943
+ symbolic: false
944
+ }
945
+ end
946
+ end
947
+ cache
948
+ end
949
+
950
+ # Build timeout cache for O(1) timeout resolution
951
+ #
952
+ # Pre-computes timeouts for all tasks to avoid repeated calculations
953
+ # during task execution hot path.
954
+ #
955
+ # @return [Hash] Cache mapping task names to timeout values
956
+ def build_timeout_cache
957
+ cache = {}
958
+ @tasks.each do |name, task|
959
+ # Guard against test doubles that don't respond to task methods
960
+ cache[name] = if task.respond_to?(:neural?) && task.respond_to?(:symbolic?)
961
+ task_timeout_for_type(task)
962
+ else
963
+ # Fallback timeout for test doubles or invalid task objects
964
+ @config[:timeout_symbolic]
965
+ end
966
+ end
967
+ cache
968
+ end
686
969
  end
687
970
  end
688
971
  end