language-operator 0.1.63 → 0.1.66

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.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/.plan.md +127 -0
  3. data/.rspec +3 -0
  4. data/Gemfile +2 -0
  5. data/Gemfile.lock +4 -1
  6. data/Makefile +34 -80
  7. data/README.md +20 -1
  8. data/components/agent/Gemfile +1 -1
  9. data/docs/cheat-sheet.md +173 -0
  10. data/docs/observability.md +208 -0
  11. data/lib/language_operator/agent/base.rb +10 -1
  12. data/lib/language_operator/agent/event_config.rb +172 -0
  13. data/lib/language_operator/agent/safety/ast_validator.rb +1 -1
  14. data/lib/language_operator/agent/safety/safe_executor.rb +5 -1
  15. data/lib/language_operator/agent/task_executor.rb +97 -7
  16. data/lib/language_operator/agent/telemetry.rb +25 -3
  17. data/lib/language_operator/agent/web_server.rb +6 -9
  18. data/lib/language_operator/agent.rb +24 -14
  19. data/lib/language_operator/cli/commands/agent/base.rb +155 -64
  20. data/lib/language_operator/cli/commands/agent/code_operations.rb +157 -16
  21. data/lib/language_operator/cli/commands/cluster.rb +2 -2
  22. data/lib/language_operator/cli/commands/status.rb +2 -2
  23. data/lib/language_operator/cli/commands/system/synthesize.rb +1 -1
  24. data/lib/language_operator/cli/errors/suggestions.rb +1 -1
  25. data/lib/language_operator/cli/formatters/value_formatter.rb +1 -1
  26. data/lib/language_operator/cli/helpers/ux_helper.rb +3 -4
  27. data/lib/language_operator/config.rb +3 -3
  28. data/lib/language_operator/constants/kubernetes_labels.rb +2 -2
  29. data/lib/language_operator/constants.rb +1 -0
  30. data/lib/language_operator/dsl/task_definition.rb +18 -7
  31. data/lib/language_operator/instrumentation/task_tracer.rb +44 -3
  32. data/lib/language_operator/kubernetes/client.rb +112 -1
  33. data/lib/language_operator/templates/schema/CHANGELOG.md +28 -0
  34. data/lib/language_operator/templates/schema/agent_dsl_openapi.yaml +1 -1
  35. data/lib/language_operator/templates/schema/agent_dsl_schema.json +1 -1
  36. data/lib/language_operator/type_coercion.rb +22 -8
  37. data/lib/language_operator/version.rb +1 -1
  38. data/synth/002/agent.rb +23 -12
  39. data/synth/002/output.log +88 -15
  40. data/synth/003/Makefile +17 -4
  41. data/synth/003/agent.txt +1 -1
  42. data/synth/004/Makefile +54 -0
  43. data/synth/004/README.md +281 -0
  44. data/synth/004/instructions.txt +1 -0
  45. metadata +11 -6
  46. data/lib/language_operator/cli/commands/agent/learning.rb +0 -289
  47. data/synth/003/agent.optimized.rb +0 -66
  48. data/synth/003/agent.synthesized.rb +0 -41
@@ -4,6 +4,7 @@ require_relative 'agent/base'
4
4
  require_relative 'agent/executor'
5
5
  require_relative 'agent/task_executor'
6
6
  require_relative 'agent/web_server'
7
+ require_relative 'agent/instrumentation'
7
8
  require_relative 'dsl'
8
9
  require_relative 'logger'
9
10
 
@@ -24,6 +25,8 @@ module LanguageOperator
24
25
  # agent.execute_goal("Summarize daily news")
25
26
  # rubocop:disable Metrics/ModuleLength
26
27
  module Agent
28
+ extend LanguageOperator::Agent::Instrumentation
29
+
27
30
  # Module-level logger for Agent framework
28
31
  @logger = LanguageOperator::Logger.new(component: 'Agent')
29
32
 
