language-operator 0.0.1 → 0.1.31

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 (120) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +125 -0
  3. data/CHANGELOG.md +88 -0
  4. data/Gemfile +8 -0
  5. data/Gemfile.lock +284 -0
  6. data/LICENSE +229 -21
  7. data/Makefile +82 -0
  8. data/README.md +3 -11
  9. data/Rakefile +63 -0
  10. data/bin/aictl +7 -0
  11. data/completions/_aictl +232 -0
  12. data/completions/aictl.bash +121 -0
  13. data/completions/aictl.fish +114 -0
  14. data/docs/architecture/agent-runtime.md +585 -0
  15. data/docs/dsl/SCHEMA_VERSION.md +250 -0
  16. data/docs/dsl/agent-reference.md +604 -0
  17. data/docs/dsl/best-practices.md +1078 -0
  18. data/docs/dsl/chat-endpoints.md +895 -0
  19. data/docs/dsl/constraints.md +671 -0
  20. data/docs/dsl/mcp-integration.md +1177 -0
  21. data/docs/dsl/webhooks.md +932 -0
  22. data/docs/dsl/workflows.md +744 -0
  23. data/lib/language_operator/agent/base.rb +110 -0
  24. data/lib/language_operator/agent/executor.rb +440 -0
  25. data/lib/language_operator/agent/instrumentation.rb +54 -0
  26. data/lib/language_operator/agent/metrics_tracker.rb +183 -0
  27. data/lib/language_operator/agent/safety/ast_validator.rb +272 -0
  28. data/lib/language_operator/agent/safety/audit_logger.rb +104 -0
  29. data/lib/language_operator/agent/safety/budget_tracker.rb +175 -0
  30. data/lib/language_operator/agent/safety/content_filter.rb +93 -0
  31. data/lib/language_operator/agent/safety/manager.rb +207 -0
  32. data/lib/language_operator/agent/safety/rate_limiter.rb +150 -0
  33. data/lib/language_operator/agent/safety/safe_executor.rb +127 -0
  34. data/lib/language_operator/agent/scheduler.rb +183 -0
  35. data/lib/language_operator/agent/telemetry.rb +116 -0
  36. data/lib/language_operator/agent/web_server.rb +610 -0
  37. data/lib/language_operator/agent/webhook_authenticator.rb +226 -0
  38. data/lib/language_operator/agent.rb +149 -0
  39. data/lib/language_operator/cli/commands/agent.rb +1205 -0
  40. data/lib/language_operator/cli/commands/cluster.rb +371 -0
  41. data/lib/language_operator/cli/commands/install.rb +404 -0
  42. data/lib/language_operator/cli/commands/model.rb +266 -0
  43. data/lib/language_operator/cli/commands/persona.rb +393 -0
  44. data/lib/language_operator/cli/commands/quickstart.rb +22 -0
  45. data/lib/language_operator/cli/commands/status.rb +143 -0
  46. data/lib/language_operator/cli/commands/system.rb +772 -0
  47. data/lib/language_operator/cli/commands/tool.rb +537 -0
  48. data/lib/language_operator/cli/commands/use.rb +47 -0
  49. data/lib/language_operator/cli/errors/handler.rb +180 -0
  50. data/lib/language_operator/cli/errors/suggestions.rb +176 -0
  51. data/lib/language_operator/cli/formatters/code_formatter.rb +77 -0
  52. data/lib/language_operator/cli/formatters/log_formatter.rb +288 -0
  53. data/lib/language_operator/cli/formatters/progress_formatter.rb +49 -0
  54. data/lib/language_operator/cli/formatters/status_formatter.rb +37 -0
  55. data/lib/language_operator/cli/formatters/table_formatter.rb +163 -0
  56. data/lib/language_operator/cli/formatters/value_formatter.rb +113 -0
  57. data/lib/language_operator/cli/helpers/cluster_context.rb +62 -0
  58. data/lib/language_operator/cli/helpers/cluster_validator.rb +101 -0
  59. data/lib/language_operator/cli/helpers/editor_helper.rb +58 -0
  60. data/lib/language_operator/cli/helpers/kubeconfig_validator.rb +167 -0
  61. data/lib/language_operator/cli/helpers/pastel_helper.rb +24 -0
  62. data/lib/language_operator/cli/helpers/resource_dependency_checker.rb +74 -0
  63. data/lib/language_operator/cli/helpers/schedule_builder.rb +108 -0
  64. data/lib/language_operator/cli/helpers/user_prompts.rb +69 -0
  65. data/lib/language_operator/cli/main.rb +236 -0
  66. data/lib/language_operator/cli/templates/tools/generic.yaml +66 -0
  67. data/lib/language_operator/cli/wizards/agent_wizard.rb +246 -0
  68. data/lib/language_operator/cli/wizards/quickstart_wizard.rb +588 -0
  69. data/lib/language_operator/client/base.rb +214 -0
  70. data/lib/language_operator/client/config.rb +136 -0
  71. data/lib/language_operator/client/cost_calculator.rb +37 -0
  72. data/lib/language_operator/client/mcp_connector.rb +123 -0
  73. data/lib/language_operator/client.rb +19 -0
  74. data/lib/language_operator/config/cluster_config.rb +101 -0
  75. data/lib/language_operator/config/tool_patterns.yaml +57 -0
  76. data/lib/language_operator/config/tool_registry.rb +96 -0
  77. data/lib/language_operator/config.rb +138 -0
  78. data/lib/language_operator/dsl/adapter.rb +124 -0
  79. data/lib/language_operator/dsl/agent_context.rb +90 -0
  80. data/lib/language_operator/dsl/agent_definition.rb +427 -0
  81. data/lib/language_operator/dsl/chat_endpoint_definition.rb +115 -0
  82. data/lib/language_operator/dsl/config.rb +119 -0
  83. data/lib/language_operator/dsl/context.rb +50 -0
  84. data/lib/language_operator/dsl/execution_context.rb +47 -0
  85. data/lib/language_operator/dsl/helpers.rb +109 -0
  86. data/lib/language_operator/dsl/http.rb +184 -0
  87. data/lib/language_operator/dsl/mcp_server_definition.rb +73 -0
  88. data/lib/language_operator/dsl/parameter_definition.rb +124 -0
  89. data/lib/language_operator/dsl/registry.rb +36 -0
  90. data/lib/language_operator/dsl/schema.rb +1102 -0
  91. data/lib/language_operator/dsl/shell.rb +125 -0
  92. data/lib/language_operator/dsl/tool_definition.rb +112 -0
  93. data/lib/language_operator/dsl/webhook_authentication.rb +114 -0
  94. data/lib/language_operator/dsl/webhook_definition.rb +106 -0
  95. data/lib/language_operator/dsl/workflow_definition.rb +259 -0
  96. data/lib/language_operator/dsl.rb +161 -0
  97. data/lib/language_operator/errors.rb +60 -0
  98. data/lib/language_operator/kubernetes/client.rb +279 -0
  99. data/lib/language_operator/kubernetes/resource_builder.rb +194 -0
  100. data/lib/language_operator/loggable.rb +47 -0
  101. data/lib/language_operator/logger.rb +141 -0
  102. data/lib/language_operator/retry.rb +123 -0
  103. data/lib/language_operator/retryable.rb +132 -0
  104. data/lib/language_operator/templates/README.md +23 -0
  105. data/lib/language_operator/templates/examples/agent_synthesis.tmpl +115 -0
  106. data/lib/language_operator/templates/examples/persona_distillation.tmpl +19 -0
  107. data/lib/language_operator/templates/schema/.gitkeep +0 -0
  108. data/lib/language_operator/templates/schema/CHANGELOG.md +93 -0
  109. data/lib/language_operator/templates/schema/agent_dsl_openapi.yaml +306 -0
  110. data/lib/language_operator/templates/schema/agent_dsl_schema.json +452 -0
  111. data/lib/language_operator/tool_loader.rb +242 -0
  112. data/lib/language_operator/validators.rb +170 -0
  113. data/lib/language_operator/version.rb +1 -1
  114. data/lib/language_operator.rb +65 -3
  115. data/requirements/tasks/challenge.md +9 -0
  116. data/requirements/tasks/iterate.md +36 -0
  117. data/requirements/tasks/optimize.md +21 -0
  118. data/requirements/tasks/tag.md +5 -0
  119. data/test_agent_dsl.rb +108 -0
  120. metadata +507 -20
