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.
- 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 +344 -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],
|
|
@@ -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
|
-
#
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
123
|
+
task = task_info[:definition]
|
|
124
|
+
task_type = task_info[:type]
|
|
112
125
|
|
|
113
|
-
#
|
|
114
|
-
timeout ||=
|
|
126
|
+
# Use cached timeout if not explicitly provided
|
|
127
|
+
timeout ||= @task_timeouts[task_name_sym]
|
|
115
128
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
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
|
-
|
|
300
|
-
|
|
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
|
-
|
|
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
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
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
|
|
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\].*?(?=\{
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
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
|
-
|
|
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
|