@@ -215,22 +218,29 @@ module LanguageOperator
215
218
  agent: agent_def.name,
216
219
  task_count: agent_def.tasks.size)
217
220
 
218
- # Get inputs from environment or default to empty hash
219
- inputs = {}
220
-
221
- # Execute main block with task executor as context
222
- result = agent_def.main.call(inputs, task_executor)
223
-
224
- logger.info('Main block execution completed',
225
- result: result)
221
+ # Execute main block within agent_executor span for learning system integration
222
+ with_span('agent_executor', attributes: {
223
+ 'agent.name' => agent_def.name,
224
+ 'agent.task_count' => agent_def.tasks.size,
225
+ 'agent.mode' => ENV.fetch('AGENT_MODE', 'unknown')
226
+ }) do
227
+ # Get inputs from environment or default to empty hash
228
+ inputs = {}
229
+
230
+ # Execute main block with task executor as context
231
+ result = agent_def.main.call(inputs, task_executor)
232
+
233
+ logger.info('Main block execution completed',
234
+ result: result)
235
+
236
+ # Call output handler if defined
237
+ if agent_def.output
238
+ logger.debug('Executing output handler', outputs: result)
239
+ execute_output_handler(agent_def, result, task_executor)
240
+ end
226
241
 
227
- # Call output handler if defined
228
- if agent_def.output
229
- logger.debug('Executing output handler', outputs: result)
230
- execute_output_handler(agent_def, result, task_executor)
242
+ result
231
243
  end
232
-
233
- result
234
244
  end
235
245
 
236
246
  # Execute main block (DSL v1) in persistent mode for autonomous agents
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'thor'
4
+ require 'json'
4
5
  require_relative '../../command_loader'
5
6
  require_relative '../../wizards/agent_wizard'
6
7
 
@@ -9,7 +10,6 @@ require_relative 'workspace'
9
10
  require_relative 'code_operations'
10
11
  require_relative 'logs'
11
12
  require_relative 'lifecycle'
12
- require_relative 'learning'
13
13
 
14
14
  # Include helper modules
15
15
  require_relative 'helpers/cluster_llm_client'
@@ -35,7 +35,6 @@ module LanguageOperator
35
35
  include CodeOperations
36
36
  include Logs
37
37
  include Lifecycle
38
- include Learning
39
38
 
40
39
  # NOTE: Core commands (create, list, inspect, delete) will be added below
41
40
  # This file is a placeholder for the refactoring process
@@ -173,6 +172,9 @@ module LanguageOperator
173
172
  # Main agent information
174
173
  puts
175
174
  status = agent.dig('status', 'phase') || 'Unknown'
175
+ creation_timestamp = agent.dig('metadata', 'creationTimestamp')
176
+ formatted_created = creation_timestamp ? Formatters::ValueFormatter.time_ago(Time.parse(creation_timestamp)) : nil
177
+
176
178
  format_agent_details(
177
179
  name: name,
178
180
  namespace: ctx.namespace,
@@ -180,8 +182,8 @@ module LanguageOperator
180
182
  status: format_status(status),
181
183
  mode: agent.dig('spec', 'executionMode') || 'autonomous',
182
184
  schedule: agent.dig('spec', 'schedule'),
183
- persona: agent.dig('spec', 'persona'),
184
- created: agent.dig('metadata', 'creationTimestamp')
185
+ persona: agent.dig('spec', 'persona') || 'None',
186
+ created: formatted_created
185
187
  )
186
188
  puts
187
189
 
@@ -189,9 +191,8 @@ module LanguageOperator
189
191
  mode = agent.dig('spec', 'executionMode') || 'autonomous'
190
192
  if mode == 'scheduled'
191
193
  exec_data = get_execution_data(name, ctx)
192
-
194
+
193
195
  exec_rows = {
194
- 'Total Runs' => exec_data[:total_runs],
195
196
  'Last Run' => exec_data[:last_run] || 'Never'
196
197
  }
