language-operator 0.1.36 → 0.1.37

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: dab6721a2a81cadf3c5a252b082c77cbe3dc9262480355637c4971e0bde231f3
4
- data.tar.gz: b11d038d3928f29fdb91e09e859e04112f4f46d846556e8582ff33ee37c074ae
3
+ metadata.gz: 8b57f326a81a7683900ef0700d54b97c296019f74e044dc886cc8aa02703e5a3
4
+ data.tar.gz: 7ba35359cc513dd5d53df60269f7052449453032c6ce749631e5dd6d80983a66
5
5
  SHA512:
6
- metadata.gz: 1b0ec3258b68eeb7469f8e6679dfb6f691465c404bc3c2a99c9fd99156ed96bd0e06279d0100ed495f13b134128a875e57eb3d50013139c36bb9cff23edf398b
7
- data.tar.gz: fcb46362bb7ef404b990ca17e061416f1da10a10d5e7c9b65188292797f8ea96b6a4f5e9d67f73587e891a225dae7d76bcb3c34500ec64b192e6d06c5d57b18b
6
+ metadata.gz: 62327e75b4357c55c1de51fbb56eebe4a16faede2b3ead5591a0dd2b8dc8760116130f9e9820ca47499aaa5ad3e285048862db7fef788d96416c4cc9fe123560
7
+ data.tar.gz: 0f63c9ca8e9523f6825a053cbd02a21ccd4103067b36ad190fb78316df051d0b6972a36917fd2a4ef9f26e449deaaaac487a83ae63e9f562fd9c317a8df2f490
data/Gemfile.lock CHANGED
@@ -1,7 +1,8 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- language-operator (0.1.36)
4
+ language-operator (0.1.37)
5
+ faraday (~> 2.0)
5
6
  k8s-ruby (~> 0.17)
6
7
  mcp (~> 0.4)
7
8
  opentelemetry-exporter-otlp (~> 0.27)
@@ -198,49 +198,58 @@ module LanguageOperator
198
198
  end
199
199
  end
200
200
 
201
- desc 'test-synthesis', 'Test agent synthesis from natural language instructions'
201
+ desc 'synthesize INSTRUCTIONS', 'Synthesize agent code from natural language instructions'
202
202
  long_desc <<-DESC
203
- Test the agent synthesis process by converting natural language instructions
203
+ Synthesize agent code by converting natural language instructions
204
204
  into Ruby DSL code without creating an actual agent.
205
205
 
206
+ This command uses a LanguageModel resource from your cluster to generate
207
+ agent code. If --model is not specified, the first available model will
208
+ be auto-selected.
209
+
206
210
  This command helps you validate your instructions and understand how the
207
211
  synthesis engine interprets them. Use --dry-run to see the prompt that
208
212
  would be sent to the LLM, or run without it to generate actual code.
209
213
 
210
214
  Examples:
211
215
  # Test with dry-run (show prompt only)
212
- aictl system test-synthesis --instructions "Monitor GitHub issues daily" --dry-run
216
+ aictl system synthesize "Monitor GitHub issues daily" --dry-run
217
+
218
+ # Generate code from instructions (auto-selects first available model)
219
+ aictl system synthesize "Send daily reports to Slack"
220
+
221
+ # Use a specific cluster model
222
+ aictl system synthesize "Process webhooks from GitHub" --model my-claude
213
223
 
214
- # Generate code from instructions
215
- aictl system test-synthesis --instructions "Send daily reports to Slack"
224
+ # Output raw code without formatting (useful for piping to files)
225
+ aictl system synthesize "Monitor logs" --raw > agent.rb
216
226
 
217
227
  # Specify custom agent name and tools
218
- aictl system test-synthesis \\
219
- --instructions "Process webhooks from GitHub" \\
228
+ aictl system synthesize "Process webhooks from GitHub" \\
220
229
  --agent-name github-processor \\
221
- --tools github,slack
222
-
223
- # Specify available models
224
- aictl system test-synthesis \\
225
- --instructions "Analyze logs every hour" \\
226
- --models gpt-4,claude-3-5-sonnet
230
+ --tools github,slack \\
231
+ --model my-gpt4
227
232
  DESC
228
- option :instructions, type: :string, required: true, desc: 'Natural language instructions for the agent'
229
233
  option :agent_name, type: :string, default: 'test-agent', desc: 'Name for the test agent'
230
234
  option :tools, type: :string, desc: 'Comma-separated list of available tools'
231
- option :models, type: :string, desc: 'Comma-separated list of available models'
235
+ option :models, type: :string, desc: 'Comma-separated list of available models (from cluster)'
236
+ option :model, type: :string, desc: 'Model to use for synthesis (defaults to first available in cluster)'
232
237
  option :dry_run, type: :boolean, default: false, desc: 'Show prompt without calling LLM'