@@ -0,0 +1,1205 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'thor'
4
+ require_relative '../formatters/progress_formatter'
5
+ require_relative '../formatters/table_formatter'
6
+ require_relative '../formatters/value_formatter'
7
+ require_relative '../formatters/log_formatter'
8
+ require_relative '../formatters/status_formatter'
9
+ require_relative '../helpers/cluster_validator'
10
+ require_relative '../helpers/cluster_context'
11
+ require_relative '../helpers/user_prompts'
12
+ require_relative '../helpers/editor_helper'
13
+ require_relative '../helpers/pastel_helper'
14
+ require_relative '../errors/handler'
15
+ require_relative '../../config/cluster_config'
16
+ require_relative '../../kubernetes/client'
17
+ require_relative '../../kubernetes/resource_builder'
18
+
19
+ module LanguageOperator
20
+ module CLI
21
+ module Commands
22
+ # Agent management commands
23
+ class Agent < Thor
24
+ include Helpers::ClusterValidator
25
+ include Helpers::PastelHelper
26
+
27
+ desc 'create [DESCRIPTION]', 'Create a new agent with natural language description'
28
+ long_desc <<-DESC
29
+ Create a new autonomous agent by describing what you want it to do in natural language.
30
+
31
+ The operator will synthesize the agent from your description and deploy it to your cluster.
32
+
33
+ Examples:
34
+ aictl agent create "review my spreadsheet at 4pm daily and email me any errors"
35
+ aictl agent create "summarize Hacker News top stories every morning at 8am"
36
+ aictl agent create "monitor my website uptime and alert me if it goes down"
37
+ aictl agent create --wizard # Interactive wizard mode
38
+ DESC
39
+ option :cluster, type: :string, desc: 'Override current cluster context'
40
+ option :create_cluster, type: :string, desc: 'Create cluster if it doesn\'t exist'
41
+ option :name, type: :string, desc: 'Agent name (generated from description if not provided)'
42
+ option :persona, type: :string, desc: 'Persona to use for the agent'
43
+ option :tools, type: :array, desc: 'Tools to make available to the agent'
44
+ option :models, type: :array, desc: 'Models to make available to the agent'
45
+ option :dry_run, type: :boolean, default: false, desc: 'Preview what would be created without applying'
46
+ option :wizard, type: :boolean, default: false, desc: 'Use interactive wizard mode'
47
+ 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
58
+ end
59
+ end
60
+
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)
69
+ 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
+ ctx = Helpers::ClusterContext.from_options(options.merge(cluster: cluster))
77
+
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)
83
+
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') }
89
+
90
+ Errors::Handler.handle_no_models_available(cluster: ctx.name) if models.empty?
91
+ end
92
+
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
+ )
102
+
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
108
+
109
+ # Apply resource to cluster
110
+ Formatters::ProgressFormatter.with_spinner("Creating agent '#{agent_name}'") do
111
+ ctx.client.apply_resource(agent_resource)
112
+ end
113
+
114
+ # Watch synthesis status
115
+ synthesis_result = watch_synthesis_status(ctx.client, agent_name, ctx.namespace)
116
+
117
+ # Exit if synthesis failed
118
+ exit 1 unless synthesis_result[:success]
119
+
120
+ # Fetch the updated agent to get complete details
121
+ agent = ctx.client.get_resource('LanguageAgent', agent_name, ctx.namespace)
122
+
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']
128
+
129
+ exit 1
130
+ end
131
+
132
+ desc 'list', 'List all agents in current cluster'
133
+ option :cluster, type: :string, desc: 'Override current cluster context'
134
+ option :all_clusters, type: :boolean, default: false, desc: 'Show agents across all clusters'
135
+ 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)
141
+ 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
+ end
148
+
149
+ desc 'inspect NAME', 'Show detailed agent information'
150
+ option :cluster, type: :string, desc: 'Override current cluster context'
151
+ 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
165
+
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
172
+
173
+ # Instructions
174
+ instructions = agent.dig('spec', 'instructions')
175
+ if instructions
176
+ puts 'Instructions:'
177
+ puts " #{instructions}"
178
+ puts
179
+ end
180
+
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
188
+
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
196
+
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
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')
218
+ puts
219
+
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
230
+
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']
238
+
239
+ exit 1
240
+ end
241
+
242
+ desc 'delete NAME', 'Delete an agent'
243
+ option :cluster, type: :string, desc: 'Override current cluster context'
244
+ option :force, type: :boolean, default: false, desc: 'Skip confirmation'
245
+ 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
255
+
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
264
+
265
+ # Delete the agent
266
+ Formatters::ProgressFormatter.with_spinner("Deleting agent '#{name}'") do
267
+ ctx.client.delete_resource('LanguageAgent', name, ctx.namespace)
268
+ end
269
+
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']
274
+
275
+ exit 1
276
+ end
277
+
278
+ desc 'logs NAME', 'Show agent execution logs'
279
+ long_desc <<-DESC
280
+ Stream agent execution logs in real-time.
281
+
282
+ Use -f to follow logs continuously (like tail -f).
283
+
284
+ Examples:
285
+ aictl agent logs my-agent
286
+ aictl agent logs my-agent -f
287
+ DESC
288
+ option :cluster, type: :string, desc: 'Override current cluster context'
289
+ option :follow, type: :boolean, aliases: '-f', default: false, desc: 'Follow logs'
290
+ option :tail, type: :numeric, default: 100, desc: 'Number of lines to show from the end'
291
+ def logs(name)
292
+ ctx = Helpers::ClusterContext.from_options(options)
293
+
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
301
+
302
+ mode = agent.dig('spec', 'mode') || 'autonomous'
303
+
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}"
319
+
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"
322
+
323
+ Formatters::ProgressFormatter.info("Streaming logs for agent '#{name}'...")
324
+ puts
325
+
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
334
+ end
335
+ end
336
+
337
+ # Handle stderr (errors)
338
+ stderr_thread = Thread.new do
339
+ stderr.each_line do |line|
340
+ warn line
341
+ end
342
+ end
343
+
344
+ # Wait for both streams to complete
345
+ stdout_thread.join
346
+ stderr_thread.join
347
+
348
+ # Check exit status
349
+ exit_status = wait_thr.value
350
+ exit exit_status.exitstatus unless exit_status.success?
351
+ 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
+ end
358
+
359
+ desc 'code NAME', 'Display synthesized agent code'
360
+ option :cluster, type: :string, desc: 'Override current cluster context'
361
+ def code(name)
362
+ require_relative '../formatters/code_formatter'
363
+
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
381
+
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
388
+
389
+ # Display with syntax highlighting
390
+ Formatters::CodeFormatter.display_ruby_code(
391
+ code_content,
392
+ title: "Synthesized Code for Agent: #{name}"
393
+ )
394
+
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']
401
+
402
+ exit 1
403
+ end
404
+
405
+ desc 'edit NAME', 'Edit agent instructions'
406
+ option :cluster, type: :string, desc: 'Override current cluster context'
407
+ 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
417
+
418
+ current_instructions = agent.dig('spec', 'instructions')
419
+
420
+ # Edit instructions in user's editor
421
+ new_instructions = Helpers::EditorHelper.edit_content(
422
+ current_instructions,
423
+ 'agent-instructions-',
424
+ '.txt'
425
+ ).strip
426
+
427
+ # Check if changed
428
+ if new_instructions == current_instructions
429
+ Formatters::ProgressFormatter.info('No changes made')
430
+ return
431
+ end
432
+
433
+ # Update agent resource
434
+ agent['spec']['instructions'] = new_instructions
435
+
436
+ Formatters::ProgressFormatter.with_spinner('Updating agent instructions') do
437
+ k8s.apply_resource(agent)
438
+ end
439
+
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']
449
+
450
+ exit 1
451
+ end
452
+
453
+ desc 'pause NAME', 'Pause scheduled agent execution'
454
+ option :cluster, type: :string, desc: 'Override current cluster context'
455
+ def pause(name)
456
+ ctx = Helpers::ClusterContext.from_options(options)
457
+
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
465
+
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
474
+
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
479
+
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]}" : ''
484
+
485
+ cmd = "kubectl #{kubeconfig_arg} #{context_arg} -n #{namespace} patch cronjob #{cronjob_name} -p '{\"spec\":{\"suspend\":true}}'"
486
+ system(cmd)
487
+ 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
+ end
501
+
502
+ desc 'resume NAME', 'Resume paused agent'
503
+ option :cluster, type: :string, desc: 'Override current cluster context'
504
+ def resume(name)
505
+ ctx = Helpers::ClusterContext.from_options(options)
506
+
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
514
+
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
522
+
523
+ # Resume the CronJob by setting spec.suspend = false
524
+ cronjob_name = name
525
+ namespace = ctx.namespace
526
+
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]}" : ''
531
+
532
+ cmd = "kubectl #{kubeconfig_arg} #{context_arg} -n #{namespace} patch cronjob #{cronjob_name} -p '{\"spec\":{\"suspend\":false}}'"
533
+ system(cmd)
534
+ 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
+ end
548
+
549
+ desc 'workspace NAME', 'Browse agent workspace files'
550
+ long_desc <<-DESC
551
+ Browse and manage the workspace files for an agent.
552
+
553
+ Workspaces provide persistent storage for agents to maintain state,
554
+ cache data, and remember information across executions.
555
+
556
+ Examples:
557
+ aictl agent workspace my-agent # List all files
558
+ aictl agent workspace my-agent --path /workspace/state.json # View specific file
559
+ aictl agent workspace my-agent --clean # Clear workspace
560
+ DESC
561
+ option :cluster, type: :string, desc: 'Override current cluster context'
562
+ option :path, type: :string, desc: 'View specific file contents'
563
+ option :clean, type: :boolean, desc: 'Clear workspace (with confirmation)'
564
+ def workspace(name)
565
+ ctx = Helpers::ClusterContext.from_options(options)
566
+
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
574
+
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
587
+
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)
594
+ 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
+ end
601
+
602
+ private
603
+
604
+ def handle_agent_not_found(name, ctx)
605
+ # Get available agents for fuzzy matching
606
+ agents = ctx.client.list_resources('LanguageAgent', namespace: ctx.namespace)
607
+ available_names = agents.map { |a| a.dig('metadata', 'name') }
608
+
609
+ error = K8s::Error::NotFound.new(404, 'Not Found', 'LanguageAgent')
610
+ Errors::Handler.handle_not_found(error,
611
+ resource_type: 'LanguageAgent',
612
+ resource_name: name,
613
+ cluster: ctx.name,
614
+ available_resources: available_names)
615
+ end
616
+
617
+ def display_agent_created(agent, cluster, _description, synthesis_result)
618
+ require_relative '../formatters/code_formatter'
619
+ agent_name = agent.dig('metadata', 'name')
620
+
621
+ puts
622
+ Formatters::ProgressFormatter.success("Agent '#{agent_name}' created and deployed!")
623
+ puts
624
+
625
+ # Get synthesized code if available
626
+ begin
627
+ ctx = Helpers::ClusterContext.from_options(cluster: cluster)
628
+ configmap_name = "#{agent_name}-code"
629
+ configmap = ctx.client.get_resource('ConfigMap', configmap_name, ctx.namespace)
630
+ code_content = configmap.dig('data', 'agent.rb')
631
+
632
+ if code_content
633
+ # Display code preview (first 20 lines)
634
+ Formatters::CodeFormatter.display_ruby_code(
635
+ code_content,
636
+ title: 'Synthesized Code Preview:',
637
+ max_lines: 20
638
+ )
639
+ puts
640
+ end
641
+ rescue StandardError
642
+ # Code not available yet, skip preview
643
+ end
644
+
645
+ # Display agent configuration
646
+ puts pastel.cyan('Agent Configuration:')
647
+ puts " Name: #{agent_name}"
648
+ puts " Cluster: #{cluster}"
649
+
650
+ # Schedule information
651
+ schedule = agent.dig('spec', 'schedule')
652
+ mode = agent.dig('spec', 'mode') || 'autonomous'
653
+ if schedule
654
+ human_schedule = parse_schedule(schedule)
655
+ puts " Schedule: #{human_schedule} (#{schedule})"
656
+
657
+ # Calculate next run
658
+ next_run = agent.dig('status', 'nextRun')
659
+ if next_run
660
+ begin
661
+ next_run_time = Time.parse(next_run)
662
+ time_until = format_time_until(next_run_time)
663
+ puts " Next run: #{next_run} (#{time_until})"
664
+ rescue StandardError
665
+ puts " Next run: #{next_run}"
666
+ end
667
+ end
668
+ else
669
+ puts " Mode: #{mode}"
670
+ end
671
+
672
+ # Persona
673
+ persona = agent.dig('spec', 'persona')
674
+ puts " Persona: #{persona || '(auto-selected)'}"
675
+
676
+ # Tools
677
+ tools = agent.dig('spec', 'tools') || []
678
+ puts " Tools: #{tools.join(', ')}" if tools.any?
679
+
680
+ # Models
681
+ model_refs = agent.dig('spec', 'modelRefs') || []
682
+ if model_refs.any?
683
+ model_names = model_refs.map { |ref| ref['name'] }
684
+ puts " Models: #{model_names.join(', ')}"
685
+ end
686
+
687
+ puts
688
+
689
+ # Synthesis stats
690
+ if synthesis_result[:duration]
691
+ puts pastel.dim("Synthesis completed in #{format_duration(synthesis_result[:duration])}")
692
+ puts pastel.dim("Model: #{synthesis_result[:model]}") if synthesis_result[:model]
693
+ puts
694
+ end
695
+
696
+ # Next steps
697
+ puts pastel.cyan('Next Steps:')
698
+ puts " aictl agent logs #{agent_name} -f # Follow agent execution logs"
699
+ puts " aictl agent code #{agent_name} # View full synthesized code"
700
+ puts " aictl agent inspect #{agent_name} # View detailed agent status"
701
+ puts
702
+ end
703
+
704
+ def parse_schedule(cron_expr)
705
+ # Simple cron to human-readable conversion
706
+ # Format: minute hour day month weekday
707
+ parts = cron_expr.split
708
+
709
+ return cron_expr if parts.length != 5
710
+
711
+ minute, hour, day, month, weekday = parts
712
+
713
+ # Common patterns
714
+ if minute == '0' && hour != '*' && day == '*' && month == '*' && weekday == '*'
715
+ # Daily at specific hour
716
+ hour12 = hour.to_i % 12
717
+ hour12 = 12 if hour12.zero?
718
+ period = hour.to_i < 12 ? 'AM' : 'PM'
719
+ return "Daily at #{hour12}:00 #{period}"
720
+ elsif minute != '*' && hour != '*' && day == '*' && month == '*' && weekday == '*'
721
+ # Daily at specific time
722
+ hour12 = hour.to_i % 12
723
+ hour12 = 12 if hour12.zero?
724
+ period = hour.to_i < 12 ? 'AM' : 'PM'
725
+ return "Daily at #{hour12}:#{minute.rjust(2, '0')} #{period}"
726
+ elsif minute.start_with?('*/') && hour == '*'
727
+ # Every N minutes
728
+ interval = minute[2..].to_i
729
+ return "Every #{interval} minutes"
730
+ elsif minute == '*' && hour.start_with?('*/')
731
+ # Every N hours
732
+ interval = hour[2..].to_i
733
+ return "Every #{interval} hours"
734
+ end
735
+
736
+ # Fallback to cron expression
737
+ cron_expr
738
+ end
739
+
740
+ def format_time_until(future_time)
741
+ Formatters::ValueFormatter.time_until(future_time)
742
+ end
743
+
744
+ def display_dry_run_preview(agent_resource, cluster, description)
745
+ require 'yaml'
746
+
747
+ puts
748
+ puts '=' * 80
749
+ puts ' DRY RUN: Agent Creation Preview'
750
+ puts '=' * 80
751
+ puts
752
+
753
+ # Extract key information
754
+ name = agent_resource.dig('metadata', 'name')
755
+ namespace = agent_resource.dig('metadata', 'namespace')
756
+ persona = agent_resource.dig('spec', 'persona')
757
+ tools = agent_resource.dig('spec', 'tools') || []
758
+ model_refs = agent_resource.dig('spec', 'modelRefs') || []
759
+ models = model_refs.map { |ref| ref['name'] }
760
+ mode = agent_resource.dig('spec', 'mode') || 'autonomous'
761
+ schedule = agent_resource.dig('spec', 'schedule')
762
+
763
+ # Display summary
764
+ puts 'Agent Summary:'
765
+ puts " Name: #{name}"
766
+ puts " Cluster: #{cluster}"
767
+ puts " Namespace: #{namespace}"
768
+ puts " Mode: #{mode}"
769
+ puts " Schedule: #{schedule || 'N/A'}" if schedule
770
+ puts " Instructions: #{description}"
771
+ puts
772
+
773
+ # Show detected configuration
774
+ if persona
775
+ puts 'Detected Configuration:'
776
+ puts " Persona: #{persona}"
777
+ end
778
+
779
+ puts " Tools: #{tools.join(', ')}" if tools.any?
780
+
781
+ puts " Models: #{models.join(', ')}" if models.any?
782
+
783
+ puts if persona || tools.any? || models.any?
784
+
785
+ # Show full YAML
786
+ puts 'Generated YAML:'
787
+ puts '─' * 80
788
+ puts YAML.dump(agent_resource)
789
+ puts '─' * 80
790
+ puts
791
+
792
+ # Show what would happen
793
+ puts 'What would happen:'
794
+ puts ' 1. Agent resource would be created in the cluster'
795
+ puts ' 2. Operator would synthesize Ruby code from instructions'
796
+ puts ' 3. Agent would be deployed and start running'
797
+ puts
798
+
799
+ # Show how to actually create
800
+ Formatters::ProgressFormatter.info('No changes made (dry-run mode)')
801
+ puts
802
+ puts 'To create this agent for real, run:'
803
+ cmd_parts = ["aictl agent create \"#{description}\""]
804
+ cmd_parts << "--name #{name}" if options[:name]
805
+ cmd_parts << "--persona #{persona}" if persona
806
+ cmd_parts << "--tools #{tools.join(' ')}" if tools.any?
807
+ cmd_parts << "--models #{models.join(' ')}" if models.any?
808
+ cmd_parts << "--cluster #{cluster}" if options[:cluster]
809
+ puts " #{cmd_parts.join(' ')}"
810
+ end
811
+
812
+ def format_status(status)
813
+ Formatters::StatusFormatter.format(status)
814
+ end
815
+
816
+ def generate_agent_name(description)
817
+ # Simple name generation from description
818
+ # Take first few words, lowercase, hyphenate
819
+ words = description.downcase.gsub(/[^a-z0-9\s]/, '').split[0..2]
820
+ name = words.join('-')
821
+ # Add random suffix to avoid collisions
822
+ "#{name}-#{Time.now.to_i.to_s[-4..]}"
823
+ end
824
+
825
+ def watch_synthesis_status(k8s, agent_name, namespace)
826
+ # Start with analyzing description
827
+ puts
828
+ Formatters::ProgressFormatter.info('Synthesizing agent code...')
829
+ puts
830
+
831
+ max_wait = 600 # Wait up to 10 minutes (local models can be slow)
832
+ interval = 2 # Check every 2 seconds
833
+ elapsed = 0
834
+ start_time = Time.now
835
+ synthesis_data = {}
836
+
837
+ result = Formatters::ProgressFormatter.with_spinner('Analyzing description and generating code') do
838
+ loop do
839
+ status = check_synthesis_status(k8s, agent_name, namespace, synthesis_data, start_time)
840
+ return status if status
841
+
842
+ # Timeout check
843
+ if elapsed >= max_wait
844
+ Formatters::ProgressFormatter.warn('Synthesis taking longer than expected, continuing in background...')
845
+ puts
846
+ puts 'Check synthesis status with:'
847
+ puts " aictl agent inspect #{agent_name}"
848
+ return { success: true, timeout: true }
849
+ end
850
+
851
+ sleep interval
852
+ elapsed += interval
853
+ end
854
+ rescue K8s::Error::NotFound
855
+ # Agent not found yet, keep waiting
856
+ sleep interval
857
+ elapsed += interval
858
+ retry if elapsed < max_wait
859
+
860
+ Formatters::ProgressFormatter.error('Agent resource not found')
861
+ return { success: false }
862
+ rescue StandardError => e
863
+ Formatters::ProgressFormatter.warn("Could not watch synthesis: #{e.message}")
864
+ return { success: true } # Continue anyway
865
+ end
866
+
867
+ # Show synthesis details after spinner completes
868
+ if result[:success] && !result[:timeout]
869
+ duration = result[:duration]
870
+ Formatters::ProgressFormatter.success("Code synthesis completed in #{format_duration(duration)}")
871
+ puts " Model: #{synthesis_data[:model]}" if synthesis_data[:model]
872
+ puts " Tokens: #{synthesis_data[:token_count]}" if synthesis_data[:token_count]
873
+ end
874
+
875
+ result
876
+ end
877
+
878
+ def check_synthesis_status(k8s, agent_name, namespace, synthesis_data, start_time)
879
+ agent = k8s.get_resource('LanguageAgent', agent_name, namespace)
880
+ conditions = agent.dig('status', 'conditions') || []
881
+ synthesis_status = agent.dig('status', 'synthesis')
882
+
883
+ # Capture synthesis metadata
884
+ if synthesis_status
885
+ synthesis_data[:model] = synthesis_status['model']
886
+ synthesis_data[:token_count] = synthesis_status['tokenCount']
887
+ end
888
+
889
+ # Check for synthesis completion
890
+ synthesized = conditions.find { |c| c['type'] == 'Synthesized' }
891
+ return nil unless synthesized
892
+
893
+ if synthesized['status'] == 'True'
894
+ duration = Time.now - start_time
895
+ { success: true, duration: duration, **synthesis_data }
896
+ elsif synthesized['status'] == 'False'
897
+ Formatters::ProgressFormatter.error("Synthesis failed: #{synthesized['message']}")
898
+ { success: false }
899
+ end
900
+ end
901
+
902
+ def format_duration(seconds)
903
+ Formatters::ValueFormatter.duration(seconds)
904
+ end
905
+
906
+ def list_cluster_agents(cluster)
907
+ ctx = Helpers::ClusterContext.from_options(cluster: cluster)
908
+
909
+ Formatters::ProgressFormatter.info("Agents in cluster '#{cluster}'")
910
+
911
+ agents = ctx.client.list_resources('LanguageAgent', namespace: ctx.namespace)
912
+
913
+ table_data = agents.map do |agent|
914
+ {
915
+ name: agent.dig('metadata', 'name'),
916
+ mode: agent.dig('spec', 'mode') || 'autonomous',
917
+ status: agent.dig('status', 'phase') || 'Unknown',
918
+ next_run: agent.dig('status', 'nextRun') || 'N/A',
919
+ executions: agent.dig('status', 'executionCount') || 0
920
+ }
921
+ end
922
+
923
+ Formatters::TableFormatter.agents(table_data)
924
+
925
+ return unless agents.empty?
926
+
927
+ puts
928
+ puts 'Create an agent with:'
929
+ puts ' aictl agent create "<description>"'
930
+ end
931
+
932
+ def list_all_clusters
933
+ clusters = Config::ClusterConfig.list_clusters
934
+
935
+ if clusters.empty?
936
+ Formatters::ProgressFormatter.info('No clusters found')
937
+ puts
938
+ puts 'Create a cluster first:'
939
+ puts ' aictl cluster create <name>'
940
+ return
941
+ end
942
+
943
+ all_agents = []
944
+
945
+ clusters.each do |cluster|
946
+ ctx = Helpers::ClusterContext.from_options(cluster: cluster[:name])
947
+
948
+ agents = ctx.client.list_resources('LanguageAgent', namespace: ctx.namespace)
949
+
950
+ agents.each do |agent|
951
+ all_agents << {
952
+ cluster: cluster[:name],
953
+ name: agent.dig('metadata', 'name'),
954
+ mode: agent.dig('spec', 'mode') || 'autonomous',
955
+ status: agent.dig('status', 'phase') || 'Unknown',
956
+ next_run: agent.dig('status', 'nextRun') || 'N/A',
957
+ executions: agent.dig('status', 'executionCount') || 0
958
+ }
959
+ end
960
+ rescue StandardError => e
961
+ Formatters::ProgressFormatter.warn("Failed to get agents from cluster '#{cluster[:name]}': #{e.message}")
962
+ end
963
+
964
+ # Group agents by cluster for formatted display
965
+ agents_by_cluster = all_agents.group_by { |agent| agent[:cluster] }
966
+ .transform_values { |agents| agents.map { |a| a.except(:cluster) } }
967
+
968
+ Formatters::TableFormatter.all_agents(agents_by_cluster)
969
+ end
970
+
971
+ # Workspace-related helper methods
972
+
973
+ def get_agent_pod(ctx, agent_name)
974
+ # Find pod for this agent using label selector
975
+ label_selector = "app.kubernetes.io/name=#{agent_name}"
976
+ pods = ctx.client.list_resources('Pod', namespace: ctx.namespace, label_selector: label_selector)
977
+
978
+ if pods.empty?
979
+ Formatters::ProgressFormatter.error("No running pods found for agent '#{agent_name}'")
980
+ puts
981
+ puts 'Possible reasons:'
982
+ puts ' - Agent pod has not started yet'
983
+ puts ' - Agent is paused or stopped'
984
+ puts ' - Agent failed to deploy'
985
+ puts
986
+ puts 'Check agent status with:'
987
+ puts " aictl agent inspect #{agent_name}"
988
+ exit 1
989
+ end
990
+
991
+ # Find a running pod
992
+ running_pod = pods.find do |pod|
993
+ pod.dig('status', 'phase') == 'Running'
994
+ end
995
+
996
+ unless running_pod
997
+ Formatters::ProgressFormatter.error('Agent pod exists but is not running')
998
+ puts
999
+ puts "Current pod status: #{pods.first.dig('status', 'phase')}"
1000
+ puts
1001
+ puts 'Check pod logs with:'
1002
+ puts " aictl agent logs #{agent_name}"
1003
+ exit 1
1004
+ end
1005
+
1006
+ running_pod.dig('metadata', 'name')
1007
+ end
1008
+
1009
+ def exec_in_pod(ctx, pod_name, command)
1010
+ # Properly escape command for shell
1011
+ cmd_str = command.is_a?(Array) ? command.join(' ') : command
1012
+ kubectl_cmd = "#{ctx.kubectl_prefix} exec #{pod_name} -- #{cmd_str}"
1013
+
1014
+ # Execute and capture output
1015
+ require 'open3'
1016
+ stdout, stderr, status = Open3.capture3(kubectl_cmd)
1017
+
1018
+ raise "Command failed: #{stderr}" unless status.success?
1019
+
1020
+ stdout
1021
+ end
1022
+
1023
+ def list_workspace_files(ctx, agent_name)
1024
+ pod_name = get_agent_pod(ctx, agent_name)
1025
+
1026
+ # Check if workspace directory exists
1027
+ begin
1028
+ exec_in_pod(ctx, pod_name, 'test -d /workspace')
1029
+ rescue StandardError
1030
+ Formatters::ProgressFormatter.error('Workspace directory not found in agent pod')
1031
+ puts
1032
+ puts 'The /workspace directory does not exist in the agent pod.'
1033
+ puts 'This agent may not have workspace support enabled.'
1034
+ exit 1
1035
+ end
1036
+
1037
+ # Get workspace usage
1038
+ usage_output = exec_in_pod(
1039
+ ctx,
1040
+ pod_name,
1041
+ 'du -sh /workspace 2>/dev/null || echo "0\t/workspace"'
1042
+ )
1043
+ workspace_size = usage_output.split("\t").first.strip
1044
+
1045
+ # List files with details
1046
+ file_list = exec_in_pod(
1047
+ ctx,
1048
+ pod_name,
1049
+ 'find /workspace -ls 2>/dev/null | tail -n +2'
1050
+ )
1051
+
1052
+ puts
1053
+ puts pastel.cyan("Workspace for agent '#{agent_name}' (#{workspace_size})")
1054
+ puts '=' * 60
1055
+ puts
1056
+
1057
+ if file_list.strip.empty?
1058
+ puts pastel.dim('Workspace is empty')
1059
+ puts
1060
+ puts 'The agent will create files here as it runs.'
1061
+ puts
1062
+ return
1063
+ end
1064
+
1065
+ # Parse and display file list
1066
+ file_list.each_line do |line|
1067
+ parts = line.strip.split(/\s+/, 11)
1068
+ next if parts.length < 11
1069
+
1070
+ # Extract relevant parts
1071
+ # Format: inode blocks perms links user group size month day time path
1072
+ perms = parts[2]
1073
+ size = parts[6]
1074
+ month = parts[7]
1075
+ day = parts[8]
1076
+ time_or_year = parts[9]
1077
+ path = parts[10]
1078
+
1079
+ # Skip the /workspace directory itself
1080
+ next if path == '/workspace'
1081
+
1082
+ # Determine file type and icon
1083
+ icon = if perms.start_with?('d')
1084
+ pastel.blue('📁')
1085
+ else
1086
+ pastel.white('📄')
1087
+ end
1088
+
1089
+ # Format path relative to workspace
1090
+ relative_path = path.sub('/workspace/', '')
1091
+ indent = ' ' * relative_path.count('/')
1092
+
1093
+ # Format size
1094
+ formatted_size = format_file_size(size.to_i).rjust(8)
1095
+
1096
+ # Format time
1097
+ formatted_time = "#{month} #{day.rjust(2)} #{time_or_year}"
1098
+
1099
+ puts "#{indent}#{icon} #{File.basename(relative_path).ljust(30)} #{pastel.dim(formatted_size)} #{pastel.dim(formatted_time)}"
1100
+ end
1101
+
1102
+ puts
1103
+ puts pastel.dim('Commands:')
1104
+ puts pastel.dim(" aictl agent workspace #{agent_name} --path /workspace/<file> # View file")
1105
+ puts pastel.dim(" aictl agent workspace #{agent_name} --clean # Clear workspace")
1106
+ puts
1107
+ end
1108
+
1109
+ def view_workspace_file(ctx, agent_name, file_path)
1110
+ pod_name = get_agent_pod(ctx, agent_name)
1111
+
1112
+ # Check if file exists
1113
+ begin
1114
+ exec_in_pod(ctx, pod_name, "test -f #{file_path}")
1115
+ rescue StandardError
1116
+ Formatters::ProgressFormatter.error("File not found: #{file_path}")
1117
+ puts
1118
+ puts 'List available files with:'
1119
+ puts " aictl agent workspace #{agent_name}"
1120
+ exit 1
1121
+ end
1122
+
1123
+ # Get file metadata
1124
+ stat_output = exec_in_pod(
1125
+ ctx,
1126
+ pod_name,
1127
+ "stat -c '%s %Y' #{file_path}"
1128
+ )
1129
+ size, mtime = stat_output.strip.split
1130
+
1131
+ # Get file contents
1132
+ contents = exec_in_pod(
1133
+ ctx,
1134
+ pod_name,
1135
+ "cat #{file_path}"
1136
+ )
1137
+
1138
+ # Display file
1139
+ puts
1140
+ puts pastel.cyan("File: #{file_path}")
1141
+ puts "Size: #{format_file_size(size.to_i)}"
1142
+ puts "Modified: #{format_timestamp(Time.at(mtime.to_i))}"
1143
+ puts '=' * 60
1144
+ puts
1145
+ puts contents
1146
+ puts
1147
+ end
1148
+
1149
+ def clean_workspace(ctx, agent_name)
1150
+ pod_name = get_agent_pod(ctx, agent_name)
1151
+
1152
+ # Get current workspace usage
1153
+ usage_output = exec_in_pod(
1154
+ ctx,
1155
+ pod_name,
1156
+ 'du -sh /workspace 2>/dev/null || echo "0\t/workspace"'
1157
+ )
1158
+ workspace_size = usage_output.split("\t").first.strip
1159
+
1160
+ # Count files
1161
+ file_count = exec_in_pod(
1162
+ ctx,
1163
+ pod_name,
1164
+ 'find /workspace -type f | wc -l'
1165
+ ).strip.to_i
1166
+
1167
+ puts
1168
+ puts pastel.yellow("This will delete ALL files in the workspace for '#{agent_name}'")
1169
+ puts
1170
+ puts 'The agent will lose:'
1171
+ puts ' • Execution history'
1172
+ puts ' • Cached data'
1173
+ puts ' • State information'
1174
+ puts
1175
+ puts "Current workspace: #{file_count} files, #{workspace_size}"
1176
+ puts
1177
+
1178
+ # Use UserPrompts helper
1179
+ return unless Helpers::UserPrompts.confirm('Are you sure?')
1180
+
1181
+ # Delete all files in workspace
1182
+ Formatters::ProgressFormatter.with_spinner('Cleaning workspace') do
1183
+ exec_in_pod(
1184
+ ctx,
1185
+ pod_name,
1186
+ 'find /workspace -mindepth 1 -delete'
1187
+ )
1188
+ end
1189
+
1190
+ Formatters::ProgressFormatter.success("Workspace cleared (freed #{workspace_size})")
1191
+ puts
1192
+ puts 'The agent will start fresh on its next execution.'
1193
+ end
1194
+
1195
+ def format_file_size(bytes)
1196
+ Formatters::ValueFormatter.file_size(bytes)
1197
+ end
1198
+
1199
+ def format_timestamp(time)
1200
+ Formatters::ValueFormatter.timestamp(time)
1201
+ end
1202
+ end
1203
+ end
1204
+ end
1205
+ end