197
198
  exec_rows['Next Run'] = exec_data[:next_run] || 'N/A' if agent.dig('spec', 'schedule')
@@ -200,6 +201,10 @@ module LanguageOperator
200
201
  puts
201
202
  end
202
203
 
204
+ # Learning status
205
+ display_learning_section(agent, name, ctx)
206
+ puts
207
+
203
208
  # Resources
204
209
  resources = agent.dig('spec', 'resources')
205
210
  if resources
@@ -302,62 +307,71 @@ module LanguageOperator
302
307
  Formatters::ProgressFormatter.with_spinner("Deleting agent '#{name}'") do
303
308
  ctx.client.delete_resource(RESOURCE_AGENT, name, ctx.namespace)
304
309
  end
310
+
311
+ # Verify deletion completed
312
+ verify_agent_deletion(ctx, name)
305
313
  end
306
314
  end
307
315
 
308
- desc 'versions NAME', 'Show ConfigMap versions managed by operator'
309
- long_desc <<-DESC
310
- List the versioned ConfigMaps created by the operator for an agent.
316
+ private
311
317
 
312
- Shows the automatic optimization history and available versions for rollback.
318
+ # Display learning status section in agent inspect
319
+ def display_learning_section(agent, _name, _ctx)
320
+ annotations = agent.dig('metadata', 'annotations')
321
+ annotations = annotations.respond_to?(:to_h) ? annotations.to_h : (annotations || {})
313
322
 
314
- Examples:
315
- aictl agent versions my-agent
316
- aictl agent versions my-agent --cluster production
317
- DESC
318
- option :cluster, type: :string, desc: 'Override current cluster context'
319
- def versions(name)
320
- handle_command_error('list agent versions') do
321
- ctx = CLI::Helpers::ClusterContext.from_options(options)
323
+ # Determine learning state
324
+ learning_enabled = !annotations.key?(Constants::KubernetesLabels::LEARNING_DISABLED_LABEL)
322
325
 
323
- # Get agent to verify it exists
324
- get_resource_or_exit(RESOURCE_AGENT, name)
326
+ # Get runs pending learning from agent status
327
+ runs_pending_learning = agent.dig('status', 'runsPendingLearning') || 0
328
+ learning_threshold = 10 # Standard threshold
325
329
 
326
- # List all ConfigMaps with the agent label
327
- config_maps = ctx.client.list_resources('ConfigMap', namespace: ctx.namespace)
330
+ # Calculate progress percentage
331
+ progress_percent = [(runs_pending_learning.to_f / learning_threshold * 100).round, 100].min
332
+ runs_display = if runs_pending_learning >= learning_threshold
333
+ "#{runs_pending_learning}/#{learning_threshold} #{pastel.green('(Ready)')}"
334
+ else
335
+ "#{runs_pending_learning}/#{learning_threshold} (#{progress_percent}%)"
336
+ end
328
337
 
329
- # Filter for versioned ConfigMaps for this agent
330
- agent_configs = config_maps.select do |cm|
331
- labels = cm.dig('metadata', 'labels') || {}
332
- labels['agent'] == name && labels['version']
333
- end
338
+ status_color = learning_enabled ? :green : :yellow
339
+ status_text = learning_enabled ? 'Enabled' : 'Disabled'
334
340
 
335
- # Sort by version (assuming numeric versions)
336
- agent_configs.sort! do |a, b|
337
- version_a = a.dig('metadata', 'labels', 'version').to_i
338
- version_b = b.dig('metadata', 'labels', 'version').to_i
339
- version_b <=> version_a # Reverse order (newest first)
340
- end
341
-
342
- display_agent_versions(agent_configs, name, ctx.name)
343
- end
341
+ highlighted_box(
342
+ title: 'Learning',
343
+ color: :cyan,
344
+ rows: {
345
+ 'Status' => pastel.send(status_color).bold(status_text),
346
+ 'Threshold' => "#{pastel.cyan('10 successful runs')} (auto-learning trigger)",
347
+ 'Confidence Target' => "#{pastel.cyan('85%')} (pattern detection)",
348
+ 'Runs Recorded' => runs_display
349
+ }
350
+ )
344
351
  end
