language-operator 0.1.56 → 0.1.58

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: ac246ba1f01262701f8e193f697a2df7753f23f376c71a8cbfc24ab87a59e088
4
- data.tar.gz: b75afe1475935a9b9ccbe3cd717d46e5b180ba123818fc53ff954f228f5663c8
3
+ metadata.gz: afc74193530a3f556debc6b31b0a43769da67eaf9c956d0a7b4f17477792bf94
4
+ data.tar.gz: ffc401b931b0b3bd427c92a36847986442b738f2508acd1206547aa4d14c92eb
5
5
  SHA512:
6
- metadata.gz: d66f44b6d599faaf6c0d93368ff03a50f50372c0924ad8b85f387c842c0302fa5e31c37472dbe484236fc917957ed33a2fa8b80502df0605a1d91909a5613b80
7
- data.tar.gz: 727ac1463af4516edcc552582909f77351f913fbdad2ae260bcc62f1f0f058006cf56dbb71ccd4910ec0f63aeb41ec7c812ea2ab3ee8f413dab5e620443cd5a1
6
+ metadata.gz: 5079fbb3621d2e3d0f00a9381bc7b6fa09c3e28ae029d3e6cde4b7e0ac42643c7881953dd66549f0379414c0a74053dfed301e832812d82902d0de553f791158
7
+ data.tar.gz: f24aea2d8cb473e7e9633fe713e663f637391c258695eacdd091b94dc12b206606408162f1d3cf3ae60484aa8097177bd4a858d392b132e07f6aaafadf63f35a
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- language-operator (0.1.56)
4
+ language-operator (0.1.58)
5
5
  faraday (~> 2.0)
6
6
  k8s-ruby (~> 0.17)
7
7
  mcp (~> 0.4)
@@ -107,6 +107,17 @@ module LanguageOperator
107
107
  )
108
108
  end
109
109
 
110
+ # Capture thinking blocks before stripping (for observability)
111
+ thinking_blocks = result_text.scan(%r{\[THINK\](.*?)\[/THINK\]}m).flatten
112
+ if thinking_blocks.any?
113
+ logger.info('LLM thinking captured',
114
+ event: 'llm_thinking',
115
+ iteration: @iteration_count,
116
+ thinking_steps: thinking_blocks.length,
117
+ thinking: thinking_blocks,
118
+ thinking_preview: thinking_blocks.first&.[](0..500))
119
+ end
120
+
110
121
  # Log the actual LLM response content (strip [THINK] blocks)
111
122
  cleaned_response = result_text.gsub(%r{\[THINK\].*?\[/THINK\]}m, '').strip
112
123
  response_preview = cleaned_response.length > 500 ? "#{cleaned_response[0..500]}..." : cleaned_response
@@ -155,15 +155,24 @@ module LanguageOperator
155
155
 
156
156
  logger.info('Sending prompt to LLM',
157
157
  task: task.name,
158
- prompt_length: prompt.length)
158
+ prompt_length: prompt.length,
159
+ available_tools: @agent.respond_to?(:tools) ? @agent.tools.map(&:name) : 'N/A')
159
160
 
160
161
  # Execute LLM call within traced span
161
162
  outputs = tracer.in_span('gen_ai.chat', attributes: neural_task_attributes(task, prompt, validated_inputs)) do |span|
162
163
  # Call LLM with full tool access
164
+ logger.debug('Calling LLM with prompt', task: task.name, prompt_preview: prompt[0..200])
163
165
  response = @agent.send_message(prompt)
164
166
 
167
+ # Check for tool calls and log details
168
+ has_tool_calls = response.respond_to?(:tool_calls) && response.tool_calls&.any?
169
+ tool_call_count = has_tool_calls ? response.tool_calls.length : 0
170
+
165
171
  logger.info('LLM response received, extracting content',
166
- task: task.name)
172
+ task: task.name,
173
+ response_class: response.class.name,
174
+ has_tool_calls: has_tool_calls,
175
+ tool_call_count: tool_call_count)
167
176
 
