language-operator 0.1.61 → 0.1.62

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 (143) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/commands/persona.md +9 -0
  3. data/.claude/commands/task.md +46 -1
  4. data/.rubocop.yml +13 -0
  5. data/.rubocop_custom/use_ux_helper.rb +44 -0
  6. data/CHANGELOG.md +8 -0
  7. data/Gemfile.lock +12 -1
  8. data/Makefile +26 -7
  9. data/Makefile.common +50 -0
  10. data/bin/aictl +8 -1
  11. data/components/agent/Gemfile +1 -1
  12. data/components/agent/bin/langop-agent +7 -0
  13. data/docs/README.md +58 -0
  14. data/docs/{dsl/best-practices.md → best-practices.md} +4 -4
  15. data/docs/cli-reference.md +274 -0
  16. data/docs/{dsl/constraints.md → constraints.md} +5 -5
  17. data/docs/how-agents-work.md +156 -0
  18. data/docs/installation.md +218 -0
  19. data/docs/quickstart.md +299 -0
  20. data/docs/understanding-generated-code.md +265 -0
  21. data/docs/using-tools.md +457 -0
  22. data/docs/webhooks.md +509 -0
  23. data/examples/ux_helpers_demo.rb +296 -0
  24. data/lib/language_operator/agent/base.rb +11 -1
  25. data/lib/language_operator/agent/executor.rb +23 -6
  26. data/lib/language_operator/agent/safety/safe_executor.rb +41 -39
  27. data/lib/language_operator/agent/task_executor.rb +346 -63
  28. data/lib/language_operator/agent/web_server.rb +110 -14
  29. data/lib/language_operator/agent/webhook_authenticator.rb +39 -5
  30. data/lib/language_operator/agent.rb +88 -2
  31. data/lib/language_operator/cli/base_command.rb +17 -11
  32. data/lib/language_operator/cli/command_loader.rb +72 -0
  33. data/lib/language_operator/cli/commands/agent/base.rb +837 -0
  34. data/lib/language_operator/cli/commands/agent/code_operations.rb +102 -0
  35. data/lib/language_operator/cli/commands/agent/helpers/cluster_llm_client.rb +116 -0
  36. data/lib/language_operator/cli/commands/agent/helpers/code_parser.rb +115 -0
  37. data/lib/language_operator/cli/commands/agent/helpers/synthesis_watcher.rb +96 -0
  38. data/lib/language_operator/cli/commands/agent/learning.rb +289 -0
  39. data/lib/language_operator/cli/commands/agent/lifecycle.rb +102 -0
  40. data/lib/language_operator/cli/commands/agent/logs.rb +125 -0
  41. data/lib/language_operator/cli/commands/agent/workspace.rb +327 -0
  42. data/lib/language_operator/cli/commands/cluster.rb +129 -84
  43. data/lib/language_operator/cli/commands/install.rb +1 -1
  44. data/lib/language_operator/cli/commands/model/base.rb +215 -0
  45. data/lib/language_operator/cli/commands/model/test.rb +165 -0
  46. data/lib/language_operator/cli/commands/persona.rb +16 -34
  47. data/lib/language_operator/cli/commands/quickstart.rb +3 -2
  48. data/lib/language_operator/cli/commands/status.rb +40 -67
  49. data/lib/language_operator/cli/commands/system/base.rb +44 -0
  50. data/lib/language_operator/cli/commands/system/exec.rb +147 -0
  51. data/lib/language_operator/cli/commands/system/helpers/llm_synthesis.rb +183 -0
  52. data/lib/language_operator/cli/commands/system/helpers/pod_manager.rb +212 -0
  53. data/lib/language_operator/cli/commands/system/helpers/template_loader.rb +57 -0
  54. data/lib/language_operator/cli/commands/system/helpers/template_validator.rb +174 -0
  55. data/lib/language_operator/cli/commands/system/schema.rb +92 -0
  56. data/lib/language_operator/cli/commands/system/synthesis_template.rb +151 -0
  57. data/lib/language_operator/cli/commands/system/synthesize.rb +224 -0
  58. data/lib/language_operator/cli/commands/system/validate_template.rb +130 -0
  59. data/lib/language_operator/cli/commands/tool/base.rb +271 -0
  60. data/lib/language_operator/cli/commands/tool/install.rb +255 -0
  61. data/lib/language_operator/cli/commands/tool/search.rb +69 -0
  62. data/lib/language_operator/cli/commands/tool/test.rb +115 -0
  63. data/lib/language_operator/cli/commands/use.rb +29 -6
  64. data/lib/language_operator/cli/errors/handler.rb +20 -17
  65. data/lib/language_operator/cli/errors/suggestions.rb +3 -5
  66. data/lib/language_operator/cli/errors/thor_errors.rb +55 -0
  67. data/lib/language_operator/cli/formatters/code_formatter.rb +4 -11
  68. data/lib/language_operator/cli/formatters/log_formatter.rb +8 -15
  69. data/lib/language_operator/cli/formatters/progress_formatter.rb +6 -8
  70. data/lib/language_operator/cli/formatters/status_formatter.rb +26 -7
  71. data/lib/language_operator/cli/formatters/table_formatter.rb +47 -36
  72. data/lib/language_operator/cli/formatters/value_formatter.rb +75 -0
  73. data/lib/language_operator/cli/helpers/cluster_context.rb +5 -3
  74. data/lib/language_operator/cli/helpers/kubeconfig_validator.rb +2 -1
  75. data/lib/language_operator/cli/helpers/label_utils.rb +97 -0
  76. data/lib/language_operator/{ux/concerns/provider_helpers.rb → cli/helpers/provider_helper.rb} +10 -29
  77. data/lib/language_operator/cli/helpers/schedule_builder.rb +21 -1
  78. data/lib/language_operator/cli/helpers/user_prompts.rb +19 -11
  79. data/lib/language_operator/cli/helpers/ux_helper.rb +538 -0
  80. data/lib/language_operator/{ux/concerns/input_validation.rb → cli/helpers/validation_helper.rb} +13 -66
  81. data/lib/language_operator/cli/main.rb +50 -40
  82. data/lib/language_operator/cli/templates/tools/generic.yaml +3 -0
  83. data/lib/language_operator/cli/wizards/agent_wizard.rb +12 -20
  84. data/lib/language_operator/cli/wizards/model_wizard.rb +271 -0
  85. data/lib/language_operator/cli/wizards/quickstart_wizard.rb +8 -34
  86. data/lib/language_operator/client/base.rb +28 -0
  87. data/lib/language_operator/client/config.rb +4 -1
  88. data/lib/language_operator/client/mcp_connector.rb +1 -1
  89. data/lib/language_operator/config/cluster_config.rb +3 -2
  90. data/lib/language_operator/config.rb +38 -11
  91. data/lib/language_operator/constants/kubernetes_labels.rb +80 -0
  92. data/lib/language_operator/constants.rb +13 -0
  93. data/lib/language_operator/dsl/http.rb +127 -10
  94. data/lib/language_operator/dsl.rb +153 -6
  95. data/lib/language_operator/errors.rb +50 -0
  96. data/lib/language_operator/kubernetes/client.rb +11 -6
  97. data/lib/language_operator/kubernetes/resource_builder.rb +58 -84
  98. data/lib/language_operator/templates/schema/agent_dsl_openapi.yaml +1 -1
  99. data/lib/language_operator/templates/schema/agent_dsl_schema.json +1 -1
  100. data/lib/language_operator/type_coercion.rb +118 -34
  101. data/lib/language_operator/utils/secure_path.rb +74 -0
  102. data/lib/language_operator/utils.rb +7 -0
  103. data/lib/language_operator/validators.rb +54 -2
  104. data/lib/language_operator/version.rb +1 -1
  105. data/synth/001/Makefile +10 -2
  106. data/synth/001/agent.rb +16 -15
  107. data/synth/001/output.log +27 -10
  108. data/synth/002/Makefile +10 -2
  109. data/synth/003/Makefile +1 -1
  110. data/synth/003/README.md +205 -133
  111. data/synth/003/agent.optimized.rb +66 -0
  112. data/synth/003/agent.synthesized.rb +41 -0
  113. metadata +111 -35
  114. data/docs/dsl/agent-reference.md +0 -604
  115. data/docs/dsl/mcp-integration.md +0 -1177
  116. data/docs/dsl/webhooks.md +0 -932
  117. data/docs/dsl/workflows.md +0 -744
  118. data/lib/language_operator/cli/commands/agent.rb +0 -1712
  119. data/lib/language_operator/cli/commands/model.rb +0 -366
  120. data/lib/language_operator/cli/commands/system.rb +0 -1259
  121. data/lib/language_operator/cli/commands/tool.rb +0 -654
  122. data/lib/language_operator/cli/formatters/optimization_formatter.rb +0 -226
  123. data/lib/language_operator/cli/helpers/pastel_helper.rb +0 -24
  124. data/lib/language_operator/learning/adapters/base_adapter.rb +0 -149
  125. data/lib/language_operator/learning/adapters/jaeger_adapter.rb +0 -221
  126. data/lib/language_operator/learning/adapters/signoz_adapter.rb +0 -435
  127. data/lib/language_operator/learning/adapters/tempo_adapter.rb +0 -239
  128. data/lib/language_operator/learning/optimizer.rb +0 -319
  129. data/lib/language_operator/learning/pattern_detector.rb +0 -260
  130. data/lib/language_operator/learning/task_synthesizer.rb +0 -288
  131. data/lib/language_operator/learning/trace_analyzer.rb +0 -285
  132. data/lib/language_operator/templates/task_synthesis.tmpl +0 -98
  133. data/lib/language_operator/ux/base.rb +0 -81
  134. data/lib/language_operator/ux/concerns/README.md +0 -155
  135. data/lib/language_operator/ux/concerns/headings.rb +0 -90
  136. data/lib/language_operator/ux/create_agent.rb +0 -255
  137. data/lib/language_operator/ux/create_model.rb +0 -267
  138. data/lib/language_operator/ux/quickstart.rb +0 -594
  139. data/synth/003/agent.rb +0 -41
  140. data/synth/003/output.log +0 -68
  141. /data/docs/{architecture/agent-runtime.md → agent-internals.md} +0 -0
  142. /data/docs/{dsl/chat-endpoints.md → chat-endpoints.md} +0 -0
  143. /data/docs/{dsl/SCHEMA_VERSION.md → schema-versioning.md} +0 -0