345
352
 
346
- private
347
-
348
353
  # Shared helper methods that are used across multiple commands
349
354
  # These will be extracted from the original agent.rb
350
355
 
351
- def handle_agent_not_found(name, ctx, error)
356
+ def handle_agent_not_found(name, ctx, error = nil)
352
357
  # Get available agents for fuzzy matching
353
358
  agents = ctx.client.list_resources(RESOURCE_AGENT, namespace: ctx.namespace)
354
359
  available_names = agents.map { |a| a.dig('metadata', 'name') }
355
360
 
356
- CLI::Errors::Handler.handle_not_found(error,
357
- resource_type: RESOURCE_AGENT,
358
- resource_name: name,
359
- cluster: ctx.name,
360
- available_resources: available_names)
361
+ # Create error if not provided
362
+ error ||= K8s::Error::NotFound.new('GET', "/apis/langop.io/v1alpha1/namespaces/#{ctx.namespace}/languageagents/#{name}", 404, 'Not Found')
363
+
364
+ begin
365
+ CLI::Errors::Handler.handle_not_found(error, {
366
+ resource_type: RESOURCE_AGENT,
367
+ resource_name: name,
368
+ cluster: ctx.name,
369
+ available_resources: available_names
370
+ })
371
+ rescue CLI::Errors::NotFoundError
372
+ # Error message already displayed by handler, just exit gracefully
373
+ exit 1
374
+ end
361
375
  end
362
376
 
363
377
  def display_agent_created(agent, ctx, _description, _synthesis_result)
@@ -372,8 +386,8 @@ module LanguageOperator
372
386
  status: format_status(status),
373
387
  mode: agent.dig('spec', 'executionMode') || 'autonomous',
374
388
  schedule: agent.dig('spec', 'schedule'),
375
- persona: agent.dig('spec', 'persona') || '(auto-selected)',
376
- created: Time.now.strftime('%Y-%m-%dT%H:%M:%SZ')
389
+ persona: agent.dig('spec', 'persona') || 'None',
390
+ created: 'just now'
377
391
  )
378
392
 
379
393
  puts
@@ -526,11 +540,17 @@ module LanguageOperator
526
540
  end
527
541
 
528
542
  table_data = agents.map do |agent|
543
+ status = if agent.dig('metadata', 'deletionTimestamp')
544
+ 'Pending Deletion'
545
+ else
546
+ agent.dig('status', 'phase') || 'Unknown'
547
+ end
548
+
529
549
  {
530
550
  name: agent.dig('metadata', 'name'),
531
551
  namespace: agent.dig('metadata', 'namespace') || context.namespace,
532
552
  mode: agent.dig('spec', 'executionMode') || 'autonomous',
533
- status: agent.dig('status', 'phase') || 'Unknown'
553
+ status: status
534
554
  }
535
555
  end
536
556
 
@@ -556,11 +576,17 @@ module LanguageOperator
556
576
  agents = ctx.client.list_resources(RESOURCE_AGENT, namespace: ctx.namespace)
557
577
 
558
578
  agents.each do |agent|
579
+ status = if agent.dig('metadata', 'deletionTimestamp')
580
+ 'Pending Deletion'
581
+ else
582
+ agent.dig('status', 'phase') || 'Unknown'
583
+ end
584
+
559
585
  all_agents << {
560
586
  cluster: cluster[:name],
561
587
  name: agent.dig('metadata', 'name'),
562
588
  mode: agent.dig('spec', 'executionMode') || 'autonomous',
563
- status: agent.dig('status', 'phase') || 'Unknown',
589
+ status: status,
564
590
  next_run: agent.dig('status', 'nextRun') || 'N/A',
565
591
  executions: agent.dig('status', 'executionCount') || 0
566
592
  }
