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.
- checksums.yaml +4 -4
- data/.claude/commands/persona.md +9 -0
- data/.claude/commands/task.md +46 -1
- data/.rubocop.yml +13 -0
- data/.rubocop_custom/use_ux_helper.rb +44 -0
- data/CHANGELOG.md +8 -0
- data/Gemfile.lock +12 -1
- data/Makefile +26 -7
- data/Makefile.common +50 -0
- data/bin/aictl +8 -1
- data/components/agent/Gemfile +1 -1
- data/components/agent/bin/langop-agent +7 -0
- data/docs/README.md +58 -0
- data/docs/{dsl/best-practices.md → best-practices.md} +4 -4
- data/docs/cli-reference.md +274 -0
- data/docs/{dsl/constraints.md → constraints.md} +5 -5
- data/docs/how-agents-work.md +156 -0
- data/docs/installation.md +218 -0
- data/docs/quickstart.md +299 -0
- data/docs/understanding-generated-code.md +265 -0
- data/docs/using-tools.md +457 -0
- data/docs/webhooks.md +509 -0
- data/examples/ux_helpers_demo.rb +296 -0
- data/lib/language_operator/agent/base.rb +11 -1
- data/lib/language_operator/agent/executor.rb +23 -6
- data/lib/language_operator/agent/safety/safe_executor.rb +41 -39
- data/lib/language_operator/agent/task_executor.rb +346 -63
- data/lib/language_operator/agent/web_server.rb +110 -14
- data/lib/language_operator/agent/webhook_authenticator.rb +39 -5
- data/lib/language_operator/agent.rb +88 -2
- data/lib/language_operator/cli/base_command.rb +17 -11
- data/lib/language_operator/cli/command_loader.rb +72 -0
- data/lib/language_operator/cli/commands/agent/base.rb +837 -0
- data/lib/language_operator/cli/commands/agent/code_operations.rb +102 -0
- data/lib/language_operator/cli/commands/agent/helpers/cluster_llm_client.rb +116 -0
- data/lib/language_operator/cli/commands/agent/helpers/code_parser.rb +115 -0
- data/lib/language_operator/cli/commands/agent/helpers/synthesis_watcher.rb +96 -0
- data/lib/language_operator/cli/commands/agent/learning.rb +289 -0
- data/lib/language_operator/cli/commands/agent/lifecycle.rb +102 -0
- data/lib/language_operator/cli/commands/agent/logs.rb +125 -0
- data/lib/language_operator/cli/commands/agent/workspace.rb +327 -0
- data/lib/language_operator/cli/commands/cluster.rb +129 -84
- data/lib/language_operator/cli/commands/install.rb +1 -1
- data/lib/language_operator/cli/commands/model/base.rb +215 -0
- data/lib/language_operator/cli/commands/model/test.rb +165 -0
- data/lib/language_operator/cli/commands/persona.rb +16 -34
- data/lib/language_operator/cli/commands/quickstart.rb +3 -2
- data/lib/language_operator/cli/commands/status.rb +40 -67
- data/lib/language_operator/cli/commands/system/base.rb +44 -0
- data/lib/language_operator/cli/commands/system/exec.rb +147 -0
- data/lib/language_operator/cli/commands/system/helpers/llm_synthesis.rb +183 -0
- data/lib/language_operator/cli/commands/system/helpers/pod_manager.rb +212 -0
- data/lib/language_operator/cli/commands/system/helpers/template_loader.rb +57 -0
- data/lib/language_operator/cli/commands/system/helpers/template_validator.rb +174 -0
- data/lib/language_operator/cli/commands/system/schema.rb +92 -0
- data/lib/language_operator/cli/commands/system/synthesis_template.rb +151 -0
- data/lib/language_operator/cli/commands/system/synthesize.rb +224 -0
- data/lib/language_operator/cli/commands/system/validate_template.rb +130 -0
- data/lib/language_operator/cli/commands/tool/base.rb +271 -0
- data/lib/language_operator/cli/commands/tool/install.rb +255 -0
- data/lib/language_operator/cli/commands/tool/search.rb +69 -0
- data/lib/language_operator/cli/commands/tool/test.rb +115 -0
- data/lib/language_operator/cli/commands/use.rb +29 -6
- data/lib/language_operator/cli/errors/handler.rb +20 -17
- data/lib/language_operator/cli/errors/suggestions.rb +3 -5
- data/lib/language_operator/cli/errors/thor_errors.rb +55 -0
- data/lib/language_operator/cli/formatters/code_formatter.rb +4 -11
- data/lib/language_operator/cli/formatters/log_formatter.rb +8 -15
- data/lib/language_operator/cli/formatters/progress_formatter.rb +6 -8
- data/lib/language_operator/cli/formatters/status_formatter.rb +26 -7
- data/lib/language_operator/cli/formatters/table_formatter.rb +47 -36
- data/lib/language_operator/cli/formatters/value_formatter.rb +75 -0
- data/lib/language_operator/cli/helpers/cluster_context.rb +5 -3
- data/lib/language_operator/cli/helpers/kubeconfig_validator.rb +2 -1
- data/lib/language_operator/cli/helpers/label_utils.rb +97 -0
- data/lib/language_operator/{ux/concerns/provider_helpers.rb → cli/helpers/provider_helper.rb} +10 -29
- data/lib/language_operator/cli/helpers/schedule_builder.rb +21 -1
- data/lib/language_operator/cli/helpers/user_prompts.rb +19 -11
- data/lib/language_operator/cli/helpers/ux_helper.rb +538 -0
- data/lib/language_operator/{ux/concerns/input_validation.rb → cli/helpers/validation_helper.rb} +13 -66
- data/lib/language_operator/cli/main.rb +50 -40
- data/lib/language_operator/cli/templates/tools/generic.yaml +3 -0
- data/lib/language_operator/cli/wizards/agent_wizard.rb +12 -20
- data/lib/language_operator/cli/wizards/model_wizard.rb +271 -0
- data/lib/language_operator/cli/wizards/quickstart_wizard.rb +8 -34
- data/lib/language_operator/client/base.rb +28 -0
- data/lib/language_operator/client/config.rb +4 -1
- data/lib/language_operator/client/mcp_connector.rb +1 -1
- data/lib/language_operator/config/cluster_config.rb +3 -2
- data/lib/language_operator/config.rb +38 -11
- data/lib/language_operator/constants/kubernetes_labels.rb +80 -0
- data/lib/language_operator/constants.rb +13 -0
- data/lib/language_operator/dsl/http.rb +127 -10
- data/lib/language_operator/dsl.rb +153 -6
- data/lib/language_operator/errors.rb +50 -0
- data/lib/language_operator/kubernetes/client.rb +11 -6
- data/lib/language_operator/kubernetes/resource_builder.rb +58 -84
- data/lib/language_operator/templates/schema/agent_dsl_openapi.yaml +1 -1
- data/lib/language_operator/templates/schema/agent_dsl_schema.json +1 -1
- data/lib/language_operator/type_coercion.rb +118 -34
- data/lib/language_operator/utils/secure_path.rb +74 -0
- data/lib/language_operator/utils.rb +7 -0
- data/lib/language_operator/validators.rb +54 -2
- data/lib/language_operator/version.rb +1 -1
- data/synth/001/Makefile +10 -2
- data/synth/001/agent.rb +16 -15
- data/synth/001/output.log +27 -10
- data/synth/002/Makefile +10 -2
- data/synth/003/Makefile +1 -1
- data/synth/003/README.md +205 -133
- data/synth/003/agent.optimized.rb +66 -0
- data/synth/003/agent.synthesized.rb +41 -0
- metadata +111 -35
- data/docs/dsl/agent-reference.md +0 -604
- data/docs/dsl/mcp-integration.md +0 -1177
- data/docs/dsl/webhooks.md +0 -932
- data/docs/dsl/workflows.md +0 -744
- data/lib/language_operator/cli/commands/agent.rb +0 -1712
- data/lib/language_operator/cli/commands/model.rb +0 -366
- data/lib/language_operator/cli/commands/system.rb +0 -1259
- data/lib/language_operator/cli/commands/tool.rb +0 -654
- data/lib/language_operator/cli/formatters/optimization_formatter.rb +0 -226
- data/lib/language_operator/cli/helpers/pastel_helper.rb +0 -24
- data/lib/language_operator/learning/adapters/base_adapter.rb +0 -149
- data/lib/language_operator/learning/adapters/jaeger_adapter.rb +0 -221
- data/lib/language_operator/learning/adapters/signoz_adapter.rb +0 -435
- data/lib/language_operator/learning/adapters/tempo_adapter.rb +0 -239
- data/lib/language_operator/learning/optimizer.rb +0 -319
- data/lib/language_operator/learning/pattern_detector.rb +0 -260
- data/lib/language_operator/learning/task_synthesizer.rb +0 -288
- data/lib/language_operator/learning/trace_analyzer.rb +0 -285
- data/lib/language_operator/templates/task_synthesis.tmpl +0 -98
- data/lib/language_operator/ux/base.rb +0 -81
- data/lib/language_operator/ux/concerns/README.md +0 -155
- data/lib/language_operator/ux/concerns/headings.rb +0 -90
- data/lib/language_operator/ux/create_agent.rb +0 -255
- data/lib/language_operator/ux/create_model.rb +0 -267
- data/lib/language_operator/ux/quickstart.rb +0 -594
- data/synth/003/agent.rb +0 -41
- data/synth/003/output.log +0 -68
- /data/docs/{architecture/agent-runtime.md → agent-internals.md} +0 -0
- /data/docs/{dsl/chat-endpoints.md → chat-endpoints.md} +0 -0
- /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
|