language-operator 0.1.59 → 0.1.61

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 478995080aedadd2299a94fb609d3180634abd352440b985f4eece929275bdf8
4
- data.tar.gz: f0b890c5825447e6ead0ec172d9467afd59138f92350457e733cf269125e6757
3
+ metadata.gz: 614dadae712dd0b6c1b72314497c73cdbcba8aa247a63cf3d01dbd789b0becaf
4
+ data.tar.gz: 72ad29bec20d5fdc6da1f235cd054617567c2516522e1a45bbd52f09c15bdb65
5
5
  SHA512:
6
- metadata.gz: 55a51a910de5d8580b741690ca56a2ba489cf5424892b6a9db2b31756936b8c8c11b1fe7d85b9c042cd8c56f04ab6f117af027dc28bc6cd47c6e571cbf53d7e6
7
- data.tar.gz: fcf2dcad6af25f904c806d0a7b0216a7c6aa4d6d3803000096c33c350cb56050387dcb783394534226d1cc52def72628d57157dc54af60e694c229607c796147
6
+ metadata.gz: cd062a5b0420dd602f411dd30b1e6fd7aa3dd7bce77ddbf7e94af95eb6991b609f57aa797c59f519150e518e89ba0839b43ca5be9a216a576b26ad9bb3b05ba0
7
+ data.tar.gz: cfdaa40cc2ced23bd59343b6c995499e9c718c1782f8c8c50766d1180399adc2b40c6fafd21865bb0c9130f1e9c46713639faa7985afa739f28bc47d4e66f993
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- language-operator (0.1.59)
4
+ language-operator (0.1.61)
5
5
  faraday (~> 2.0)
6
6
  k8s-ruby (~> 0.17)
7
7
  mcp (~> 0.4)
@@ -2,7 +2,7 @@
2
2
 
3
3
  source 'https://rubygems.org'
4
4
 
5
- gem 'language-operator', '~> 0.1.46'
5
+ gem 'language-operator', '~> 0.1.59'
6
6
 
7
7
  # Agent-specific dependencies for autonomous execution
8
8
  gem 'concurrent-ruby', '~> 1.3'
@@ -29,6 +29,9 @@ module LanguageOperator
29
29
  def initialize(config)
30
30
  super
31
31
 
32
+ # Log version
33
+ logger.info "Language Operator v#{LanguageOperator::VERSION}"
34
+
32
35
  # Initialize OpenTelemetry
33
36
  LanguageOperator::Agent::Telemetry.configure
34
37
  otel_enabled = !ENV.fetch('OTEL_EXPORTER_OTLP_ENDPOINT', nil).nil?
@@ -234,19 +234,28 @@ module LanguageOperator
234
234
  # Execute the tool (it's a Proc/lambda wrapped by RubyLLM)
235
235
  result = tool.call(**params)
236
236
 
237
+ # Extract text from MCP Content objects
238
+ text_result = if result.is_a?(RubyLLM::MCP::Content)
239
+ result.text
240
+ elsif result.respond_to?(:map) && result.first.is_a?(RubyLLM::MCP::Content)
241
+ result.map(&:text).join
242
+ else
243
+ result
244
+ end
245
+
237
246
  logger.debug('Tool call completed',
238
247
  tool: tool_name_str,
239
- result_preview: result.is_a?(String) ? result[0..200] : result.class.name)
248
+ result_preview: text_result.is_a?(String) ? text_result[0..200] : text_result.class.name)
240
249
 
241
250
  # Try to parse JSON response if it looks like JSON
242
- if result.is_a?(String) && (result.strip.start_with?('{') || result.strip.start_with?('['))
243
- JSON.parse(result, symbolize_names: true)
251
+ if text_result.is_a?(String) && (text_result.strip.start_with?('{') || text_result.strip.start_with?('['))
252
+ JSON.parse(text_result, symbolize_names: true)
244
253
  else
245
- result
254
+ text_result
246
255
  end
247
256
  rescue JSON::ParserError