@@ -740,7 +766,7 @@ module LanguageOperator
740
766
  begin
741
767
  # Get CronJob to find last execution time and next run
742
768
  cronjob = ctx.client.get_resource('CronJob', agent_name, ctx.namespace)
743
-
769
+
744
770
  # Get last successful execution time
745
771
  last_successful = cronjob.dig('status', 'lastSuccessfulTime')
746
772
  if last_successful
@@ -750,9 +776,7 @@ module LanguageOperator
750
776
 
751
777
  # Calculate next run time from schedule
752
778
  schedule = cronjob.dig('spec', 'schedule')
753
- if schedule
754
- execution_data[:next_run] = calculate_next_run(schedule)
755
- end
779
+ execution_data[:next_run] = calculate_next_run(schedule) if schedule
756
780
  rescue K8s::Error::NotFound, StandardError
757
781
  # CronJob not found or parsing error, continue with job counting
758
782
  end
@@ -761,7 +785,7 @@ module LanguageOperator
761
785
  begin
762
786
  # Count total completed jobs for this agent
763
787
  jobs = ctx.client.list_resources('Job', namespace: ctx.namespace)
764
-
788
+
765
789
  agent_jobs = jobs.select do |job|
766
790
  labels = job.dig('metadata', 'labels') || {}
767
791
  labels['app.kubernetes.io/name'] == agent_name
@@ -772,7 +796,7 @@ module LanguageOperator
772
796
  conditions = job.dig('status', 'conditions') || []
773
797
  conditions.any? { |c| c['type'] == 'Complete' && c['status'] == 'True' }
774
798
  end
775
-
799
+
776
800
  execution_data[:total_runs] = successful_jobs.length
777
801
  rescue StandardError
778
802
  # If job listing fails, keep default count of 0
@@ -784,32 +808,32 @@ module LanguageOperator
784
808
  def calculate_next_run(schedule)
785
809
  # Simple next run calculation for common cron patterns
786
810
  # Handle the most common case: */N * * * * (every N minutes)
787
-
811
+
788
812
  parts = schedule.split
789
813
  return schedule unless parts.length == 5 # Not a valid cron expression
790
-
814
+
791
815
  minute, hour, day, month, weekday = parts
792
816
  current_time = Time.now
793
-
817
+
794
818
  # Handle every-N-minutes pattern: */10 * * * *
795
819
  if minute.start_with?('*/') && hour == '*' && day == '*' && month == '*' && weekday == '*'
796
820
  interval = minute[2..].to_i
797
821
  if interval > 0 && interval < 60
798
822
  current_minute = current_time.min
799
- current_second = current_time.sec
800
-
823
+ current_time.sec
824
+
801
825
  # Find the next occurrence
802
826
  next_minute_mark = ((current_minute / interval) + 1) * interval
803
-
827
+
804
828
  if next_minute_mark < 60
805
829
  # Same hour
806
- next_time = Time.new(current_time.year, current_time.month, current_time.day,
830
+ next_time = Time.new(current_time.year, current_time.month, current_time.day,
807
831
  current_time.hour, next_minute_mark, 0)
808
832
  else
809
833
  # Next hour
810
834
  next_hour = current_time.hour + 1
811
835
  next_minute = next_minute_mark - 60
812
-
836
+
813
837
  if next_hour < 24
814
838
  next_time = Time.new(current_time.year, current_time.month, current_time.day,
815
839
  next_hour, next_minute, 0)
@@ -820,16 +844,83 @@ module LanguageOperator
820
844
  0, next_minute, 0)
821
845
  end
822
846
  end
823
-
847
+
824
848
  return Formatters::ValueFormatter.time_until(next_time)
825
849
  end
826
850
  end
827
-
851
+
828
852
  # For other patterns, show the schedule (could add more patterns later)
829
853
  schedule
830
854
  rescue StandardError
831
855
  schedule
832
856
  end
