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