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.
- checksums.yaml +4 -4
- data/.claude/commands/persona.md +9 -0
- data/.claude/commands/task.md +46 -1
- data/.rubocop.yml +13 -0
- data/.rubocop_custom/use_ux_helper.rb +44 -0
- data/CHANGELOG.md +8 -0
- data/Gemfile.lock +12 -1
- data/Makefile +26 -7
- data/Makefile.common +50 -0
- data/bin/aictl +8 -1
- data/components/agent/Gemfile +1 -1
- data/components/agent/bin/langop-agent +7 -0
- data/docs/README.md +58 -0
- data/docs/{dsl/best-practices.md → best-practices.md} +4 -4
- data/docs/cli-reference.md +274 -0
- data/docs/{dsl/constraints.md → constraints.md} +5 -5
- data/docs/how-agents-work.md +156 -0
- data/docs/installation.md +218 -0
- data/docs/quickstart.md +299 -0
- data/docs/understanding-generated-code.md +265 -0
- data/docs/using-tools.md +457 -0
- data/docs/webhooks.md +509 -0
- data/examples/ux_helpers_demo.rb +296 -0
- data/lib/language_operator/agent/base.rb +11 -1
- data/lib/language_operator/agent/executor.rb +23 -6
- data/lib/language_operator/agent/safety/safe_executor.rb +41 -39
- data/lib/language_operator/agent/task_executor.rb +346 -63
- data/lib/language_operator/agent/web_server.rb +110 -14
- data/lib/language_operator/agent/webhook_authenticator.rb +39 -5
- data/lib/language_operator/agent.rb +88 -2
- data/lib/language_operator/cli/base_command.rb +17 -11
- data/lib/language_operator/cli/command_loader.rb +72 -0
- data/lib/language_operator/cli/commands/agent/base.rb +837 -0
- data/lib/language_operator/cli/commands/agent/code_operations.rb +102 -0
- data/lib/language_operator/cli/commands/agent/helpers/cluster_llm_client.rb +116 -0
- data/lib/language_operator/cli/commands/agent/helpers/code_parser.rb +115 -0
- data/lib/language_operator/cli/commands/agent/helpers/synthesis_watcher.rb +96 -0
- data/lib/language_operator/cli/commands/agent/learning.rb +289 -0
- data/lib/language_operator/cli/commands/agent/lifecycle.rb +102 -0
- data/lib/language_operator/cli/commands/agent/logs.rb +125 -0
- data/lib/language_operator/cli/commands/agent/workspace.rb +327 -0
- data/lib/language_operator/cli/commands/cluster.rb +129 -84
- data/lib/language_operator/cli/commands/install.rb +1 -1
- data/lib/language_operator/cli/commands/model/base.rb +215 -0
- data/lib/language_operator/cli/commands/model/test.rb +165 -0
- data/lib/language_operator/cli/commands/persona.rb +16 -34
- data/lib/language_operator/cli/commands/quickstart.rb +3 -2
- data/lib/language_operator/cli/commands/status.rb +40 -67
- data/lib/language_operator/cli/commands/system/base.rb +44 -0
- data/lib/language_operator/cli/commands/system/exec.rb +147 -0
- data/lib/language_operator/cli/commands/system/helpers/llm_synthesis.rb +183 -0
- data/lib/language_operator/cli/commands/system/helpers/pod_manager.rb +212 -0
- data/lib/language_operator/cli/commands/system/helpers/template_loader.rb +57 -0
- data/lib/language_operator/cli/commands/system/helpers/template_validator.rb +174 -0
- data/lib/language_operator/cli/commands/system/schema.rb +92 -0
- data/lib/language_operator/cli/commands/system/synthesis_template.rb +151 -0
- data/lib/language_operator/cli/commands/system/synthesize.rb +224 -0
- data/lib/language_operator/cli/commands/system/validate_template.rb +130 -0
- data/lib/language_operator/cli/commands/tool/base.rb +271 -0
- data/lib/language_operator/cli/commands/tool/install.rb +255 -0
- data/lib/language_operator/cli/commands/tool/search.rb +69 -0
- data/lib/language_operator/cli/commands/tool/test.rb +115 -0
- data/lib/language_operator/cli/commands/use.rb +29 -6
- data/lib/language_operator/cli/errors/handler.rb +20 -17
- data/lib/language_operator/cli/errors/suggestions.rb +3 -5
- data/lib/language_operator/cli/errors/thor_errors.rb +55 -0
- data/lib/language_operator/cli/formatters/code_formatter.rb +4 -11
- data/lib/language_operator/cli/formatters/log_formatter.rb +8 -15
- data/lib/language_operator/cli/formatters/progress_formatter.rb +6 -8
- data/lib/language_operator/cli/formatters/status_formatter.rb +26 -7
- data/lib/language_operator/cli/formatters/table_formatter.rb +47 -36
- data/lib/language_operator/cli/formatters/value_formatter.rb +75 -0
- data/lib/language_operator/cli/helpers/cluster_context.rb +5 -3
- data/lib/language_operator/cli/helpers/kubeconfig_validator.rb +2 -1
- data/lib/language_operator/cli/helpers/label_utils.rb +97 -0
- data/lib/language_operator/{ux/concerns/provider_helpers.rb → cli/helpers/provider_helper.rb} +10 -29
- data/lib/language_operator/cli/helpers/schedule_builder.rb +21 -1
- data/lib/language_operator/cli/helpers/user_prompts.rb +19 -11
- data/lib/language_operator/cli/helpers/ux_helper.rb +538 -0
- data/lib/language_operator/{ux/concerns/input_validation.rb → cli/helpers/validation_helper.rb} +13 -66
- data/lib/language_operator/cli/main.rb +50 -40
- data/lib/language_operator/cli/templates/tools/generic.yaml +3 -0
- data/lib/language_operator/cli/wizards/agent_wizard.rb +12 -20
- data/lib/language_operator/cli/wizards/model_wizard.rb +271 -0
- data/lib/language_operator/cli/wizards/quickstart_wizard.rb +8 -34
- data/lib/language_operator/client/base.rb +28 -0
- data/lib/language_operator/client/config.rb +4 -1
- data/lib/language_operator/client/mcp_connector.rb +1 -1
- data/lib/language_operator/config/cluster_config.rb +3 -2
- data/lib/language_operator/config.rb +38 -11
- data/lib/language_operator/constants/kubernetes_labels.rb +80 -0
- data/lib/language_operator/constants.rb +13 -0
- data/lib/language_operator/dsl/http.rb +127 -10
- data/lib/language_operator/dsl.rb +153 -6
- data/lib/language_operator/errors.rb +50 -0
- data/lib/language_operator/kubernetes/client.rb +11 -6
- data/lib/language_operator/kubernetes/resource_builder.rb +58 -84
- data/lib/language_operator/templates/schema/agent_dsl_openapi.yaml +1 -1
- data/lib/language_operator/templates/schema/agent_dsl_schema.json +1 -1
- data/lib/language_operator/type_coercion.rb +118 -34
- data/lib/language_operator/utils/secure_path.rb +74 -0
- data/lib/language_operator/utils.rb +7 -0
- data/lib/language_operator/validators.rb +54 -2
- data/lib/language_operator/version.rb +1 -1
- data/synth/001/Makefile +10 -2
- data/synth/001/agent.rb +16 -15
- data/synth/001/output.log +27 -10
- data/synth/002/Makefile +10 -2
- data/synth/003/Makefile +1 -1
- data/synth/003/README.md +205 -133
- data/synth/003/agent.optimized.rb +66 -0
- data/synth/003/agent.synthesized.rb +41 -0
- metadata +111 -35
- data/docs/dsl/agent-reference.md +0 -604
- data/docs/dsl/mcp-integration.md +0 -1177
- data/docs/dsl/webhooks.md +0 -932
- data/docs/dsl/workflows.md +0 -744
- data/lib/language_operator/cli/commands/agent.rb +0 -1712
- data/lib/language_operator/cli/commands/model.rb +0 -366
- data/lib/language_operator/cli/commands/system.rb +0 -1259
- data/lib/language_operator/cli/commands/tool.rb +0 -654
- data/lib/language_operator/cli/formatters/optimization_formatter.rb +0 -226
- data/lib/language_operator/cli/helpers/pastel_helper.rb +0 -24
- data/lib/language_operator/learning/adapters/base_adapter.rb +0 -149
- data/lib/language_operator/learning/adapters/jaeger_adapter.rb +0 -221
- data/lib/language_operator/learning/adapters/signoz_adapter.rb +0 -435
- data/lib/language_operator/learning/adapters/tempo_adapter.rb +0 -239
- data/lib/language_operator/learning/optimizer.rb +0 -319
- data/lib/language_operator/learning/pattern_detector.rb +0 -260
- data/lib/language_operator/learning/task_synthesizer.rb +0 -288
- data/lib/language_operator/learning/trace_analyzer.rb +0 -285
- data/lib/language_operator/templates/task_synthesis.tmpl +0 -98
- data/lib/language_operator/ux/base.rb +0 -81
- data/lib/language_operator/ux/concerns/README.md +0 -155
- data/lib/language_operator/ux/concerns/headings.rb +0 -90
- data/lib/language_operator/ux/create_agent.rb +0 -255
- data/lib/language_operator/ux/create_model.rb +0 -267
- data/lib/language_operator/ux/quickstart.rb +0 -594
- data/synth/003/agent.rb +0 -41
- data/synth/003/output.log +0 -68
- /data/docs/{architecture/agent-runtime.md → agent-internals.md} +0 -0
- /data/docs/{dsl/chat-endpoints.md → chat-endpoints.md} +0 -0
- /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
|
-
#
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
120
|
+
task = task_info[:definition]
|
|
121
|
+
task_type = task_info[:type]
|
|
112
122
|
|
|
113
|
-
#
|
|
114
|
-
timeout ||=
|
|
123
|
+
# Use cached timeout if not explicitly provided
|
|
124
|
+
timeout ||= @task_timeouts[task_name_sym]
|
|
115
125
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
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
|
-
|
|
300
|
-
|
|
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
|
-
|
|
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
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
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
|
|
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\].*?(?=\{
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
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
|
-
|
|
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
|