857
+
858
+ def verify_agent_deletion(ctx, name)
859
+ max_wait = 30 # Wait up to 30 seconds
860
+ interval = 2 # Check every 2 seconds
861
+ elapsed = 0
862
+
863
+ Formatters::ProgressFormatter.with_spinner('Verifying deletion') do
864
+ loop do
865
+ begin
866
+ agent = ctx.client.get_resource(RESOURCE_AGENT, name, ctx.namespace)
867
+
868
+ # Check if deletion is stuck on finalizers
869
+ deletion_timestamp = agent.dig('metadata', 'deletionTimestamp')
870
+ if deletion_timestamp
871
+ finalizers = agent.dig('metadata', 'finalizers') || []
872
+ if finalizers.any?
873
+ if elapsed >= max_wait
874
+ deletion_stuck_error(name, finalizers)
875
+ return
876
+ end
877
+ end
878
+ end
879
+ rescue K8s::Error::NotFound
880
+ # Agent successfully deleted
881
+ break
882
+ end
883
+
884
+ if elapsed >= max_wait
885
+ deletion_timeout_error(name)
886
+ return
887
+ end
888
+
889
+ sleep interval
890
+ elapsed += interval
891
+ end
892
+ end
893
+
894
+ # Deletion verified - no additional success message needed
895
+ end
896
+
897
+ def deletion_stuck_error(name, finalizers)
898
+ puts
899
+ Formatters::ProgressFormatter.error("Deletion of agent '#{name}' is stuck")
900
+ puts
901
+ puts "The agent has the following finalizers preventing deletion:"
902
+ finalizers.each { |f| puts " - #{pastel.yellow(f)}" }
903
+ puts
904
+ puts "This usually indicates the operator is not running properly."
905
+ puts
906
+ puts "To diagnose:"
907
+ puts " kubectl get pods -n kube-system | grep language-operator"
908
+ puts " kubectl logs -n kube-system -l app.kubernetes.io/name=language-operator"
909
+ puts
910
+ puts "Emergency cleanup (advanced users only):"
911
+ puts " kubectl patch languageagent #{name} -p '{\"metadata\":{\"finalizers\":null}}' --type=merge"
912
+ end
913
+
914
+ def deletion_timeout_error(name)
915
+ puts
916
+ Formatters::ProgressFormatter.warn("Could not verify deletion of agent '#{name}' within 30 seconds")
917
+ puts
918
+ puts "Check deletion status with:"
919
+ puts " aictl agent list"
920
+ puts " kubectl get languageagent #{name}"
921
+ puts
922
+ puts "If the agent shows 'Unknown' status, it may be pending deletion."
923
+ end
833
924
  end
834
925
  end
835
926
  end
@@ -4,42 +4,57 @@ module LanguageOperator
4
4
  module CLI
5
5
  module Commands
6
6
  module Agent
7
- # Code viewing and editing for agents
7
+ # Code viewing and editing for agents using LanguageAgentVersion CRD
8
8
  module CodeOperations
9
9
  def self.included(base)
10
10
  base.class_eval do
11
- desc 'code NAME', 'Display synthesized agent code'
11
+ desc 'code NAME', 'Display agent code from LanguageAgentVersion'
12
12
  option :cluster, type: :string, desc: 'Override current cluster context'
13
13
  option :raw, type: :boolean, default: false, desc: 'Output raw code without formatting'
14
+ option :version, type: :string, desc: 'Display specific version (e.g., --version=2)'
14
15
  def code(name)
15
16
  handle_command_error('get code') do
16
17
  require_relative '../../formatters/code_formatter'
17
18
 
18
19
  ctx = CLI::Helpers::ClusterContext.from_options(options)
19
20
 