168
177
  response_text = response.is_a?(String) ? response : response.content
169
178
 
@@ -322,6 +331,17 @@ module LanguageOperator
322
331
  # @return [Hash] Parsed outputs
323
332
  # @raise [RuntimeError] If parsing fails
324
333
  def parse_neural_response(response_text, task)
334
+ # Capture thinking blocks before stripping (for observability)
335
+ thinking_blocks = response_text.scan(%r{\[THINK\](.*?)\[/THINK\]}m).flatten
336
+ if thinking_blocks.any?
337
+ logger.info('LLM thinking captured',
338
+ event: 'llm_thinking',
339
+ task: task.name,
340
+ thinking_steps: thinking_blocks.length,
341
+ thinking: thinking_blocks,
342
+ thinking_preview: thinking_blocks.first&.[](0..500))
343
+ end
344
+
325
345
  # Strip thinking tags that some models add (e.g., [THINK]...[/THINK])
326
346
  cleaned_text = response_text.gsub(%r{\[THINK\].*?\[/THINK\]}m, '').strip
327
347
 
@@ -367,8 +387,8 @@ module LanguageOperator
367
387
  def default_config
368
388
  {
369
389
  timeout_symbolic: 30.0, # Default timeout for symbolic tasks (seconds)
370
- timeout_neural: 240.0, # Default timeout for neural tasks (seconds)
371
- timeout_hybrid: 240.0, # Default timeout for hybrid tasks (seconds)
390
+ timeout_neural: 360.0, # Default timeout for neural tasks (seconds)
391
+ timeout_hybrid: 360.0, # Default timeout for hybrid tasks (seconds)
372
392
  max_retries: 3, # Default max retry attempts
373
393
  retry_delay_base: 1.0, # Base delay for exponential backoff
374
394
  retry_delay_max: 10.0 # Maximum delay between retries
@@ -44,6 +44,7 @@ module LanguageOperator
44
44
  option :persona, type: :string, desc: 'Persona to use for the agent'
45
45
  option :tools, type: :array, desc: 'Tools to make available to the agent'
46
46
  option :models, type: :array, desc: 'Models to make available to the agent'
47
+ option :workspace, type: :boolean, default: true, desc: 'Enable workspace for state persistence'
47
48
  option :dry_run, type: :boolean, default: false, desc: 'Preview what would be created without applying'
48
49
  option :wizard, type: :boolean, default: false, desc: 'Use interactive wizard mode'
49
50
  def create(description = nil)
@@ -98,7 +99,8 @@ module LanguageOperator
98
99
  cluster: ctx.namespace,
99
100
  persona: options[:persona],
100
101
  tools: options[:tools] || [],
101
- models: models
102
+ models: models,
103
+ workspace: options[:workspace]
102
104
  )
103
105
 
104
106
  # Dry-run mode: preview without applying
@@ -3,6 +3,7 @@
3
3
  require 'yaml'
4
4
  require 'erb'
5
5
  require 'net/http'
6
+ require 'json'
6
7
  require_relative '../base_command'
7
8
  require_relative '../formatters/progress_formatter'
8
9
  require_relative '../formatters/table_formatter'
@@ -66,6 +67,140 @@ module LanguageOperator
66
67
  end
67
68
  end
68
69
 
70
+ desc 'inspect NAME', 'Show detailed tool information'
71
+ option :cluster, type: :string, desc: 'Override current cluster context'
72
+ def inspect(name)
73
+ handle_command_error('inspect tool') do
74
+ tool = get_resource_or_exit('LanguageTool', name)
75
+
76
+ puts "Tool: #{name}"
77
+ puts " Cluster: #{ctx.name}"
78
+ puts " Namespace: #{ctx.namespace}"
79
+ puts
80
+
81
+ # Status
82
+ status = tool.dig('status', 'phase') || 'Unknown'
83
+ puts "Status: #{status}"
84
+ puts
85
+
86
+ # Spec details
87
+ puts 'Configuration:'
88
+ puts " Type: #{tool.dig('spec', 'type') || 'mcp'}"
89
+ puts " Image: #{tool.dig('spec', 'image')}"
90
+ puts " Deployment Mode: #{tool.dig('spec', 'deploymentMode') || 'sidecar'}"
91
+ puts " Port: #{tool.dig('spec', 'port') || 8080}"
92
+ puts " Replicas: #{tool.dig('spec', 'replicas') || 1}"
93
+ puts
94
+
95
+ # Resources
96
+ resources = tool.dig('spec', 'resources')
97
+ if resources
98
+ puts 'Resources:'
99
+ if resources['requests']
100
+ puts ' Requests:'
101
+ puts " CPU: #{resources['requests']['cpu']}"
102
+ puts " Memory: #{resources['requests']['memory']}"
103
+ end
104
+ if resources['limits']
105
+ puts ' Limits:'
106
+ puts " CPU: #{resources['limits']['cpu']}"
107
+ puts " Memory: #{resources['limits']['memory']}"
108
+ end
109
+ puts
110
+ end
111
+
112
+ # RBAC
113
+ rbac = tool.dig('spec', 'rbac')
114
+ if rbac && rbac['clusterRole']
115
+ rules = rbac.dig('clusterRole', 'rules') || []
116
+ puts "RBAC Permissions (#{rules.length} rules):"
117
+ rules.each_with_index do |rule, idx|
118
+ puts " Rule #{idx + 1}:"
119
+ puts " API Groups: #{rule['apiGroups'].join(', ')}"
120
+ puts " Resources: #{rule['resources'].join(', ')}"
121
+ puts " Verbs: #{rule['verbs'].join(', ')}"
122
+ end
123
+ puts
124
+ end
125
+
126
+ # Egress rules
127
+ egress = tool.dig('spec', 'egress') || []
128
+ if egress.any?
129
+ puts "Network Egress (#{egress.length} rules):"
130
+ egress.each_with_index do |rule, idx|
131
+ puts " Rule #{idx + 1}: #{rule['description']}"
132
+ puts " DNS: #{rule['dns'].join(', ')}" if rule['dns']
133
+ puts " CIDR: #{rule['cidr']}" if rule['cidr']
134
+ if rule['ports']
135
+ ports_str = rule['ports'].map { |p| "#{p['port']}/#{p['protocol']}" }.join(', ')
136
+ puts " Ports: #{ports_str}"
137
+ end
138
+ end
139
+ puts
140
+ end
141
+
142
+ # Try to fetch MCP capabilities
143
+ capabilities = fetch_mcp_capabilities(name, tool, ctx.namespace)
144
+ if capabilities && capabilities['tools'] && capabilities['tools'].any?
145
+ puts "MCP Tools (#{capabilities['tools'].length}):"
146
+ capabilities['tools'].each_with_index do |mcp_tool, idx|
147
+ tool_name = mcp_tool['name']
148
+
149
+ # Generate a meaningful name if empty
150
+ if tool_name.nil? || tool_name.empty?
151
+ # Try to derive from description (first few words)
152
+ if mcp_tool['description']
153
+ # Take first 3-4 words and convert to snake_case
154
+ words = mcp_tool['description'].split(/\s+/).first(4)
155
+ derived_name = words.join('_').downcase.gsub(/[^a-z0-9_]/, '')
156
+ tool_name = "#{name}_#{derived_name}".gsub(/__+/, '_').sub(/_$/, '')
157
+ else
158
+ tool_name = "#{name}_tool_#{idx + 1}"
159
+ end
160
+ end
161
+
162
+ puts " #{tool_name}"
163
+ puts " Description: #{mcp_tool['description']}" if mcp_tool['description']
164
+ next unless mcp_tool['inputSchema'] && mcp_tool['inputSchema']['properties']
165
+
166
+ params = mcp_tool['inputSchema']['properties'].keys
167
+ required = mcp_tool['inputSchema']['required'] || []
168
+ param_list = params.map { |p| required.include?(p) ? "#{p}*" : p }
169
+ puts " Parameters: #{param_list.join(', ')}"
170
+ end
171
+ puts ' (* = required)'
172
+ puts
173
+ end
174
+
175
+ # Get agents using this tool
176
+ agents = ctx.client.list_resources('LanguageAgent', namespace: ctx.namespace)
177
+ agents_using = agents.select do |agent|
178
+ tools = agent.dig('spec', 'tools') || []
179
+ tools.include?(name)
180
+ end
181
+
182
+ if agents_using.any?
183
+ puts "Agents using this tool (#{agents_using.count}):"
184
+ agents_using.each do |agent|
185
+ puts " - #{agent.dig('metadata', 'name')}"
186
+ end
187
+ else
188
+ puts 'No agents using this tool'
189
+ end
190
+
191
+ puts
192
+ puts 'Labels:'
193
+ labels = tool.dig('metadata', 'labels') || {}
194
+ if labels.empty?
195
+ puts ' (none)'
196
+ else
197
+ labels.each do |key, value|
198
+ puts " #{key}: #{value}"
199
+ end
200
+ end
201
+ end
202
+ end
203
+
69
204
  desc 'delete NAME', 'Delete a tool'
70
205
  option :cluster, type: :string, desc: 'Override current cluster context'
71
206
  option :force, type: :boolean, default: false, desc: 'Skip confirmation'
@@ -465,6 +600,54 @@ module LanguageOperator
465
600
  end
466
601
  end
467
602
  end
603
+
604
+ private
605
+
606
+ # Fetch MCP capabilities from a running tool server
607
+ #
608
+ # @param name [String] Tool name
609
+ # @param tool [Hash] Tool resource
610
+ # @param namespace [String] Kubernetes namespace
611
+ # @return [Hash, nil] MCP capabilities or nil if unavailable
612
+ def fetch_mcp_capabilities(name, tool, namespace)
613
+ return nil unless tool.dig('status', 'phase') == 'Running'
614
+
615
+ # Get the service endpoint
616
+ port = tool.dig('spec', 'port') || 80
617
+
618
+ # Try to query the MCP server using kubectl port-forward
619
+ # This is a fallback approach since we can't directly connect from CLI
620
+ begin
621
+ # Try to find a pod for this tool
622
+ label_selector = "app.kubernetes.io/name=#{name}"
623
+ pods = ctx.client.list_resources('Pod', namespace: namespace, label_selector: label_selector)
624
+
625
+ return nil if pods.empty?
626
+
627
+ pod_name = pods.first.dig('metadata', 'name')
628
+
629
+ # Query the MCP server using JSON-RPC protocol
630
+ # MCP uses the tools/list method to list available tools
631
+ json_rpc_request = {
632
+ jsonrpc: '2.0',
633
+ id: 1,
634
+ method: 'tools/list',
635
+ params: {}
636
+ }.to_json
637
+
638
+ result = `kubectl exec -n #{namespace} #{pod_name} -- curl -s -X POST \
639
+ http://localhost:#{port}/mcp/tools/list -H "Content-Type: application/json" \
640
+ -d '#{json_rpc_request}' 2>/dev/null`
641
+
642
+ return nil if result.empty?
643
+
644
+ response = JSON.parse(result)
645
+ response['result']
646
+ rescue StandardError
647
+ # Silently fail - capabilities are optional information
648
+ nil
649
+ end
650
+ end
468
651
  end
469
652
  end
470
653
  end
@@ -51,15 +51,15 @@ spec:
51
51
  <% rbac['clusterRole']['rules'].each do |rule| -%>
52
52
  - apiGroups:
53
53
  <% rule['apiGroups'].each do |group| -%>
54
- - <%= group %>
54
+ - "<%= group %>"
55
55
  <% end -%>
56
56
  resources:
57
57
  <% rule['resources'].each do |resource| -%>
58
- - <%= resource %>
58
+ - "<%= resource %>"
59
59
  <% end -%>