@@ -0,0 +1,837 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'thor'
4
+ require_relative '../../command_loader'
5
+ require_relative '../../wizards/agent_wizard'
6
+
7
+ # Include all agent subcommand modules
8
+ require_relative 'workspace'
9
+ require_relative 'code_operations'
10
+ require_relative 'logs'
11
+ require_relative 'lifecycle'
12
+ require_relative 'learning'
13
+
14
+ # Include helper modules
15
+ require_relative 'helpers/cluster_llm_client'
16
+ require_relative 'helpers/code_parser'
17
+ require_relative 'helpers/synthesis_watcher'
18
+ require_relative '../../helpers/cluster_context'
19
+ require_relative '../../../kubernetes/resource_builder'
20
+
21
+ module LanguageOperator
22
+ module CLI
23
+ module Commands
24
+ module Agent
25
+ # Base agent command class
26
+ class Base < BaseCommand
27
+ include Constants
28
+ include ::LanguageOperator::CLI::Helpers::ClusterValidator
29
+ include CLI::Helpers::UxHelper
30
+ include Agent::Helpers::CodeParser
31
+ include Agent::Helpers::SynthesisWatcher
32
+
33
+ # Include all subcommand modules
34
+ include Workspace
35
+ include CodeOperations
36
+ include Logs
37
+ include Lifecycle
38
+ include Learning
39
+
40
+ # NOTE: Core commands (create, list, inspect, delete) will be added below
41
+ # This file is a placeholder for the refactoring process
42
+ # The full implementation needs to be extracted from the original agent.rb
43
+
44
+ desc 'create [DESCRIPTION]', 'Create a new agent with natural language description'
45
+ long_desc <<-DESC
46
+ Create a new autonomous agent by describing what you want it to do in natural language.
47
+
48
+ The operator will synthesize the agent from your description and deploy it to your cluster.
49
+
50
+ Examples:
51
+ aictl agent create "review my spreadsheet at 4pm daily and email me any errors"
52
+ aictl agent create "summarize Hacker News top stories every morning at 8am"
53
+ aictl agent create "monitor my website uptime and alert me if it goes down"
54
+ aictl agent create --wizard # Interactive wizard mode
55
+ DESC
56
+ option :cluster, type: :string, desc: 'Override current cluster context'
57
+ option :create_cluster, type: :string, desc: 'Create cluster if it doesn\'t exist'
58
+ option :name, type: :string, desc: 'Agent name (generated from description if not provided)'
59
+ option :persona, type: :string, desc: 'Persona to use for the agent'
60
+ option :tools, type: :array, desc: 'Tools to make available to the agent'
61
+ option :models, type: :array, desc: 'Models to make available to the agent'
62
+ option :workspace, type: :boolean, default: true, desc: 'Enable workspace for state persistence'
63
+ option :dry_run, type: :boolean, default: false, desc: 'Preview what would be created without applying'
64
+ option :wizard, type: :boolean, default: false, desc: 'Use interactive wizard mode'
65
+ def create(description = nil)
66
+ handle_command_error('create agent') do
67
+ # Read from stdin if available and no description provided
68
+ description = $stdin.read.strip if description.nil? && !$stdin.tty?
69
+
70
+ # Activate wizard mode if --wizard flag or no description provided
71
+ if options[:wizard] || description.nil? || description.empty?
72
+ wizard = Wizards::AgentWizard.new
73
+ description = wizard.run
74
+
75
+ # User cancelled wizard
76
+ unless description
77
+ Formatters::ProgressFormatter.info('Agent creation cancelled')
78
+ return
79
+ end
80
+ end
81
+
82
+ # Handle --create-cluster flag
83
+ if options[:create_cluster]
84
+ cluster_name = options[:create_cluster]
85
+ unless Config::ClusterConfig.cluster_exists?(cluster_name)
86
+ Formatters::ProgressFormatter.info("Creating cluster '#{cluster_name}'...")
87
+ # Delegate to cluster create command
88
+ require_relative '../cluster'
89
+ Cluster.new.invoke(:create, [cluster_name], switch: true)
90
+ end
91
+ cluster = cluster_name
92
+ else
93
+ # Validate cluster selection (this will exit if none selected)
94
+ cluster = CLI::Helpers::ClusterValidator.get_cluster(options[:cluster])
95
+ end
96
+
97
+ ctx = CLI::Helpers::ClusterContext.from_options(options.merge(cluster: cluster))
98
+
99
+ # Generate agent name from description if not provided
100
+ agent_name = options[:name] || generate_agent_name(description)
101
+
102
+ # Get models: use specified models, or default to all available models in cluster
103
+ models = options[:models]
104
+ if models.nil? || models.empty?
105
+ available_models = ctx.client.list_resources(RESOURCE_MODEL, namespace: ctx.namespace)
106
+ models = available_models.map { |m| m.dig('metadata', 'name') }
107
+
108
+ Errors::Handler.handle_no_models_available(cluster: ctx.name) if models.empty?
109
+ end
110
+
111
+ # Build LanguageAgent resource
112
+ agent_resource = Kubernetes::ResourceBuilder.language_agent(
113
+ agent_name,
114
+ instructions: description,
115
+ cluster: ctx.namespace,
116
+ cluster_ref: ctx.name,
117
+ persona: options[:persona],
118
+ tools: options[:tools] || [],
119
+ models: models,
120
+ workspace: options[:workspace]
121
+ )
122
+
123
+ # Dry-run mode: preview without applying
124
+ if options[:dry_run]
125
+ display_dry_run_preview(agent_resource, ctx.name, description)
126
+ return
127
+ end
128
+
129
+ # Apply resource to cluster
130
+ Formatters::ProgressFormatter.with_spinner("Creating agent '#{agent_name}'") do
131
+ ctx.client.apply_resource(agent_resource)
132
+ end
133
+
134
+ # Watch synthesis status
135
+ synthesis_result = watch_synthesis_status(ctx.client, agent_name, ctx.namespace)
136
+
137
+ # Exit if synthesis failed
138
+ exit 1 unless synthesis_result[:success]
139
+
140
+ # Fetch the updated agent to get complete details
141
+ agent = ctx.client.get_resource(RESOURCE_AGENT, agent_name, ctx.namespace)
142
+
143
+ # Display enhanced success output
144
+ display_agent_created(agent, ctx, description, synthesis_result)
145
+ end
146
+ end
147
+
148
+ desc 'list', 'List all agents in current cluster'
149
+ option :cluster, type: :string, desc: 'Override current cluster context'
150
+ option :all_clusters, type: :boolean, default: false, desc: 'Show agents across all clusters'
151
+ def list
152
+ if options[:all_clusters]
153
+ list_all_clusters
154
+ else
155
+ cluster = options[:cluster]
156
+ list_cluster_agents(cluster)
157
+ end
158
+ end
159
+
160
+ desc 'inspect NAME', 'Show detailed agent information'
161
+ option :cluster, type: :string, desc: 'Override current cluster context'
162
+ def inspect(name)
163
+ handle_command_error('inspect agent') do
164
+ ctx = CLI::Helpers::ClusterContext.from_options(options)
165
+
166
+ begin
167
+ agent = ctx.client.get_resource(RESOURCE_AGENT, name, ctx.namespace)
168
+ rescue K8s::Error::NotFound
169
+ handle_agent_not_found(name, ctx)
170
+ return
171
+ end
172
+
173
+ # Main agent information
174
+ puts
175
+ status = agent.dig('status', 'phase') || 'Unknown'
176
+ format_agent_details(
177
+ name: name,
178
+ namespace: ctx.namespace,
179
+ cluster: ctx.name,
180
+ status: format_status(status),
181
+ mode: agent.dig('spec', 'executionMode') || 'autonomous',
182
+ schedule: agent.dig('spec', 'schedule'),
183
+ persona: agent.dig('spec', 'persona'),
184
+ created: agent.dig('metadata', 'creationTimestamp')
185
+ )
186
+ puts
187
+
188
+ # Execution stats (only for scheduled agents)
189
+ mode = agent.dig('spec', 'executionMode') || 'autonomous'
190
+ if mode == 'scheduled'
191
+ exec_data = get_execution_data(name, ctx)
192
+
193
+ exec_rows = {
194
+ 'Total Runs' => exec_data[:total_runs],
195
+ 'Last Run' => exec_data[:last_run] || 'Never'
196
+ }
197
+ exec_rows['Next Run'] = exec_data[:next_run] || 'N/A' if agent.dig('spec', 'schedule')
198
+
199
+ highlighted_box(title: 'Executions', rows: exec_rows, color: :blue)
200
+ puts
201
+ end
202
+
203
+ # Resources
204
+ resources = agent.dig('spec', 'resources')
205
+ if resources
206
+ resource_rows = {}
207
+ requests = resources['requests'] || {}
208
+ limits = resources['limits'] || {}
209
+
210
+ # CPU
211
+ cpu_request = requests['cpu']
212
+ cpu_limit = limits['cpu']
213
+ resource_rows['CPU'] = [cpu_request, cpu_limit].compact.join(' / ') if cpu_request || cpu_limit
214
+
215
+ # Memory
216
+ memory_request = requests['memory']
217
+ memory_limit = limits['memory']
218
+ resource_rows['Memory'] = [memory_request, memory_limit].compact.join(' / ') if memory_request || memory_limit
219
+
220
+ highlighted_box(title: 'Resources (Request/Limit)', rows: resource_rows, color: :cyan) unless resource_rows.empty?
221
+ puts
222
+ end
223
+
224
+ # Instructions
225
+ instructions = agent.dig('spec', 'instructions')
226
+ if instructions
227
+ puts pastel.white.bold('Instructions')
228
+ puts instructions
229
+ puts
230
+ end
231
+
232
+ # Tools
233
+ tools = agent.dig('spec', 'tools') || []
234
+ unless tools.empty?
235
+ list_box(title: 'Tools', items: tools)
236
+ puts
237
+ end
238
+
239
+ # Models
240
+ model_refs = agent.dig('spec', 'modelRefs') || []
241
+ unless model_refs.empty?
242
+ model_names = model_refs.map { |ref| ref['name'] }
243
+ list_box(title: 'Models', items: model_names, bullet: '⛁')
244
+ puts
245
+ end
246
+
247
+ # Synthesis info
248
+ synthesis = agent.dig('status', 'synthesis')
249
+ if synthesis
250
+ highlighted_box(
251
+ title: 'Synthesis',
252
+ rows: {
253
+ 'Status' => synthesis['status'],
254
+ 'Model' => synthesis['model'],
255
+ 'Completed' => synthesis['completedAt'],
256
+ 'Duration' => synthesis['duration'],
257
+ 'Token Count' => synthesis['tokenCount']
258
+ }
259
+ )
260
+ puts
261
+ end
262
+
263
+ # Conditions
264
+ conditions = agent.dig('status', 'conditions') || []
265
+ unless conditions.empty?
266
+ list_box(
267
+ title: 'Conditions',
268
+ items: conditions,
269
+ style: :conditions
270
+ )
271
+ puts
272
+ end
273
+
274
+ # Labels
275
+ labels = agent.dig('metadata', 'labels') || {}
276
+ list_box(
277
+ title: 'Labels',
278
+ items: labels,
279
+ style: :key_value
280
+ )
281
+
282
+ # Recent events (if available)
283
+ # This would require querying events, which we can add later
284
+ end
285
+ end
286
+
287
+ desc 'delete NAME', 'Delete an agent'
288
+ option :cluster, type: :string, desc: 'Override current cluster context'
289
+ option :force, type: :boolean, default: false, desc: 'Skip confirmation'
290
+ def delete(name)
291
+ handle_command_error('delete agent') do
292
+ ctx = CLI::Helpers::ClusterContext.from_options(options)
293
+
294
+ # Get agent to verify it exists
295
+ get_resource_or_exit(RESOURCE_AGENT, name)
296
+
297
+ # Confirm deletion
298
+ return unless confirm_deletion_with_force('agent', name, ctx.name, force: options[:force])
299
+
300
+ # Delete the agent
301
+ puts
302
+ Formatters::ProgressFormatter.with_spinner("Deleting agent '#{name}'") do
303
+ ctx.client.delete_resource(RESOURCE_AGENT, name, ctx.namespace)
304
+ end
305
+ end
306
+ end
307
+
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.
311
+
312
+ Shows the automatic optimization history and available versions for rollback.
313
+
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)
322
+
323
+ # Get agent to verify it exists
324
+ get_resource_or_exit(RESOURCE_AGENT, name)
325
+
326
+ # List all ConfigMaps with the agent label
327
+ config_maps = ctx.client.list_resources('ConfigMap', namespace: ctx.namespace)
328
+
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
334
+
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
344
+ end
345
+
346
+ private
347
+
348
+ # Shared helper methods that are used across multiple commands
349
+ # These will be extracted from the original agent.rb
350
+
351
+ def handle_agent_not_found(name, ctx, error)
352
+ # Get available agents for fuzzy matching
353
+ agents = ctx.client.list_resources(RESOURCE_AGENT, namespace: ctx.namespace)
354
+ available_names = agents.map { |a| a.dig('metadata', 'name') }
355
+
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
+ end
362
+
363
+ def display_agent_created(agent, ctx, _description, _synthesis_result)
364
+ agent_name = agent.dig('metadata', 'name')
365
+ status = agent.dig('status', 'phase') || 'Unknown'
366
+
367
+ puts
368
+ format_agent_details(
369
+ name: agent_name,
370
+ namespace: ctx.namespace,
371
+ cluster: ctx.name,
372
+ status: format_status(status),
373
+ mode: agent.dig('spec', 'executionMode') || 'autonomous',
374
+ schedule: agent.dig('spec', 'schedule'),
375
+ persona: agent.dig('spec', 'persona') || '(auto-selected)',
376
+ created: Time.now.strftime('%Y-%m-%dT%H:%M:%SZ')
377
+ )
378
+
379
+ puts
380
+ puts 'Next steps:'
381
+ puts pastel.dim("aictl agent logs #{agent_name} -f")
382
+ puts pastel.dim("aictl agent code #{agent_name}")
383
+ puts pastel.dim("aictl agent inspect #{agent_name}")
384
+ puts
385
+ end
386
+
387
+ def parse_schedule(cron_expr)
388
+ # Simple cron to human-readable conversion
389
+ # Format: minute hour day month weekday
390
+ parts = cron_expr.split
391
+
392
+ return cron_expr if parts.length != 5
393
+
394
+ minute, hour, day, month, weekday = parts
395
+
396
+ # Common patterns
397
+ if minute == '0' && hour != '*' && day == '*' && month == '*' && weekday == '*'
398
+ # Daily at specific hour
399
+ hour12 = hour.to_i % 12
400
+ hour12 = 12 if hour12.zero?
401
+ period = hour.to_i < 12 ? 'AM' : 'PM'
402
+ return "Daily at #{hour12}:00 #{period}"
403
+ elsif minute != '*' && hour != '*' && day == '*' && month == '*' && weekday == '*'
404
+ # Daily at specific time
405
+ hour12 = hour.to_i % 12
406
+ hour12 = 12 if hour12.zero?
407
+ period = hour.to_i < 12 ? 'AM' : 'PM'
408
+ return "Daily at #{hour12}:#{minute.rjust(2, '0')} #{period}"
409
+ elsif minute.start_with?('*/') && hour == '*'
410
+ # Every N minutes
411
+ interval = minute[2..].to_i
412
+ return "Every #{interval} minutes"
413
+ elsif minute == '*' && hour.start_with?('*/')
414
+ # Every N hours
415
+ interval = hour[2..].to_i
416
+ return "Every #{interval} hours"
417
+ end
418
+
419
+ # Fallback to cron expression
420
+ cron_expr
421
+ end
422
+
423
+ def format_time_until(future_time)
424
+ Formatters::ValueFormatter.time_until(future_time)
425
+ end
426
+
427
+ def display_dry_run_preview(agent_resource, cluster, description)
428
+ require 'yaml'
429
+
430
+ puts
431
+ puts '=' * 80
432
+ puts ' DRY RUN: Agent Creation Preview'
433
+ puts '=' * 80
434
+ puts
435
+
436
+ # Extract key information
437
+ name = agent_resource.dig('metadata', 'name')
438
+ namespace = agent_resource.dig('metadata', 'namespace')
439
+ persona = agent_resource.dig('spec', 'persona')
440
+ tools = agent_resource.dig('spec', 'tools') || []
441
+ model_refs = agent_resource.dig('spec', 'modelRefs') || []
442
+ models = model_refs.map { |ref| ref['name'] }
443
+ mode = agent_resource.dig('spec', 'executionMode') || 'autonomous'
444
+ schedule = agent_resource.dig('spec', 'schedule')
445
+
446
+ # Display summary
447
+ puts 'Agent Summary:'
448
+ puts " Name: #{name}"
449
+ puts " Cluster: #{cluster}"
450
+ puts " Namespace: #{namespace}"
451
+ puts " Mode: #{mode}"
452
+ puts " Schedule: #{schedule || 'N/A'}" if schedule
453
+ puts " Instructions: #{description}"
454
+ puts
455
+
456
+ # Show detected configuration
457
+ if persona
458
+ puts 'Detected Configuration:'
459
+ puts " Persona: #{persona}"
460
+ end
461
+
462
+ puts " Tools: #{tools.join(', ')}" if tools.any?
463
+
464
+ puts " Models: #{models.join(', ')}" if models.any?
465
+
466
+ puts if persona || tools.any? || models.any?
467
+
468
+ # Show full YAML
469
+ puts 'Generated YAML:'
470
+ puts '─' * 80
471
+ puts YAML.dump(agent_resource)
472
+ puts '─' * 80
473
+ puts
474
+
475
+ # Show what would happen
476
+ puts 'What would happen:'
477
+ puts ' 1. Agent resource would be created in the cluster'
478
+ puts ' 2. Operator would synthesize Ruby code from instructions'
479
+ puts ' 3. Agent would be deployed and start running'
480
+ puts
481
+
482
+ # Show how to actually create
483
+ Formatters::ProgressFormatter.info('No changes made (dry-run mode)')
484
+ puts
485
+ puts 'To create this agent for real, run:'
486
+ cmd_parts = ["aictl agent create \"#{description}\""]
487
+ cmd_parts << "--name #{name}" if options[:name]
488
+ cmd_parts << "--persona #{persona}" if persona
489
+ cmd_parts << "--tools #{tools.join(' ')}" if tools.any?
490
+ cmd_parts << "--models #{models.join(' ')}" if models.any?
491
+ cmd_parts << "--cluster #{cluster}" if options[:cluster]
492
+ puts " #{cmd_parts.join(' ')}"
493
+ end
494
+
495
+ def format_status(status)
496
+ Formatters::StatusFormatter.format(status)
497
+ end
498
+
499
+ def generate_agent_name(description)
500
+ # Simple name generation from description
501
+ # Take first few words, lowercase, hyphenate
502
+ words = description.downcase.gsub(/[^a-z0-9\s]/, '').split[0..2]
503
+ name = words.join('-')
504
+
505
+ # Ensure name starts with a letter (Kubernetes requirement)
506
+ name = "agent-#{name}" unless name.match?(/^[a-z]/)
507
+
508
+ # Add random suffix to avoid collisions
509
+ "#{name}-#{Time.now.to_i.to_s[-4..]}"
510
+ end
511
+
512
+ def format_duration(seconds)
513
+ Formatters::ValueFormatter.duration(seconds)
514
+ end
515
+
516
+ def list_cluster_agents(cluster)
517
+ context = CLI::Helpers::ClusterContext.from_options({ cluster: cluster })
518
+ agents = context.client.list_resources(RESOURCE_AGENT, namespace: context.namespace)
519
+
520
+ if agents.empty?
521
+ Formatters::ProgressFormatter.info('No agents found')
522
+ puts
523
+ puts 'Create an agent with:'
524
+ puts ' aictl agent create "<description>"'
525
+ return
526
+ end
527
+
528
+ table_data = agents.map do |agent|
529
+ {
530
+ name: agent.dig('metadata', 'name'),
531
+ namespace: agent.dig('metadata', 'namespace') || context.namespace,
532
+ mode: agent.dig('spec', 'executionMode') || 'autonomous',
533
+ status: agent.dig('status', 'phase') || 'Unknown'
534
+ }
535
+ end
536
+
537
+ Formatters::TableFormatter.agents(table_data)
538
+ end
539
+
540
+ def list_all_clusters
541
+ clusters = Config::ClusterConfig.list_clusters
542
+
543
+ if clusters.empty?
544
+ Formatters::ProgressFormatter.info('No clusters found')
545
+ puts
546
+ puts 'Create a cluster first:'
547
+ puts ' aictl cluster create <name>'
548
+ return
549
+ end
550
+
551
+ all_agents = []
552
+
553
+ clusters.each do |cluster|
554
+ ctx = CLI::Helpers::ClusterContext.from_options({ cluster: cluster[:name] })
555
+
556
+ agents = ctx.client.list_resources(RESOURCE_AGENT, namespace: ctx.namespace)
557
+
558
+ agents.each do |agent|
559
+ all_agents << {
560
+ cluster: cluster[:name],
561
+ name: agent.dig('metadata', 'name'),
562
+ mode: agent.dig('spec', 'executionMode') || 'autonomous',
563
+ status: agent.dig('status', 'phase') || 'Unknown',
564
+ next_run: agent.dig('status', 'nextRun') || 'N/A',
565
+ executions: agent.dig('status', 'executionCount') || 0
566
+ }
567
+ end
568
+ rescue StandardError => e
569
+ Formatters::ProgressFormatter.warn("Failed to get agents from cluster '#{cluster[:name]}': #{e.message}")
570
+ end
571
+
572
+ # Group agents by cluster for formatted display
573
+ agents_by_cluster = all_agents.group_by { |agent| agent[:cluster] }
574
+ .transform_values { |agents| agents.map { |a| a.except(:cluster) } }
575
+
576
+ Formatters::TableFormatter.all_agents(agents_by_cluster)
577
+ end
578
+
579
+ def watch_synthesis_status(k8s, agent_name, namespace)
580
+ max_wait = 600 # Wait up to 10 minutes (local models can be slow)
581
+ interval = 2 # Check every 2 seconds
582
+ elapsed = 0
583
+ start_time = Time.now
584
+ synthesis_data = {}
585
+
586
+ Formatters::ProgressFormatter.with_spinner('Synthesizing code from instructions') do
587
+ synthesis_result = nil
588
+ loop do
589
+ status = check_synthesis_status(k8s, agent_name, namespace, synthesis_data, start_time)
590
+ if status
591
+ synthesis_result = status
592
+ break
593
+ end
594
+
595
+ # Timeout check
596
+ if elapsed >= max_wait
597
+ Formatters::ProgressFormatter.warn('Synthesis taking longer than expected, continuing in background...')
598
+ puts
599
+ puts 'Check synthesis status with:'
600
+ puts " aictl agent inspect #{agent_name}"
601
+ synthesis_result = { success: true, timeout: true }
602
+ break
603
+ end
604
+
605
+ sleep interval
606
+ elapsed += interval
607
+ end
608
+ synthesis_result
609
+ rescue K8s::Error::NotFound
610
+ # Agent not found yet, keep waiting
611
+ sleep interval
612
+ elapsed += interval
613
+ retry if elapsed < max_wait
614
+
615
+ Formatters::ProgressFormatter.error('Agent resource not found')
616
+ return { success: false }
617
+ rescue StandardError => e
618
+ Formatters::ProgressFormatter.warn("Could not watch synthesis: #{e.message}")
619
+ return { success: true } # Continue anyway
620
+ end
621
+ end
622
+
623
+ def check_synthesis_status(k8s, agent_name, namespace, synthesis_data, start_time)
624
+ agent = k8s.get_resource(RESOURCE_AGENT, agent_name, namespace)
625
+ conditions = agent.dig('status', 'conditions') || []
626
+ synthesis_status = agent.dig('status', 'synthesis')
627
+
628
+ # Capture synthesis metadata
629
+ if synthesis_status
630
+ synthesis_data[:model] = synthesis_status['model']
631
+ synthesis_data[:token_count] = synthesis_status['tokenCount']
632
+ end
633
+
634
+ # Check for synthesis completion
635
+ synthesized = conditions.find { |c| c['type'] == 'Synthesized' }
636
+ return nil unless synthesized
637
+
638
+ if synthesized['status'] == 'True'
639
+ duration = Time.now - start_time
640
+ { success: true, duration: duration, **synthesis_data }
641
+ elsif synthesized['status'] == 'False'
642
+ Formatters::ProgressFormatter.error("Synthesis failed: #{synthesized['message']}")
643
+ { success: false }
644
+ end
645
+ end
646
+
647
+ def get_resource_or_exit(resource_type, name)
648
+ ctx = CLI::Helpers::ClusterContext.from_options(options)
649
+ begin
650
+ ctx.client.get_resource(resource_type, name, ctx.namespace)
651
+ rescue K8s::Error::NotFound => e
652
+ handle_agent_not_found(name, ctx, e) if resource_type == RESOURCE_AGENT
653
+ exit 1
654
+ end
655
+ end
656
+
657
+ def display_agent_versions(agent_configs, agent_name, cluster_name)
658
+ puts
659
+
660
+ if agent_configs.empty?
661
+ puts pastel.yellow("No versioned ConfigMaps found for agent '#{agent_name}'")
662
+ puts
663
+ puts 'Versioned ConfigMaps are created by the operator during automatic learning.'
664
+ puts 'Run the agent a few times to see optimization versions appear here.'
665
+ return
666
+ end
667
+
668
+ highlighted_box(
669
+ title: "Agent Versions: #{agent_name}",
670
+ rows: {
671
+ 'Agent' => pastel.white.bold(agent_name),
672
+ 'Cluster' => cluster_name,
673
+ 'Total Versions' => agent_configs.length
674
+ }
675
+ )
676
+ puts
677
+
678
+ puts pastel.white.bold('Version History:')
679
+
680
+ agent_configs.each do |config_map|
681
+ labels = config_map.dig('metadata', 'labels') || {}
682
+ annotations = config_map.dig('metadata', 'annotations') || {}
683
+
684
+ version = labels['version']
685
+ synthesis_type = labels['synthesis-type'] || 'unknown'
686
+ created_at = config_map.dig('metadata', 'creationTimestamp')
687
+ learned_at = annotations['learned-at']
688
+ learned_tasks = annotations['learned-tasks']
689
+
690
+ # Format creation time
691
+ if created_at
692
+ begin
693
+ time = Time.parse(created_at)
694
+ formatted_time = time.strftime('%Y-%m-%d %H:%M:%S UTC')
695
+ rescue StandardError
696
+ formatted_time = created_at
697
+ end
698
+ else
699
+ formatted_time = 'Unknown'
700
+ end
701
+
702
+ # Format version display
703
+ version_display = case synthesis_type
704
+ when 'initial'
705
+ pastel.blue("v#{version} (initial)")
706
+ when 'learned'
707
+ pastel.green("v#{version} (learned)")
708
+ when 'manual'
709
+ pastel.yellow("v#{version} (manual)")
710
+ else
711
+ pastel.dim("v#{version} (#{synthesis_type})")
712
+ end
713
+
714
+ puts " #{version_display}"
715
+ puts " Created: #{pastel.dim(formatted_time)}"
716
+
717
+ puts " Learned: #{pastel.dim(learned_at)}" if learned_at
718
+
719
+ if learned_tasks && !learned_tasks.empty?
720
+ tasks = learned_tasks.split(',').map(&:strip)
721
+ puts " Tasks: #{pastel.cyan(tasks.join(', '))}"
722
+ end
723
+
724
+ puts
725
+ end
726
+
727
+ puts pastel.white.bold('Available Commands:')
728
+ puts pastel.dim(" aictl agent learning status #{agent_name}")
729
+ puts pastel.dim(" aictl agent inspect #{agent_name}")
730
+ end
731
+
732
+ def get_execution_data(agent_name, ctx)
733
+ execution_data = {
734
+ total_runs: 0,
735
+ last_run: nil,
736
+ next_run: nil
737
+ }
738
+
739
+ # Get data from CronJob
740
+ begin
741
+ # Get CronJob to find last execution time and next run
742
+ cronjob = ctx.client.get_resource('CronJob', agent_name, ctx.namespace)
743
+
744
+ # Get last successful execution time
745
+ last_successful = cronjob.dig('status', 'lastSuccessfulTime')
746
+ if last_successful
747
+ last_time = Time.parse(last_successful)
748
+ execution_data[:last_run] = Formatters::ValueFormatter.time_ago(last_time)
749
+ end
750
+
751
+ # Calculate next run time from schedule
752
+ schedule = cronjob.dig('spec', 'schedule')
753
+ if schedule
754
+ execution_data[:next_run] = calculate_next_run(schedule)
755
+ end
756
+ rescue K8s::Error::NotFound, StandardError
757
+ # CronJob not found or parsing error, continue with job counting
758
+ end
759
+
760
+ # Count completed jobs (separate from CronJob processing)
761
+ begin
762
+ # Count total completed jobs for this agent
763
+ jobs = ctx.client.list_resources('Job', namespace: ctx.namespace)
764
+
765
+ agent_jobs = jobs.select do |job|
766
+ labels = job.dig('metadata', 'labels') || {}
767
+ labels['app.kubernetes.io/name'] == agent_name
768
+ end
769
+
770
+ # Count successful completions
771
+ successful_jobs = agent_jobs.select do |job|
772
+ conditions = job.dig('status', 'conditions') || []
773
+ conditions.any? { |c| c['type'] == 'Complete' && c['status'] == 'True' }
774
+ end
775
+
776
+ execution_data[:total_runs] = successful_jobs.length
777
+ rescue StandardError
778
+ # If job listing fails, keep default count of 0
779
+ end
780
+
781
+ execution_data
782
+ end
783
+
784
+ def calculate_next_run(schedule)
785
+ # Simple next run calculation for common cron patterns
786
+ # Handle the most common case: */N * * * * (every N minutes)
787
+
788
+ parts = schedule.split
789
+ return schedule unless parts.length == 5 # Not a valid cron expression
790
+
791
+ minute, hour, day, month, weekday = parts
792
+ current_time = Time.now
793
+
794
+ # Handle every-N-minutes pattern: */10 * * * *
795
+ if minute.start_with?('*/') && hour == '*' && day == '*' && month == '*' && weekday == '*'
796
+ interval = minute[2..].to_i
797
+ if interval > 0 && interval < 60
798
+ current_minute = current_time.min
799
+ current_second = current_time.sec
800
+
801
+ # Find the next occurrence
802
+ next_minute_mark = ((current_minute / interval) + 1) * interval
803
+
804
+ if next_minute_mark < 60
805
+ # Same hour
806
+ next_time = Time.new(current_time.year, current_time.month, current_time.day,
807
+ current_time.hour, next_minute_mark, 0)
808
+ else
809
+ # Next hour
810
+ next_hour = current_time.hour + 1
811
+ next_minute = next_minute_mark - 60
812
+
813
+ if next_hour < 24
814
+ next_time = Time.new(current_time.year, current_time.month, current_time.day,
815
+ next_hour, next_minute, 0)
816
+ else
817
+ # Next day
818
+ next_day = current_time + (24 * 60 * 60) # Add one day
819
+ next_time = Time.new(next_day.year, next_day.month, next_day.day,
820
+ 0, next_minute, 0)
821
+ end
822
+ end
823
+
824
+ return Formatters::ValueFormatter.time_until(next_time)
825
+ end
826
+ end
827
+
828
+ # For other patterns, show the schedule (could add more patterns later)
829
+ schedule
830
+ rescue StandardError
831
+ schedule
832
+ end
833
+ end
834
+ end
835
+ end
836
+ end
837
+ end