20
- # Get the code ConfigMap for this agent
21
- configmap_name = "#{name}-code"
22
- begin
23
- configmap = ctx.client.get_resource('ConfigMap', configmap_name, ctx.namespace)
24
- rescue K8s::Error::NotFound
25
- Formatters::ProgressFormatter.error("Synthesized code not found for agent '#{name}'")
21
+ # Get the LanguageAgentVersion resource
22
+ version_resource = get_agent_version_resource(ctx, name, options[:version])
23
+
24
+ unless version_resource
25
+ Formatters::ProgressFormatter.error("No code versions found for agent '#{name}'")
26
26
  puts
27
- puts 'Possible reasons:'
28
- puts ' - Agent synthesis not yet complete'
27
+ puts 'This may indicate:'
28
+ puts ' - Agent has not been synthesized yet'
29
29
  puts ' - Agent synthesis failed'
30
30
  puts
31
- puts 'Check synthesis status with:'
31
+ puts 'Check agent status with:'
32
32
  puts " aictl agent inspect #{name}"
33
33
  exit 1
34
34
  end
35
35
 
36
- # Get the agent.rb code from the ConfigMap
37
- code_content = configmap.dig('data', 'agent.rb')
36
+ # Get code from LanguageAgentVersion spec
37
+ code_content = version_resource.dig('spec', 'code')
38
38
  unless code_content
39
- Formatters::ProgressFormatter.error('Code content not found in ConfigMap')
39
+ Formatters::ProgressFormatter.error('Code content not found in LanguageAgentVersion')
40
40
  exit 1
41
41
  end
42
42
 
43
+ # Determine version info for title
44
+ version_num = version_resource.dig('spec', 'version')
45
+ source_type = version_resource.dig('spec', 'sourceType') || 'manual'
46
+
47
+ title = if options[:version]
48
+ "Code for Agent: #{name} (Version #{version_num})"
49
+ else
50
+ active_version = get_active_version(ctx, name)
51
+ if version_num.to_s == active_version
52
+ "Current Code for Agent: #{name} (Version #{version_num} - #{source_type})"
53
+ else
54
+ "Code for Agent: #{name} (Version #{version_num} - #{source_type})"
55
+ end
56
+ end
57
+
43
58
  # Raw output mode - just print the code
44
59
  if options[:raw]
45
60
  puts code_content
@@ -49,11 +64,137 @@ module LanguageOperator
49
64
  # Display with syntax highlighting
50
65
  Formatters::CodeFormatter.display_ruby_code(
51
66
  code_content,
52
- title: "Synthesized Code for Agent: #{name}"
67
+ title: title
53
68
  )
54
69
  end
55
70
  end
56
71
 
