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,289 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require_relative '../../command_loader'
5
+ require_relative '../../../constants/kubernetes_labels'
6
+
7
+ module LanguageOperator
8
+ module CLI
9
+ module Commands
10
+ module Agent
11
+ # Learning monitoring and control for agents
12
+ module Learning
13
+ def self.included(base)
14
+ base.class_eval do
15
+ desc 'learning SUBCOMMAND ...ARGS', 'Monitor and control agent learning'
16
+ subcommand 'learning', LearningCommands
17
+ end
18
+ end
19
+
20
+ # Learning subcommand class
21
+ class LearningCommands < BaseCommand
22
+ include Constants
23
+ include CLI::Helpers::ClusterValidator
24
+ include CLI::Helpers::UxHelper
25
+
26
+ desc 'status NAME', 'Show current learning status and optimization history'
27
+ long_desc <<-DESC
28
+ Display the current learning status and optimization history for an agent.
29
+
30
+ Shows learned tasks, confidence scores, and automatic optimization progress
31
+ managed by the operator.
32
+
33
+ Examples:
34
+ aictl agent learning status my-agent
35
+ aictl agent learning status my-agent --cluster production
36
+ DESC
37
+ option :cluster, type: :string, desc: 'Override current cluster context'
38
+ def status(name)
39
+ handle_command_error('get learning status') do
40
+ ctx = CLI::Helpers::ClusterContext.from_options(options)
41
+
42
+ # Get agent to verify it exists
43
+ agent = ctx.client.get_resource(RESOURCE_AGENT, name, ctx.namespace)
44
+
45
+ # Query learning status ConfigMap
46
+ learning_status = get_learning_status(ctx.client, name, ctx.namespace)
47
+
48
+ # Display learning information
49
+ display_learning_status(agent, learning_status, ctx.name)
50
+ end
51
+ rescue K8s::Error::NotFound
52
+ # Handle agent not found
53
+ ctx = CLI::Helpers::ClusterContext.from_options(options)
54
+ available_agents = ctx.client.list_resources(RESOURCE_AGENT, namespace: ctx.namespace)
55
+ available_names = available_agents.map { |a| a.dig('metadata', 'name') }
56
+
57
+ error = K8s::Error::NotFound.new(404, 'Not Found', RESOURCE_AGENT)
58
+ CLI::Errors::Handler.handle_not_found(error,
59
+ resource_type: RESOURCE_AGENT,
60
+ resource_name: name,
61
+ cluster: ctx.name,
62
+ available_resources: available_names)
63
+ end
64
+
65
+ desc 'enable NAME', 'Enable automatic learning for an agent'
66
+ long_desc <<-DESC
67
+ Enable automatic learning for an agent by removing the learning-disabled annotation.
68
+
69
+ Learning is enabled by default, so this command only needs to be used if learning
70
+ was previously disabled.
71
+
72
+ Examples:
73
+ aictl agent learning enable my-agent
74
+ DESC
75
+ option :cluster, type: :string, desc: 'Override current cluster context'
76
+ def enable(name)
77
+ handle_command_error('enable learning') do
78
+ ctx = CLI::Helpers::ClusterContext.from_options(options)
79
+
80
+ # Get agent to verify it exists
81
+ agent = ctx.client.get_resource(RESOURCE_AGENT, name, ctx.namespace)
82
+
83
+ # Check current status
84
+ annotations = agent.dig('metadata', 'annotations') || {}
85
+ disabled_annotation = Constants::KubernetesLabels::LEARNING_DISABLED_LABEL
86
+
87
+ unless annotations.key?(disabled_annotation)
88
+ Formatters::ProgressFormatter.info("Learning is already enabled for agent '#{name}'")
89
+ return
90
+ end
91
+
92
+ # Remove the learning-disabled annotation
93
+ Formatters::ProgressFormatter.with_spinner("Enabling learning for agent '#{name}'") do
94
+ remove_annotation(ctx.client, name, ctx.namespace, disabled_annotation)
95
+ end
96
+
97
+ Formatters::ProgressFormatter.success("Learning enabled for agent '#{name}'")
98
+ end
99
+ end
100
+
101
+ desc 'disable NAME', 'Disable automatic learning for an agent'
102
+ long_desc <<-DESC
103
+ Disable automatic learning for an agent by adding the learning-disabled annotation.
104
+
105
+ This prevents the operator from automatically optimizing the agent's tasks but
106
+ does not affect existing learned optimizations.
107
+
108
+ Examples:
109
+ aictl agent learning disable my-agent
110
+ DESC
111
+ option :cluster, type: :string, desc: 'Override current cluster context'
112
+ def disable(name)
113
+ handle_command_error('disable learning') do
114
+ ctx = CLI::Helpers::ClusterContext.from_options(options)
115
+
116
+ # Get agent to verify it exists
117
+ agent = ctx.client.get_resource(RESOURCE_AGENT, name, ctx.namespace)
118
+
119
+ # Check current status
120
+ annotations = agent.dig('metadata', 'annotations') || {}
121
+ disabled_annotation = Constants::KubernetesLabels::LEARNING_DISABLED_LABEL
122
+
123
+ if annotations.key?(disabled_annotation)
124
+ Formatters::ProgressFormatter.info("Learning is already disabled for agent '#{name}'")
125
+ return
126
+ end
127
+
128
+ # Add the learning-disabled annotation
129
+ Formatters::ProgressFormatter.with_spinner("Disabling learning for agent '#{name}'") do
130
+ add_annotation(ctx.client, name, ctx.namespace, disabled_annotation, 'true')
131
+ end
132
+
133
+ Formatters::ProgressFormatter.success("Learning disabled for agent '#{name}'")
134
+ end
135
+ end
136
+
137
+ private
138
+
139
+ def get_learning_status(client, name, namespace)
140
+ config_map_name = "#{name}-learning-status"
141
+ begin
142
+ client.get_resource('ConfigMap', config_map_name, namespace)
143
+ rescue K8s::Error::NotFound
144
+ # Learning status ConfigMap doesn't exist yet - return nil
145
+ nil
146
+ end
147
+ end
148
+
149
+ def display_learning_status(agent, learning_status, cluster_name)
150
+ agent_name = agent.dig('metadata', 'name')
151
+ annotations = agent.dig('metadata', 'annotations') || {}
152
+
153
+ puts
154
+
155
+ # Learning enablement status
156
+ learning_enabled = !annotations.key?(Constants::KubernetesLabels::LEARNING_DISABLED_LABEL)
157
+ status_color = learning_enabled ? :green : :yellow
158
+ status_text = learning_enabled ? 'Enabled' : 'Disabled'
159
+
160
+ highlighted_box(
161
+ title: 'Learning Status',
162
+ rows: {
163
+ 'Agent' => pastel.white.bold(agent_name),
164
+ 'Cluster' => cluster_name,
165
+ 'Learning' => pastel.send(status_color).bold(status_text),
166
+ 'Last Updated' => agent.dig('metadata', 'resourceVersion') || 'Unknown'
167
+ }
168
+ )
169
+ puts
170
+
171
+ # If learning status ConfigMap exists, show detailed information
172
+ if learning_status
173
+ display_detailed_learning_status(learning_status)
174
+ else
175
+ puts pastel.dim('No learning status data available yet.')
176
+ puts pastel.dim('Learning data will appear after the agent has run and the operator has analyzed its behavior.')
177
+ puts
178
+ end
179
+
180
+ # Show next steps
181
+ puts pastel.white.bold('Available Commands:')
182
+ if learning_enabled
183
+ puts pastel.dim(" aictl agent learning disable #{agent_name}")
184
+ else
185
+ puts pastel.dim(" aictl agent learning enable #{agent_name}")
186
+ end
187
+ puts pastel.dim(" aictl agent versions #{agent_name}")
188
+ puts pastel.dim(" aictl agent inspect #{agent_name}")
189
+ end
190
+
191
+ def display_detailed_learning_status(learning_status)
192
+ data = learning_status['data'] || {}
193
+
194
+ # Parse learning data if available
195
+ if data['tasks']
196
+ tasks_data = begin
197
+ JSON.parse(data['tasks'])
198
+ rescue StandardError
199
+ {}
200
+ end
201
+
202
+ if tasks_data.any?
203
+ puts pastel.white.bold('Learned Tasks:')
204
+ tasks_data.each do |task_name, task_info|
205
+ confidence = task_info['confidence'] || 0
206
+ executions = task_info['executions'] || 0
207
+ status = task_info['status'] || 'neural'
208
+
209
+ confidence_color = if confidence >= 85
210
+ :green
211
+ else
212
+ confidence >= 70 ? :yellow : :red
213
+ end
214
+
215
+ puts " #{pastel.cyan(task_name)}"
216
+ puts " Status: #{format_task_status(status)}"
217
+ confidence_text = pastel.send(confidence_color, "#{confidence}%")
218
+ puts " Confidence: #{confidence_text} (#{executions} executions)"
219
+ end
220
+ puts
221
+ end
222
+ end
223
+
224
+ # Show optimization history if available
225
+ return unless data['history']
226
+
227
+ history_data = begin
228
+ JSON.parse(data['history'])
229
+ rescue StandardError
230
+ []
231
+ end
232
+
233
+ return unless history_data.any?
234
+
235
+ puts pastel.white.bold('Optimization History:')
236
+ history_data.last(5).each do |event|
237
+ timestamp = event['timestamp'] || 'Unknown'
238
+ action = event['action'] || 'Unknown'
239
+ task = event['task'] || 'Unknown'
240
+
241
+ puts " #{pastel.dim(timestamp)} - #{action} #{pastel.cyan(task)}"
242
+ end
243
+ puts
244
+ end
245
+
246
+ def format_task_status(status)
247
+ case status
248
+ when 'symbolic'
249
+ pastel.green('Learned (Symbolic)')
250
+ when 'neural'
251
+ pastel.yellow('Learning (Neural)')
252
+ when 'hybrid'
253
+ pastel.blue('Hybrid')
254
+ else
255
+ pastel.dim(status.capitalize)
256
+ end
257
+ end
258
+
259
+ def add_annotation(client, name, namespace, annotation_key, annotation_value)
260
+ # Get current agent
261
+ agent = client.get_resource(RESOURCE_AGENT, name, namespace)
262
+
263
+ # Add annotation
264
+ annotations = agent.dig('metadata', 'annotations') || {}
265
+ annotations[annotation_key] = annotation_value
266
+ agent['metadata']['annotations'] = annotations
267
+
268
+ # Update the agent
269
+ client.update_resource(agent)
270
+ end
271
+
272
+ def remove_annotation(client, name, namespace, annotation_key)
273
+ # Get current agent
274
+ agent = client.get_resource(RESOURCE_AGENT, name, namespace)
275
+
276
+ # Remove annotation
277
+ annotations = agent.dig('metadata', 'annotations') || {}
278
+ annotations.delete(annotation_key)
279
+ agent['metadata']['annotations'] = annotations
280
+
281
+ # Update the agent
282
+ client.update_resource(agent)
283
+ end
284
+ end
285
+ end
286
+ end
287
+ end
288
+ end
289
+ end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'open3'
4
+
5
+ module LanguageOperator
6
+ module CLI
7
+ module Commands
8
+ module Agent
9
+ # Lifecycle management for agents (pause/resume)
10
+ module Lifecycle
11
+ def self.included(base)
12
+ base.class_eval do
13
+ desc 'pause NAME', 'Pause scheduled agent execution'
14
+ option :cluster, type: :string, desc: 'Override current cluster context'
15
+ def pause(name)
16
+ handle_command_error('pause agent') do
17
+ ctx = CLI::Helpers::ClusterContext.from_options(options)
18
+
19
+ # Get agent
20
+ agent = get_resource_or_exit(LanguageOperator::Constants::RESOURCE_AGENT, name)
21
+
22
+ mode = agent.dig('spec', 'mode') || 'autonomous'
23
+ unless mode == 'scheduled'
24
+ Formatters::ProgressFormatter.warn("Agent '#{name}' is not a scheduled agent (mode: #{mode})")
25
+ puts
26
+ puts 'Only scheduled agents can be paused.'
27
+ puts 'Autonomous agents can be stopped by deleting them.'
28
+ exit 1
29
+ end
30
+
31
+ # Suspend the CronJob by setting spec.suspend = true
32
+ # This is done by patching the underlying CronJob resource
33
+ cronjob_name = name
34
+
35
+ Formatters::ProgressFormatter.with_spinner("Pausing agent '#{name}'") do
36
+ # Use kubectl to patch the cronjob
37
+ cmd = "#{ctx.kubectl_prefix} patch cronjob #{cronjob_name} -p '{\"spec\":{\"suspend\":true}}'"
38
+ _, stderr, status = Open3.capture3(cmd)
39
+
40
+ unless status.success?
41
+ error_msg = "Failed to pause agent '#{name}': kubectl command failed (exit code: #{status.exitstatus})"
42
+ error_msg += "\nError: #{stderr.strip}" unless stderr.nil? || stderr.strip.empty?
43
+ raise error_msg
44
+ end
45
+ end
46
+
47
+ Formatters::ProgressFormatter.success("Agent '#{name}' paused")
48
+ puts
49
+ puts 'The agent will not execute on its schedule until resumed.'
50
+ puts
51
+ puts 'Resume with:'
52
+ puts " aictl agent resume #{name}"
53
+ end
54
+ end
55
+
56
+ desc 'resume NAME', 'Resume paused agent'
57
+ option :cluster, type: :string, desc: 'Override current cluster context'
58
+ def resume(name)
59
+ handle_command_error('resume agent') do
60
+ ctx = CLI::Helpers::ClusterContext.from_options(options)
61
+
62
+ # Get agent
63
+ agent = get_resource_or_exit(LanguageOperator::Constants::RESOURCE_AGENT, name)
64
+
65
+ mode = agent.dig('spec', 'mode') || 'autonomous'
66
+ unless mode == 'scheduled'
67
+ Formatters::ProgressFormatter.warn("Agent '#{name}' is not a scheduled agent (mode: #{mode})")
68
+ puts
69
+ puts 'Only scheduled agents can be resumed.'
70
+ exit 1
71
+ end
72
+
73
+ # Resume the CronJob by setting spec.suspend = false
74
+ cronjob_name = name
75
+
76
+ Formatters::ProgressFormatter.with_spinner("Resuming agent '#{name}'") do
77
+ # Use kubectl to patch the cronjob
78
+ cmd = "#{ctx.kubectl_prefix} patch cronjob #{cronjob_name} -p '{\"spec\":{\"suspend\":false}}'"
79
+ _, stderr, status = Open3.capture3(cmd)
80
+
81
+ unless status.success?
82
+ error_msg = "Failed to resume agent '#{name}': kubectl command failed (exit code: #{status.exitstatus})"
83
+ error_msg += "\nError: #{stderr.strip}" unless stderr.nil? || stderr.strip.empty?
84
+ raise error_msg
85
+ end
86
+ end
87
+
88
+ Formatters::ProgressFormatter.success("Agent '#{name}' resumed")
89
+ puts
90
+ puts 'The agent will now execute according to its schedule.'
91
+ puts
92
+ puts 'View next execution time with:'
93
+ puts " aictl agent inspect #{name}"
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'open3'
4
+ require_relative '../../helpers/label_utils'
5
+
6
+ module LanguageOperator
7
+ module CLI
8
+ module Commands
9
+ module Agent
10
+ # Log streaming for agents
11
+ module Logs
12
+ def self.included(base)
13
+ base.class_eval do
14
+ desc 'logs NAME', 'Show agent execution logs'
15
+ long_desc <<-DESC
16
+ Stream agent execution logs in real-time.
17
+
18
+ Use -f to follow logs continuously (like tail -f).
19
+
20
+ Examples:
21
+ aictl agent logs my-agent
22
+ aictl agent logs my-agent -f
23
+ DESC
24
+ option :cluster, type: :string, desc: 'Override current cluster context'
25
+ option :follow, type: :boolean, aliases: '-f', default: false, desc: 'Follow logs'
26
+ option :tail, type: :numeric, default: 100, desc: 'Number of lines to show from the end'
27
+ def logs(name)
28
+ handle_command_error('get logs') do
29
+ ctx = CLI::Helpers::ClusterContext.from_options(options)
30
+
31
+ # Get agent to determine the pod name
32
+ agent = get_resource_or_exit(LanguageOperator::Constants::RESOURCE_AGENT, name)
33
+
34
+ mode = agent.dig('spec', 'mode') || 'autonomous'
35
+
36
+ # Build kubectl command for log streaming
37
+ tail_arg = "--tail=#{options[:tail]}"
38
+ follow_arg = options[:follow] ? '-f' : ''
39
+
40
+ # For scheduled agents, logs come from CronJob pods
41
+ # For autonomous agents, logs come from Deployment pods
42
+ if mode == 'scheduled'
43
+ # Get most recent job from cronjob
44
+ else
45
+ # Get pod from deployment
46
+ end
47
+ # Use normalized label selector for pod discovery
48
+ label_selector = CLI::Helpers::LabelUtils.agent_pod_selector(name)
49
+
50
+ # Use kubectl logs with label selector
51
+ cmd = "#{ctx.kubectl_prefix} logs -l #{label_selector} #{tail_arg} #{follow_arg} --all-containers"
52
+
53
+ Formatters::ProgressFormatter.info("Streaming logs for agent '#{name}'...")
54
+ puts
55
+
56
+ # Track threads and interruption state for cleanup
57
+ stdout_thread = nil
58
+ stderr_thread = nil
59
+ interrupted = false
60
+
61
+ # Install signal handler for graceful interruption
62
+ original_int_handler = Signal.trap('INT') do
63
+ interrupted = true
64
+ stdout_thread&.terminate
65
+ stderr_thread&.terminate
66
+ puts "\n[Interrupted]"
67
+ exit(0)
68
+ end
69
+
70
+ begin
71
+ # Stream raw logs in real-time without formatting
72
+ Open3.popen3(cmd) do |stdin, stdout, stderr, wait_thr|
73
+ # Close unused stdin immediately to prevent resource leak
74
+ stdin.close
75
+
76
+ # Handle stdout (logs)
77
+ stdout_thread = Thread.new do
78
+ stdout.each_line do |line|
79
+ break if interrupted
80
+
81
+ puts line
82
+ $stdout.flush
83
+ end
84
+ rescue IOError
85
+ # Expected when stream is closed during interruption
86
+ end
87
+
88
+ # Handle stderr (errors)
89
+ stderr_thread = Thread.new do
90
+ stderr.each_line do |line|
91
+ break if interrupted
92
+
93
+ warn line
94
+ end
95
+ rescue IOError
96
+ # Expected when stream is closed during interruption
97
+ end
98
+
99
+ # Wait for both streams to complete or interruption
100
+ stdout_thread.join unless interrupted
101
+ stderr_thread.join unless interrupted
102
+
103
+ # Check exit status if not interrupted
104
+ unless interrupted
105
+ exit_status = wait_thr.value
106
+ exit exit_status.exitstatus unless exit_status.success?
107
+ end
108
+ end
109
+ ensure
110
+ # Restore original signal handler
111
+ Signal.trap('INT', original_int_handler)
112
+
113
+ # Cleanup threads if they're still running
114
+ stdout_thread&.terminate
115
+ stderr_thread&.terminate
116
+ end
117
+ end
118
+ end
119
+ end
120
+ end
121
+ end
122
+ end
123
+ end
124
+ end
125
+ end