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
@@ -1,1712 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'thor'
4
- require_relative '../base_command'
5
- require_relative '../formatters/progress_formatter'
6
- require_relative '../formatters/table_formatter'
7
- require_relative '../formatters/value_formatter'
8
- require_relative '../formatters/log_formatter'
9
- require_relative '../formatters/status_formatter'
10
- require_relative '../helpers/cluster_validator'
11
- require_relative '../helpers/cluster_context'
12
- require_relative '../helpers/user_prompts'
13
- require_relative '../helpers/editor_helper'
14
- require_relative '../helpers/pastel_helper'
15
- require_relative '../errors/handler'
16
- require_relative '../../config/cluster_config'
17
- require_relative '../../kubernetes/client'
18
- require_relative '../../kubernetes/resource_builder'
19
- require_relative '../../ux/create_agent'
20
-
21
- module LanguageOperator
22
- module CLI
23
- module Commands
24
- # Agent management commands
25
- class Agent < BaseCommand
26
- include Helpers::ClusterValidator
27
- include Helpers::PastelHelper
28
-
29
- desc 'create [DESCRIPTION]', 'Create a new agent with natural language description'
30
- long_desc <<-DESC
31
- Create a new autonomous agent by describing what you want it to do in natural language.
32
-
33
- The operator will synthesize the agent from your description and deploy it to your cluster.
34
-
35
- Examples:
36
- aictl agent create "review my spreadsheet at 4pm daily and email me any errors"
37
- aictl agent create "summarize Hacker News top stories every morning at 8am"
38
- aictl agent create "monitor my website uptime and alert me if it goes down"
39
- aictl agent create --wizard # Interactive wizard mode
40
- DESC
41
- option :cluster, type: :string, desc: 'Override current cluster context'
42
- option :create_cluster, type: :string, desc: 'Create cluster if it doesn\'t exist'
43
- option :name, type: :string, desc: 'Agent name (generated from description if not provided)'
44
- option :persona, type: :string, desc: 'Persona to use for the agent'
45
- option :tools, type: :array, desc: 'Tools to make available to the agent'
46
- option :models, type: :array, desc: 'Models to make available to the agent'
47
- option :workspace, type: :boolean, default: true, desc: 'Enable workspace for state persistence'
48
- option :dry_run, type: :boolean, default: false, desc: 'Preview what would be created without applying'
49
- option :wizard, type: :boolean, default: false, desc: 'Use interactive wizard mode'
50
- def create(description = nil)
51
- handle_command_error('create agent') do
52
- # Read from stdin if available and no description provided
53
- description = $stdin.read.strip if description.nil? && !$stdin.tty?
54
-
55
- # Activate wizard mode if --wizard flag or no description provided
56
- if options[:wizard] || description.nil? || description.empty?
57
- description = Ux::CreateAgent.execute(ctx)
58
-
59
- # User cancelled wizard
60
- unless description
61
- Formatters::ProgressFormatter.info('Agent creation cancelled')
62
- return
63
- end
64
- end
65
-
66
- # Handle --create-cluster flag
67
- if options[:create_cluster]
68
- cluster_name = options[:create_cluster]
69
- unless Config::ClusterConfig.cluster_exists?(cluster_name)
70
- Formatters::ProgressFormatter.info("Creating cluster '#{cluster_name}'...")
71
- # Delegate to cluster create command
72
- require_relative 'cluster'
73
- Cluster.new.invoke(:create, [cluster_name], switch: true)
74
- end
75
- cluster = cluster_name
76
- else
77
- # Validate cluster selection (this will exit if none selected)
78
- cluster = Helpers::ClusterValidator.get_cluster(options[:cluster])
79
- end
80
-
81
- ctx = Helpers::ClusterContext.from_options(options.merge(cluster: cluster))
82
-
83
- # Generate agent name from description if not provided
84
- agent_name = options[:name] || generate_agent_name(description)
85
-
86
- # Get models: use specified models, or default to all available models in cluster
87
- models = options[:models]
88
- if models.nil? || models.empty?
89
- available_models = ctx.client.list_resources('LanguageModel', namespace: ctx.namespace)
90
- models = available_models.map { |m| m.dig('metadata', 'name') }
91
-
92
- Errors::Handler.handle_no_models_available(cluster: ctx.name) if models.empty?
93
- end
94
-
95
- # Build LanguageAgent resource
96
- agent_resource = Kubernetes::ResourceBuilder.language_agent(
97
- agent_name,
98
- instructions: description,
99
- cluster: ctx.namespace,
100
- persona: options[:persona],
101
- tools: options[:tools] || [],
102
- models: models,
103
- workspace: options[:workspace]
104
- )
105
-
106
- # Dry-run mode: preview without applying
107
- if options[:dry_run]
108
- display_dry_run_preview(agent_resource, ctx.name, description)
109
- return
110
- end
111
-
112
- # Apply resource to cluster
113
- Formatters::ProgressFormatter.with_spinner("Creating agent '#{agent_name}'") do
114
- ctx.client.apply_resource(agent_resource)
115
- end
116
-
117
- # Watch synthesis status
118
- synthesis_result = watch_synthesis_status(ctx.client, agent_name, ctx.namespace)
119
-
120
- # Exit if synthesis failed
121
- exit 1 unless synthesis_result[:success]
122
-
123
- # Fetch the updated agent to get complete details
124
- agent = ctx.client.get_resource('LanguageAgent', agent_name, ctx.namespace)
125
-
126
- # Display enhanced success output
127
- display_agent_created(agent, ctx.name, description, synthesis_result)
128
- end
129
- end
130
-
131
- desc 'list', 'List all agents in current cluster'
132
- option :cluster, type: :string, desc: 'Override current cluster context'
133
- option :all_clusters, type: :boolean, default: false, desc: 'Show agents across all clusters'
134
- def list
135
- handle_command_error('list agents') do
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
- end
143
- end
144
-
145
- desc 'inspect NAME', 'Show detailed agent information'
146
- option :cluster, type: :string, desc: 'Override current cluster context'
147
- def inspect(name)
148
- handle_command_error('inspect agent') do
149
- ctx = Helpers::ClusterContext.from_options(options)
150
-
151
- begin
152
- agent = ctx.client.get_resource('LanguageAgent', name, ctx.namespace)
153
- rescue K8s::Error::NotFound
154
- handle_agent_not_found(name, ctx)
155
- return
156
- end
157
-
158
- puts "Agent: #{name}"
159
- puts " Cluster: #{ctx.name}"
160
- puts " Namespace: #{ctx.namespace}"
161
- puts
162
-
163
- # Status
164
- status = agent.dig('status', 'phase') || 'Unknown'
165
- puts "Status: #{format_status(status)}"
166
- puts
167
-
168
- # Spec details
169
- puts 'Configuration:'
170
- puts " Mode: #{agent.dig('spec', 'mode') || 'autonomous'}"
171
- puts " Schedule: #{agent.dig('spec', 'schedule') || 'N/A'}" if agent.dig('spec', 'schedule')
172
- puts " Persona: #{agent.dig('spec', 'persona') || '(auto-selected)'}"
173
- puts
174
-
175
- # Instructions
176
- instructions = agent.dig('spec', 'instructions')
177
- if instructions
178
- puts 'Instructions:'
179
- puts " #{instructions}"
180
- puts
181
- end
182
-
183
- # Tools
184
- tools = agent.dig('spec', 'tools') || []
185
- if tools.any?
186
- puts "Tools (#{tools.length}):"
187
- tools.each { |tool| puts " - #{tool}" }
188
- puts
189
- end
190
-
191
- # Models
192
- model_refs = agent.dig('spec', 'modelRefs') || []
193
- if model_refs.any?
194
- puts "Models (#{model_refs.length}):"
195
- model_refs.each { |ref| puts " - #{ref['name']}" }
196
- puts
197
- end
198
-
199
- # Synthesis info
200
- synthesis = agent.dig('status', 'synthesis')
201
- if synthesis
202
- puts 'Synthesis:'
203
- puts " Status: #{synthesis['status']}"
204
- puts " Model: #{synthesis['model']}" if synthesis['model']
205
- puts " Completed: #{synthesis['completedAt']}" if synthesis['completedAt']
206
- puts " Duration: #{synthesis['duration']}" if synthesis['duration']
207
- puts " Token Count: #{synthesis['tokenCount']}" if synthesis['tokenCount']
208
- puts
209
- end
210
-
211
- # Execution stats
212
- execution_count = agent.dig('status', 'executionCount') || 0
213
- last_execution = agent.dig('status', 'lastExecution')
214
- next_run = agent.dig('status', 'nextRun')
215
-
216
- puts 'Execution:'
217
- puts " Total Runs: #{execution_count}"
218
- puts " Last Run: #{last_execution || 'Never'}"
219
- puts " Next Run: #{next_run || 'N/A'}" if agent.dig('spec', 'schedule')
220
- puts
221
-
222
- # Conditions
223
- conditions = agent.dig('status', 'conditions') || []
224
- if conditions.any?
225
- puts "Conditions (#{conditions.length}):"
226
- conditions.each do |condition|
227
- status_icon = condition['status'] == 'True' ? '✓' : '✗'
228
- puts " #{status_icon} #{condition['type']}: #{condition['message'] || condition['reason']}"
229
- end
230
- puts
231
- end
232
-
233
- # Recent events (if available)
234
- # This would require querying events, which we can add later
235
- end
236
- end
237
-
238
- desc 'delete NAME', 'Delete an agent'
239
- option :cluster, type: :string, desc: 'Override current cluster context'
240
- option :force, type: :boolean, default: false, desc: 'Skip confirmation'
241
- def delete(name)
242
- handle_command_error('delete agent') do
243
- ctx = Helpers::ClusterContext.from_options(options)
244
-
245
- # Get agent to show details before deletion
246
- agent = get_resource_or_exit('LanguageAgent', name)
247
-
248
- # Confirm deletion
249
- details = {
250
- 'Instructions' => agent.dig('spec', 'instructions'),
251
- 'Mode' => agent.dig('spec', 'mode') || 'autonomous'
252
- }
253
- return unless confirm_deletion('agent', name, ctx.name, details: details, force: options[:force])
254
-
255
- # Delete the agent
256
- Formatters::ProgressFormatter.with_spinner("Deleting agent '#{name}'") do
257
- ctx.client.delete_resource('LanguageAgent', name, ctx.namespace)
258
- end
259
-
260
- Formatters::ProgressFormatter.success("Agent '#{name}' deleted successfully")
261
- end
262
- end
263
-
264
- desc 'logs NAME', 'Show agent execution logs'
265
- long_desc <<-DESC
266
- Stream agent execution logs in real-time.
267
-
268
- Use -f to follow logs continuously (like tail -f).
269
-
270
- Examples:
271
- aictl agent logs my-agent
272
- aictl agent logs my-agent -f
273
- DESC
274
- option :cluster, type: :string, desc: 'Override current cluster context'
275
- option :follow, type: :boolean, aliases: '-f', default: false, desc: 'Follow logs'
276
- option :tail, type: :numeric, default: 100, desc: 'Number of lines to show from the end'
277
- def logs(name)
278
- handle_command_error('get logs') do
279
- ctx = Helpers::ClusterContext.from_options(options)
280
-
281
- # Get agent to determine the pod name
282
- agent = get_resource_or_exit('LanguageAgent', name)
283
-
284
- mode = agent.dig('spec', 'mode') || 'autonomous'
285
-
286
- # Build kubectl command for log streaming
287
- tail_arg = "--tail=#{options[:tail]}"
288
- follow_arg = options[:follow] ? '-f' : ''
289
-
290
- # For scheduled agents, logs come from CronJob pods
291
- # For autonomous agents, logs come from Deployment pods
292
- if mode == 'scheduled'
293
- # Get most recent job from cronjob
294
- else
295
- # Get pod from deployment
296
- end
297
- label_selector = "app.kubernetes.io/name=#{name}"
298
-
299
- # Use kubectl logs with label selector
300
- cmd = "#{ctx.kubectl_prefix} logs -l #{label_selector} #{tail_arg} #{follow_arg} --all-containers"
301
-
302
- Formatters::ProgressFormatter.info("Streaming logs for agent '#{name}'...")
303
- puts
304
-
305
- # Stream raw logs in real-time without formatting
306
- require 'open3'
307
- Open3.popen3(cmd) do |_stdin, stdout, stderr, wait_thr|
308
- # Handle stdout (logs)
309
- stdout_thread = Thread.new do
310
- stdout.each_line do |line|
311
- puts line
312
- $stdout.flush
313
- end
314
- end
315
-
316
- # Handle stderr (errors)
317
- stderr_thread = Thread.new do
318
- stderr.each_line do |line|
319
- warn line
320
- end
321
- end
322
-
323
- # Wait for both streams to complete
324
- stdout_thread.join
325
- stderr_thread.join
326
-
327
- # Check exit status
328
- exit_status = wait_thr.value
329
- exit exit_status.exitstatus unless exit_status.success?
330
- end
331
- end
332
- end
333
-
334
- desc 'code NAME', 'Display synthesized agent code'
335
- option :cluster, type: :string, desc: 'Override current cluster context'
336
- option :raw, type: :boolean, default: false, desc: 'Output raw code without formatting'
337
- def code(name)
338
- handle_command_error('get code') do
339
- require_relative '../formatters/code_formatter'
340
-
341
- ctx = Helpers::ClusterContext.from_options(options)
342
-
343
- # Get the code ConfigMap for this agent
344
- configmap_name = "#{name}-code"
345
- begin
346
- configmap = ctx.client.get_resource('ConfigMap', configmap_name, ctx.namespace)
347
- rescue K8s::Error::NotFound
348
- Formatters::ProgressFormatter.error("Synthesized code not found for agent '#{name}'")
349
- puts
350
- puts 'Possible reasons:'
351
- puts ' - Agent synthesis not yet complete'
352
- puts ' - Agent synthesis failed'
353
- puts
354
- puts 'Check synthesis status with:'
355
- puts " aictl agent inspect #{name}"
356
- exit 1
357
- end
358
-
359
- # Get the agent.rb code from the ConfigMap
360
- code_content = configmap.dig('data', 'agent.rb')
361
- unless code_content
362
- Formatters::ProgressFormatter.error('Code content not found in ConfigMap')
363
- exit 1
364
- end
365
-
366
- # Raw output mode - just print the code
367
- if options[:raw]
368
- puts code_content
369
- return
370
- end
371
-
372
- # Display with syntax highlighting
373
- Formatters::CodeFormatter.display_ruby_code(
374
- code_content,
375
- title: "Synthesized Code for Agent: #{name}"
376
- )
377
- end
378
- end
379
-
380
- desc 'edit NAME', 'Edit agent instructions'
381
- option :cluster, type: :string, desc: 'Override current cluster context'
382
- def edit(name)
383
- handle_command_error('edit agent') do
384
- ctx = Helpers::ClusterContext.from_options(options)
385
-
386
- # Get current agent
387
- agent = get_resource_or_exit('LanguageAgent', name)
388
-
389
- current_instructions = agent.dig('spec', 'instructions')
390
-
391
- # Edit instructions in user's editor
392
- new_instructions = Helpers::EditorHelper.edit_content(
393
- current_instructions,
394
- 'agent-instructions-',
395
- '.txt'
396
- ).strip
397
-
398
- # Check if changed
399
- if new_instructions == current_instructions
400
- Formatters::ProgressFormatter.info('No changes made')
401
- return
402
- end
403
-
404
- # Update agent resource
405
- agent['spec']['instructions'] = new_instructions
406
-
407
- Formatters::ProgressFormatter.with_spinner('Updating agent instructions') do
408
- ctx.client.apply_resource(agent)
409
- end
410
-
411
- Formatters::ProgressFormatter.success('Agent instructions updated')
412
- puts
413
- puts 'The operator will automatically re-synthesize the agent code.'
414
- puts
415
- puts 'Watch synthesis progress with:'
416
- puts " aictl agent inspect #{name}"
417
- end
418
- end
419
-
420
- desc 'pause NAME', 'Pause scheduled agent execution'
421
- option :cluster, type: :string, desc: 'Override current cluster context'
422
- def pause(name)
423
- handle_command_error('pause agent') do
424
- ctx = Helpers::ClusterContext.from_options(options)
425
-
426
- # Get agent
427
- agent = get_resource_or_exit('LanguageAgent', name)
428
-
429
- mode = agent.dig('spec', 'mode') || 'autonomous'
430
- unless mode == 'scheduled'
431
- Formatters::ProgressFormatter.warn("Agent '#{name}' is not a scheduled agent (mode: #{mode})")
432
- puts
433
- puts 'Only scheduled agents can be paused.'
434
- puts 'Autonomous agents can be stopped by deleting them.'
435
- exit 1
436
- end
437
-
438
- # Suspend the CronJob by setting spec.suspend = true
439
- # This is done by patching the underlying CronJob resource
440
- cronjob_name = name
441
- ctx.namespace
442
-
443
- Formatters::ProgressFormatter.with_spinner("Pausing agent '#{name}'") do
444
- # Use kubectl to patch the cronjob
445
- cmd = "#{ctx.kubectl_prefix} patch cronjob #{cronjob_name} -p '{\"spec\":{\"suspend\":true}}'"
446
- system(cmd)
447
- end
448
-
449
- Formatters::ProgressFormatter.success("Agent '#{name}' paused")
450
- puts
451
- puts 'The agent will not execute on its schedule until resumed.'
452
- puts
453
- puts 'Resume with:'
454
- puts " aictl agent resume #{name}"
455
- end
456
- end
457
-
458
- desc 'resume NAME', 'Resume paused agent'
459
- option :cluster, type: :string, desc: 'Override current cluster context'
460
- def resume(name)
461
- handle_command_error('resume agent') do
462
- ctx = Helpers::ClusterContext.from_options(options)
463
-
464
- # Get agent
465
- agent = get_resource_or_exit('LanguageAgent', name)
466
-
467
- mode = agent.dig('spec', 'mode') || 'autonomous'
468
- unless mode == 'scheduled'
469
- Formatters::ProgressFormatter.warn("Agent '#{name}' is not a scheduled agent (mode: #{mode})")
470
- puts
471
- puts 'Only scheduled agents can be resumed.'
472
- exit 1
473
- end
474
-
475
- # Resume the CronJob by setting spec.suspend = false
476
- cronjob_name = name
477
- ctx.namespace
478
-
479
- Formatters::ProgressFormatter.with_spinner("Resuming agent '#{name}'") do
480
- # Use kubectl to patch the cronjob
481
- cmd = "#{ctx.kubectl_prefix} patch cronjob #{cronjob_name} -p '{\"spec\":{\"suspend\":false}}'"
482
- system(cmd)
483
- end
484
-
485
- Formatters::ProgressFormatter.success("Agent '#{name}' resumed")
486
- puts
487
- puts 'The agent will now execute according to its schedule.'
488
- puts
489
- puts 'View next execution time with:'
490
- puts " aictl agent inspect #{name}"
491
- end
492
- end
493
-
494
- desc 'optimize NAME', 'Optimize neural tasks to symbolic based on learned patterns'
495
- long_desc <<-DESC
496
- Analyze agent execution patterns and propose optimizations to convert
497
- neural (LLM-based) tasks into symbolic (code-based) implementations.
498
-
499
- This command queries OpenTelemetry traces to detect deterministic patterns
500
- in task execution, then generates optimized symbolic code that runs faster
501
- and costs less while maintaining the same behavior.
502
-
503
- Requirements:
504
- • OpenTelemetry backend configured (SigNoz, Jaeger, or Tempo)
505
- • Neural task has executed at least 10 times
506
- • Execution pattern consistency >= 85%
507
-
508
- Examples:
509
- aictl agent optimize my-agent # Analyze and propose optimizations
510
- aictl agent optimize my-agent --dry-run # Show what would be optimized
511
- aictl agent optimize my-agent --status-only # Show learning status only
512
- aictl agent optimize my-agent --auto-accept # Auto-accept high-confidence optimizations
513
- aictl agent optimize my-agent --tasks task1,task2 # Optimize specific tasks only
514
- DESC
515
- option :cluster, type: :string, desc: 'Override current cluster context'
516
- option :dry_run, type: :boolean, default: false, desc: 'Show what would be optimized without applying'
517
- option :status_only, type: :boolean, default: false, desc: 'Show learning status without optimizing'
518
- option :auto_accept, type: :boolean, default: false, desc: 'Auto-accept optimizations above min-confidence'
519
- option :min_confidence, type: :numeric, default: 0.90, desc: 'Minimum consistency for auto-accept (0.0-1.0)'
520
- option :tasks, type: :array, desc: 'Only optimize specific tasks'
521
- option :since, type: :string, desc: 'Only analyze traces since (e.g., "2h", "1d", "7d")'
522
- option :use_synthesis, type: :boolean, default: false, desc: 'Use LLM synthesis instead of pattern detection'
523
- option :synthesis_model, type: :string, desc: 'Model to use for synthesis (default: cluster default)'
524
- def optimize(name)
525
- handle_command_error('optimize agent') do
526
- require_relative '../../learning/trace_analyzer'
527
- require_relative '../../learning/pattern_detector'
528
- require_relative '../../learning/optimizer'
529
- require_relative '../../learning/task_synthesizer'
530
- require_relative '../../agent/safety/ast_validator'
531
- require_relative '../formatters/optimization_formatter'
532
-
533
- ctx = Helpers::ClusterContext.from_options(options)
534
-
535
- # Get agent to verify it exists
536
- get_resource_or_exit('LanguageAgent', name)
537
-
538
- # Get agent code/definition
539
- agent_definition = load_agent_definition(ctx, name)
540
- unless agent_definition
541
- Formatters::ProgressFormatter.error("Could not load agent definition for '#{name}'")
542
- exit 1
543
- end
544
-
545
- # Check for OpenTelemetry configuration
546
- unless ENV['OTEL_QUERY_ENDPOINT']
547
- Formatters::ProgressFormatter.warn('OpenTelemetry endpoint not configured')
548
- puts
549
- puts 'Set OTEL_QUERY_ENDPOINT to enable learning:'
550
- puts ' export OTEL_QUERY_ENDPOINT=https://your-signoz-instance.com'
551
- puts ' export OTEL_QUERY_API_KEY=your-api-key # For SigNoz authentication'
552
- puts ' export OTEL_QUERY_BACKEND=signoz # Optional: signoz, jaeger, or tempo'
553
- puts
554
- puts 'Auto-detection tries backends in order: SigNoz → Jaeger → Tempo'
555
- puts 'Set OTEL_QUERY_BACKEND to skip auto-detection and use a specific backend.'
556
- puts
557
- exit 1
558
- end
559
-
560
- # Initialize learning components
561
- trace_analyzer = Learning::TraceAnalyzer.new(
562
- endpoint: ENV.fetch('OTEL_QUERY_ENDPOINT', nil),
563
- api_key: ENV.fetch('OTEL_QUERY_API_KEY', nil),
564
- backend: ENV.fetch('OTEL_QUERY_BACKEND', nil)
565
- )
566
-
567
- unless trace_analyzer.available?
568
- Formatters::ProgressFormatter.error('OpenTelemetry backend not available')
569
- puts
570
- puts 'Check your OTEL_QUERY_ENDPOINT configuration and backend status.'
571
- exit 1
572
- end
573
-
574
- validator = LanguageOperator::Agent::Safety::ASTValidator.new
575
- pattern_detector = LanguageOperator::Learning::PatternDetector.new(
576
- trace_analyzer: trace_analyzer,
577
- validator: validator
578
- )
579
-
580
- # Create task synthesizer for fallback (or forced via --use-synthesis)
581
- # Synthesis is used when pattern detection fails OR --use-synthesis is set
582
- task_synthesizer = nil
583
- llm_client = create_synthesis_llm_client(ctx, options[:synthesis_model])
584
- if llm_client
585
- task_synthesizer = LanguageOperator::Learning::TaskSynthesizer.new(
586
- llm_client: llm_client,
587
- validator: validator
588
- )
589
- Formatters::ProgressFormatter.info('LLM synthesis mode (forced)') if options[:use_synthesis]
590
- elsif options[:use_synthesis]
591
- Formatters::ProgressFormatter.warn('Could not create LLM client for synthesis')
592
- end
593
-
594
- optimizer = LanguageOperator::Learning::Optimizer.new(
595
- agent_name: name,
596
- agent_definition: agent_definition,
597
- trace_analyzer: trace_analyzer,
598
- pattern_detector: pattern_detector,
599
- task_synthesizer: task_synthesizer
600
- )
601
-
602
- formatter = Formatters::OptimizationFormatter.new
603
-
604
- # Parse --since option into time range
605
- time_range = parse_since_option(options[:since])
606
-
607
- # Analyze for opportunities
608
- opportunities = optimizer.analyze(time_range: time_range)
609
-
610
- # Display analysis only in status-only mode
611
- if options[:status_only]
612
- puts formatter.format_analysis(agent_name: name, opportunities: opportunities)
613
- return
614
- end
615
-
616
- # Exit if no opportunities
617
- return if opportunities.empty?
618
-
619
- # Filter opportunities:
620
- # - If synthesis available: try any task with enough executions
621
- # - Otherwise: only tasks ready for pattern detection
622
- candidates = if task_synthesizer
623
- # With synthesis, try any task that has min executions
624
- opportunities.select { |opp| opp[:execution_count] >= 10 }
625
- else
626
- opportunities.select { |opp| opp[:ready_for_learning] }
627
- end
628
- return if candidates.empty?
629
-
630
- # Process each opportunity
631
- candidates.each do |opp|
632
- task_name = opp[:task_name]
633
-
634
- # Skip if not in requested tasks list
635
- next if options[:tasks] && !options[:tasks].include?(task_name)
636
-
637
- # Generate proposal
638
- begin
639
- proposal = optimizer.propose(task_name: task_name, use_synthesis: options[:use_synthesis])
640
- rescue ArgumentError => e
641
- Formatters::ProgressFormatter.warn("Cannot optimize '#{task_name}': #{e.message}")
642
- next
643
- end
644
-
645
- # Display proposal
646
- puts formatter.format_proposal(proposal: proposal)
647
-
648
- # Get user confirmation or auto-accept
649
- accepted = if options[:auto_accept] && proposal[:consistency_score] >= options[:min_confidence]
650
- consistency_pct = (proposal[:consistency_score] * 100).round(1)
651
- threshold_pct = (options[:min_confidence] * 100).round(1)
652
- puts pastel.green("✓ Auto-accepting (consistency: #{consistency_pct}% >= #{threshold_pct}%)")
653
- true
654
- elsif options[:dry_run]
655
- puts pastel.yellow('[DRY RUN] Would prompt for acceptance')
656
- false
657
- else
658
- prompt_for_optimization_acceptance(proposal)
659
- end
660
-
661
- # Apply if accepted
662
- if accepted && !options[:dry_run]
663
- result = apply_optimization(ctx, name, proposal)
664
- puts formatter.format_success(result: result)
665
- elsif accepted
666
- puts pastel.yellow('[DRY RUN] Would apply optimization')
667
- else
668
- puts pastel.yellow("Skipped optimization for '#{task_name}'")
669
- end
670
-
671
- puts
672
- end
673
- end
674
- end
675
-
676
- desc 'workspace NAME', 'Browse agent workspace files'
677
- long_desc <<-DESC
678
- Browse and manage the workspace files for an agent.
679
-
680
- Workspaces provide persistent storage for agents to maintain state,
681
- cache data, and remember information across executions.
682
-
683
- Examples:
684
- aictl agent workspace my-agent # List all files
685
- aictl agent workspace my-agent --path /workspace/state.json # View specific file
686
- aictl agent workspace my-agent --clean # Clear workspace
687
- DESC
688
- option :cluster, type: :string, desc: 'Override current cluster context'
689
- option :path, type: :string, desc: 'View specific file contents'
690
- option :clean, type: :boolean, desc: 'Clear workspace (with confirmation)'
691
- def workspace(name)
692
- handle_command_error('access workspace') do
693
- ctx = Helpers::ClusterContext.from_options(options)
694
-
695
- # Get agent to verify it exists
696
- agent = get_resource_or_exit('LanguageAgent', name)
697
-
698
- # Check if workspace is enabled
699
- workspace_enabled = agent.dig('spec', 'workspace', 'enabled')
700
- unless workspace_enabled
701
- Formatters::ProgressFormatter.warn("Workspace is not enabled for agent '#{name}'")
702
- puts
703
- puts 'Enable workspace in agent configuration:'
704
- puts ' spec:'
705
- puts ' workspace:'
706
- puts ' enabled: true'
707
- puts ' size: 10Gi'
708
- exit 1
709
- end
710
-
711
- if options[:path]
712
- view_workspace_file(ctx, name, options[:path])
713
- elsif options[:clean]
714
- clean_workspace(ctx, name)
715
- else
716
- list_workspace_files(ctx, name)
717
- end
718
- end
719
- end
720
-
721
- private
722
-
723
- # Parse --since option into seconds (time range)
724
- #
725
- # @param since [String, nil] Duration string (e.g., "2h", "1d", "7d")
726
- # @return [Integer, nil] Seconds or nil if not specified
727
- def parse_since_option(since)
728
- return nil unless since
729
-
730
- match = since.match(/^(\d+)([hHdDwW])$/)
731
- unless match
732
- Formatters::ProgressFormatter.warn("Invalid --since format '#{since}', using default (24h)")
733
- Formatters::ProgressFormatter.info('Valid formats: 2h (hours), 1d (days), 1w (weeks)')
734
- return nil
735
- end
736
-
737
- value = match[1].to_i
738
- unit = match[2].downcase
739
-
740
- case unit
741
- when 'h' then value * 3600
742
- when 'd' then value * 86_400
743
- when 'w' then value * 604_800
744
- end
745
- end
746
-
747
- # Create LLM client for task synthesis using cluster model
748
- #
749
- # @param ctx [ClusterContext] Cluster context
750
- # @param model_name [String, nil] Specific model to use (defaults to first available)
751
- # @return [Object, nil] LLM client or nil if unavailable
752
- def create_synthesis_llm_client(ctx, model_name = nil)
753
- # Get model from cluster
754
- selected_model = model_name || select_synthesis_model(ctx)
755
- return nil unless selected_model
756
-
757
- # Get model resource to extract model ID
758
- # Always use port-forwarding to deployment (LiteLLM proxy for cost controls)
759
- begin
760
- model = ctx.client.get_resource('LanguageModel', selected_model, ctx.namespace)
761
- model_id = model.dig('spec', 'modelName')
762
- return nil unless model_id
763
-
764
- ClusterLLMClient.new(
765
- ctx: ctx,
766
- model_name: selected_model,
767
- model_id: model_id,
768
- agent_command: self
769
- )
770
- rescue StandardError => e
771
- @logger&.warn("Failed to create cluster LLM client: #{e.message}")
772
- nil
773
- end
774
- end
775
-
776
- # Select model for synthesis (first available if not specified)
777
- #
778
- # @param ctx [ClusterContext] Cluster context
779
- # @return [String, nil] Model name or nil
780
- def select_synthesis_model(ctx)
781
- models = ctx.client.list_resources('LanguageModel', namespace: ctx.namespace)
782
- return nil if models.empty?
783
-
784
- models.first.dig('metadata', 'name')
785
- rescue StandardError
786
- nil
787
- end
788
-
789
- # LLM client that uses port-forwarding to cluster model deployments (LiteLLM proxy)
790
- class ClusterLLMClient
791
- def initialize(ctx:, model_name:, model_id:, agent_command:)
792
- @ctx = ctx
793
- @model_name = model_name
794
- @model_id = model_id
795
- @agent_command = agent_command
796
- end
797
-
798
- def chat(prompt)
799
- require 'faraday'
800
- require 'json'
801
-
802
- pod = get_model_pod
803
- pod_name = pod.dig('metadata', 'name')
804
-
805
- local_port = find_available_port
806
- port_forward_pid = nil
807
-
808
- begin
809
- port_forward_pid = start_port_forward(pod_name, local_port, 4000)
810
- wait_for_port(local_port)
811
-
812
- conn = Faraday.new(url: "http://localhost:#{local_port}") do |f|
813
- f.request :json
814
- f.response :json
815
- f.adapter Faraday.default_adapter
816
- f.options.timeout = 120
817
- f.options.open_timeout = 10
818
- end
819
-
820
- payload = {
821
- model: @model_id,
822
- messages: [{ role: 'user', content: prompt }],
823
- max_tokens: 4000,
824
- temperature: 0.3
825
- }
826
-
827
- response = conn.post('/v1/chat/completions', payload)
828
- result = response.body
829
-
830
- raise "LLM error: #{result['error']['message'] || result['error']}" if result['error']
831
-
832
- result.dig('choices', 0, 'message', 'content')
833
- ensure
834
- cleanup_port_forward(port_forward_pid) if port_forward_pid
835
- end
836
- end
837
-
838
- private
839
-
840
- def get_model_pod
841
- # Get the deployment for the model
842
- deployment = @ctx.client.get_resource('Deployment', @model_name, @ctx.namespace)
843
- raise "Deployment '#{@model_name}' not found in namespace '#{@ctx.namespace}'" if deployment.nil?
844
-
845
- labels = deployment.dig('spec', 'selector', 'matchLabels')
846
- raise "Deployment '#{@model_name}' has no selector labels" if labels.nil?
847
-
848
- # Convert to hash if needed (K8s API may return K8s::Resource)
849
- labels_hash = labels.respond_to?(:to_h) ? labels.to_h : labels
850
- raise "Deployment '#{@model_name}' has empty selector labels" if labels_hash.empty?
851
-
852
- label_selector = labels_hash.map { |k, v| "#{k}=#{v}" }.join(',')
853
-
854
- # Find a running pod
855
- pods = @ctx.client.list_resources('Pod', namespace: @ctx.namespace, label_selector: label_selector)
856
- raise "No pods found for model '#{@model_name}'" if pods.empty?
857
-
858
- running_pods = pods.select { |p| p.dig('status', 'phase') == 'Running' }
859
- raise "No running pods found for model '#{@model_name}'" if running_pods.empty?
860
-
861
- running_pods.first
862
- end
863
-
864
- def find_available_port
865
- server = TCPServer.new('127.0.0.1', 0)
866
- port = server.addr[1]
867
- server.close
868
- port
869
- end
870
-
871
- def start_port_forward(pod_name, local_port, remote_port)
872
- pid = spawn(
873
- 'kubectl', 'port-forward',
874
- '-n', @ctx.namespace,
875
- "pod/#{pod_name}",
876
- "#{local_port}:#{remote_port}",
877
- %i[out err] => '/dev/null'
878
- )
879
- Process.detach(pid)
880
- pid
881
- end
882
-
883
- def wait_for_port(port, max_attempts: 30)
884
- max_attempts.times do
885
- TCPSocket.new('127.0.0.1', port).close
886
- return true
887
- rescue Errno::ECONNREFUSED
888
- sleep 0.1
889
- end
890
- raise "Port #{port} not available after #{max_attempts} attempts"
891
- end
892
-
893
- def cleanup_port_forward(pid)
894
- Process.kill('TERM', pid)
895
- rescue Errno::ESRCH
896
- # Process already gone
897
- end
898
- end
899
-
900
- def handle_agent_not_found(name, ctx)
901
- # Get available agents for fuzzy matching
902
- agents = ctx.client.list_resources('LanguageAgent', namespace: ctx.namespace)
903
- available_names = agents.map { |a| a.dig('metadata', 'name') }
904
-
905
- error = K8s::Error::NotFound.new(404, 'Not Found', 'LanguageAgent')
906
- Errors::Handler.handle_not_found(error,
907
- resource_type: 'LanguageAgent',
908
- resource_name: name,
909
- cluster: ctx.name,
910
- available_resources: available_names)
911
- end
912
-
913
- def display_agent_created(agent, cluster, _description, synthesis_result)
914
- require_relative '../formatters/code_formatter'
915
- agent_name = agent.dig('metadata', 'name')
916
-
917
- puts
918
- Formatters::ProgressFormatter.success("Agent '#{agent_name}' created and deployed!")
919
- puts
920
-
921
- # Get synthesized code if available
922
- begin
923
- ctx = Helpers::ClusterContext.from_options(cluster: cluster)
924
- configmap_name = "#{agent_name}-code"
925
- configmap = ctx.client.get_resource('ConfigMap', configmap_name, ctx.namespace)
926
- code_content = configmap.dig('data', 'agent.rb')
927
-
928
- if code_content
929
- # Display code preview (first 20 lines)
930
- Formatters::CodeFormatter.display_ruby_code(
931
- code_content,
932
- title: 'Synthesized Code Preview:',
933
- max_lines: 20
934
- )
935
- puts
936
- end
937
- rescue StandardError
938
- # Code not available yet, skip preview
939
- end
940
-
941
- # Display agent configuration
942
- puts pastel.cyan('Agent Configuration:')
943
- puts " Name: #{agent_name}"
944
- puts " Cluster: #{cluster}"
945
-
946
- # Schedule information
947
- schedule = agent.dig('spec', 'schedule')
948
- mode = agent.dig('spec', 'mode') || 'autonomous'
949
- if schedule
950
- human_schedule = parse_schedule(schedule)
951
- puts " Schedule: #{human_schedule} (#{schedule})"
952
-
953
- # Calculate next run
954
- next_run = agent.dig('status', 'nextRun')
955
- if next_run
956
- begin
957
- next_run_time = Time.parse(next_run)
958
- time_until = format_time_until(next_run_time)
959
- puts " Next run: #{next_run} (#{time_until})"
960
- rescue StandardError
961
- puts " Next run: #{next_run}"
962
- end
963
- end
964
- else
965
- puts " Mode: #{mode}"
966
- end
967
-
968
- # Persona
969
- persona = agent.dig('spec', 'persona')
970
- puts " Persona: #{persona || '(auto-selected)'}"
971
-
972
- # Tools
973
- tools = agent.dig('spec', 'tools') || []
974
- puts " Tools: #{tools.join(', ')}" if tools.any?
975
-
976
- # Models
977
- model_refs = agent.dig('spec', 'modelRefs') || []
978
- if model_refs.any?
979
- model_names = model_refs.map { |ref| ref['name'] }
980
- puts " Models: #{model_names.join(', ')}"
981
- end
982
-
983
- puts
984
-
985
- # Synthesis stats
986
- if synthesis_result[:duration]
987
- puts pastel.dim("Synthesis completed in #{format_duration(synthesis_result[:duration])}")
988
- puts pastel.dim("Model: #{synthesis_result[:model]}") if synthesis_result[:model]
989
- puts
990
- end
991
-
992
- # Next steps
993
- puts pastel.cyan('Next Steps:')
994
- puts " aictl agent logs #{agent_name} -f # Follow agent execution logs"
995
- puts " aictl agent code #{agent_name} # View full synthesized code"
996
- puts " aictl agent inspect #{agent_name} # View detailed agent status"
997
- puts
998
- end
999
-
1000
- def parse_schedule(cron_expr)
1001
- # Simple cron to human-readable conversion
1002
- # Format: minute hour day month weekday
1003
- parts = cron_expr.split
1004
-
1005
- return cron_expr if parts.length != 5
1006
-
1007
- minute, hour, day, month, weekday = parts
1008
-
1009
- # Common patterns
1010
- if minute == '0' && hour != '*' && day == '*' && month == '*' && weekday == '*'
1011
- # Daily at specific hour
1012
- hour12 = hour.to_i % 12
1013
- hour12 = 12 if hour12.zero?
1014
- period = hour.to_i < 12 ? 'AM' : 'PM'
1015
- return "Daily at #{hour12}:00 #{period}"
1016
- elsif minute != '*' && hour != '*' && day == '*' && month == '*' && weekday == '*'
1017
- # Daily at specific time
1018
- hour12 = hour.to_i % 12
1019
- hour12 = 12 if hour12.zero?
1020
- period = hour.to_i < 12 ? 'AM' : 'PM'
1021
- return "Daily at #{hour12}:#{minute.rjust(2, '0')} #{period}"
1022
- elsif minute.start_with?('*/') && hour == '*'
1023
- # Every N minutes
1024
- interval = minute[2..].to_i
1025
- return "Every #{interval} minutes"
1026
- elsif minute == '*' && hour.start_with?('*/')
1027
- # Every N hours
1028
- interval = hour[2..].to_i
1029
- return "Every #{interval} hours"
1030
- end
1031
-
1032
- # Fallback to cron expression
1033
- cron_expr
1034
- end
1035
-
1036
- def format_time_until(future_time)
1037
- Formatters::ValueFormatter.time_until(future_time)
1038
- end
1039
-
1040
- def display_dry_run_preview(agent_resource, cluster, description)
1041
- require 'yaml'
1042
-
1043
- puts
1044
- puts '=' * 80
1045
- puts ' DRY RUN: Agent Creation Preview'
1046
- puts '=' * 80
1047
- puts
1048
-
1049
- # Extract key information
1050
- name = agent_resource.dig('metadata', 'name')
1051
- namespace = agent_resource.dig('metadata', 'namespace')
1052
- persona = agent_resource.dig('spec', 'persona')
1053
- tools = agent_resource.dig('spec', 'tools') || []
1054
- model_refs = agent_resource.dig('spec', 'modelRefs') || []
1055
- models = model_refs.map { |ref| ref['name'] }
1056
- mode = agent_resource.dig('spec', 'mode') || 'autonomous'
1057
- schedule = agent_resource.dig('spec', 'schedule')
1058
-
1059
- # Display summary
1060
- puts 'Agent Summary:'
1061
- puts " Name: #{name}"
1062
- puts " Cluster: #{cluster}"
1063
- puts " Namespace: #{namespace}"
1064
- puts " Mode: #{mode}"
1065
- puts " Schedule: #{schedule || 'N/A'}" if schedule
1066
- puts " Instructions: #{description}"
1067
- puts
1068
-
1069
- # Show detected configuration
1070
- if persona
1071
- puts 'Detected Configuration:'
1072
- puts " Persona: #{persona}"
1073
- end
1074
-
1075
- puts " Tools: #{tools.join(', ')}" if tools.any?
1076
-
1077
- puts " Models: #{models.join(', ')}" if models.any?
1078
-
1079
- puts if persona || tools.any? || models.any?
1080
-
1081
- # Show full YAML
1082
- puts 'Generated YAML:'
1083
- puts '─' * 80
1084
- puts YAML.dump(agent_resource)
1085
- puts '─' * 80
1086
- puts
1087
-
1088
- # Show what would happen
1089
- puts 'What would happen:'
1090
- puts ' 1. Agent resource would be created in the cluster'
1091
- puts ' 2. Operator would synthesize Ruby code from instructions'
1092
- puts ' 3. Agent would be deployed and start running'
1093
- puts
1094
-
1095
- # Show how to actually create
1096
- Formatters::ProgressFormatter.info('No changes made (dry-run mode)')
1097
- puts
1098
- puts 'To create this agent for real, run:'
1099
- cmd_parts = ["aictl agent create \"#{description}\""]
1100
- cmd_parts << "--name #{name}" if options[:name]
1101
- cmd_parts << "--persona #{persona}" if persona
1102
- cmd_parts << "--tools #{tools.join(' ')}" if tools.any?
1103
- cmd_parts << "--models #{models.join(' ')}" if models.any?
1104
- cmd_parts << "--cluster #{cluster}" if options[:cluster]
1105
- puts " #{cmd_parts.join(' ')}"
1106
- end
1107
-
1108
- def format_status(status)
1109
- Formatters::StatusFormatter.format(status)
1110
- end
1111
-
1112
- def generate_agent_name(description)
1113
- # Simple name generation from description
1114
- # Take first few words, lowercase, hyphenate
1115
- words = description.downcase.gsub(/[^a-z0-9\s]/, '').split[0..2]
1116
- name = words.join('-')
1117
- # Add random suffix to avoid collisions
1118
- "#{name}-#{Time.now.to_i.to_s[-4..]}"
1119
- end
1120
-
1121
- def watch_synthesis_status(k8s, agent_name, namespace)
1122
- max_wait = 600 # Wait up to 10 minutes (local models can be slow)
1123
- interval = 2 # Check every 2 seconds
1124
- elapsed = 0
1125
- start_time = Time.now
1126
- synthesis_data = {}
1127
-
1128
- result = Formatters::ProgressFormatter.with_spinner('Synthesizing code from instructions') do
1129
- loop do
1130
- status = check_synthesis_status(k8s, agent_name, namespace, synthesis_data, start_time)
1131
- return status if status
1132
-
1133
- # Timeout check
1134
- if elapsed >= max_wait
1135
- Formatters::ProgressFormatter.warn('Synthesis taking longer than expected, continuing in background...')
1136
- puts
1137
- puts 'Check synthesis status with:'
1138
- puts " aictl agent inspect #{agent_name}"
1139
- return { success: true, timeout: true }
1140
- end
1141
-
1142
- sleep interval
1143
- elapsed += interval
1144
- end
1145
- rescue K8s::Error::NotFound
1146
- # Agent not found yet, keep waiting
1147
- sleep interval
1148
- elapsed += interval
1149
- retry if elapsed < max_wait
1150
-
1151
- Formatters::ProgressFormatter.error('Agent resource not found')
1152
- return { success: false }
1153
- rescue StandardError => e
1154
- Formatters::ProgressFormatter.warn("Could not watch synthesis: #{e.message}")
1155
- return { success: true } # Continue anyway
1156
- end
1157
-
1158
- # Show synthesis details after spinner completes
1159
- if result[:success] && !result[:timeout]
1160
- duration = result[:duration]
1161
- Formatters::ProgressFormatter.success("Code synthesis completed in #{format_duration(duration)}")
1162
- puts " Model: #{synthesis_data[:model]}" if synthesis_data[:model]
1163
- puts " Tokens: #{synthesis_data[:token_count]}" if synthesis_data[:token_count]
1164
- end
1165
-
1166
- result
1167
- end
1168
-
1169
- def check_synthesis_status(k8s, agent_name, namespace, synthesis_data, start_time)
1170
- agent = k8s.get_resource('LanguageAgent', agent_name, namespace)
1171
- conditions = agent.dig('status', 'conditions') || []
1172
- synthesis_status = agent.dig('status', 'synthesis')
1173
-
1174
- # Capture synthesis metadata
1175
- if synthesis_status
1176
- synthesis_data[:model] = synthesis_status['model']
1177
- synthesis_data[:token_count] = synthesis_status['tokenCount']
1178
- end
1179
-
1180
- # Check for synthesis completion
1181
- synthesized = conditions.find { |c| c['type'] == 'Synthesized' }
1182
- return nil unless synthesized
1183
-
1184
- if synthesized['status'] == 'True'
1185
- duration = Time.now - start_time
1186
- { success: true, duration: duration, **synthesis_data }
1187
- elsif synthesized['status'] == 'False'
1188
- Formatters::ProgressFormatter.error("Synthesis failed: #{synthesized['message']}")
1189
- { success: false }
1190
- end
1191
- end
1192
-
1193
- def format_duration(seconds)
1194
- Formatters::ValueFormatter.duration(seconds)
1195
- end
1196
-
1197
- def list_cluster_agents(cluster)
1198
- ctx = Helpers::ClusterContext.from_options(cluster: cluster)
1199
-
1200
- Formatters::ProgressFormatter.info("Agents in cluster '#{cluster}'")
1201
-
1202
- agents = ctx.client.list_resources('LanguageAgent', namespace: ctx.namespace)
1203
-
1204
- table_data = agents.map do |agent|
1205
- {
1206
- name: agent.dig('metadata', 'name'),
1207
- mode: agent.dig('spec', 'mode') || 'autonomous',
1208
- status: agent.dig('status', 'phase') || 'Unknown',
1209
- next_run: agent.dig('status', 'nextRun') || 'N/A',
1210
- executions: agent.dig('status', 'executionCount') || 0
1211
- }
1212
- end
1213
-
1214
- Formatters::TableFormatter.agents(table_data)
1215
-
1216
- return unless agents.empty?
1217
-
1218
- puts
1219
- puts 'Create an agent with:'
1220
- puts ' aictl agent create "<description>"'
1221
- end
1222
-
1223
- def list_all_clusters
1224
- clusters = Config::ClusterConfig.list_clusters
1225
-
1226
- if clusters.empty?
1227
- Formatters::ProgressFormatter.info('No clusters found')
1228
- puts
1229
- puts 'Create a cluster first:'
1230
- puts ' aictl cluster create <name>'
1231
- return
1232
- end
1233
-
1234
- all_agents = []
1235
-
1236
- clusters.each do |cluster|
1237
- ctx = Helpers::ClusterContext.from_options(cluster: cluster[:name])
1238
-
1239
- agents = ctx.client.list_resources('LanguageAgent', namespace: ctx.namespace)
1240
-
1241
- agents.each do |agent|
1242
- all_agents << {
1243
- cluster: cluster[:name],
1244
- name: agent.dig('metadata', 'name'),
1245
- mode: agent.dig('spec', 'mode') || 'autonomous',
1246
- status: agent.dig('status', 'phase') || 'Unknown',
1247
- next_run: agent.dig('status', 'nextRun') || 'N/A',
1248
- executions: agent.dig('status', 'executionCount') || 0
1249
- }
1250
- end
1251
- rescue StandardError => e
1252
- Formatters::ProgressFormatter.warn("Failed to get agents from cluster '#{cluster[:name]}': #{e.message}")
1253
- end
1254
-
1255
- # Group agents by cluster for formatted display
1256
- agents_by_cluster = all_agents.group_by { |agent| agent[:cluster] }
1257
- .transform_values { |agents| agents.map { |a| a.except(:cluster) } }
1258
-
1259
- Formatters::TableFormatter.all_agents(agents_by_cluster)
1260
- end
1261
-
1262
- # Workspace-related helper methods
1263
-
1264
- def get_agent_pod(ctx, agent_name)
1265
- # Find pod for this agent using label selector
1266
- label_selector = "app.kubernetes.io/name=#{agent_name}"
1267
- pods = ctx.client.list_resources('Pod', namespace: ctx.namespace, label_selector: label_selector)
1268
-
1269
- if pods.empty?
1270
- Formatters::ProgressFormatter.error("No running pods found for agent '#{agent_name}'")
1271
- puts
1272
- puts 'Possible reasons:'
1273
- puts ' - Agent pod has not started yet'
1274
- puts ' - Agent is paused or stopped'
1275
- puts ' - Agent failed to deploy'
1276
- puts
1277
- puts 'Check agent status with:'
1278
- puts " aictl agent inspect #{agent_name}"
1279
- exit 1
1280
- end
1281
-
1282
- # Find a running pod
1283
- running_pod = pods.find do |pod|
1284
- pod.dig('status', 'phase') == 'Running'
1285
- end
1286
-
1287
- unless running_pod
1288
- Formatters::ProgressFormatter.error('Agent pod exists but is not running')
1289
- puts
1290
- puts "Current pod status: #{pods.first.dig('status', 'phase')}"
1291
- puts
1292
- puts 'Check pod logs with:'
1293
- puts " aictl agent logs #{agent_name}"
1294
- exit 1
1295
- end
1296
-
1297
- running_pod.dig('metadata', 'name')
1298
- end
1299
-
1300
- def exec_in_pod(ctx, pod_name, command)
1301
- # Properly escape command for shell
1302
- cmd_str = command.is_a?(Array) ? command.join(' ') : command
1303
- kubectl_cmd = "#{ctx.kubectl_prefix} exec #{pod_name} -- #{cmd_str}"
1304
-
1305
- # Execute and capture output
1306
- require 'open3'
1307
- stdout, stderr, status = Open3.capture3(kubectl_cmd)
1308
-
1309
- raise "Command failed: #{stderr}" unless status.success?
1310
-
1311
- stdout
1312
- end
1313
-
1314
- def list_workspace_files(ctx, agent_name)
1315
- pod_name = get_agent_pod(ctx, agent_name)
1316
-
1317
- # Check if workspace directory exists
1318
- begin
1319
- exec_in_pod(ctx, pod_name, 'test -d /workspace')
1320
- rescue StandardError
1321
- Formatters::ProgressFormatter.error('Workspace directory not found in agent pod')
1322
- puts
1323
- puts 'The /workspace directory does not exist in the agent pod.'
1324
- puts 'This agent may not have workspace support enabled.'
1325
- exit 1
1326
- end
1327
-
1328
- # Get workspace usage
1329
- usage_output = exec_in_pod(
1330
- ctx,
1331
- pod_name,
1332
- 'du -sh /workspace 2>/dev/null || echo "0\t/workspace"'
1333
- )
1334
- workspace_size = usage_output.split("\t").first.strip
1335
-
1336
- # List files with details
1337
- file_list = exec_in_pod(
1338
- ctx,
1339
- pod_name,
1340
- 'find /workspace -ls 2>/dev/null | tail -n +2'
1341
- )
1342
-
1343
- puts
1344
- puts pastel.cyan("Workspace for agent '#{agent_name}' (#{workspace_size})")
1345
- puts '=' * 60
1346
- puts
1347
-
1348
- if file_list.strip.empty?
1349
- puts pastel.dim('Workspace is empty')
1350
- puts
1351
- puts 'The agent will create files here as it runs.'
1352
- puts
1353
- return
1354
- end
1355
-
1356
- # Parse and display file list
1357
- file_list.each_line do |line|
1358
- parts = line.strip.split(/\s+/, 11)
1359
- next if parts.length < 11
1360
-
1361
- # Extract relevant parts
1362
- # Format: inode blocks perms links user group size month day time path
1363
- perms = parts[2]
1364
- size = parts[6]
1365
- month = parts[7]
1366
- day = parts[8]
1367
- time_or_year = parts[9]
1368
- path = parts[10]
1369
-
1370
- # Skip the /workspace directory itself
1371
- next if path == '/workspace'
1372
-
1373
- # Determine file type and icon
1374
- icon = if perms.start_with?('d')
1375
- pastel.blue('📁')
1376
- else
1377
- pastel.white('📄')
1378
- end
1379
-
1380
- # Format path relative to workspace
1381
- relative_path = path.sub('/workspace/', '')
1382
- indent = ' ' * relative_path.count('/')
1383
-
1384
- # Format size
1385
- formatted_size = format_file_size(size.to_i).rjust(8)
1386
-
1387
- # Format time
1388
- formatted_time = "#{month} #{day.rjust(2)} #{time_or_year}"
1389
-
1390
- puts "#{indent}#{icon} #{File.basename(relative_path).ljust(30)} #{pastel.dim(formatted_size)} #{pastel.dim(formatted_time)}"
1391
- end
1392
-
1393
- puts
1394
- puts pastel.dim('Commands:')
1395
- puts pastel.dim(" aictl agent workspace #{agent_name} --path /workspace/<file> # View file")
1396
- puts pastel.dim(" aictl agent workspace #{agent_name} --clean # Clear workspace")
1397
- puts
1398
- end
1399
-
1400
- def view_workspace_file(ctx, agent_name, file_path)
1401
- pod_name = get_agent_pod(ctx, agent_name)
1402
-
1403
- # Check if file exists
1404
- begin
1405
- exec_in_pod(ctx, pod_name, "test -f #{file_path}")
1406
- rescue StandardError
1407
- Formatters::ProgressFormatter.error("File not found: #{file_path}")
1408
- puts
1409
- puts 'List available files with:'
1410
- puts " aictl agent workspace #{agent_name}"
1411
- exit 1
1412
- end
1413
-
1414
- # Get file metadata
1415
- stat_output = exec_in_pod(
1416
- ctx,
1417
- pod_name,
1418
- "stat -c '%s %Y' #{file_path}"
1419
- )
1420
- size, mtime = stat_output.strip.split
1421
-
1422
- # Get file contents
1423
- contents = exec_in_pod(
1424
- ctx,
1425
- pod_name,
1426
- "cat #{file_path}"
1427
- )
1428
-
1429
- # Display file
1430
- puts
1431
- puts pastel.cyan("File: #{file_path}")
1432
- puts "Size: #{format_file_size(size.to_i)}"
1433
- puts "Modified: #{format_timestamp(Time.at(mtime.to_i))}"
1434
- puts '=' * 60
1435
- puts
1436
- puts contents
1437
- puts
1438
- end
1439
-
1440
- def clean_workspace(ctx, agent_name)
1441
- pod_name = get_agent_pod(ctx, agent_name)
1442
-
1443
- # Get current workspace usage
1444
- usage_output = exec_in_pod(
1445
- ctx,
1446
- pod_name,
1447
- 'du -sh /workspace 2>/dev/null || echo "0\t/workspace"'
1448
- )
1449
- workspace_size = usage_output.split("\t").first.strip
1450
-
1451
- # Count files
1452
- file_count = exec_in_pod(
1453
- ctx,
1454
- pod_name,
1455
- 'find /workspace -type f | wc -l'
1456
- ).strip.to_i
1457
-
1458
- puts
1459
- puts pastel.yellow("This will delete ALL files in the workspace for '#{agent_name}'")
1460
- puts
1461
- puts 'The agent will lose:'
1462
- puts ' • Execution history'
1463
- puts ' • Cached data'
1464
- puts ' • State information'
1465
- puts
1466
- puts "Current workspace: #{file_count} files, #{workspace_size}"
1467
- puts
1468
-
1469
- # Use UserPrompts helper
1470
- return unless Helpers::UserPrompts.confirm('Are you sure?')
1471
-
1472
- # Delete all files in workspace
1473
- Formatters::ProgressFormatter.with_spinner('Cleaning workspace') do
1474
- exec_in_pod(
1475
- ctx,
1476
- pod_name,
1477
- 'find /workspace -mindepth 1 -delete'
1478
- )
1479
- end
1480
-
1481
- Formatters::ProgressFormatter.success("Workspace cleared (freed #{workspace_size})")
1482
- puts
1483
- puts 'The agent will start fresh on its next execution.'
1484
- end
1485
-
1486
- def format_file_size(bytes)
1487
- Formatters::ValueFormatter.file_size(bytes)
1488
- end
1489
-
1490
- def format_timestamp(time)
1491
- Formatters::ValueFormatter.timestamp(time)
1492
- end
1493
-
1494
- # Load agent definition from ConfigMap
1495
- def load_agent_definition(ctx, agent_name)
1496
- # Try to get the agent code ConfigMap
1497
- configmap_name = "#{agent_name}-code"
1498
- begin
1499
- configmap = ctx.client.get_resource('ConfigMap', configmap_name, ctx.namespace)
1500
- code_content = configmap.dig('data', 'agent.rb')
1501
-
1502
- return nil unless code_content
1503
-
1504
- # Parse the code to extract agent definition
1505
- # For now, we'll create a mock definition with the task structure
1506
- # In a full implementation, this would eval the code safely
1507
- parse_agent_code(code_content)
1508
- rescue K8s::Error::NotFound
1509
- nil
1510
- rescue StandardError => e
1511
- @logger&.error("Failed to load agent definition: #{e.message}")
1512
- nil
1513
- end
1514
- end
1515
-
1516
- # Parse agent code to extract definition
1517
- def parse_agent_code(code)
1518
- require_relative '../../dsl/agent_definition'
1519
-
1520
- # Create a minimal agent definition structure
1521
- agent_def = Struct.new(:tasks, :name, :mcp_servers) do
1522
- def initialize
1523
- super({}, 'agent', {})
1524
- end
1525
- end
1526
-
1527
- agent = agent_def.new
1528
-
1529
- # Parse tasks from code - extract full task definitions
1530
- code.scan(/task\s+:(\w+),?\s*(.*?)(?=\n\s*(?:task\s+:|main\s+do|end\s*$))/m) do |match|
1531
- task_name = match[0].to_sym
1532
- task_block = match[1]
1533
-
1534
- # Check if neural (has instructions but no do block) or symbolic
1535
- is_neural = task_block.include?('instructions:') && !task_block.match?(/\bdo\s*\|/)
1536
-
1537
- # Extract instructions
1538
- instructions = extract_string_value(task_block, 'instructions')
1539
-
1540
- # Extract inputs hash
1541
- inputs = extract_hash_value(task_block, 'inputs')
1542
-
1543
- # Extract outputs hash
1544
- outputs = extract_hash_value(task_block, 'outputs')
1545
-
1546
- task = Struct.new(:name, :neural?, :instructions, :inputs, :outputs).new(
1547
- task_name, is_neural, instructions, inputs, outputs
1548
- )
1549
-
1550
- agent.tasks[task_name] = task
1551
- end
1552
-
1553
- agent
1554
- end
1555
-
1556
- # Extract a string value from DSL code (e.g., instructions: "...")
1557
- def extract_string_value(code, key)
1558
- # Match both single and double quoted strings, including multi-line
1559
- match = code.match(/#{key}:\s*(['"])(.*?)\1/m) ||
1560
- code.match(/#{key}:\s*(['"])(.+?)\1/m)
1561
- match ? match[2] : ''
1562
- end
1563
-
1564
- # Extract a hash value from DSL code (e.g., inputs: { foo: 'bar' })
1565
- def extract_hash_value(code, key)
1566
- match = code.match(/#{key}:\s*\{([^}]*)\}/)
1567
- return {} unless match
1568
-
1569
- hash_content = match[1].strip
1570
- return {} if hash_content.empty?
1571
-
1572
- # Parse simple key: 'value' or key: "value" pairs
1573
- result = {}
1574
- hash_content.scan(/(\w+):\s*(['"])([^'"]*)\2/) do |k, _quote, v|
1575
- result[k.to_sym] = v
1576
- end
1577
- result
1578
- end
1579
-
1580
- # Prompt user for optimization acceptance
1581
- def prompt_for_optimization_acceptance(proposal)
1582
- require 'tty-prompt'
1583
- prompt = TTY::Prompt.new
1584
-
1585
- choices = [
1586
- { name: 'Yes - apply this optimization', value: :yes },
1587
- { name: 'No - skip this task', value: :no },
1588
- { name: 'View full code diff', value: :diff },
1589
- { name: 'Skip all remaining', value: :skip_all }
1590
- ]
1591
-
1592
- loop do
1593
- choice = prompt.select(
1594
- "Accept optimization for '#{proposal[:task_name]}'?",
1595
- choices,
1596
- per_page: 10
1597
- )
1598
-
1599
- case choice
1600
- when :yes
1601
- return true
1602
- when :no
1603
- return false
1604
- when :diff
1605
- show_code_diff(proposal)
1606
- # Loop to ask again
1607
- when :skip_all
1608
- throw :skip_all
1609
- end
1610
- end
1611
- end
1612
-
1613
- # Show full code diff
1614
- def show_code_diff(proposal)
1615
- puts
1616
- puts pastel.bold('Full Generated Code:')
1617
- puts pastel.dim('=' * 70)
1618
- puts proposal[:full_generated_code]
1619
- puts pastel.dim('=' * 70)
1620
- puts
1621
- end
1622
-
1623
- # Apply optimization by updating ConfigMap and restarting pod
1624
- def apply_optimization(ctx, agent_name, proposal)
1625
- configmap_name = "#{agent_name}-code"
1626
- task_name = proposal[:task_name]
1627
-
1628
- # Get current ConfigMap
1629
- configmap = ctx.client.get_resource('ConfigMap', configmap_name, ctx.namespace)
1630
- current_code = configmap.dig('data', 'agent.rb')
1631
-
1632
- raise "ConfigMap '#{configmap_name}' does not contain agent.rb" unless current_code
1633
-
1634
- # Replace the neural task with the symbolic implementation
1635
- updated_code = replace_task_in_code(current_code, task_name, proposal[:proposed_code])
1636
-
1637
- # Build updated ConfigMap resource
1638
- # Add annotation to prevent controller from overwriting optimized code
1639
- updated_configmap = {
1640
- 'apiVersion' => 'v1',
1641
- 'kind' => 'ConfigMap',
1642
- 'metadata' => {
1643
- 'name' => configmap_name,
1644
- 'namespace' => ctx.namespace,
1645
- 'resourceVersion' => configmap.metadata.resourceVersion,
1646
- 'annotations' => {
1647
- 'langop.io/optimized' => 'true',
1648
- 'langop.io/optimized-at' => Time.now.iso8601,
1649
- 'langop.io/optimized-task' => task_name
1650
- }
1651
- },
1652
- 'data' => {
1653
- 'agent.rb' => updated_code
1654
- }
1655
- }
1656
-
1657
- # Update ConfigMap
1658
- ctx.client.update_resource('ConfigMap', configmap_name, ctx.namespace, updated_configmap, 'v1')
1659
-
1660
- # Restart the agent pod to pick up changes
1661
- restart_agent_pod(ctx, agent_name)
1662
-
1663
- {
1664
- success: true,
1665
- task_name: task_name,
1666
- updated_code: proposal[:proposed_code],
1667
- action: 'applied',
1668
- message: "Optimization for '#{task_name}' applied successfully"
1669
- }
1670
- rescue StandardError => e
1671
- {
1672
- success: false,
1673
- task_name: task_name,
1674
- error: e.message,
1675
- action: 'failed',
1676
- message: "Failed to apply optimization: #{e.message}"
1677
- }
1678
- end
1679
-
1680
- # Replace a task definition in agent code
1681
- def replace_task_in_code(code, task_name, new_task_code)
1682
- # Match the task definition including any trailing do block
1683
- # Pattern matches: task :name, ... (neural) or task :name, ... do |inputs| ... end (symbolic)
1684
- task_pattern = /task\s+:#{Regexp.escape(task_name.to_s)},?\s*.*?(?=\n\s*(?:task\s+:|main\s+do|end\s*$))/m
1685
-
1686
- raise "Could not find task ':#{task_name}' in agent code" unless code.match?(task_pattern)
1687
-
1688
- # Ensure new_task_code has proper trailing newline
1689
- new_code = "#{new_task_code.strip}\n\n"
1690
-
1691
- code.gsub(task_pattern, new_code.strip)
1692
- end
1693
-
1694
- # Restart agent pod by deleting it (Deployment will recreate)
1695
- def restart_agent_pod(ctx, agent_name)
1696
- # Find pods for this agent
1697
- pods = ctx.client.list_resources('Pod', namespace: ctx.namespace, label_selector: "app=#{agent_name}")
1698
-
1699
- pods.each do |pod|
1700
- pod_name = pod.dig('metadata', 'name')
1701
- begin
1702
- ctx.client.delete_resource('Pod', pod_name, ctx.namespace)
1703
- Formatters::ProgressFormatter.info("Restarting pod '#{pod_name}'")
1704
- rescue StandardError => e
1705
- Formatters::ProgressFormatter.warn("Could not delete pod '#{pod_name}': #{e.message}")
1706
- end
1707
- end
1708
- end
1709
- end
1710
- end
1711
- end
1712
- end