60
60
  verbs:
61
61
  <% rule['verbs'].each do |verb| -%>
62
- - <%= verb %>
62
+ - "<%= verb %>"
63
63
  <% end -%>
64
64
  <% end -%>
65
65
  <% end -%>
@@ -27,6 +27,13 @@ module LanguageOperator
27
27
  tool_count = client.tools.length
28
28
  all_tools.concat(client.tools)
29
29
 
30
+ # Debug: inspect tool objects
31
+ if @debug
32
+ logger.debug('MCP tool objects inspection',
33
+ server: server_config['name'],
34
+ tools_inspect: client.tools.map { |t| { class: t.class.name, name: t.name, methods: t.methods.grep(/name/) } })
35
+ end
36
+
30
37
  logger.info('MCP server connected',
31
38
  server: server_config['name'],
32
39
  tool_count: tool_count,
@@ -54,6 +61,9 @@ module LanguageOperator
54
61
 
55
62
  @chat.with_tools(*all_tools) unless all_tools.empty?
56
63
 
64
+ # Set up callbacks to log tool invocations
65
+ setup_tool_callbacks
66
+
57
67
  logger.info('Chat session initialized', with_tools: !all_tools.empty?)
58
68
  end
59
69
 
@@ -118,6 +128,27 @@ module LanguageOperator
118
128
 
119
129
  chat_params
120
130
  end
131
+
132
+ # Set up callbacks to log tool calls and results
133
+ #
134
+ # @return [void]
135
+ def setup_tool_callbacks
136
+ @chat.on_tool_call do |tool_call|
137
+ logger.info('Tool call initiated by LLM',
138
+ event: 'tool_call_initiated',
139
+ tool_name: tool_call.name,
140
+ tool_id: tool_call.id,
141
+ arguments: tool_call.arguments,
142
+ arguments_json: tool_call.arguments.to_json)
143
+ end
144
+
145
+ @chat.on_tool_result do |result|
146
+ logger.info('Tool call result received',
147
+ event: 'tool_result_received',
148
+ result: result,
149
+ result_preview: result.to_s[0..500])
150
+ end
151
+ end
121
152
  end
122
153
  end
123
154
  end
@@ -20,8 +20,8 @@ module LanguageOperator
20
20
 
21
21
  # Create a dynamic class that extends MCP::Tool
22
22
  Class.new(MCP::Tool) do
23
- # Set the tool name
24
- define_singleton_method(:name) { tool_def.name }
23
+ # Set the tool name using the MCP SDK's method
24
+ tool_name tool_def.name
25
25
 
26
26
  # Set the description
27
27
  description tool_def.description
@@ -26,6 +26,7 @@ module LanguageOperator
26
26
  # Instrumentation adds <5% overhead with default settings
27
27
  # Overhead may increase to ~10% with full data capture enabled
28
28
  #
29
+ # rubocop:disable Metrics/ModuleLength
29
30
  module TaskTracer
30
31
  # Maximum length for captured data before truncation
31
32
  MAX_CAPTURED_LENGTH = 1000
@@ -169,6 +170,11 @@ module LanguageOperator
169
170
  return unless response.respond_to?(:tool_calls)
170
171
  return unless response.tool_calls&.any?
171
172
 
173
+ logger&.info('Tool calls detected in LLM response',
174
+ event: 'tool_calls_detected',
175
+ tool_call_count: response.tool_calls.length,
176
+ tool_names: response.tool_calls.map { |tc| extract_tool_name(tc) })
177
+
172
178
  response.tool_calls.each do |tool_call|
173
179
  record_single_tool_call(tool_call, parent_span)
174
180
  end
@@ -182,11 +188,26 @@ module LanguageOperator
182
188
  # @param parent_span [OpenTelemetry::Trace::Span] Parent span
183
189
  def record_single_tool_call(tool_call, _parent_span)
184
190
  tool_name = extract_tool_name(tool_call)
191
+ tool_id = tool_call.respond_to?(:id) ? tool_call.id : nil
192
+
193
+ # Extract and log tool arguments
194
+ arguments = extract_tool_arguments(tool_call)
185
195
 
196
+ logger&.info('Tool invoked by LLM',
197
+ event: 'tool_call_invoked',
198
+ tool_name: tool_name,
199
+ tool_id: tool_id,
200
+ arguments: arguments,
201
+ arguments_json: (arguments.is_a?(Hash) ? JSON.generate(arguments) : arguments.to_s))
202
+
203
+ start_time = Time.now
186
204
  tracer.in_span("execute_tool #{tool_name}", attributes: build_tool_call_attributes(tool_call)) do |tool_span|
187
205
  # Tool execution already completed by ruby_llm
188
206
  # Just record the metadata
189
- record_tool_result(tool_call.result, tool_span) if tool_call.respond_to?(:result) && tool_call.result
207
+ if tool_call.respond_to?(:result) && tool_call.result
208
+ duration_ms = ((Time.now - start_time) * 1000).round(2)
209
+ record_tool_result(tool_call.result, tool_span, tool_name, tool_id, duration_ms)
210
+ end
190
211
  end
191
212
  rescue StandardError => e
192
213
  logger&.warn('Failed to record tool call span', error: e.message, tool: tool_name)
@@ -206,6 +227,34 @@ module LanguageOperator
206
227
  end
207
228
  end
208
229
 
230
+ # Extract tool arguments from tool call object
231
+ #
232
+ # @param tool_call [Object] Tool call object
233
+ # @return [Hash, String] Tool arguments
234
+ def extract_tool_arguments(tool_call)
235
+ if tool_call.respond_to?(:arguments)
236
+ args = tool_call.arguments
237
+ parse_json_args(args)
238
+ elsif tool_call.respond_to?(:function) && tool_call.function.respond_to?(:arguments)
239
+ args = tool_call.function.arguments
240
+ parse_json_args(args)
241
+ else
242
+ {}
243
+ end
244
+ end
245
+
246
+ # Parse JSON arguments safely
247
+ #
248
+ # @param args [String, Object] Arguments to parse
249
+ # @return [Hash, String] Parsed arguments or original
250
+ def parse_json_args(args)
251
+ return args unless args.is_a?(String)
252
+
253
+ JSON.parse(args)
254
+ rescue JSON::ParserError
255
+ args
256
+ end
257
+
209
258
  # Build attributes for tool call span
210
259
  #
211
260
  # @param tool_call [Object] Tool call object
@@ -244,13 +293,25 @@ module LanguageOperator
244
293
  #
245
294
  # @param result [Object] Tool call result
246
295
  # @param span [OpenTelemetry::Trace::Span] The span to update
247
- def record_tool_result(result, span)
296
+ # @param tool_name [String] Tool name (for logging)
297
+ # @param tool_id [String] Tool call ID (for logging)
298
+ # @param duration_ms [Float] Execution duration in milliseconds (for logging)
299
+ def record_tool_result(result, span, tool_name = nil, tool_id = nil, duration_ms = nil)
248
300
  result_str = result.is_a?(String) ? result : JSON.generate(result)
249
301
  span.set_attribute('gen_ai.tool.call.result.size', result_str.bytesize)
250
302
 
251
303
  if (sanitized_result = sanitize_data(result, :tool_results))
252
304
  span.set_attribute('gen_ai.tool.call.result', sanitized_result)
253
305
  end
306
+
307
+ # Log tool execution completion
308
+ logger&.info('Tool execution completed',
309
+ event: 'tool_call_completed',
310
+ tool_name: tool_name,
311
+ tool_id: tool_id,
312
+ result_size: result_str.bytesize,
313
+ result: sanitize_data(result, :tool_results),
314
+ duration_ms: duration_ms)
254
315
  rescue StandardError => e
255
316
  logger&.warn('Failed to record tool result', error: e.message)
256
317
  end
@@ -281,5 +342,6 @@ module LanguageOperator
281
342
  logger&.warn('Failed to record output metadata', error: e.message)
282
343
  end
283
344
  end
345
+ # rubocop:enable Metrics/ModuleLength
284
346
  end
285
347
  end
@@ -25,7 +25,7 @@ module LanguageOperator
25
25
 
26
26
  # Build a LanguageAgent resource
27
27
  def language_agent(name, instructions:, cluster: nil, schedule: nil, persona: nil, tools: [], models: [],
28
- mode: nil, labels: {})
28
+ mode: nil, workspace: true, labels: {})
29
29
  # Determine mode: reactive, scheduled, or autonomous
30
30
  spec_mode = mode || (schedule ? 'scheduled' : 'autonomous')
31
31
 
@@ -41,6 +41,8 @@ module LanguageOperator
41
41
  spec['toolRefs'] = tools.map { |t| { 'name' => t } } unless tools.empty?
42
42
  # Convert model names to modelRef objects
43
43
  spec['modelRefs'] = models.map { |m| { 'name' => m } } unless models.empty?
44
+ # Enable workspace by default for state persistence
45
+ spec['workspace'] = { 'enabled' => workspace } if workspace
44
46
 
45
47
  {
46
48
  'apiVersion' => 'langop.io/v1alpha1',
@@ -2,7 +2,7 @@
2
2
  :openapi: 3.0.3
3
3
  :info:
4
4
  :title: Language Operator Agent API
5
- :version: 0.1.56
5
+ :version: 0.1.58
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.56",
6
+ "version": "0.1.58",
7
7
  "type": "object",
8
8
  "properties": {
9
9
  "name": {
@@ -173,7 +173,7 @@ module LanguageOperator
173
173
  #
174
174
  # @param tool_def [LanguageOperator::Dsl::ToolDefinition] Tool definition from DSL
175
175
  # @return [Class] MCP::Tool subclass
176
- # rubocop:disable Metrics/MethodLength
176
+ # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
177
177
  def self.create_mcp_tool(tool_def)
178
178
  # Capture tool name and tracer for use in the dynamic class
179
179
  tool_name = tool_def.name
@@ -181,6 +181,9 @@ module LanguageOperator
181
181
 
182
182
  # Create a dynamic MCP::Tool class
183
183
  Class.new(MCP::Tool) do
184
+ # Set tool name (required for MCP protocol)
185
+ tool_name tool_def.name
186
+
184
187
  description tool_def.description || "Tool: #{tool_def.name}"
185
188
 
186
189
  # Build input schema from parameters
@@ -210,7 +213,9 @@ module LanguageOperator
210
213
  'tool.type' => 'custom'
211
214
  }) do |span|
212
215
  # Execute the tool's block
213
- result = @execute_block.call(params)
216
+ # Convert symbol keys to string keys for consistency with DSL expectations
217
+ string_params = params.transform_keys(&:to_s)
218
+ result = @execute_block.call(string_params)
214
219
 
215
220
  # Set success attribute
216
221
  span.set_attribute('tool.result', 'success')
@@ -232,6 +237,6 @@ module LanguageOperator
232
237
  end
233
238
  end
234
239
  end
235
- # rubocop:enable Metrics/MethodLength
240
+ # rubocop:enable Metrics/MethodLength, Metrics/AbcSize
236
241
  end
237
242
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LanguageOperator
4
- VERSION = '0.1.56'
4
+ VERSION = '0.1.58'
5
5
  end
data/synth/003/Makefile CHANGED
@@ -7,6 +7,9 @@ TOOLS := workspace
7
7
  create:
8
8
  cat agent.txt | $(AICTL) create --name $(AGENT) --tools "$(TOOLS)"
9
9
 
10
+ run:
11
+ kubectl create job --from=cronjob/$(AGENT) $(AGENT)-manual
12
+
10
13
  code:
11
14
  $(AICTL) code $(AGENT)
12
15
 
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.56
4
+ version: 0.1.58
5
5
  platform: ruby
6
6
  authors:
7
7
  - James Ryan