72
+ desc 'versions NAME', 'List available LanguageAgentVersion resources'
73
+ option :cluster, type: :string, desc: 'Override current cluster context'
74
+ def versions(name)
75
+ handle_command_error('list versions') do
76
+ ctx = CLI::Helpers::ClusterContext.from_options(options)
77
+
78
+ # Get agent to verify it exists
79
+ get_resource_or_exit(Constants::RESOURCE_AGENT, name)
80
+
81
+ # Find all LanguageAgentVersion resources for this agent
82
+ versions = ctx.client.list_resources(Constants::RESOURCE_AGENT_VERSION, namespace: ctx.namespace)
83
+ .select { |v| v.dig('spec', 'agentRef', 'name') == name }
84
+ .sort_by { |v| v.dig('spec', 'version').to_i }
85
+
86
+ if versions.empty?
87
+ puts
88
+ puts "No LanguageAgentVersion resources found for agent: #{pastel.bold(name)}"
89
+ puts
90
+ puts pastel.yellow('No versions have been created yet.')
91
+ puts
92
+ puts 'Versions are created automatically during agent synthesis.'
93
+ puts "Check agent status: #{pastel.dim("aictl agent inspect #{name}")}"
94
+ return
95
+ end
96
+
97
+ # Get current active version
98
+ active_version = get_active_version(ctx, name)
99
+
100
+ # Build table data
101
+ table_data = versions.map do |version_resource|
102
+ version_num = version_resource.dig('spec', 'version').to_s
103
+ status = version_num == active_version ? 'current' : 'available'
104
+ source_type = version_resource.dig('spec', 'sourceType') || 'manual'
105
+ created = version_resource.dig('metadata', 'creationTimestamp')
106
+ created = created ? Time.parse(created).strftime('%Y-%m-%d %H:%M') : 'Unknown'
107
+
108
+ {
109
+ version: "v#{version_num}",
110
+ status: status,
111
+ type: source_type.capitalize,
112
+ created: created
113
+ }
114
+ end
115
+
116
+ # Display table
117
+ headers = %w[VERSION STATUS TYPE CREATED]
118
+ rows = table_data.map do |row|
119
+ status_indicator = row[:status] == 'current' ? pastel.green('●') : pastel.dim('○')
120
+ status_text = row[:status] == 'current' ? pastel.green('current') : 'available'
121
+
122
+ [
123
+ row[:version],
124
+ "#{status_indicator} #{status_text}",
125
+ row[:type],
126
+ row[:created]
127
+ ]
128
+ end
129
+
130
+ puts
131
+ puts "Code versions for agent: #{pastel.bold(name)}"
132
+ puts
133
+ puts table(headers, rows)
134
+ puts
135
+ puts 'Usage:'
136
+ puts " #{pastel.dim("aictl agent code #{name}")}"
137
+ puts " #{pastel.dim("aictl agent code #{name} --version=X")}"
138
+ end
139
+ end
140
+
141
+ private
142
+
143
+ def get_active_version(ctx, agent_name)
144
+ begin
145
+ agent = ctx.client.get_resource(Constants::RESOURCE_AGENT, agent_name, ctx.namespace)
146
+ version_ref = agent.dig('spec', 'agentVersionRef', 'name')
147
+ return nil unless version_ref
148
+
149
+ # Extract version number from "agent-name-vX" format
150
+ version_ref.split('-v').last
151
+ rescue K8s::Error::NotFound
152
+ nil
153
+ end
154
+ end
155
+
156
+ def get_agent_version_resource(ctx, agent_name, requested_version = nil)
157
+ # Query LanguageAgentVersion resources for this agent
158
+ versions = ctx.client.list_resources(Constants::RESOURCE_AGENT_VERSION, namespace: ctx.namespace)
159
+ .select { |v| v.dig('spec', 'agentRef', 'name') == agent_name }
160
+
161
+ if versions.empty?
162
+ return nil
163
+ end
164
+
165
+ if requested_version
166
+ # Find specific version
167
+ target_version = versions.find { |v| v.dig('spec', 'version').to_s == requested_version }
168
+
169
+ unless target_version
170
+ Formatters::ProgressFormatter.error("Version #{requested_version} not found for agent '#{agent_name}'")
171
+ puts
172
+ puts 'Available versions:'
173
+ versions.each do |v|
174
+ version_num = v.dig('spec', 'version')
175
+ puts " v#{version_num}"
176
+ end
177
+ puts
178
+ puts "Use 'aictl agent versions #{agent_name}' to see all available versions"
179
+ exit 1
180
+ end
181
+
182
+ return target_version
183
+ else
184
+ # Get currently active version
185
+ active_version = get_active_version(ctx, agent_name)
186
+
187
+ if active_version
188
+ # Find the currently active version resource
189
+ active_version_resource = versions.find { |v| v.dig('spec', 'version').to_s == active_version }
190
+ return active_version_resource if active_version_resource
191
+ end
192
+
193
+ # Fall back to latest version if no active version or active version not found
194
+ return versions.max_by { |v| v.dig('spec', 'version').to_i }
195
+ end
196
+ end
197
+
57
198
  desc 'edit NAME', 'Edit agent instructions'
58
199
  option :cluster, type: :string, desc: 'Override current cluster context'
59
200
  def edit(name)
@@ -99,4 +240,4 @@ module LanguageOperator
99
240
  end
100
241
  end
101
242
  end
102
- end
243
+ end