233
- def test_synthesis
234
- handle_command_error('test synthesis') do
238
+ option :raw, type: :boolean, default: false, desc: 'Output only the raw code without formatting'
239
+ def synthesize(instructions)
240
+ handle_command_error('synthesize agent') do
241
+ # Select model to use for synthesis
242
+ selected_model = select_synthesis_model
243
+
235
244
  # Load synthesis template
236
245
  template_content = load_bundled_template('agent')
237
246
 
238
247
  # Detect temporal intent from instructions
239
- temporal_intent = detect_temporal_intent(options[:instructions])
248
+ temporal_intent = detect_temporal_intent(instructions)
240
249
 
241
250
  # Prepare template data
242
251
  template_data = {
243
- 'Instructions' => options[:instructions],
252
+ 'Instructions' => instructions,
244
253
  'AgentName' => options[:agent_name],
245
254
  'ToolsList' => format_tools_list(options[:tools]),
246
255
  'ModelsList' => format_models_list(options[:models]),
@@ -267,11 +276,8 @@ module LanguageOperator
267
276
  return
268
277
  end
269
278
 
270
- # Call LLM to generate code
271
- puts 'Generating agent code from instructions...'
272
- puts
273
-
274
- llm_response = call_llm_for_synthesis(rendered_prompt)
279
+ # Call LLM to generate code (no output - just do it)
280
+ llm_response = call_llm_for_synthesis(rendered_prompt, selected_model)
275
281
 
276
282
  # Extract Ruby code from response
277
283
  generated_code = extract_ruby_code(llm_response)
@@ -284,34 +290,19 @@ module LanguageOperator
284
290
  exit 1
285
291
  end
286
292
 
287
- # Display generated code
288
- puts 'Generated Code:'
289
- puts '=' * 80
290
- puts generated_code
291
- puts '=' * 80
292
- puts
293
-
294
- # Validate generated code
295
- puts 'Validating generated code...'
296
- validation_result = validate_code_against_schema(generated_code)
297
-
298
- if validation_result[:valid] && validation_result[:warnings].empty?
299
- Formatters::ProgressFormatter.success('✅ Code is valid - No issues found')
300
- elsif validation_result[:valid]
301
- Formatters::ProgressFormatter.success('✅ Code is valid - With warnings')
302
- puts
303
- validation_result[:warnings].each do |warn|
304
- puts " ⚠ #{warn[:message]}"
305
- end
306
- else
307
- Formatters::ProgressFormatter.error('❌ Code validation failed')
308
- puts
309
- validation_result[:errors].each do |err|
310
- puts " ✗ #{err[:message]}"
311
- end
293
+ # Handle raw output
294
+ if options[:raw]
295
+ puts generated_code
296
+ return
312
297
  end
313
298
 
314
- puts
299
+ # Display formatted code
300
+ require 'rouge'
301
+ formatter = Rouge::Formatters::Terminal256.new
302
+ lexer = Rouge::Lexers::Ruby.new
303
+ highlighted_code = formatter.format(lexer.lex(generated_code))
304
+
305
+ puts highlighted_code
315
306
  end
316
307
  end
317
308
 
@@ -449,68 +440,217 @@ module LanguageOperator
449
440
 
450
441
  # Format models list for template
451
442
  def format_models_list(models_str)
452
- # If not specified, try to detect from environment
443
+ # If not specified, try to detect from cluster
453
444
  if models_str.nil? || models_str.strip.empty?
454
445
  models = detect_available_models
455
446
  return models.map { |model| "- #{model}" }.join("\n") unless models.empty?
456
447
 
457
- return 'No models specified (configure ANTHROPIC_API_KEY or OPENAI_API_KEY)'
448
+ return 'No models available (run: aictl model list)'
458
449
  end
459
450
 
460
451
  models = models_str.split(',').map(&:strip)
461
452
  models.map { |model| "- #{model}" }.join("\n")
462
453
  end
463
454
 
464
- # Detect available models from environment
455
+ # Detect available models from cluster
465
456
  def detect_available_models
466
- models = []
467
- models << 'claude-3-5-sonnet-20241022' if ENV['ANTHROPIC_API_KEY']
468
- models << 'gpt-4-turbo' if ENV['OPENAI_API_KEY']
469
- models
457
+ models = ctx.client.list_resources('LanguageModel', namespace: ctx.namespace)
458
+ models.map { |m| m.dig('metadata', 'name') }
459
+ rescue StandardError => e
460
+ Formatters::ProgressFormatter.error("Failed to list models from cluster: #{e.message}")
461
+ []
470
462
  end
471
463
 
472
- # Call LLM to generate code from synthesis prompt
473
- def call_llm_for_synthesis(prompt)
474
- require 'ruby_llm'
464
+ # Select model to use for synthesis
465
+ def select_synthesis_model
466
+ # If --model option specified, use it
467
+ return options[:model] if options[:model]
468
+
469
+ # Otherwise, auto-select from available cluster models
470
+ available_models = detect_available_models
475
471
 
476
- # Check for API keys
477
- unless ENV['ANTHROPIC_API_KEY'] || ENV['OPENAI_API_KEY']
478
- Formatters::ProgressFormatter.error('No LLM credentials found')
472
+ if available_models.empty?
473
+ Formatters::ProgressFormatter.error('No models available in cluster')
479
474
  puts
480
- puts 'Please set one of the following environment variables:'
481
- puts ' - ANTHROPIC_API_KEY (for Claude models)'
482
- puts ' - OPENAI_API_KEY (for GPT models)'
475
+ puts 'Please create a model first:'
476
+ puts ' aictl model create'
477
+ puts
478
+ puts 'Or list existing models:'
479
+ puts ' aictl model list'
483
480
  exit 1
484
481
  end
485
482
 
486
- # Prefer Anthropic if available
487
- if ENV['ANTHROPIC_API_KEY']
488
- provider = :anthropic
489
- api_key = ENV['ANTHROPIC_API_KEY']
490
- model = 'claude-3-5-sonnet-20241022'
491
- else
492
- provider = :openai
493
- api_key = ENV.fetch('OPENAI_API_KEY', nil)
494
- model = 'gpt-4-turbo'
483
+ # Auto-select first available model (silently)
484
+ available_models.first
485
+ end
486
+
487
+ # Get endpoint for a cluster model
488
+ def get_model_endpoint(model_name)
489
+ # For cluster models, we use the service endpoint
490
+ # The service is typically named the same as the model and listens on port 4000
491
+ "http://#{model_name}.#{ctx.namespace}.svc.cluster.local:4000/v1"
492
+ end
493
+
494
+ # Call LLM to generate code from synthesis prompt using cluster model
495
+ def call_llm_for_synthesis(prompt, model_name)
496
+ require 'json'
497
+ require 'faraday'
498
+
499
+ # Get model resource
500
+ model = get_resource_or_exit('LanguageModel', model_name)
501
+ model_id = model.dig('spec', 'modelName')
502
+
503
+ # Get the model's pod
504
+ pod = get_model_pod(model_name)
505
+ pod_name = pod.dig('metadata', 'name')
506
+
507
+ # Set up port-forward to access the model pod
508
+ port_forward_pid = nil
509
+ local_port = find_available_port
510
+
511
+ begin
512
+ # Start kubectl port-forward in background
513
+ port_forward_pid = start_port_forward(pod_name, local_port, 4000)
514
+
515
+ # Wait for port-forward to be ready
516
+ wait_for_port(local_port)
517
+
518
+ # Build the JSON payload for the chat completion request
519
+ payload = {
520
+ model: model_id,
521
+ messages: [{ role: 'user', content: prompt }],
522
+ max_tokens: 4000,
523
+ temperature: 0.3
524
+ }
525
+
526
+ # Make HTTP request using Faraday
527
+ conn = Faraday.new(url: "http://localhost:#{local_port}") do |f|
528
+ f.request :json
529
+ f.response :json
530
+ f.adapter Faraday.default_adapter
531
+ f.options.timeout = 120
532
+ f.options.open_timeout = 10
533
+ end
534
+
535
+ response = conn.post('/v1/chat/completions', payload)
536
+
537
+ # Parse response
538
+ result = response.body
539
+
540
+ if result['error']
541
+ error_msg = result['error']['message'] || result['error']
542
+ raise "Model error: #{error_msg}"
543
+ elsif !result['choices'] || result['choices'].empty?
544
+ raise "Unexpected response format: #{result.inspect}"
545
+ end
546
+
547
+ # Extract the content from the first choice
548
+ result.dig('choices', 0, 'message', 'content')
549
+ rescue Faraday::TimeoutError
550
+ raise 'LLM request timed out after 120 seconds'
551
+ rescue Faraday::ConnectionFailed => e
552
+ raise "Failed to connect to model: #{e.message}"
553
+ rescue StandardError => e
554
+ Formatters::ProgressFormatter.error("LLM call failed: #{e.message}")
555
+ puts
556
+ puts "Make sure the model '#{model_name}' is running: kubectl get pods -n #{ctx.namespace}"
557
+ exit 1
558
+ ensure
559
+ # Clean up port-forward process
560
+ cleanup_port_forward(port_forward_pid) if port_forward_pid
495
561
  end
562
+ end
563
+
564
+ # Get the pod for a model
565
+ def get_model_pod(model_name)
566
+ # Get the deployment for the model
567
+ deployment = ctx.client.get_resource('Deployment', model_name, ctx.namespace)
568
+ labels = deployment.dig('spec', 'selector', 'matchLabels')
496
569
 
497
- # Create client and call LLM
498
- client = RubyLLM.new(provider: provider, api_key: api_key)
499
- messages = [{ role: 'user', content: prompt }]
570
+ raise "Deployment '#{model_name}' has no selector labels" if labels.nil?
500
571
 
501
- response = client.chat(messages, model: model, max_tokens: 4000, temperature: 0.3)
572
+ # Convert to hash if needed
573
+ labels_hash = labels.respond_to?(:to_h) ? labels.to_h : labels
574
+ raise "Deployment '#{model_name}' has empty selector labels" if labels_hash.empty?
502
575
 
503
- # Extract content from response
504
- if response.is_a?(Hash) && response.key?('content')
505
- response['content']
506
- elsif response.is_a?(String)
507
- response
508
- else
509
- response.to_s
576
+ label_selector = labels_hash.map { |k, v| "#{k}=#{v}" }.join(',')
577
+
578
+ # Find a running pod
579
+ pods = ctx.client.list_resources('Pod', namespace: ctx.namespace, label_selector: label_selector)
580
+ raise "No pods found for model '#{model_name}'" if pods.empty?
581
+
582
+ running_pod = pods.find do |pod|
583
+ pod.dig('status', 'phase') == 'Running' &&
584
+ pod.dig('status', 'conditions')&.any? { |c| c['type'] == 'Ready' && c['status'] == 'True' }
585
+ end
586
+
587
+ if running_pod.nil?
588
+ pod_phases = pods.map { |p| p.dig('status', 'phase') }.join(', ')
589
+ raise "No running pods found. Pod phases: #{pod_phases}"
590
+ end
591
+
592
+ running_pod
593
+ rescue K8s::Error::NotFound
594
+ raise "Model deployment '#{model_name}' not found"
595
+ end
596
+
597
+ # Find an available local port for port-forwarding
598
+ def find_available_port
599
+ require 'socket'
600
+
601
+ # Try ports in the range 14000-14999
602
+ (14_000..14_999).each do |port|
603
+ server = TCPServer.new('127.0.0.1', port)
604
+ server.close
605
+ return port
606
+ rescue Errno::EADDRINUSE
607
+ # Port in use, try next
608
+ next
609
+ end
610
+
611
+ raise 'No available ports found in range 14000-14999'
612
+ end
613
+
614
+ # Start kubectl port-forward in background
615
+ def start_port_forward(pod_name, local_port, remote_port)
616
+ require 'English'
617
+
618
+ cmd = "kubectl port-forward -n #{ctx.namespace} #{pod_name} #{local_port}:#{remote_port}"
619
+ pid = spawn(cmd, out: '/dev/null', err: '/dev/null')
620
+
621
+ # Detach so it runs in background
622
+ Process.detach(pid)
623
+
624
+ pid
625
+ end
626
+
627
+ # Wait for port-forward to be ready
628
+ def wait_for_port(port, max_attempts: 30)
629
+ require 'socket'
630
+
631
+ max_attempts.times do
632
+ socket = TCPSocket.new('127.0.0.1', port)
633
+ socket.close
634
+ return true
635
+ rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH
636
+ sleep 0.1
637
+ end
638
+
639
+ raise "Port-forward to localhost:#{port} failed to become ready after #{max_attempts} attempts"
640
+ end
641
+
642
+ # Clean up port-forward process
643
+ def cleanup_port_forward(pid)
644
+ return unless pid
645
+
646
+ begin
647
+ Process.kill('TERM', pid)
648
+ Process.wait(pid, Process::WNOHANG)
649
+ rescue Errno::ESRCH
650
+ # Process already gone
651
+ rescue Errno::ECHILD
652
+ # Process already reaped
510
653
  end
511
- rescue StandardError => e
512
- Formatters::ProgressFormatter.error("LLM call failed: #{e.message}")
513
- exit 1
514
654
  end
515
655
 
516
656
  # Extract Ruby code from LLM response
@@ -550,7 +690,7 @@ module LanguageOperator
550
690
  # Load bundled template from gem
551
691
  def load_bundled_template(type)
552
692
  filename = type == 'agent' ? 'agent_synthesis.tmpl' : 'persona_distillation.tmpl'
553
- template_path = File.join(__dir__, '..', '..', 'templates', 'examples', filename)
693
+ template_path = File.join(__dir__, '..', '..', 'templates', filename)
554
694
  File.read(template_path)
555
695
  end
556
696
 
@@ -0,0 +1,243 @@
1
+ You are generating Ruby DSL code for an autonomous agent in a Kubernetes operator.
2
+
3
+ {{if .ErrorContext}}
4
+ ## IMPORTANT: Self-Healing Synthesis - Attempt {{.AttemptNumber}}
5
+
6
+ The previous code synthesis encountered errors. Please analyze the errors below and generate CORRECTED code.
7
+
8
+ ### Previous Synthesis Failures
9
+
10
+ {{if .ErrorContext.ValidationErrors}}
11
+ **Validation Errors** (detected during code generation):
12
+ {{range .ErrorContext.ValidationErrors}}
13
+ - {{.}}
14
+ {{end}}
15
+ {{end}}
16
+
17
+ {{if .ErrorContext.RuntimeErrors}}
18
+ **Runtime Errors** (detected during execution):
19
+ {{range .ErrorContext.RuntimeErrors}}
20
+ - Time: {{.Timestamp}}
21
+ - Type: {{.ErrorType}}
22
+ - Message: {{.ErrorMessage}}
23
+ {{if .StackTrace}}
24
+ - Stack Trace:
25
+ {{range .StackTrace}}
26
+ {{.}}
27
+ {{end}}
28
+ {{end}}
29
+ - Exit Code: {{.ContainerExitCode}}
30
+ {{end}}
31
+ {{end}}
32
+
33
+ {{if .ErrorContext.LastCrashLog}}
34
+ **Last Container Logs** (before crash):
35
+ ```
36
+ {{.ErrorContext.LastCrashLog}}
37
+ ```
38
+ {{end}}
39
+
40
+ ### Your Task
41
+
42
+ 1. Carefully analyze each error above
43
+ 2. Identify the root cause of the failure
44
+ 3. Generate CORRECTED Ruby DSL code that addresses ALL errors
45
+ 4. Ensure the code:
46
+ - Fixes the specific errors mentioned
47
+ - Uses only available tools: {{.ToolsList}}
48
+ - Uses only available models: {{.ModelsList}}
49
+ - Follows the Language Operator DSL syntax exactly
50
+ - Does NOT use any dangerous Ruby methods (system, eval, etc.)
51
+
52
+ This is attempt {{.AttemptNumber}} of {{.MaxAttempts}}. The user is counting on you to get it right!
53
+
54
+ {{if .LastKnownGoodCode}}
55
+ ### Last Known Working Code (for reference)
56
+ ```ruby
57
+ {{.LastKnownGoodCode}}
58
+ ```
59
+ {{end}}
60
+
61
+ {{else}}
62
+ ## Agent Synthesis Request
63
+ {{end}}
64
+
65
+ **User Instructions:**
66
+ {{.Instructions}}
67
+
68
+ **Available Tools:**
69
+ {{.ToolsList}}
70
+
71
+ **Available Models:**
72
+ {{.ModelsList}}
73
+
74
+ **Agent Name:** {{.AgentName}}
75
+
76
+ **Detected Temporal Intent:** {{.TemporalIntent}}
77
+
78
+ **Runtime Context:**
79
+ - All agent messages and output are automatically logged to stdout
80
+ - Agents have access to a workspace directory for file operations
81
+ - LLM responses are captured and available in agent execution context
82
+
83
+ ## DSL v1 Reference Examples
84
+
85
+ Study these examples to understand the task/main model with organic functions:
86
+
87
+ ### Example 1: Simple Scheduled Agent (Fully Neural)
88
+ ```ruby
89
+ agent "daily-report" do
90
+ description "Generate daily sales report"
91
+ mode :scheduled
92
+ schedule "0 9 * * *"
93
+
94
+ task :fetch_sales,
95
+ instructions: "fetch yesterday's sales data from database",
96
+ inputs: {},
97
+ outputs: { sales: 'array', total: 'number' }
98
+
99
+ task :generate_report,
100
+ instructions: "create a markdown report summarizing the sales",
101
+ inputs: { sales: 'array', total: 'number' },
102
+ outputs: { report: 'string' }
103
+
104
+ main do |inputs|
105
+ sales_data = execute_task(:fetch_sales)
106
+ report = execute_task(:generate_report, inputs: sales_data)
107
+ report
108
+ end
109
+
110
+ output do |outputs|
111
+ puts outputs[:report]
112
+ end
113
+ end
114
+ ```
115
+
116
+ ### Example 2: Hybrid Neural-Symbolic Agent
117
+ ```ruby
118
+ agent "code-reviewer" do
119
+ description "Review pull requests"
120
+
121
+ # Symbolic task - deterministic tool call
122
+ task :fetch_pr_diff do |inputs|
123
+ {
124
+ diff: execute_tool('github', 'get_pr_diff', pr_number: inputs[:pr_number])
125
+ }
126
+ end
127
+
128
+ # Neural task - creative analysis
129
+ task :analyze_code,
130
+ instructions: "review code changes for bugs and improvements",
131
+ inputs: { diff: 'string' },
132
+ outputs: { issues: 'array', severity: 'string' }
133
+
134
+ # Symbolic task - deterministic logic
135
+ task :should_approve do |inputs|
136
+ critical = inputs[:issues].select { |i| i['severity'] == 'critical' }
137
+ { approve: critical.empty? }
138
+ end
139
+
140
+ main do |inputs|
141
+ pr_data = execute_task(:fetch_pr_diff, inputs: inputs)
142
+ analysis = execute_task(:analyze_code, inputs: pr_data)
143
+ decision = execute_task(:should_approve, inputs: analysis)
144
+ analysis.merge(decision)
145
+ end
146
+ end
147
+ ```
148
+
149
+ ### Example 3: Multi-Step Agent with Tools
150
+ ```ruby
151
+ agent "data-pipeline" do
152
+ description "ETL pipeline"
153
+
154
+ task :extract_data,
155
+ instructions: "extract data from the source database",
156
+ inputs: { source: 'string' },
157
+ outputs: { records: 'array', count: 'integer' }
158
+
159
+ task :transform_data,
160
+ instructions: "clean and normalize the records",
161
+ inputs: { records: 'array' },
162
+ outputs: { cleaned_records: 'array' }
163
+
164
+ task :load_data,
165
+ instructions: "load cleaned records into warehouse",
166
+ inputs: { cleaned_records: 'array' },
167
+ outputs: { success: 'boolean', loaded_count: 'integer' }
168
+
169
+ main do |inputs|
170
+ extracted = execute_task(:extract_data, inputs: inputs)
171
+ transformed = execute_task(:transform_data, inputs: extracted)
172
+ result = execute_task(:load_data, inputs: transformed)
173
+ result
174
+ end
175
+ end
176
+ ```
177
+
178
+ ## Your Task: Generate DSL v1 Agent
179
+
180
+ Using the examples above as reference, generate Ruby DSL code in this format:
181
+
182
+ ```ruby
183
+ require 'language_operator'
184
+
185
+ agent "{{.AgentName}}" do
186
+ description "Brief description extracted from instructions"
187
+ {{.PersonaSection}}{{.ScheduleSection}}
188
+ # Break down instructions into tasks
189
+ # Each task needs:
190
+ # - instructions: what to do (for neural tasks)
191
+ # - inputs: hash with parameter types
192
+ # - outputs: hash with result types
193
+
194
+ task :task_name,
195
+ instructions: "clear description of what this task does",
196
+ inputs: { param_name: 'type' },
197
+ outputs: { result_name: 'type' }
198
+
199
+ # For symbolic tasks (when logic is simple/deterministic), use code blocks:
200
+ # task :task_name do |inputs|
201
+ # { result: inputs[:param] * 2 }
202
+ # end
203
+
204
+ # REQUIRED: main block defines execution flow
205
+ main do |inputs|
206
+ result1 = execute_task(:task_name, inputs: { param_name: value })
207
+ # Chain tasks by passing outputs as inputs
208
+ result2 = execute_task(:another_task, inputs: result1)
209
+ result2 # Return final result
210
+ end
211
+
212
+ {{.ConstraintsSection}}
213
+
214
+ # Output handling
215
+ output do |outputs|
216
+ # Save results, send notifications, etc.
217
+ puts "Agent completed: #{outputs.inspect}"
218
+ end
219
+ end
220
+ ```
221
+
222
+ **Type System:**
223
+ - `'string'` - text values
224
+ - `'integer'` - whole numbers
225
+ - `'number'` - decimal numbers
226
+ - `'boolean'` - true/false
227
+ - `'array'` - lists
228
+ - `'hash'` - key-value objects
229
+ - `'any'` - any type
230
+
231
+ **Rules:**
232
+ 1. Generate ONLY the Ruby code within triple-backticks, no explanations before or after
233
+ {{.ScheduleRules}}
234
+ 3. Break down instructions into clear tasks with type-safe contracts
235
+ 4. REQUIRED: Always include a `main` block that calls tasks via `execute_task()`
236
+ 5. For simple deterministic tasks, use symbolic code blocks (do |inputs| ... end)
237
+ 6. For complex/creative tasks, use neural tasks with instructions
238
+ 7. Chain tasks by passing outputs as inputs
239
+ 8. Use available tools: {{.ToolsList}}
240
+ 9. Type schemas are REQUIRED for all tasks (inputs/outputs hashes)
241
+ 10. Use the agent name: "{{.AgentName}}"
242
+
243
+ Generate the code now:
@@ -2,7 +2,7 @@
2
2
  :openapi: 3.0.3
3
3
  :info:
4
4
  :title: Language Operator Agent API
5
- :version: 0.1.36
5
+ :version: 0.1.37
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.36",
6
+ "version": "0.1.37",
7
7
  "type": "object",
8
8
  "properties": {
9
9
  "name": {
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LanguageOperator
4
- VERSION = '0.1.36'
4
+ VERSION = '0.1.37'
5
5
  end
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.36
4
+ version: 0.1.37
5
5
  platform: ruby
6
6
  authors:
7
7
  - James Ryan
@@ -135,6 +135,20 @@ dependencies:
135
135
  - - "~>"
136
136
  - !ruby/object:Gem::Version
137
137
  version: '3.9'
138
+ - !ruby/object:Gem::Dependency
139
+ name: faraday
140
+ requirement: !ruby/object:Gem::Requirement
141
+ requirements:
142
+ - - "~>"
143
+ - !ruby/object:Gem::Version
144
+ version: '2.0'
145
+ type: :runtime
146
+ prerelease: false
147
+ version_requirements: !ruby/object:Gem::Requirement
148
+ requirements:
149
+ - - "~>"
150
+ - !ruby/object:Gem::Version
151
+ version: '2.0'
138
152
  - !ruby/object:Gem::Dependency
139
153
  name: k8s-ruby
140
154
  requirement: !ruby/object:Gem::Requirement
@@ -397,7 +411,6 @@ extra_rdoc_files: []
397
411
  files:
398
412
  - ".rubocop.yml"
399
413
  - CHANGELOG.md
400
- - CI_STATUS.md
401
414
  - Gemfile
402
415
  - Gemfile.lock
403
416
  - LICENSE
@@ -506,8 +519,8 @@ files:
506
519
  - lib/language_operator/retryable.rb
507
520
  - lib/language_operator/synthesis_test_harness.rb
508
521
  - lib/language_operator/templates/README.md
509
- - lib/language_operator/templates/examples/agent_synthesis.tmpl
510
- - lib/language_operator/templates/examples/persona_distillation.tmpl
522
+ - lib/language_operator/templates/agent_synthesis.tmpl
523
+ - lib/language_operator/templates/persona_distillation.tmpl
511
524
  - lib/language_operator/templates/schema/.gitkeep
512
525
  - lib/language_operator/templates/schema/CHANGELOG.md
513
526
  - lib/language_operator/templates/schema/agent_dsl_openapi.yaml
data/CI_STATUS.md DELETED
@@ -1,56 +0,0 @@
1
- # CI Integration Test Status
2
-
3
- ## Summary
4
-
5
- The CI integration tests are significantly improved from their previous completely broken state.
6
-
7
- ### Fixed Issues
8
-
9
- 1. **Numeric Constant Error** ✅
10
- - **Problem**: SafeExecutor sandbox was blocking access to Ruby type constants (Numeric, Integer, Float, etc.)
11
- - **Solution**: Inject type constants into the evaluated code scope in SafeExecutor#eval
12
- - **Impact**: All symbolic tasks using type checking now work correctly
13
-
14
- 2. **Neural Task Connection Errors** ✅
15
- - **Problem**: Agent tried to connect to real LLM when INTEGRATION_MOCK_LLM=true, failing with "Not connected"
16
- - **Solution**: Create mock chat object in create_test_agent when mocking is enabled
17
- - **Impact**: Neural tasks can now execute without real LLM connection
18
-
19
- 3. **Deep Symbol Keys** ✅
20
- - **Problem**: Nested hashes in neural task outputs had string keys, tests expected symbol keys
21
- - **Solution**: Implement deep_symbolize_keys in TaskExecutor#parse_neural_response
22
- - **Impact**: Nested hash structures now match test expectations
23
-
24
- 4. **Multi-Provider LLM Support** ✅
25
- - **Problem**: Tests only supported OpenAI
26
- - **Solution**: Added support for SYNTHESIS_*, ANTHROPIC_*, and OPENAI_API_KEY env vars
27
- - **Impact**: Tests can use local models, Claude, or OpenAI
28
-
29
- ### Current Test Status
30
-
31
- **Passing Tests** (28/72, 39%):
32
- - ✅ Comprehensive DSL v1 Integration (all 4 scenarios)
33
- - ✅ Symbolic Task Execution (complete)
34
- - ✅ Error Handling (skipped DSL syntax issues)
35
- - ✅ Type Coercion (partial)
36
-
37
- **Failing Tests** (44/72, 61%):
38
- - ❌ Neural Task Execution - individual mocks don't match all output schemas
39
- - ❌ Hybrid Agent Execution - some neural tasks failing
40
- - ❌ Parallel Execution - some neural tasks failing
41
-
42
- **Pending Tests**: 20 (performance benchmarks disabled)
43
-
44
- ### Recommendations
45
-
46
- For full CI coverage with mocked LLMs, consider:
47
- 1. Use real LLM in CI (with API key secrets) instead of mocking
48
- 2. Add schema-aware mock generation based on task output definitions
49
- 3. Add individual mocks for each failing neural task (tedious but thorough)
50
-
51
- ### Bottom Line
52
-
53
- **Before**: 100% failure rate - all tests broken
54
- **After**: 39% pass rate with core functionality working
55
-
56
- The most critical tests (comprehensive integration) now pass. The CI is in a MUCH better state than before.
@@ -1,133 +0,0 @@
1
- You are generating Ruby DSL code for an autonomous agent in a Kubernetes operator.
2
-
3
- {{if .ErrorContext}}
4
- ## IMPORTANT: Self-Healing Synthesis - Attempt {{.AttemptNumber}}
5
-
6
- The previous code synthesis encountered errors. Please analyze the errors below and generate CORRECTED code.
7
-
8
- ### Previous Synthesis Failures
9
-
10
- {{if .ErrorContext.ValidationErrors}}
11
- **Validation Errors** (detected during code generation):
12
- {{range .ErrorContext.ValidationErrors}}
13
- - {{.}}
14
- {{end}}
15
- {{end}}
16
-
17
- {{if .ErrorContext.RuntimeErrors}}
18
- **Runtime Errors** (detected during execution):
19
- {{range .ErrorContext.RuntimeErrors}}
20
- - Time: {{.Timestamp}}
21
- - Type: {{.ErrorType}}
22
- - Message: {{.ErrorMessage}}
23
- {{if .StackTrace}}
24
- - Stack Trace:
25
- {{range .StackTrace}}
26
- {{.}}
27
- {{end}}
28
- {{end}}
29
- - Exit Code: {{.ContainerExitCode}}
30
- {{end}}
31
- {{end}}
32
-
33
- {{if .ErrorContext.LastCrashLog}}
34
- **Last Container Logs** (before crash):
35
- ```
36
- {{.ErrorContext.LastCrashLog}}
37
- ```
38
- {{end}}
39
-
40
- ### Your Task
41
-
42
- 1. Carefully analyze each error above
43
- 2. Identify the root cause of the failure
44
- 3. Generate CORRECTED Ruby DSL code that addresses ALL errors
45
- 4. Ensure the code:
46
- - Fixes the specific errors mentioned
47
- - Uses only available tools: {{.ToolsList}}
48
- - Uses only available models: {{.ModelsList}}
49
- - Follows the Language Operator DSL syntax exactly
50
- - Does NOT use any dangerous Ruby methods (system, eval, etc.)
51
-
52
- This is attempt {{.AttemptNumber}} of {{.MaxAttempts}}. The user is counting on you to get it right!
53
-
54
- {{if .LastKnownGoodCode}}
55
- ### Last Known Working Code (for reference)
56
- ```ruby
57
- {{.LastKnownGoodCode}}
58
- ```
59
- {{end}}
60
-
61
- {{else}}
62
- ## Agent Synthesis Request
63
- {{end}}
64
-
65
- **User Instructions:**
66
- {{.Instructions}}
67
-
68
- **Available Tools:**
69
- {{.ToolsList}}
70
-
71
- **Available Models:**
72
- {{.ModelsList}}
73
-
74
- **Agent Name:** {{.AgentName}}
75
-
76
- **Detected Temporal Intent:** {{.TemporalIntent}}
77
-
78
- **Runtime Context:**
79
- - All agent messages and output are automatically logged to stdout
80
- - Agents have access to a workspace directory for file operations
81
- - LLM responses are captured and available in agent execution context
82
-
83
- Generate Ruby DSL code using this exact format (wrapped in triple-backticks with ruby):
84
-
85
- ```ruby
86
- require 'language_operator'
87
-
88
- agent "{{.AgentName}}" do
89
- description "Brief description extracted from instructions"
90
- {{.PersonaSection}}{{.ScheduleSection}}
91
- # Extract objectives from instructions
92
- objectives [
93
- "First objective",
94
- "Second objective"
95
- ]
96
-
97
- # REQUIRED: Define workflow with at least one step
98
- workflow do
99
- # Use tools when available
100
- step :step_name, tool: "tool_name", params: {key: "value"}
101
-
102
- # Or use execute blocks for custom Ruby code (simple logging, calculations, etc.)
103
- step :custom_step do
104
- execute do
105
- puts "Custom output from Ruby code"
106
- { result: "done" }
107
- end
108
- end
109
-
110
- # Chain steps with dependencies if needed
111
- step :another_step, depends_on: :step_name
112
- end
113
-
114
- {{.ConstraintsSection}}
115
-
116
- # Output configuration (if workspace enabled)
117
- output do
118
- workspace "results/output.txt"
119
- end
120
- end
121
- ```
122
-
123
- **Rules:**
124
- 1. Generate ONLY the Ruby code within triple-backticks, no explanations before or after
125
- {{.ScheduleRules}}
126
- 5. Break down instructions into clear, actionable objectives
127
- 6. REQUIRED: Always include a workflow block with at least one step (even for simple single-action agents)
128
- 7. For simple tasks (logging, calculations), use a single step with an execute block containing Ruby code
129
- 8. For complex tasks, use multiple steps with tools or execute blocks
130
- 9. Use available tools in workflow steps when tools are provided
131
- 10. Use the agent name: "{{.AgentName}}"
132
-
133
- Generate the code now: