language-operator 0.1.31 → 0.1.35

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 (71) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +7 -8
  3. data/CHANGELOG.md +14 -0
  4. data/CI_STATUS.md +56 -0
  5. data/Gemfile.lock +2 -2
  6. data/Makefile +22 -6
  7. data/lib/language_operator/agent/base.rb +10 -6
  8. data/lib/language_operator/agent/executor.rb +19 -97
  9. data/lib/language_operator/agent/safety/ast_validator.rb +62 -43
  10. data/lib/language_operator/agent/safety/safe_executor.rb +27 -2
  11. data/lib/language_operator/agent/scheduler.rb +60 -0
  12. data/lib/language_operator/agent/task_executor.rb +548 -0
  13. data/lib/language_operator/agent.rb +90 -27
  14. data/lib/language_operator/cli/base_command.rb +117 -0
  15. data/lib/language_operator/cli/commands/agent.rb +339 -407
  16. data/lib/language_operator/cli/commands/cluster.rb +274 -290
  17. data/lib/language_operator/cli/commands/install.rb +110 -119
  18. data/lib/language_operator/cli/commands/model.rb +284 -184
  19. data/lib/language_operator/cli/commands/persona.rb +218 -284
  20. data/lib/language_operator/cli/commands/quickstart.rb +4 -5
  21. data/lib/language_operator/cli/commands/status.rb +31 -35
  22. data/lib/language_operator/cli/commands/system.rb +221 -233
  23. data/lib/language_operator/cli/commands/tool.rb +356 -422
  24. data/lib/language_operator/cli/commands/use.rb +19 -22
  25. data/lib/language_operator/cli/helpers/resource_dependency_checker.rb +0 -18
  26. data/lib/language_operator/cli/wizards/quickstart_wizard.rb +0 -1
  27. data/lib/language_operator/client/config.rb +20 -21
  28. data/lib/language_operator/config.rb +115 -3
  29. data/lib/language_operator/constants.rb +54 -0
  30. data/lib/language_operator/dsl/agent_context.rb +7 -7
  31. data/lib/language_operator/dsl/agent_definition.rb +111 -26
  32. data/lib/language_operator/dsl/config.rb +30 -66
  33. data/lib/language_operator/dsl/main_definition.rb +114 -0
  34. data/lib/language_operator/dsl/schema.rb +84 -43
  35. data/lib/language_operator/dsl/task_definition.rb +315 -0
  36. data/lib/language_operator/dsl.rb +0 -1
  37. data/lib/language_operator/instrumentation/task_tracer.rb +285 -0
  38. data/lib/language_operator/logger.rb +4 -4
  39. data/lib/language_operator/synthesis_test_harness.rb +324 -0
  40. data/lib/language_operator/templates/examples/agent_synthesis.tmpl +26 -8
  41. data/lib/language_operator/templates/schema/CHANGELOG.md +26 -0
  42. data/lib/language_operator/templates/schema/agent_dsl_openapi.yaml +1 -1
  43. data/lib/language_operator/templates/schema/agent_dsl_schema.json +84 -42
  44. data/lib/language_operator/type_coercion.rb +250 -0
  45. data/lib/language_operator/ux/base.rb +81 -0
  46. data/lib/language_operator/ux/concerns/README.md +155 -0
  47. data/lib/language_operator/ux/concerns/headings.rb +90 -0
  48. data/lib/language_operator/ux/concerns/input_validation.rb +146 -0
  49. data/lib/language_operator/ux/concerns/provider_helpers.rb +167 -0
  50. data/lib/language_operator/ux/create_agent.rb +252 -0
  51. data/lib/language_operator/ux/create_model.rb +267 -0
  52. data/lib/language_operator/ux/quickstart.rb +594 -0
  53. data/lib/language_operator/version.rb +1 -1
  54. data/lib/language_operator.rb +2 -0
  55. data/requirements/ARCHITECTURE.md +1 -0
  56. data/requirements/SCRATCH.md +153 -0
  57. data/requirements/dsl.md +0 -0
  58. data/requirements/features +1 -0
  59. data/requirements/personas +1 -0
  60. data/requirements/proposals +1 -0
  61. data/requirements/tasks/iterate.md +14 -15
  62. data/requirements/tasks/optimize.md +13 -4
  63. data/synth/001/Makefile +90 -0
  64. data/synth/001/agent.rb +26 -0
  65. data/synth/001/agent.yaml +7 -0
  66. data/synth/001/output.log +44 -0
  67. data/synth/Makefile +39 -0
  68. data/synth/README.md +342 -0
  69. metadata +37 -10
  70. data/lib/language_operator/dsl/workflow_definition.rb +0 -259
  71. data/test_agent_dsl.rb +0 -108
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'thor'
4
+ require_relative '../base_command'
4
5
  require_relative '../formatters/progress_formatter'
5
6
  require_relative '../formatters/table_formatter'
6
7
  require_relative '../formatters/value_formatter'
@@ -15,12 +16,13 @@ require_relative '../errors/handler'
15
16
  require_relative '../../config/cluster_config'
16
17
  require_relative '../../kubernetes/client'
17
18
  require_relative '../../kubernetes/resource_builder'
19
+ require_relative '../../ux/create_agent'
18
20
 
19
21
  module LanguageOperator
20
22
  module CLI
21
23
  module Commands
22
24
  # Agent management commands
23
- class Agent < Thor
25
+ class Agent < BaseCommand
24
26
  include Helpers::ClusterValidator
25
27
  include Helpers::PastelHelper
26
28
 
@@ -45,234 +47,216 @@ module LanguageOperator
45
47
  option :dry_run, type: :boolean, default: false, desc: 'Preview what would be created without applying'
46
48
  option :wizard, type: :boolean, default: false, desc: 'Use interactive wizard mode'
47
49
  def create(description = nil)
48
- # Activate wizard mode if --wizard flag or no description provided
49
- if options[:wizard] || description.nil?
50
- require_relative '../wizards/agent_wizard'
51
- wizard = Wizards::AgentWizard.new
52
- description = wizard.run
53
-
54
- # User cancelled wizard
55
- unless description
56
- Formatters::ProgressFormatter.info('Agent creation cancelled')
57
- return
50
+ handle_command_error('create agent') do
51
+ # Activate wizard mode if --wizard flag or no description provided
52
+ if options[:wizard] || description.nil?
53
+ description = Ux::CreateAgent.execute(ctx)
54
+
55
+ # User cancelled wizard
56
+ unless description
57
+ Formatters::ProgressFormatter.info('Agent creation cancelled')
58
+ return
59
+ end
58
60
  end
59
- end
60
61
 
61
- # Handle --create-cluster flag
62
- if options[:create_cluster]
63
- cluster_name = options[:create_cluster]
64
- unless Config::ClusterConfig.cluster_exists?(cluster_name)
65
- Formatters::ProgressFormatter.info("Creating cluster '#{cluster_name}'...")
66
- # Delegate to cluster create command
67
- require_relative 'cluster'
68
- Cluster.new.invoke(:create, [cluster_name], switch: true)
62
+ # Handle --create-cluster flag
63
+ if options[:create_cluster]
64
+ cluster_name = options[:create_cluster]
65
+ unless Config::ClusterConfig.cluster_exists?(cluster_name)
66
+ Formatters::ProgressFormatter.info("Creating cluster '#{cluster_name}'...")
67
+ # Delegate to cluster create command
68
+ require_relative 'cluster'
69
+ Cluster.new.invoke(:create, [cluster_name], switch: true)
70
+ end
71
+ cluster = cluster_name
72
+ else
73
+ # Validate cluster selection (this will exit if none selected)
74
+ cluster = Helpers::ClusterValidator.get_cluster(options[:cluster])
69
75
  end
70
- cluster = cluster_name
71
- else
72
- # Validate cluster selection (this will exit if none selected)
73
- cluster = Helpers::ClusterValidator.get_cluster(options[:cluster])
74
- end
75
76
 
76
- ctx = Helpers::ClusterContext.from_options(options.merge(cluster: cluster))
77
+ ctx = Helpers::ClusterContext.from_options(options.merge(cluster: cluster))
77
78
 
78
- Formatters::ProgressFormatter.info("Creating agent in cluster '#{ctx.name}'")
79
- puts
80
-
81
- # Generate agent name from description if not provided
82
- agent_name = options[:name] || generate_agent_name(description)
79
+ Formatters::ProgressFormatter.info("Creating agent in cluster '#{ctx.name}'")
80
+ puts
83
81
 
84
- # Get models: use specified models, or default to all available models in cluster
85
- models = options[:models]
86
- if models.nil? || models.empty?
87
- available_models = ctx.client.list_resources('LanguageModel', namespace: ctx.namespace)
88
- models = available_models.map { |m| m.dig('metadata', 'name') }
82
+ # Generate agent name from description if not provided
83
+ agent_name = options[:name] || generate_agent_name(description)
89
84
 
90
- Errors::Handler.handle_no_models_available(cluster: ctx.name) if models.empty?
91
- end
85
+ # Get models: use specified models, or default to all available models in cluster
86
+ models = options[:models]
87
+ if models.nil? || models.empty?
88
+ available_models = ctx.client.list_resources('LanguageModel', namespace: ctx.namespace)
89
+ models = available_models.map { |m| m.dig('metadata', 'name') }
92
90
 
93
- # Build LanguageAgent resource
94
- agent_resource = Kubernetes::ResourceBuilder.language_agent(
95
- agent_name,
96
- instructions: description,
97
- cluster: ctx.namespace,
98
- persona: options[:persona],
99
- tools: options[:tools] || [],
100
- models: models
101
- )
91
+ Errors::Handler.handle_no_models_available(cluster: ctx.name) if models.empty?
92
+ end
102
93
 
103
- # Dry-run mode: preview without applying
104
- if options[:dry_run]
105
- display_dry_run_preview(agent_resource, ctx.name, description)
106
- return
107
- end
94
+ # Build LanguageAgent resource
95
+ agent_resource = Kubernetes::ResourceBuilder.language_agent(
96
+ agent_name,
97
+ instructions: description,
98
+ cluster: ctx.namespace,
99
+ persona: options[:persona],
100
+ tools: options[:tools] || [],
101
+ models: models
102
+ )
108
103
 
109
- # Apply resource to cluster
110
- Formatters::ProgressFormatter.with_spinner("Creating agent '#{agent_name}'") do
111
- ctx.client.apply_resource(agent_resource)
112
- end
104
+ # Dry-run mode: preview without applying
105
+ if options[:dry_run]
106
+ display_dry_run_preview(agent_resource, ctx.name, description)
107
+ return
108
+ end
113
109
 
114
- # Watch synthesis status
115
- synthesis_result = watch_synthesis_status(ctx.client, agent_name, ctx.namespace)
110
+ # Apply resource to cluster
111
+ Formatters::ProgressFormatter.with_spinner("Creating agent '#{agent_name}'") do
112
+ ctx.client.apply_resource(agent_resource)
113
+ end
116
114
 
117
- # Exit if synthesis failed
118
- exit 1 unless synthesis_result[:success]
115
+ # Watch synthesis status
116
+ synthesis_result = watch_synthesis_status(ctx.client, agent_name, ctx.namespace)
119
117
 
120
- # Fetch the updated agent to get complete details
121
- agent = ctx.client.get_resource('LanguageAgent', agent_name, ctx.namespace)
118
+ # Exit if synthesis failed
119
+ exit 1 unless synthesis_result[:success]
122
120
 
123
- # Display enhanced success output
124
- display_agent_created(agent, ctx.name, description, synthesis_result)
125
- rescue StandardError => e
126
- Formatters::ProgressFormatter.error("Failed to create agent: #{e.message}")
127
- raise if ENV['DEBUG']
121
+ # Fetch the updated agent to get complete details
122
+ agent = ctx.client.get_resource('LanguageAgent', agent_name, ctx.namespace)
128
123
 
129
- exit 1
124
+ # Display enhanced success output
125
+ display_agent_created(agent, ctx.name, description, synthesis_result)
126
+ end
130
127
  end
131
128
 
132
129
  desc 'list', 'List all agents in current cluster'
133
130
  option :cluster, type: :string, desc: 'Override current cluster context'
134
131
  option :all_clusters, type: :boolean, default: false, desc: 'Show agents across all clusters'
135
132
  def list
136
- if options[:all_clusters]
137
- list_all_clusters
138
- else
139
- ctx = Helpers::ClusterContext.from_options(options)
140
- list_cluster_agents(ctx.name)
133
+ handle_command_error('list agents') do
134
+ if options[:all_clusters]
135
+ list_all_clusters
136
+ else
137
+ ctx = Helpers::ClusterContext.from_options(options)
138
+ list_cluster_agents(ctx.name)
139
+ end
141
140
  end
142
- rescue StandardError => e
143
- Formatters::ProgressFormatter.error("Failed to list agents: #{e.message}")
144
- raise if ENV['DEBUG']
145
-
146
- exit 1
147
141
  end
148
142
 
149
143
  desc 'inspect NAME', 'Show detailed agent information'
150
144
  option :cluster, type: :string, desc: 'Override current cluster context'
151
145
  def inspect(name)
152
- ctx = Helpers::ClusterContext.from_options(options)
153
-
154
- agent = ctx.client.get_resource('LanguageAgent', name, ctx.namespace)
155
-
156
- puts "Agent: #{name}"
157
- puts " Cluster: #{ctx.name}"
158
- puts " Namespace: #{ctx.namespace}"
159
- puts
160
-
161
- # Status
162
- status = agent.dig('status', 'phase') || 'Unknown'
163
- puts "Status: #{format_status(status)}"
164
- puts
146
+ handle_command_error('inspect agent') do
147
+ ctx = Helpers::ClusterContext.from_options(options)
165
148
 
166
- # Spec details
167
- puts 'Configuration:'
168
- puts " Mode: #{agent.dig('spec', 'mode') || 'autonomous'}"
169
- puts " Schedule: #{agent.dig('spec', 'schedule') || 'N/A'}" if agent.dig('spec', 'schedule')
170
- puts " Persona: #{agent.dig('spec', 'persona') || '(auto-selected)'}"
171
- puts
149
+ begin
150
+ agent = ctx.client.get_resource('LanguageAgent', name, ctx.namespace)
151
+ rescue K8s::Error::NotFound
152
+ handle_agent_not_found(name, ctx)
153
+ return
154
+ end
172
155
 
173
- # Instructions
174
- instructions = agent.dig('spec', 'instructions')
175
- if instructions
176
- puts 'Instructions:'
177
- puts " #{instructions}"
156
+ puts "Agent: #{name}"
157
+ puts " Cluster: #{ctx.name}"
158
+ puts " Namespace: #{ctx.namespace}"
178
159
  puts
179
- end
180
160
 
181
- # Tools
182
- tools = agent.dig('spec', 'tools') || []
183
- if tools.any?
184
- puts "Tools (#{tools.length}):"
185
- tools.each { |tool| puts " - #{tool}" }
161
+ # Status
162
+ status = agent.dig('status', 'phase') || 'Unknown'
163
+ puts "Status: #{format_status(status)}"
186
164
  puts
187
- end
188
165
 
189
- # Models
190
- model_refs = agent.dig('spec', 'modelRefs') || []
191
- if model_refs.any?
192
- puts "Models (#{model_refs.length}):"
193
- model_refs.each { |ref| puts " - #{ref['name']}" }
166
+ # Spec details
167
+ puts 'Configuration:'
168
+ puts " Mode: #{agent.dig('spec', 'mode') || 'autonomous'}"
169
+ puts " Schedule: #{agent.dig('spec', 'schedule') || 'N/A'}" if agent.dig('spec', 'schedule')
170
+ puts " Persona: #{agent.dig('spec', 'persona') || '(auto-selected)'}"
194
171
  puts
195
- end
196
172
 
197
- # Synthesis info
198
- synthesis = agent.dig('status', 'synthesis')
199
- if synthesis
200
- puts 'Synthesis:'
201
- puts " Status: #{synthesis['status']}"
202
- puts " Model: #{synthesis['model']}" if synthesis['model']
203
- puts " Completed: #{synthesis['completedAt']}" if synthesis['completedAt']
204
- puts " Duration: #{synthesis['duration']}" if synthesis['duration']
205
- puts " Token Count: #{synthesis['tokenCount']}" if synthesis['tokenCount']
206
- puts
207
- end
173
+ # Instructions
174
+ instructions = agent.dig('spec', 'instructions')
175
+ if instructions
176
+ puts 'Instructions:'
177
+ puts " #{instructions}"
178
+ puts
179
+ end
208
180
 
209
- # Execution stats
210
- execution_count = agent.dig('status', 'executionCount') || 0
211
- last_execution = agent.dig('status', 'lastExecution')
212
- next_run = agent.dig('status', 'nextRun')
181
+ # Tools
182
+ tools = agent.dig('spec', 'tools') || []
183
+ if tools.any?
184
+ puts "Tools (#{tools.length}):"
185
+ tools.each { |tool| puts " - #{tool}" }
186
+ puts
187
+ end
213
188
 
214
- puts 'Execution:'
215
- puts " Total Runs: #{execution_count}"
216
- puts " Last Run: #{last_execution || 'Never'}"
217
- puts " Next Run: #{next_run || 'N/A'}" if agent.dig('spec', 'schedule')
218
- puts
189
+ # Models
190
+ model_refs = agent.dig('spec', 'modelRefs') || []
191
+ if model_refs.any?
192
+ puts "Models (#{model_refs.length}):"
193
+ model_refs.each { |ref| puts " - #{ref['name']}" }
194
+ puts
195
+ end
219
196
 
220
- # Conditions
221
- conditions = agent.dig('status', 'conditions') || []
222
- if conditions.any?
223
- puts "Conditions (#{conditions.length}):"
224
- conditions.each do |condition|
225
- status_icon = condition['status'] == 'True' ? '✓' : '✗'
226
- puts " #{status_icon} #{condition['type']}: #{condition['message'] || condition['reason']}"
197
+ # Synthesis info
198
+ synthesis = agent.dig('status', 'synthesis')
199
+ if synthesis
200
+ puts 'Synthesis:'
201
+ puts " Status: #{synthesis['status']}"
202
+ puts " Model: #{synthesis['model']}" if synthesis['model']
203
+ puts " Completed: #{synthesis['completedAt']}" if synthesis['completedAt']
204
+ puts " Duration: #{synthesis['duration']}" if synthesis['duration']
205
+ puts " Token Count: #{synthesis['tokenCount']}" if synthesis['tokenCount']
206
+ puts
227
207
  end
208
+
209
+ # Execution stats
210
+ execution_count = agent.dig('status', 'executionCount') || 0
211
+ last_execution = agent.dig('status', 'lastExecution')
212
+ next_run = agent.dig('status', 'nextRun')
213
+
214
+ puts 'Execution:'
215
+ puts " Total Runs: #{execution_count}"
216
+ puts " Last Run: #{last_execution || 'Never'}"
217
+ puts " Next Run: #{next_run || 'N/A'}" if agent.dig('spec', 'schedule')
228
218
  puts
229
- end
230
219
 
231
- # Recent events (if available)
232
- # This would require querying events, which we can add later
233
- rescue K8s::Error::NotFound
234
- handle_agent_not_found(name, ctx)
235
- rescue StandardError => e
236
- Formatters::ProgressFormatter.error("Failed to inspect agent: #{e.message}")
237
- raise if ENV['DEBUG']
220
+ # Conditions
221
+ conditions = agent.dig('status', 'conditions') || []
222
+ if conditions.any?
223
+ puts "Conditions (#{conditions.length}):"
224
+ conditions.each do |condition|
225
+ status_icon = condition['status'] == 'True' ? '✓' : '✗'
226
+ puts " #{status_icon} #{condition['type']}: #{condition['message'] || condition['reason']}"
227
+ end
228
+ puts
229
+ end
238
230
 
239
- exit 1
231
+ # Recent events (if available)
232
+ # This would require querying events, which we can add later
233
+ end
240
234
  end
241
235
 
242
236
  desc 'delete NAME', 'Delete an agent'
243
237
  option :cluster, type: :string, desc: 'Override current cluster context'
244
238
  option :force, type: :boolean, default: false, desc: 'Skip confirmation'
245
239
  def delete(name)
246
- ctx = Helpers::ClusterContext.from_options(options)
247
-
248
- # Get agent to show details before deletion
249
- begin
250
- agent = ctx.client.get_resource('LanguageAgent', name, ctx.namespace)
251
- rescue K8s::Error::NotFound
252
- Formatters::ProgressFormatter.error("Agent '#{name}' not found in cluster '#{ctx.name}'")
253
- exit 1
254
- end
240
+ handle_command_error('delete agent') do
241
+ ctx = Helpers::ClusterContext.from_options(options)
255
242
 
256
- # Confirm deletion using UserPrompts helper
257
- unless options[:force]
258
- puts "This will delete agent '#{name}' from cluster '#{ctx.name}':"
259
- puts " Instructions: #{agent.dig('spec', 'instructions')}"
260
- puts " Mode: #{agent.dig('spec', 'mode') || 'autonomous'}"
261
- puts
262
- return unless Helpers::UserPrompts.confirm('Are you sure?')
263
- end
243
+ # Get agent to show details before deletion
244
+ agent = get_resource_or_exit('LanguageAgent', name)
264
245
 
265
- # Delete the agent
266
- Formatters::ProgressFormatter.with_spinner("Deleting agent '#{name}'") do
267
- ctx.client.delete_resource('LanguageAgent', name, ctx.namespace)
268
- end
246
+ # Confirm deletion
247
+ details = {
248
+ 'Instructions' => agent.dig('spec', 'instructions'),
249
+ 'Mode' => agent.dig('spec', 'mode') || 'autonomous'
250
+ }
251
+ return unless confirm_deletion('agent', name, ctx.name, details: details, force: options[:force])
269
252
 
270
- Formatters::ProgressFormatter.success("Agent '#{name}' deleted successfully")
271
- rescue StandardError => e
272
- Formatters::ProgressFormatter.error("Failed to delete agent: #{e.message}")
273
- raise if ENV['DEBUG']
253
+ # Delete the agent
254
+ Formatters::ProgressFormatter.with_spinner("Deleting agent '#{name}'") do
255
+ ctx.client.delete_resource('LanguageAgent', name, ctx.namespace)
256
+ end
274
257
 
275
- exit 1
258
+ Formatters::ProgressFormatter.success("Agent '#{name}' deleted successfully")
259
+ end
276
260
  end
277
261
 
278
262
  desc 'logs NAME', 'Show agent execution logs'
@@ -289,261 +273,217 @@ module LanguageOperator
289
273
  option :follow, type: :boolean, aliases: '-f', default: false, desc: 'Follow logs'
290
274
  option :tail, type: :numeric, default: 100, desc: 'Number of lines to show from the end'
291
275
  def logs(name)
292
- ctx = Helpers::ClusterContext.from_options(options)
276
+ handle_command_error('get logs') do
277
+ ctx = Helpers::ClusterContext.from_options(options)
293
278
 
294
- # Get agent to determine the pod name
295
- begin
296
- agent = ctx.client.get_resource('LanguageAgent', name, ctx.namespace)
297
- rescue K8s::Error::NotFound
298
- Formatters::ProgressFormatter.error("Agent '#{name}' not found in cluster '#{ctx.name}'")
299
- exit 1
300
- end
279
+ # Get agent to determine the pod name
280
+ agent = get_resource_or_exit('LanguageAgent', name)
301
281
 
302
- mode = agent.dig('spec', 'mode') || 'autonomous'
282
+ mode = agent.dig('spec', 'mode') || 'autonomous'
303
283
 
304
- # Build kubectl command for log streaming
305
- kubeconfig_arg = ctx.config[:kubeconfig] ? "--kubeconfig=#{ctx.config[:kubeconfig]}" : ''
306
- context_arg = ctx.config[:context] ? "--context=#{ctx.config[:context]}" : ''
307
- namespace_arg = "-n #{ctx.namespace}"
308
- tail_arg = "--tail=#{options[:tail]}"
309
- follow_arg = options[:follow] ? '-f' : ''
310
-
311
- # For scheduled agents, logs come from CronJob pods
312
- # For autonomous agents, logs come from Deployment pods
313
- if mode == 'scheduled'
314
- # Get most recent job from cronjob
315
- else
316
- # Get pod from deployment
317
- end
318
- label_selector = "app.kubernetes.io/name=#{name}"
284
+ # Build kubectl command for log streaming
285
+ tail_arg = "--tail=#{options[:tail]}"
286
+ follow_arg = options[:follow] ? '-f' : ''
319
287
 
320
- # Use kubectl logs with label selector
321
- cmd = "kubectl #{kubeconfig_arg} #{context_arg} #{namespace_arg} logs -l #{label_selector} #{tail_arg} #{follow_arg} --prefix --all-containers"
288
+ # For scheduled agents, logs come from CronJob pods
289
+ # For autonomous agents, logs come from Deployment pods
290
+ if mode == 'scheduled'
291
+ # Get most recent job from cronjob
292
+ else
293
+ # Get pod from deployment
294
+ end
295
+ label_selector = "app.kubernetes.io/name=#{name}"
322
296
 
323
- Formatters::ProgressFormatter.info("Streaming logs for agent '#{name}'...")
324
- puts
297
+ # Use kubectl logs with label selector
298
+ cmd = "#{ctx.kubectl_prefix} logs -l #{label_selector} #{tail_arg} #{follow_arg} --prefix --all-containers"
325
299
 
326
- # Stream and format logs in real-time
327
- require 'open3'
328
- Open3.popen3(cmd) do |_stdin, stdout, stderr, wait_thr|
329
- # Handle stdout (logs)
330
- stdout_thread = Thread.new do
331
- stdout.each_line do |line|
332
- puts Formatters::LogFormatter.format_line(line.chomp)
333
- $stdout.flush
300
+ Formatters::ProgressFormatter.info("Streaming logs for agent '#{name}'...")
301
+ puts
302
+
303
+ # Stream and format logs in real-time
304
+ require 'open3'
305
+ Open3.popen3(cmd) do |_stdin, stdout, stderr, wait_thr|
306
+ # Handle stdout (logs)
307
+ stdout_thread = Thread.new do
308
+ stdout.each_line do |line|
309
+ puts Formatters::LogFormatter.format_line(line.chomp)
310
+ $stdout.flush
311
+ end
334
312
  end
335
- end
336
313
 
337
- # Handle stderr (errors)
338
- stderr_thread = Thread.new do
339
- stderr.each_line do |line|
340
- warn line
314
+ # Handle stderr (errors)
315
+ stderr_thread = Thread.new do
316
+ stderr.each_line do |line|
317
+ warn line
318
+ end
341
319
  end
342
- end
343
320
 
344
- # Wait for both streams to complete
345
- stdout_thread.join
346
- stderr_thread.join
321
+ # Wait for both streams to complete
322
+ stdout_thread.join
323
+ stderr_thread.join
347
324
 
348
- # Check exit status
349
- exit_status = wait_thr.value
350
- exit exit_status.exitstatus unless exit_status.success?
325
+ # Check exit status
326
+ exit_status = wait_thr.value
327
+ exit exit_status.exitstatus unless exit_status.success?
328
+ end
351
329
  end
352
- rescue StandardError => e
353
- Formatters::ProgressFormatter.error("Failed to get logs: #{e.message}")
354
- raise if ENV['DEBUG']
355
-
356
- exit 1
357
330
  end
358
331
 
359
332
  desc 'code NAME', 'Display synthesized agent code'
360
333
  option :cluster, type: :string, desc: 'Override current cluster context'
361
334
  def code(name)
362
- require_relative '../formatters/code_formatter'
335
+ handle_command_error('get code') do
336
+ require_relative '../formatters/code_formatter'
363
337
 
364
- ctx = Helpers::ClusterContext.from_options(options)
365
-
366
- # Get the code ConfigMap for this agent
367
- configmap_name = "#{name}-code"
368
- begin
369
- configmap = ctx.client.get_resource('ConfigMap', configmap_name, ctx.namespace)
370
- rescue K8s::Error::NotFound
371
- Formatters::ProgressFormatter.error("Synthesized code not found for agent '#{name}'")
372
- puts
373
- puts 'Possible reasons:'
374
- puts ' - Agent synthesis not yet complete'
375
- puts ' - Agent synthesis failed'
376
- puts
377
- puts 'Check synthesis status with:'
378
- puts " aictl agent inspect #{name}"
379
- exit 1
380
- end
338
+ ctx = Helpers::ClusterContext.from_options(options)
381
339
 
382
- # Get the agent.rb code from the ConfigMap
383
- code_content = configmap.dig('data', 'agent.rb')
384
- unless code_content
385
- Formatters::ProgressFormatter.error('Code content not found in ConfigMap')
386
- exit 1
387
- end
340
+ # Get the code ConfigMap for this agent
341
+ configmap_name = "#{name}-code"
342
+ begin
343
+ configmap = ctx.client.get_resource('ConfigMap', configmap_name, ctx.namespace)
344
+ rescue K8s::Error::NotFound
345
+ Formatters::ProgressFormatter.error("Synthesized code not found for agent '#{name}'")
346
+ puts
347
+ puts 'Possible reasons:'
348
+ puts ' - Agent synthesis not yet complete'
349
+ puts ' - Agent synthesis failed'
350
+ puts
351
+ puts 'Check synthesis status with:'
352
+ puts " aictl agent inspect #{name}"
353
+ exit 1
354
+ end
388
355
 
389
- # Display with syntax highlighting
390
- Formatters::CodeFormatter.display_ruby_code(
391
- code_content,
392
- title: "Synthesized Code for Agent: #{name}"
393
- )
356
+ # Get the agent.rb code from the ConfigMap
357
+ code_content = configmap.dig('data', 'agent.rb')
358
+ unless code_content
359
+ Formatters::ProgressFormatter.error('Code content not found in ConfigMap')
360
+ exit 1
361
+ end
394
362
 
395
- puts
396
- puts 'This code was automatically synthesized from the agent instructions.'
397
- puts "View full agent details with: aictl agent inspect #{name}"
398
- rescue StandardError => e
399
- Formatters::ProgressFormatter.error("Failed to get code: #{e.message}")
400
- raise if ENV['DEBUG']
363
+ # Display with syntax highlighting
364
+ Formatters::CodeFormatter.display_ruby_code(
365
+ code_content,
366
+ title: "Synthesized Code for Agent: #{name}"
367
+ )
401
368
 
402
- exit 1
369
+ puts
370
+ puts 'This code was automatically synthesized from the agent instructions.'
371
+ puts "View full agent details with: aictl agent inspect #{name}"
372
+ end
403
373
  end
404
374
 
405
375
  desc 'edit NAME', 'Edit agent instructions'
406
376
  option :cluster, type: :string, desc: 'Override current cluster context'
407
377
  def edit(name)
408
- ctx = Helpers::ClusterContext.from_options(options)
409
-
410
- # Get current agent
411
- begin
412
- agent = ctx.client.get_resource('LanguageAgent', name, ctx.namespace)
413
- rescue K8s::Error::NotFound
414
- Formatters::ProgressFormatter.error("Agent '#{name}' not found in cluster '#{ctx.name}'")
415
- exit 1
416
- end
378
+ handle_command_error('edit agent') do
379
+ ctx = Helpers::ClusterContext.from_options(options)
417
380
 
418
- current_instructions = agent.dig('spec', 'instructions')
381
+ # Get current agent
382
+ agent = get_resource_or_exit('LanguageAgent', name)
419
383
 
420
- # Edit instructions in user's editor
421
- new_instructions = Helpers::EditorHelper.edit_content(
422
- current_instructions,
423
- 'agent-instructions-',
424
- '.txt'
425
- ).strip
384
+ current_instructions = agent.dig('spec', 'instructions')
426
385
 
427
- # Check if changed
428
- if new_instructions == current_instructions
429
- Formatters::ProgressFormatter.info('No changes made')
430
- return
431
- end
386
+ # Edit instructions in user's editor
387
+ new_instructions = Helpers::EditorHelper.edit_content(
388
+ current_instructions,
389
+ 'agent-instructions-',
390
+ '.txt'
391
+ ).strip
432
392
 
433
- # Update agent resource
434
- agent['spec']['instructions'] = new_instructions
393
+ # Check if changed
394
+ if new_instructions == current_instructions
395
+ Formatters::ProgressFormatter.info('No changes made')
396
+ return
397
+ end
435
398
 
436
- Formatters::ProgressFormatter.with_spinner('Updating agent instructions') do
437
- k8s.apply_resource(agent)
438
- end
399
+ # Update agent resource
400
+ agent['spec']['instructions'] = new_instructions
439
401
 
440
- Formatters::ProgressFormatter.success('Agent instructions updated')
441
- puts
442
- puts 'The operator will automatically re-synthesize the agent code.'
443
- puts
444
- puts 'Watch synthesis progress with:'
445
- puts " aictl agent inspect #{name}"
446
- rescue StandardError => e
447
- Formatters::ProgressFormatter.error("Failed to edit agent: #{e.message}")
448
- raise if ENV['DEBUG']
402
+ Formatters::ProgressFormatter.with_spinner('Updating agent instructions') do
403
+ ctx.client.apply_resource(agent)
404
+ end
449
405
 
450
- exit 1
406
+ Formatters::ProgressFormatter.success('Agent instructions updated')
407
+ puts
408
+ puts 'The operator will automatically re-synthesize the agent code.'
409
+ puts
410
+ puts 'Watch synthesis progress with:'
411
+ puts " aictl agent inspect #{name}"
412
+ end
451
413
  end
452
414
 
453
415
  desc 'pause NAME', 'Pause scheduled agent execution'
454
416
  option :cluster, type: :string, desc: 'Override current cluster context'
455
417
  def pause(name)
456
- ctx = Helpers::ClusterContext.from_options(options)
418
+ handle_command_error('pause agent') do
419
+ ctx = Helpers::ClusterContext.from_options(options)
457
420
 
458
- # Get agent
459
- begin
460
- agent = ctx.client.get_resource('LanguageAgent', name, ctx.namespace)
461
- rescue K8s::Error::NotFound
462
- Formatters::ProgressFormatter.error("Agent '#{name}' not found in cluster '#{ctx.name}'")
463
- exit 1
464
- end
421
+ # Get agent
422
+ agent = get_resource_or_exit('LanguageAgent', name)
465
423
 
466
- mode = agent.dig('spec', 'mode') || 'autonomous'
467
- unless mode == 'scheduled'
468
- Formatters::ProgressFormatter.warn("Agent '#{name}' is not a scheduled agent (mode: #{mode})")
469
- puts
470
- puts 'Only scheduled agents can be paused.'
471
- puts 'Autonomous agents can be stopped by deleting them.'
472
- exit 1
473
- end
424
+ mode = agent.dig('spec', 'mode') || 'autonomous'
425
+ unless mode == 'scheduled'
426
+ Formatters::ProgressFormatter.warn("Agent '#{name}' is not a scheduled agent (mode: #{mode})")
427
+ puts
428
+ puts 'Only scheduled agents can be paused.'
429
+ puts 'Autonomous agents can be stopped by deleting them.'
430
+ exit 1
431
+ end
474
432
 
475
- # Suspend the CronJob by setting spec.suspend = true
476
- # This is done by patching the underlying CronJob resource
477
- cronjob_name = name
478
- namespace = ctx.namespace
433
+ # Suspend the CronJob by setting spec.suspend = true
434
+ # This is done by patching the underlying CronJob resource
435
+ cronjob_name = name
436
+ ctx.namespace
479
437
 
480
- Formatters::ProgressFormatter.with_spinner("Pausing agent '#{name}'") do
481
- # Use kubectl to patch the cronjob
482
- kubeconfig_arg = ctx.config[:kubeconfig] ? "--kubeconfig=#{ctx.config[:kubeconfig]}" : ''
483
- context_arg = ctx.config[:context] ? "--context=#{ctx.config[:context]}" : ''
438
+ Formatters::ProgressFormatter.with_spinner("Pausing agent '#{name}'") do
439
+ # Use kubectl to patch the cronjob
440
+ cmd = "#{ctx.kubectl_prefix} patch cronjob #{cronjob_name} -p '{\"spec\":{\"suspend\":true}}'"
441
+ system(cmd)
442
+ end
484
443
 
485
- cmd = "kubectl #{kubeconfig_arg} #{context_arg} -n #{namespace} patch cronjob #{cronjob_name} -p '{\"spec\":{\"suspend\":true}}'"
486
- system(cmd)
444
+ Formatters::ProgressFormatter.success("Agent '#{name}' paused")
445
+ puts
446
+ puts 'The agent will not execute on its schedule until resumed.'
447
+ puts
448
+ puts 'Resume with:'
449
+ puts " aictl agent resume #{name}"
487
450
  end
488
-
489
- Formatters::ProgressFormatter.success("Agent '#{name}' paused")
490
- puts
491
- puts 'The agent will not execute on its schedule until resumed.'
492
- puts
493
- puts 'Resume with:'
494
- puts " aictl agent resume #{name}"
495
- rescue StandardError => e
496
- Formatters::ProgressFormatter.error("Failed to pause agent: #{e.message}")
497
- raise if ENV['DEBUG']
498
-
499
- exit 1
500
451
  end
501
452
 
502
453
  desc 'resume NAME', 'Resume paused agent'
503
454
  option :cluster, type: :string, desc: 'Override current cluster context'
504
455
  def resume(name)
505
- ctx = Helpers::ClusterContext.from_options(options)
456
+ handle_command_error('resume agent') do
457
+ ctx = Helpers::ClusterContext.from_options(options)
506
458
 
507
- # Get agent
508
- begin
509
- agent = ctx.client.get_resource('LanguageAgent', name, ctx.namespace)
510
- rescue K8s::Error::NotFound
511
- Formatters::ProgressFormatter.error("Agent '#{name}' not found in cluster '#{ctx.name}'")
512
- exit 1
513
- end
459
+ # Get agent
460
+ agent = get_resource_or_exit('LanguageAgent', name)
514
461
 
515
- mode = agent.dig('spec', 'mode') || 'autonomous'
516
- unless mode == 'scheduled'
517
- Formatters::ProgressFormatter.warn("Agent '#{name}' is not a scheduled agent (mode: #{mode})")
518
- puts
519
- puts 'Only scheduled agents can be resumed.'
520
- exit 1
521
- end
462
+ mode = agent.dig('spec', 'mode') || 'autonomous'
463
+ unless mode == 'scheduled'
464
+ Formatters::ProgressFormatter.warn("Agent '#{name}' is not a scheduled agent (mode: #{mode})")
465
+ puts
466
+ puts 'Only scheduled agents can be resumed.'
467
+ exit 1
468
+ end
522
469
 
523
- # Resume the CronJob by setting spec.suspend = false
524
- cronjob_name = name
525
- namespace = ctx.namespace
470
+ # Resume the CronJob by setting spec.suspend = false
471
+ cronjob_name = name
472
+ ctx.namespace
526
473
 
527
- Formatters::ProgressFormatter.with_spinner("Resuming agent '#{name}'") do
528
- # Use kubectl to patch the cronjob
529
- kubeconfig_arg = ctx.config[:kubeconfig] ? "--kubeconfig=#{ctx.config[:kubeconfig]}" : ''
530
- context_arg = ctx.config[:context] ? "--context=#{ctx.config[:context]}" : ''
474
+ Formatters::ProgressFormatter.with_spinner("Resuming agent '#{name}'") do
475
+ # Use kubectl to patch the cronjob
476
+ cmd = "#{ctx.kubectl_prefix} patch cronjob #{cronjob_name} -p '{\"spec\":{\"suspend\":false}}'"
477
+ system(cmd)
478
+ end
531
479
 
532
- cmd = "kubectl #{kubeconfig_arg} #{context_arg} -n #{namespace} patch cronjob #{cronjob_name} -p '{\"spec\":{\"suspend\":false}}'"
533
- system(cmd)
480
+ Formatters::ProgressFormatter.success("Agent '#{name}' resumed")
481
+ puts
482
+ puts 'The agent will now execute according to its schedule.'
483
+ puts
484
+ puts 'View next execution time with:'
485
+ puts " aictl agent inspect #{name}"
534
486
  end
535
-
536
- Formatters::ProgressFormatter.success("Agent '#{name}' resumed")
537
- puts
538
- puts 'The agent will now execute according to its schedule.'
539
- puts
540
- puts 'View next execution time with:'
541
- puts " aictl agent inspect #{name}"
542
- rescue StandardError => e
543
- Formatters::ProgressFormatter.error("Failed to resume agent: #{e.message}")
544
- raise if ENV['DEBUG']
545
-
546
- exit 1
547
487
  end
548
488
 
549
489
  desc 'workspace NAME', 'Browse agent workspace files'
@@ -562,41 +502,33 @@ module LanguageOperator
562
502
  option :path, type: :string, desc: 'View specific file contents'
563
503
  option :clean, type: :boolean, desc: 'Clear workspace (with confirmation)'
564
504
  def workspace(name)
565
- ctx = Helpers::ClusterContext.from_options(options)
505
+ handle_command_error('access workspace') do
506
+ ctx = Helpers::ClusterContext.from_options(options)
566
507
 
567
- # Get agent to verify it exists
568
- begin
569
- agent = ctx.client.get_resource('LanguageAgent', name, ctx.namespace)
570
- rescue K8s::Error::NotFound
571
- Formatters::ProgressFormatter.error("Agent '#{name}' not found in cluster '#{ctx.name}'")
572
- exit 1
573
- end
508
+ # Get agent to verify it exists
509
+ agent = get_resource_or_exit('LanguageAgent', name)
574
510
 
575
- # Check if workspace is enabled
576
- workspace_enabled = agent.dig('spec', 'workspace', 'enabled')
577
- unless workspace_enabled
578
- Formatters::ProgressFormatter.warn("Workspace is not enabled for agent '#{name}'")
579
- puts
580
- puts 'Enable workspace in agent configuration:'
581
- puts ' spec:'
582
- puts ' workspace:'
583
- puts ' enabled: true'
584
- puts ' size: 10Gi'
585
- exit 1
586
- end
511
+ # Check if workspace is enabled
512
+ workspace_enabled = agent.dig('spec', 'workspace', 'enabled')
513
+ unless workspace_enabled
514
+ Formatters::ProgressFormatter.warn("Workspace is not enabled for agent '#{name}'")
515
+ puts
516
+ puts 'Enable workspace in agent configuration:'
517
+ puts ' spec:'
518
+ puts ' workspace:'
519
+ puts ' enabled: true'
520
+ puts ' size: 10Gi'
521
+ exit 1
522
+ end
587
523
 
588
- if options[:path]
589
- view_workspace_file(ctx, name, options[:path])
590
- elsif options[:clean]
591
- clean_workspace(ctx, name)
592
- else
593
- list_workspace_files(ctx, name)
524
+ if options[:path]
525
+ view_workspace_file(ctx, name, options[:path])
526
+ elsif options[:clean]
527
+ clean_workspace(ctx, name)
528
+ else
529
+ list_workspace_files(ctx, name)
530
+ end
594
531
  end
595
- rescue StandardError => e
596
- Formatters::ProgressFormatter.error("Failed to access workspace: #{e.message}")
597
- raise if ENV['DEBUG']
598
-
599
- exit 1
600
532
  end
601
533
 
602
534
  private