248
257
  # Not JSON, return as-is
249
- result
258
+ text_result
250
259
  end
251
260
 
252
261
  # Helper method for symbolic tasks to call LLM directly
@@ -380,8 +389,15 @@ module LanguageOperator
380
389
  thinking_preview: thinking_blocks.first&.[](0..500))
381
390
  end
382
391
 
383
- # Strip thinking tags that some models add (e.g., [THINK]...[/THINK])
384
- cleaned_text = response_text.gsub(%r{\[THINK\].*?\[/THINK\]}m, '').strip
392
+ # 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
394
+ logger.debug('Parsing neural response', task: task.name, response_length: response_text.length, response_start: response_text[0..100])
395
+
396
+ cleaned_text = response_text.gsub(%r{\[THINK\].*?\[/THINK\]}m, '')
397
+ .gsub(/\[THINK\].*?(?=\{|$)/m, '')
398
+ .strip
399
+
400
+ logger.debug('After stripping THINK tags', cleaned_length: cleaned_text.length, cleaned_start: cleaned_text[0..100])
385
401
 
386
402
  # Try to extract JSON from response
387
403
  # Look for JSON code blocks first
@@ -394,6 +410,8 @@ module LanguageOperator
394
410
  json_object_match ? json_object_match[0] : cleaned_text
395
411
  end
396
412
 
413
+ logger.debug('Extracted JSON text', json_length: json_text.length, json_start: json_text[0..100])
414
+
397
415
  # Parse JSON
398
416
  parsed = JSON.parse(json_text)
399
417
 
@@ -215,14 +215,16 @@ module LanguageOperator
215
215
  tracer.in_span("execute_tool.#{tool_name}", attributes: {
216
216
  'gen_ai.operation.name' => 'execute_tool',
217
217
  'gen_ai.tool.name' => tool_name,
218
+ 'gen_ai.tool.call.arguments' => arguments.to_json[0..1000],
218
219
  'gen_ai.tool.call.arguments.size' => arguments.to_json.bytesize
219
220
  }) do |span|
220
221
  # Execute the original tool
221
222
  result = original_tool.call(arguments)
222
223
 
223
- # Record the result size
224
+ # Record the result (truncated for telemetry)
224
225
  result_str = result.is_a?(String) ? result : result.to_json
225
226
  span.set_attribute('gen_ai.tool.call.result.size', result_str.bytesize)
227
+ span.set_attribute('gen_ai.tool.call.result', result_str[0..2000])
226
228
 
227
229
  result
228
230
  rescue StandardError => e
@@ -229,15 +229,16 @@ module LanguageOperator
229
229
  # Execute symbolic implementation
230
230
  #
231
231
  # @param inputs [Hash] Validated inputs
232
- # @param context [Object, nil] Execution context
232
+ # @param context [Object, nil] Execution context (TaskExecutor)
233
233
  # @return [Hash] Result from execute block
234
234
  def execute_symbolic(inputs, context)
235
- if @execute_block.arity == 1
236
- @execute_block.call(inputs)
237
- elsif @execute_block.arity == 2
238
- @execute_block.call(inputs, context)
235
+ if context
236
+ # Execute block in context's scope to make helper methods available
237
+ # (execute_tool, execute_task, execute_llm, etc.)
238
+ context.instance_exec(inputs, &@execute_block)
239
239
  else
240
- @execute_block.call
240
+ # Fallback for standalone execution without context
241
+ @execute_block.call(inputs)
241
242
  end
242
243
  end
243
244
 
@@ -125,7 +125,9 @@ module LanguageOperator
125
125
  .map do |tool_span|
126
126
  {
127
127
  tool_name: tool_span.dig(:attributes, 'gen_ai.tool.name'),
128
+ arguments: tool_span.dig(:attributes, 'gen_ai.tool.call.arguments'),
128
129
  arguments_size: tool_span.dig(:attributes, 'gen_ai.tool.call.arguments.size'),
130
+ result: tool_span.dig(:attributes, 'gen_ai.tool.call.result'),
129
131
  result_size: tool_span.dig(:attributes, 'gen_ai.tool.call.result.size')
130
132
  }
131
133
  end
@@ -108,7 +108,10 @@ module LanguageOperator
108
108
  params[:service] = extract_service_name(filter)
109
109
 
110
110
  # Add tag filters
111
- params[:tags] = { 'task.name' => filter[:task_name] }.to_json if filter[:task_name]
111
+ tags = {}
112
+ tags['task.name'] = filter[:task_name] if filter[:task_name]
113
+ tags['agent.name'] = filter[:agent_name] if filter[:agent_name]
114
+ params[:tags] = tags.to_json unless tags.empty?
112
115
 
113
116
  uri = URI.join(@endpoint, SEARCH_PATH)
114
117
  uri.query = URI.encode_www_form(params)
@@ -216,6 +216,9 @@ module LanguageOperator
216
216
  # Filter by task name (attribute name should NOT be quoted)
217
217
  expressions << "task.name = '#{filter[:task_name]}'" if filter[:task_name]
218
218
 
219
+ # Filter by agent name
220
+ expressions << "agent.name = '#{filter[:agent_name]}'" if filter[:agent_name]
221
+
219
222
  # Additional attribute filters
220
223
  if filter[:attributes].is_a?(Hash)
221
224
  filter[:attributes].each do |key, value|
@@ -76,6 +76,9 @@ module LanguageOperator
76
76
  # Filter by task name
77
77
  conditions << "span.\"task.name\" = \"#{escape_traceql_value(filter[:task_name])}\"" if filter[:task_name]
78
78
 
79
+ # Filter by agent name
80
+ conditions << "span.\"agent.name\" = \"#{escape_traceql_value(filter[:agent_name])}\"" if filter[:agent_name]
81
+
79
82
  # Additional attribute filters
80
83
  if filter[:attributes].is_a?(Hash)
81
84
  filter[:attributes].each do |key, value|
@@ -74,6 +74,7 @@ module LanguageOperator
74
74
  neural_tasks.each do |task|
75
75
  analysis = @trace_analyzer.analyze_patterns(
76
76
  task_name: task[:name],
77
+ agent_name: @agent_name,
77
78
  min_executions: min_executions,
78
79
  consistency_threshold: min_consistency,
79
80
  time_range: time_range
@@ -108,10 +109,10 @@ module LanguageOperator
108
109
  task_def = find_task_definition(task_name)
109
110
  raise ArgumentError, "Task '#{task_name}' not found" unless task_def
110
111
 
111
- analysis = @trace_analyzer.analyze_patterns(task_name: task_name)
112
+ analysis = @trace_analyzer.analyze_patterns(task_name: task_name, agent_name: @agent_name)
112
113
  raise ArgumentError, "No execution data found for task '#{task_name}'" unless analysis
113
114
 
114
- traces = @trace_analyzer.query_task_traces(task_name: task_name, limit: 20)
115
+ traces = @trace_analyzer.query_task_traces(task_name: task_name, agent_name: @agent_name, limit: 20)
115
116
  detection_result = @pattern_detector.detect_pattern(analysis_result: analysis) unless use_synthesis
116
117
 
117
118
  return propose_via_synthesis(task_name, task_def, analysis, traces) if should_use_synthesis?(use_synthesis, detection_result)
@@ -38,7 +38,7 @@ module LanguageOperator
38
38
  def initialize(llm_client:, validator:, logger: nil)
39
39
  @llm_client = llm_client
40
40
  @validator = validator
41
- @logger = logger || ::Logger.new($stdout, level: ::Logger::WARN)
41
+ @logger = logger || ::Logger.new($stdout, level: ::Logger::INFO)
42
42
  end
43
43
 
44
44
  # Synthesize deterministic code for a task
@@ -59,7 +59,7 @@ module LanguageOperator
59
59
  common_pattern: common_pattern
60
60
  )
61
61
 
62
- @logger.debug("Task synthesis prompt:\n#{prompt}")
62
+ @logger.info("Task synthesis prompt:\n#{prompt}")
63
63
 
64
64
  # Call LLM
65
65
  response = call_llm(prompt)
@@ -164,16 +164,43 @@ module LanguageOperator
164
164
  return '(no traces available)' if traces.empty?
165
165
 
166
166
  traces.first(10).each_with_index.map do |trace, idx|
167
- tool_sequence = trace[:tool_calls]&.map { |tc| tc[:tool_name] }&.join(' → ') || '(no tools)'
168
- duration = trace[:duration_ms]&.round(1) || 'unknown'
169
- inputs_summary = trace[:inputs]&.keys&.join(', ') || 'none'
170
-
171
- <<~TRACE
172
- ### Execution #{idx + 1}
173
- - **Tool Sequence:** #{tool_sequence}
174
- - **Duration:** #{duration}ms
175
- - **Inputs:** #{inputs_summary}
176
- TRACE
167
+ format_single_trace(trace, idx)
168
+ end.join("\n")
169
+ end
170
+
171
+ # Format a single trace execution
172
+ #
173
+ # @param trace [Hash] Single trace data
174
+ # @param idx [Integer] Trace index
175
+ # @return [String] Formatted trace
176
+ def format_single_trace(trace, idx)
177
+ tool_sequence = trace[:tool_calls]&.map { |tc| tc[:tool_name] }&.join(' → ') || '(no tools)'
178
+ duration = trace[:duration_ms]&.round(1) || 'unknown'
179
+ inputs_summary = trace[:inputs]&.keys&.join(', ') || 'none'
180
+ tool_details = format_tool_calls(trace[:tool_calls])
181
+
182
+ <<~TRACE
183
+ ### Execution #{idx + 1}
184
+ - **Tool Sequence:** #{tool_sequence}
185
+ - **Duration:** #{duration}ms
186
+ - **Inputs:** #{inputs_summary}
187
+ - **Tool Calls:**
188
+ #{tool_details}
189
+ TRACE
190
+ end
191
+
192
+ # Format tool call details
193
+ #
194
+ # @param tool_calls [Array<Hash>, nil] Tool call data
195
+ # @return [String] Formatted tool calls
196
+ def format_tool_calls(tool_calls)
197
+ return ' (no tool calls)' unless tool_calls&.any?
198
+
199
+ tool_calls.map do |tc|
200
+ details = " - #{tc[:tool_name]}"
201
+ details += "\n Args: #{tc[:arguments]}" if tc[:arguments]
202
+ details += "\n Result: #{tc[:result]}" if tc[:result]
203
+ details
177
204
  end.join("\n")
178
205
  end
179
206
 
@@ -60,10 +60,11 @@ module LanguageOperator
60
60
  # Query task execution traces from backend
61
61
  #
62
62
  # @param task_name [String] Name of task to query
63
+ # @param agent_name [String, nil] Optional agent name to filter by
63
64
  # @param limit [Integer] Maximum number of traces to return
64
65
  # @param time_range [Integer, Range<Time>] Time range in seconds or explicit range
65
66
  # @return [Array<Hash>] Task execution data
66
- def query_task_traces(task_name:, limit: 100, time_range: DEFAULT_TIME_RANGE)
67
+ def query_task_traces(task_name:, agent_name: nil, limit: 100, time_range: DEFAULT_TIME_RANGE)
67
68
  unless available?
68
69
  @logger.warn('No OTLP backend available, learning disabled')
69
70
  return []
@@ -71,8 +72,11 @@ module LanguageOperator
71
72
 
72
73
  range = normalize_time_range(time_range)
73
74
 
75
+ filter = { task_name: task_name }
76
+ filter[:agent_name] = agent_name if agent_name
77
+
74
78
  spans = @adapter.query_spans(
75
- filter: { task_name: task_name },
79
+ filter: filter,
76
80
  time_range: range,
77
81
  limit: limit
78
82
  )
@@ -90,13 +94,14 @@ module LanguageOperator
90
94
  # learned and converted to a symbolic implementation.
91
95
  #
92
96
  # @param task_name [String] Name of task to analyze
97
+ # @param agent_name [String, nil] Optional agent name to filter by
93
98
  # @param min_executions [Integer] Minimum executions required for analysis
94
99
  # @param consistency_threshold [Float] Required consistency (0.0-1.0)
95
100
  # @param time_range [Integer, Range<Time>, nil] Time range for query (seconds or explicit range)
96
101
  # @return [Hash, nil] Analysis results or nil if insufficient data
97
- def analyze_patterns(task_name:, min_executions: 10, consistency_threshold: DEFAULT_CONSISTENCY_THRESHOLD,
102
+ def analyze_patterns(task_name:, agent_name: nil, min_executions: 10, consistency_threshold: DEFAULT_CONSISTENCY_THRESHOLD,
98
103
  time_range: nil)
99
- executions = query_task_traces(task_name: task_name, limit: 1000, time_range: time_range || DEFAULT_TIME_RANGE)
104
+ executions = query_task_traces(task_name: task_name, agent_name: agent_name, limit: 1000, time_range: time_range || DEFAULT_TIME_RANGE)
100
105
 
101
106
  if executions.empty?
102
107
  @logger.info("No executions found for task '#{task_name}'")
@@ -2,7 +2,7 @@
2
2
  :openapi: 3.0.3
3
3
  :info:
4
4
  :title: Language Operator Agent API
5
- :version: 0.1.59
5
+ :version: 0.1.61
6
6
  :description: HTTP API endpoints exposed by Language Operator reactive agents
7
7
  :contact:
8
8
  :name: Language Operator
@@ -3,7 +3,7 @@
3
3
  "$id": "https://github.com/language-operator/language-operator-gem/schema/agent-dsl.json",
4
4
  "title": "Language Operator Agent DSL",
5
5
  "description": "Schema for defining autonomous AI agents using the Language Operator DSL",
6
- "version": "0.1.59",
6
+ "version": "0.1.61",
7
7
  "type": "object",
8
8
  "properties": {
9
9
  "name": {
@@ -86,12 +86,13 @@ Respond with valid JSON:
86
86
  instructions: "Keep the original instructions for documentation",
87
87
  inputs: { ... },
88
88
  outputs: { ... } do |inputs|
89
- # Your code here
90
- execute_tool(:tool_name, { arg: value })
89
+ # Helper methods available: execute_tool, execute_task, execute_llm
90
+ result = execute_tool(:tool_name, { arg: value })
91
91
  { output_key: result }
92
92
  end
93
93
  ```
94
- - Use `execute_tool(:tool_name, { arg: value })` for tool calls
94
+ - Use `execute_tool(:tool_name, { arg: value })` for MCP tool calls
95
+ - Use `execute_task(:task_name, inputs: { ... })` to call other tasks
95
96
  - Access inputs via the `inputs` hash parameter
96
97
  - Return a hash matching the output schema
97
98
  - Do NOT use system(), eval(), or other unsafe methods
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LanguageOperator
4
- VERSION = '0.1.59'
4
+ VERSION = '0.1.61'
5
5
  end
data/synth/003/Makefile CHANGED
@@ -1,6 +1,6 @@
1
1
  .PHONY: create code logs clean
2
2
 
3
- AGENT := synth-003
3
+ AGENT := s003
4
4
  AICTL := bundle exec ../../bin/aictl agent
5
5
  TOOLS := workspace
6
6
 
@@ -28,4 +28,4 @@ clean:
28
28
 
29
29
  save:
30
30
  $(AICTL) code $(AGENT) --raw > agent.rb
31
- $(AICTL) logs $(AGENT) > output.log
31
+ $(AICTL) logs $(AGENT) > output.log
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: language-operator
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.59
4
+ version: 0.1.61
5
5
  platform: ruby
6
6
  authors:
7
7
  - James Ryan