language-operator 0.0.1 → 0.1.30
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/.rubocop.yml +125 -0
- data/CHANGELOG.md +53 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +284 -0
- data/LICENSE +229 -21
- data/Makefile +77 -0
- data/README.md +3 -11
- data/Rakefile +34 -0
- data/bin/aictl +7 -0
- data/completions/_aictl +232 -0
- data/completions/aictl.bash +121 -0
- data/completions/aictl.fish +114 -0
- data/docs/architecture/agent-runtime.md +585 -0
- data/docs/dsl/agent-reference.md +591 -0
- data/docs/dsl/best-practices.md +1078 -0
- data/docs/dsl/chat-endpoints.md +895 -0
- data/docs/dsl/constraints.md +671 -0
- data/docs/dsl/mcp-integration.md +1177 -0
- data/docs/dsl/webhooks.md +932 -0
- data/docs/dsl/workflows.md +744 -0
- data/examples/README.md +569 -0
- data/examples/agent_example.rb +86 -0
- data/examples/chat_endpoint_agent.rb +118 -0
- data/examples/github_webhook_agent.rb +171 -0
- data/examples/mcp_agent.rb +158 -0
- data/examples/oauth_callback_agent.rb +296 -0
- data/examples/stripe_webhook_agent.rb +219 -0
- data/examples/webhook_agent.rb +80 -0
- data/lib/language_operator/agent/base.rb +110 -0
- data/lib/language_operator/agent/executor.rb +440 -0
- data/lib/language_operator/agent/instrumentation.rb +54 -0
- data/lib/language_operator/agent/metrics_tracker.rb +183 -0
- data/lib/language_operator/agent/safety/ast_validator.rb +272 -0
- data/lib/language_operator/agent/safety/audit_logger.rb +104 -0
- data/lib/language_operator/agent/safety/budget_tracker.rb +175 -0
- data/lib/language_operator/agent/safety/content_filter.rb +93 -0
- data/lib/language_operator/agent/safety/manager.rb +207 -0
- data/lib/language_operator/agent/safety/rate_limiter.rb +150 -0
- data/lib/language_operator/agent/safety/safe_executor.rb +115 -0
- data/lib/language_operator/agent/scheduler.rb +183 -0
- data/lib/language_operator/agent/telemetry.rb +116 -0
- data/lib/language_operator/agent/web_server.rb +610 -0
- data/lib/language_operator/agent/webhook_authenticator.rb +226 -0
- data/lib/language_operator/agent.rb +149 -0
- data/lib/language_operator/cli/commands/agent.rb +1252 -0
- data/lib/language_operator/cli/commands/cluster.rb +335 -0
- data/lib/language_operator/cli/commands/install.rb +404 -0
- data/lib/language_operator/cli/commands/model.rb +266 -0
- data/lib/language_operator/cli/commands/persona.rb +396 -0
- data/lib/language_operator/cli/commands/quickstart.rb +22 -0
- data/lib/language_operator/cli/commands/status.rb +156 -0
- data/lib/language_operator/cli/commands/tool.rb +537 -0
- data/lib/language_operator/cli/commands/use.rb +47 -0
- data/lib/language_operator/cli/errors/handler.rb +180 -0
- data/lib/language_operator/cli/errors/suggestions.rb +176 -0
- data/lib/language_operator/cli/formatters/code_formatter.rb +81 -0
- data/lib/language_operator/cli/formatters/log_formatter.rb +290 -0
- data/lib/language_operator/cli/formatters/progress_formatter.rb +53 -0
- data/lib/language_operator/cli/formatters/table_formatter.rb +179 -0
- data/lib/language_operator/cli/formatters/value_formatter.rb +113 -0
- data/lib/language_operator/cli/helpers/cluster_context.rb +62 -0
- data/lib/language_operator/cli/helpers/cluster_validator.rb +101 -0
- data/lib/language_operator/cli/helpers/editor_helper.rb +58 -0
- data/lib/language_operator/cli/helpers/kubeconfig_validator.rb +167 -0
- data/lib/language_operator/cli/helpers/resource_dependency_checker.rb +74 -0
- data/lib/language_operator/cli/helpers/schedule_builder.rb +108 -0
- data/lib/language_operator/cli/helpers/user_prompts.rb +69 -0
- data/lib/language_operator/cli/main.rb +232 -0
- data/lib/language_operator/cli/templates/tools/generic.yaml +66 -0
- data/lib/language_operator/cli/wizards/agent_wizard.rb +246 -0
- data/lib/language_operator/cli/wizards/quickstart_wizard.rb +588 -0
- data/lib/language_operator/client/base.rb +214 -0
- data/lib/language_operator/client/config.rb +136 -0
- data/lib/language_operator/client/cost_calculator.rb +37 -0
- data/lib/language_operator/client/mcp_connector.rb +123 -0
- data/lib/language_operator/client.rb +19 -0
- data/lib/language_operator/config/cluster_config.rb +101 -0
- data/lib/language_operator/config/tool_patterns.yaml +57 -0
- data/lib/language_operator/config/tool_registry.rb +96 -0
- data/lib/language_operator/config.rb +138 -0
- data/lib/language_operator/dsl/adapter.rb +124 -0
- data/lib/language_operator/dsl/agent_context.rb +90 -0
- data/lib/language_operator/dsl/agent_definition.rb +427 -0
- data/lib/language_operator/dsl/chat_endpoint_definition.rb +115 -0
- data/lib/language_operator/dsl/config.rb +119 -0
- data/lib/language_operator/dsl/context.rb +50 -0
- data/lib/language_operator/dsl/execution_context.rb +47 -0
- data/lib/language_operator/dsl/helpers.rb +109 -0
- data/lib/language_operator/dsl/http.rb +184 -0
- data/lib/language_operator/dsl/mcp_server_definition.rb +73 -0
- data/lib/language_operator/dsl/parameter_definition.rb +124 -0
- data/lib/language_operator/dsl/registry.rb +36 -0
- data/lib/language_operator/dsl/shell.rb +125 -0
- data/lib/language_operator/dsl/tool_definition.rb +112 -0
- data/lib/language_operator/dsl/webhook_authentication.rb +114 -0
- data/lib/language_operator/dsl/webhook_definition.rb +106 -0
- data/lib/language_operator/dsl/workflow_definition.rb +259 -0
- data/lib/language_operator/dsl.rb +160 -0
- data/lib/language_operator/errors.rb +60 -0
- data/lib/language_operator/kubernetes/client.rb +279 -0
- data/lib/language_operator/kubernetes/resource_builder.rb +194 -0
- data/lib/language_operator/loggable.rb +47 -0
- data/lib/language_operator/logger.rb +141 -0
- data/lib/language_operator/retry.rb +123 -0
- data/lib/language_operator/retryable.rb +132 -0
- data/lib/language_operator/tool_loader.rb +242 -0
- data/lib/language_operator/validators.rb +170 -0
- data/lib/language_operator/version.rb +1 -1
- data/lib/language_operator.rb +65 -3
- data/requirements/tasks/challenge.md +9 -0
- data/requirements/tasks/iterate.md +36 -0
- data/requirements/tasks/optimize.md +21 -0
- data/requirements/tasks/tag.md +5 -0
- data/test_agent_dsl.rb +108 -0
- metadata +503 -20
|
@@ -0,0 +1,1252 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'thor'
|
|
4
|
+
require_relative '../formatters/progress_formatter'
|
|
5
|
+
require_relative '../formatters/table_formatter'
|
|
6
|
+
require_relative '../formatters/value_formatter'
|
|
7
|
+
require_relative '../helpers/cluster_validator'
|
|
8
|
+
require_relative '../helpers/cluster_context'
|
|
9
|
+
require_relative '../helpers/user_prompts'
|
|
10
|
+
require_relative '../helpers/editor_helper'
|
|
11
|
+
require_relative '../errors/handler'
|
|
12
|
+
require_relative '../../config/cluster_config'
|
|
13
|
+
require_relative '../../kubernetes/client'
|
|
14
|
+
require_relative '../../kubernetes/resource_builder'
|
|
15
|
+
|
|
16
|
+
module LanguageOperator
|
|
17
|
+
module CLI
|
|
18
|
+
module Commands
|
|
19
|
+
# Agent management commands
|
|
20
|
+
class Agent < Thor
|
|
21
|
+
include Helpers::ClusterValidator
|
|
22
|
+
|
|
23
|
+
desc 'create [DESCRIPTION]', 'Create a new agent with natural language description'
|
|
24
|
+
long_desc <<-DESC
|
|
25
|
+
Create a new autonomous agent by describing what you want it to do in natural language.
|
|
26
|
+
|
|
27
|
+
The operator will synthesize the agent from your description and deploy it to your cluster.
|
|
28
|
+
|
|
29
|
+
Examples:
|
|
30
|
+
aictl agent create "review my spreadsheet at 4pm daily and email me any errors"
|
|
31
|
+
aictl agent create "summarize Hacker News top stories every morning at 8am"
|
|
32
|
+
aictl agent create "monitor my website uptime and alert me if it goes down"
|
|
33
|
+
aictl agent create --wizard # Interactive wizard mode
|
|
34
|
+
DESC
|
|
35
|
+
option :cluster, type: :string, desc: 'Override current cluster context'
|
|
36
|
+
option :create_cluster, type: :string, desc: 'Create cluster if it doesn\'t exist'
|
|
37
|
+
option :name, type: :string, desc: 'Agent name (generated from description if not provided)'
|
|
38
|
+
option :persona, type: :string, desc: 'Persona to use for the agent'
|
|
39
|
+
option :tools, type: :array, desc: 'Tools to make available to the agent'
|
|
40
|
+
option :models, type: :array, desc: 'Models to make available to the agent'
|
|
41
|
+
option :dry_run, type: :boolean, default: false, desc: 'Preview what would be created without applying'
|
|
42
|
+
option :wizard, type: :boolean, default: false, desc: 'Use interactive wizard mode'
|
|
43
|
+
def create(description = nil)
|
|
44
|
+
# Activate wizard mode if --wizard flag or no description provided
|
|
45
|
+
if options[:wizard] || description.nil?
|
|
46
|
+
require_relative '../wizards/agent_wizard'
|
|
47
|
+
wizard = Wizards::AgentWizard.new
|
|
48
|
+
description = wizard.run
|
|
49
|
+
|
|
50
|
+
# User cancelled wizard
|
|
51
|
+
unless description
|
|
52
|
+
Formatters::ProgressFormatter.info('Agent creation cancelled')
|
|
53
|
+
return
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Handle --create-cluster flag
|
|
58
|
+
if options[:create_cluster]
|
|
59
|
+
cluster_name = options[:create_cluster]
|
|
60
|
+
unless Config::ClusterConfig.cluster_exists?(cluster_name)
|
|
61
|
+
Formatters::ProgressFormatter.info("Creating cluster '#{cluster_name}'...")
|
|
62
|
+
# Delegate to cluster create command
|
|
63
|
+
require_relative 'cluster'
|
|
64
|
+
Cluster.new.invoke(:create, [cluster_name], switch: true)
|
|
65
|
+
end
|
|
66
|
+
cluster = cluster_name
|
|
67
|
+
else
|
|
68
|
+
# Validate cluster selection (this will exit if none selected)
|
|
69
|
+
cluster = Helpers::ClusterValidator.get_cluster(options[:cluster])
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
cluster_config = Helpers::ClusterValidator.get_cluster_config(cluster)
|
|
73
|
+
|
|
74
|
+
Formatters::ProgressFormatter.info("Creating agent in cluster '#{cluster}'")
|
|
75
|
+
puts
|
|
76
|
+
|
|
77
|
+
# Generate agent name from description if not provided
|
|
78
|
+
agent_name = options[:name] || generate_agent_name(description)
|
|
79
|
+
|
|
80
|
+
# Get models: use specified models, or default to all available models in cluster
|
|
81
|
+
models = options[:models]
|
|
82
|
+
if models.nil? || models.empty?
|
|
83
|
+
k8s = Helpers::ClusterValidator.kubernetes_client(options[:cluster])
|
|
84
|
+
available_models = k8s.list_resources('LanguageModel', namespace: cluster_config[:namespace])
|
|
85
|
+
models = available_models.map { |m| m.dig('metadata', 'name') }
|
|
86
|
+
|
|
87
|
+
Errors::Handler.handle_no_models_available(cluster: cluster) if models.empty?
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Build LanguageAgent resource
|
|
91
|
+
agent_resource = Kubernetes::ResourceBuilder.language_agent(
|
|
92
|
+
agent_name,
|
|
93
|
+
instructions: description,
|
|
94
|
+
cluster: cluster_config[:namespace],
|
|
95
|
+
persona: options[:persona],
|
|
96
|
+
tools: options[:tools] || [],
|
|
97
|
+
models: models
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
# Dry-run mode: preview without applying
|
|
101
|
+
if options[:dry_run]
|
|
102
|
+
display_dry_run_preview(agent_resource, cluster, description)
|
|
103
|
+
return
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Connect to Kubernetes
|
|
107
|
+
k8s = Helpers::ClusterValidator.kubernetes_client(options[:cluster])
|
|
108
|
+
|
|
109
|
+
# Apply resource to cluster
|
|
110
|
+
Formatters::ProgressFormatter.with_spinner("Creating agent '#{agent_name}'") do
|
|
111
|
+
k8s.apply_resource(agent_resource)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Watch synthesis status
|
|
115
|
+
synthesis_result = watch_synthesis_status(k8s, agent_name, cluster_config[:namespace])
|
|
116
|
+
|
|
117
|
+
# Exit if synthesis failed
|
|
118
|
+
exit 1 unless synthesis_result[:success]
|
|
119
|
+
|
|
120
|
+
# Fetch the updated agent to get complete details
|
|
121
|
+
agent = k8s.get_resource('LanguageAgent', agent_name, cluster_config[:namespace])
|
|
122
|
+
|
|
123
|
+
# Display enhanced success output
|
|
124
|
+
display_agent_created(agent, cluster, description, synthesis_result)
|
|
125
|
+
rescue StandardError => e
|
|
126
|
+
Formatters::ProgressFormatter.error("Failed to create agent: #{e.message}")
|
|
127
|
+
raise if ENV['DEBUG']
|
|
128
|
+
|
|
129
|
+
exit 1
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
desc 'list', 'List all agents in current cluster'
|
|
133
|
+
option :cluster, type: :string, desc: 'Override current cluster context'
|
|
134
|
+
option :all_clusters, type: :boolean, default: false, desc: 'Show agents across all clusters'
|
|
135
|
+
def list
|
|
136
|
+
if options[:all_clusters]
|
|
137
|
+
list_all_clusters
|
|
138
|
+
else
|
|
139
|
+
cluster = Helpers::ClusterValidator.get_cluster(options[:cluster])
|
|
140
|
+
list_cluster_agents(cluster)
|
|
141
|
+
end
|
|
142
|
+
rescue StandardError => e
|
|
143
|
+
Formatters::ProgressFormatter.error("Failed to list agents: #{e.message}")
|
|
144
|
+
raise if ENV['DEBUG']
|
|
145
|
+
|
|
146
|
+
exit 1
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
desc 'inspect NAME', 'Show detailed agent information'
|
|
150
|
+
option :cluster, type: :string, desc: 'Override current cluster context'
|
|
151
|
+
def inspect(name)
|
|
152
|
+
cluster = Helpers::ClusterValidator.get_cluster(options[:cluster])
|
|
153
|
+
cluster_config = Helpers::ClusterValidator.get_cluster_config(cluster)
|
|
154
|
+
|
|
155
|
+
k8s = Helpers::ClusterValidator.kubernetes_client(options[:cluster])
|
|
156
|
+
|
|
157
|
+
agent = k8s.get_resource('LanguageAgent', name, cluster_config[:namespace])
|
|
158
|
+
|
|
159
|
+
puts "Agent: #{name}"
|
|
160
|
+
puts " Cluster: #{cluster}"
|
|
161
|
+
puts " Namespace: #{cluster_config[:namespace]}"
|
|
162
|
+
puts
|
|
163
|
+
|
|
164
|
+
# Status
|
|
165
|
+
status = agent.dig('status', 'phase') || 'Unknown'
|
|
166
|
+
puts "Status: #{format_status(status)}"
|
|
167
|
+
puts
|
|
168
|
+
|
|
169
|
+
# Spec details
|
|
170
|
+
puts 'Configuration:'
|
|
171
|
+
puts " Mode: #{agent.dig('spec', 'mode') || 'autonomous'}"
|
|
172
|
+
puts " Schedule: #{agent.dig('spec', 'schedule') || 'N/A'}" if agent.dig('spec', 'schedule')
|
|
173
|
+
puts " Persona: #{agent.dig('spec', 'persona') || '(auto-selected)'}"
|
|
174
|
+
puts
|
|
175
|
+
|
|
176
|
+
# Instructions
|
|
177
|
+
instructions = agent.dig('spec', 'instructions')
|
|
178
|
+
if instructions
|
|
179
|
+
puts 'Instructions:'
|
|
180
|
+
puts " #{instructions}"
|
|
181
|
+
puts
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Tools
|
|
185
|
+
tools = agent.dig('spec', 'tools') || []
|
|
186
|
+
if tools.any?
|
|
187
|
+
puts "Tools (#{tools.length}):"
|
|
188
|
+
tools.each { |tool| puts " - #{tool}" }
|
|
189
|
+
puts
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Models
|
|
193
|
+
model_refs = agent.dig('spec', 'modelRefs') || []
|
|
194
|
+
if model_refs.any?
|
|
195
|
+
puts "Models (#{model_refs.length}):"
|
|
196
|
+
model_refs.each { |ref| puts " - #{ref['name']}" }
|
|
197
|
+
puts
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Synthesis info
|
|
201
|
+
synthesis = agent.dig('status', 'synthesis')
|
|
202
|
+
if synthesis
|
|
203
|
+
puts 'Synthesis:'
|
|
204
|
+
puts " Status: #{synthesis['status']}"
|
|
205
|
+
puts " Model: #{synthesis['model']}" if synthesis['model']
|
|
206
|
+
puts " Completed: #{synthesis['completedAt']}" if synthesis['completedAt']
|
|
207
|
+
puts " Duration: #{synthesis['duration']}" if synthesis['duration']
|
|
208
|
+
puts " Token Count: #{synthesis['tokenCount']}" if synthesis['tokenCount']
|
|
209
|
+
puts
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# Execution stats
|
|
213
|
+
execution_count = agent.dig('status', 'executionCount') || 0
|
|
214
|
+
last_execution = agent.dig('status', 'lastExecution')
|
|
215
|
+
next_run = agent.dig('status', 'nextRun')
|
|
216
|
+
|
|
217
|
+
puts 'Execution:'
|
|
218
|
+
puts " Total Runs: #{execution_count}"
|
|
219
|
+
puts " Last Run: #{last_execution || 'Never'}"
|
|
220
|
+
puts " Next Run: #{next_run || 'N/A'}" if agent.dig('spec', 'schedule')
|
|
221
|
+
puts
|
|
222
|
+
|
|
223
|
+
# Conditions
|
|
224
|
+
conditions = agent.dig('status', 'conditions') || []
|
|
225
|
+
if conditions.any?
|
|
226
|
+
puts "Conditions (#{conditions.length}):"
|
|
227
|
+
conditions.each do |condition|
|
|
228
|
+
status_icon = condition['status'] == 'True' ? '✓' : '✗'
|
|
229
|
+
puts " #{status_icon} #{condition['type']}: #{condition['message'] || condition['reason']}"
|
|
230
|
+
end
|
|
231
|
+
puts
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
# Recent events (if available)
|
|
235
|
+
# This would require querying events, which we can add later
|
|
236
|
+
rescue K8s::Error::NotFound
|
|
237
|
+
handle_agent_not_found(name, cluster, k8s, cluster_config)
|
|
238
|
+
rescue StandardError => e
|
|
239
|
+
Formatters::ProgressFormatter.error("Failed to inspect agent: #{e.message}")
|
|
240
|
+
raise if ENV['DEBUG']
|
|
241
|
+
|
|
242
|
+
exit 1
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
desc 'delete NAME', 'Delete an agent'
|
|
246
|
+
option :cluster, type: :string, desc: 'Override current cluster context'
|
|
247
|
+
option :force, type: :boolean, default: false, desc: 'Skip confirmation'
|
|
248
|
+
def delete(name)
|
|
249
|
+
ctx = Helpers::ClusterContext.from_options(options)
|
|
250
|
+
|
|
251
|
+
# Get agent to show details before deletion
|
|
252
|
+
begin
|
|
253
|
+
agent = ctx.client.get_resource('LanguageAgent', name, ctx.namespace)
|
|
254
|
+
rescue K8s::Error::NotFound
|
|
255
|
+
Formatters::ProgressFormatter.error("Agent '#{name}' not found in cluster '#{ctx.name}'")
|
|
256
|
+
exit 1
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
# Confirm deletion using UserPrompts helper
|
|
260
|
+
unless options[:force]
|
|
261
|
+
puts "This will delete agent '#{name}' from cluster '#{ctx.name}':"
|
|
262
|
+
puts " Instructions: #{agent.dig('spec', 'instructions')}"
|
|
263
|
+
puts " Mode: #{agent.dig('spec', 'mode') || 'autonomous'}"
|
|
264
|
+
puts
|
|
265
|
+
return unless Helpers::UserPrompts.confirm('Are you sure?')
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
# Delete the agent
|
|
269
|
+
Formatters::ProgressFormatter.with_spinner("Deleting agent '#{name}'") do
|
|
270
|
+
ctx.client.delete_resource('LanguageAgent', name, ctx.namespace)
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
Formatters::ProgressFormatter.success("Agent '#{name}' deleted successfully")
|
|
274
|
+
rescue StandardError => e
|
|
275
|
+
Formatters::ProgressFormatter.error("Failed to delete agent: #{e.message}")
|
|
276
|
+
raise if ENV['DEBUG']
|
|
277
|
+
|
|
278
|
+
exit 1
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
desc 'logs NAME', 'Show agent execution logs'
|
|
282
|
+
long_desc <<-DESC
|
|
283
|
+
Stream agent execution logs in real-time.
|
|
284
|
+
|
|
285
|
+
Use -f to follow logs continuously (like tail -f).
|
|
286
|
+
|
|
287
|
+
Examples:
|
|
288
|
+
aictl agent logs my-agent
|
|
289
|
+
aictl agent logs my-agent -f
|
|
290
|
+
DESC
|
|
291
|
+
option :cluster, type: :string, desc: 'Override current cluster context'
|
|
292
|
+
option :follow, type: :boolean, aliases: '-f', default: false, desc: 'Follow logs'
|
|
293
|
+
option :tail, type: :numeric, default: 100, desc: 'Number of lines to show from the end'
|
|
294
|
+
def logs(name)
|
|
295
|
+
cluster = Helpers::ClusterValidator.get_cluster(options[:cluster])
|
|
296
|
+
cluster_config = Helpers::ClusterValidator.get_cluster_config(cluster)
|
|
297
|
+
|
|
298
|
+
k8s = Helpers::ClusterValidator.kubernetes_client(options[:cluster])
|
|
299
|
+
|
|
300
|
+
# Get agent to determine the pod name
|
|
301
|
+
begin
|
|
302
|
+
agent = k8s.get_resource('LanguageAgent', name, cluster_config[:namespace])
|
|
303
|
+
rescue K8s::Error::NotFound
|
|
304
|
+
Formatters::ProgressFormatter.error("Agent '#{name}' not found in cluster '#{cluster}'")
|
|
305
|
+
exit 1
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
mode = agent.dig('spec', 'mode') || 'autonomous'
|
|
309
|
+
|
|
310
|
+
# Build kubectl command for log streaming
|
|
311
|
+
kubeconfig_arg = cluster_config[:kubeconfig] ? "--kubeconfig=#{cluster_config[:kubeconfig]}" : ''
|
|
312
|
+
context_arg = cluster_config[:context] ? "--context=#{cluster_config[:context]}" : ''
|
|
313
|
+
namespace_arg = "-n #{cluster_config[:namespace]}"
|
|
314
|
+
tail_arg = "--tail=#{options[:tail]}"
|
|
315
|
+
follow_arg = options[:follow] ? '-f' : ''
|
|
316
|
+
|
|
317
|
+
# For scheduled agents, logs come from CronJob pods
|
|
318
|
+
# For autonomous agents, logs come from Deployment pods
|
|
319
|
+
if mode == 'scheduled'
|
|
320
|
+
# Get most recent job from cronjob
|
|
321
|
+
else
|
|
322
|
+
# Get pod from deployment
|
|
323
|
+
end
|
|
324
|
+
label_selector = "app.kubernetes.io/name=#{name}"
|
|
325
|
+
|
|
326
|
+
# Use kubectl logs with label selector
|
|
327
|
+
cmd = "kubectl #{kubeconfig_arg} #{context_arg} #{namespace_arg} logs -l #{label_selector} #{tail_arg} #{follow_arg} --prefix --all-containers"
|
|
328
|
+
|
|
329
|
+
Formatters::ProgressFormatter.info("Streaming logs for agent '#{name}'...")
|
|
330
|
+
puts
|
|
331
|
+
|
|
332
|
+
# Stream and format logs in real-time
|
|
333
|
+
require 'open3'
|
|
334
|
+
Open3.popen3(cmd) do |_stdin, stdout, stderr, wait_thr|
|
|
335
|
+
# Handle stdout (logs)
|
|
336
|
+
stdout_thread = Thread.new do
|
|
337
|
+
stdout.each_line do |line|
|
|
338
|
+
puts Formatters::LogFormatter.format_line(line.chomp)
|
|
339
|
+
$stdout.flush
|
|
340
|
+
end
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
# Handle stderr (errors)
|
|
344
|
+
stderr_thread = Thread.new do
|
|
345
|
+
stderr.each_line do |line|
|
|
346
|
+
warn line
|
|
347
|
+
end
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
# Wait for both streams to complete
|
|
351
|
+
stdout_thread.join
|
|
352
|
+
stderr_thread.join
|
|
353
|
+
|
|
354
|
+
# Check exit status
|
|
355
|
+
exit_status = wait_thr.value
|
|
356
|
+
exit exit_status.exitstatus unless exit_status.success?
|
|
357
|
+
end
|
|
358
|
+
rescue StandardError => e
|
|
359
|
+
Formatters::ProgressFormatter.error("Failed to get logs: #{e.message}")
|
|
360
|
+
raise if ENV['DEBUG']
|
|
361
|
+
|
|
362
|
+
exit 1
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
desc 'code NAME', 'Display synthesized agent code'
|
|
366
|
+
option :cluster, type: :string, desc: 'Override current cluster context'
|
|
367
|
+
def code(name)
|
|
368
|
+
require_relative '../formatters/code_formatter'
|
|
369
|
+
|
|
370
|
+
cluster = Helpers::ClusterValidator.get_cluster(options[:cluster])
|
|
371
|
+
cluster_config = Helpers::ClusterValidator.get_cluster_config(cluster)
|
|
372
|
+
|
|
373
|
+
k8s = Helpers::ClusterValidator.kubernetes_client(options[:cluster])
|
|
374
|
+
|
|
375
|
+
# Get the code ConfigMap for this agent
|
|
376
|
+
configmap_name = "#{name}-code"
|
|
377
|
+
begin
|
|
378
|
+
configmap = k8s.get_resource('ConfigMap', configmap_name, cluster_config[:namespace])
|
|
379
|
+
rescue K8s::Error::NotFound
|
|
380
|
+
Formatters::ProgressFormatter.error("Synthesized code not found for agent '#{name}'")
|
|
381
|
+
puts
|
|
382
|
+
puts 'Possible reasons:'
|
|
383
|
+
puts ' - Agent synthesis not yet complete'
|
|
384
|
+
puts ' - Agent synthesis failed'
|
|
385
|
+
puts
|
|
386
|
+
puts 'Check synthesis status with:'
|
|
387
|
+
puts " aictl agent inspect #{name}"
|
|
388
|
+
exit 1
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
# Get the agent.rb code from the ConfigMap
|
|
392
|
+
code_content = configmap.dig('data', 'agent.rb')
|
|
393
|
+
unless code_content
|
|
394
|
+
Formatters::ProgressFormatter.error('Code content not found in ConfigMap')
|
|
395
|
+
exit 1
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
# Display with syntax highlighting
|
|
399
|
+
Formatters::CodeFormatter.display_ruby_code(
|
|
400
|
+
code_content,
|
|
401
|
+
title: "Synthesized Code for Agent: #{name}"
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
puts
|
|
405
|
+
puts 'This code was automatically synthesized from the agent instructions.'
|
|
406
|
+
puts "View full agent details with: aictl agent inspect #{name}"
|
|
407
|
+
rescue StandardError => e
|
|
408
|
+
Formatters::ProgressFormatter.error("Failed to get code: #{e.message}")
|
|
409
|
+
raise if ENV['DEBUG']
|
|
410
|
+
|
|
411
|
+
exit 1
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
desc 'edit NAME', 'Edit agent instructions'
|
|
415
|
+
option :cluster, type: :string, desc: 'Override current cluster context'
|
|
416
|
+
def edit(name)
|
|
417
|
+
cluster = Helpers::ClusterValidator.get_cluster(options[:cluster])
|
|
418
|
+
cluster_config = Helpers::ClusterValidator.get_cluster_config(cluster)
|
|
419
|
+
|
|
420
|
+
k8s = Helpers::ClusterValidator.kubernetes_client(options[:cluster])
|
|
421
|
+
|
|
422
|
+
# Get current agent
|
|
423
|
+
begin
|
|
424
|
+
agent = k8s.get_resource('LanguageAgent', name, cluster_config[:namespace])
|
|
425
|
+
rescue K8s::Error::NotFound
|
|
426
|
+
Formatters::ProgressFormatter.error("Agent '#{name}' not found in cluster '#{cluster}'")
|
|
427
|
+
exit 1
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
current_instructions = agent.dig('spec', 'instructions')
|
|
431
|
+
|
|
432
|
+
# Edit instructions in user's editor
|
|
433
|
+
new_instructions = Helpers::EditorHelper.edit_content(
|
|
434
|
+
current_instructions,
|
|
435
|
+
'agent-instructions-',
|
|
436
|
+
'.txt'
|
|
437
|
+
).strip
|
|
438
|
+
|
|
439
|
+
# Check if changed
|
|
440
|
+
if new_instructions == current_instructions
|
|
441
|
+
Formatters::ProgressFormatter.info('No changes made')
|
|
442
|
+
return
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
# Update agent resource
|
|
446
|
+
agent['spec']['instructions'] = new_instructions
|
|
447
|
+
|
|
448
|
+
Formatters::ProgressFormatter.with_spinner('Updating agent instructions') do
|
|
449
|
+
k8s.apply_resource(agent)
|
|
450
|
+
end
|
|
451
|
+
|
|
452
|
+
Formatters::ProgressFormatter.success('Agent instructions updated')
|
|
453
|
+
puts
|
|
454
|
+
puts 'The operator will automatically re-synthesize the agent code.'
|
|
455
|
+
puts
|
|
456
|
+
puts 'Watch synthesis progress with:'
|
|
457
|
+
puts " aictl agent inspect #{name}"
|
|
458
|
+
rescue StandardError => e
|
|
459
|
+
Formatters::ProgressFormatter.error("Failed to edit agent: #{e.message}")
|
|
460
|
+
raise if ENV['DEBUG']
|
|
461
|
+
|
|
462
|
+
exit 1
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
desc 'pause NAME', 'Pause scheduled agent execution'
|
|
466
|
+
option :cluster, type: :string, desc: 'Override current cluster context'
|
|
467
|
+
def pause(name)
|
|
468
|
+
cluster = Helpers::ClusterValidator.get_cluster(options[:cluster])
|
|
469
|
+
cluster_config = Helpers::ClusterValidator.get_cluster_config(cluster)
|
|
470
|
+
|
|
471
|
+
k8s = Helpers::ClusterValidator.kubernetes_client(options[:cluster])
|
|
472
|
+
|
|
473
|
+
# Get agent
|
|
474
|
+
begin
|
|
475
|
+
agent = k8s.get_resource('LanguageAgent', name, cluster_config[:namespace])
|
|
476
|
+
rescue K8s::Error::NotFound
|
|
477
|
+
Formatters::ProgressFormatter.error("Agent '#{name}' not found in cluster '#{cluster}'")
|
|
478
|
+
exit 1
|
|
479
|
+
end
|
|
480
|
+
|
|
481
|
+
mode = agent.dig('spec', 'mode') || 'autonomous'
|
|
482
|
+
unless mode == 'scheduled'
|
|
483
|
+
Formatters::ProgressFormatter.warn("Agent '#{name}' is not a scheduled agent (mode: #{mode})")
|
|
484
|
+
puts
|
|
485
|
+
puts 'Only scheduled agents can be paused.'
|
|
486
|
+
puts 'Autonomous agents can be stopped by deleting them.'
|
|
487
|
+
exit 1
|
|
488
|
+
end
|
|
489
|
+
|
|
490
|
+
# Suspend the CronJob by setting spec.suspend = true
|
|
491
|
+
# This is done by patching the underlying CronJob resource
|
|
492
|
+
cronjob_name = name
|
|
493
|
+
namespace = cluster_config[:namespace]
|
|
494
|
+
|
|
495
|
+
Formatters::ProgressFormatter.with_spinner("Pausing agent '#{name}'") do
|
|
496
|
+
# Use kubectl to patch the cronjob
|
|
497
|
+
kubeconfig_arg = cluster_config[:kubeconfig] ? "--kubeconfig=#{cluster_config[:kubeconfig]}" : ''
|
|
498
|
+
context_arg = cluster_config[:context] ? "--context=#{cluster_config[:context]}" : ''
|
|
499
|
+
|
|
500
|
+
cmd = "kubectl #{kubeconfig_arg} #{context_arg} -n #{namespace} patch cronjob #{cronjob_name} -p '{\"spec\":{\"suspend\":true}}'"
|
|
501
|
+
system(cmd)
|
|
502
|
+
end
|
|
503
|
+
|
|
504
|
+
Formatters::ProgressFormatter.success("Agent '#{name}' paused")
|
|
505
|
+
puts
|
|
506
|
+
puts 'The agent will not execute on its schedule until resumed.'
|
|
507
|
+
puts
|
|
508
|
+
puts 'Resume with:'
|
|
509
|
+
puts " aictl agent resume #{name}"
|
|
510
|
+
rescue StandardError => e
|
|
511
|
+
Formatters::ProgressFormatter.error("Failed to pause agent: #{e.message}")
|
|
512
|
+
raise if ENV['DEBUG']
|
|
513
|
+
|
|
514
|
+
exit 1
|
|
515
|
+
end
|
|
516
|
+
|
|
517
|
+
desc 'resume NAME', 'Resume paused agent'
|
|
518
|
+
option :cluster, type: :string, desc: 'Override current cluster context'
|
|
519
|
+
def resume(name)
|
|
520
|
+
cluster = Helpers::ClusterValidator.get_cluster(options[:cluster])
|
|
521
|
+
cluster_config = Helpers::ClusterValidator.get_cluster_config(cluster)
|
|
522
|
+
|
|
523
|
+
k8s = Helpers::ClusterValidator.kubernetes_client(options[:cluster])
|
|
524
|
+
|
|
525
|
+
# Get agent
|
|
526
|
+
begin
|
|
527
|
+
agent = k8s.get_resource('LanguageAgent', name, cluster_config[:namespace])
|
|
528
|
+
rescue K8s::Error::NotFound
|
|
529
|
+
Formatters::ProgressFormatter.error("Agent '#{name}' not found in cluster '#{cluster}'")
|
|
530
|
+
exit 1
|
|
531
|
+
end
|
|
532
|
+
|
|
533
|
+
mode = agent.dig('spec', 'mode') || 'autonomous'
|
|
534
|
+
unless mode == 'scheduled'
|
|
535
|
+
Formatters::ProgressFormatter.warn("Agent '#{name}' is not a scheduled agent (mode: #{mode})")
|
|
536
|
+
puts
|
|
537
|
+
puts 'Only scheduled agents can be resumed.'
|
|
538
|
+
exit 1
|
|
539
|
+
end
|
|
540
|
+
|
|
541
|
+
# Resume the CronJob by setting spec.suspend = false
|
|
542
|
+
cronjob_name = name
|
|
543
|
+
namespace = cluster_config[:namespace]
|
|
544
|
+
|
|
545
|
+
Formatters::ProgressFormatter.with_spinner("Resuming agent '#{name}'") do
|
|
546
|
+
# Use kubectl to patch the cronjob
|
|
547
|
+
kubeconfig_arg = cluster_config[:kubeconfig] ? "--kubeconfig=#{cluster_config[:kubeconfig]}" : ''
|
|
548
|
+
context_arg = cluster_config[:context] ? "--context=#{cluster_config[:context]}" : ''
|
|
549
|
+
|
|
550
|
+
cmd = "kubectl #{kubeconfig_arg} #{context_arg} -n #{namespace} patch cronjob #{cronjob_name} -p '{\"spec\":{\"suspend\":false}}'"
|
|
551
|
+
system(cmd)
|
|
552
|
+
end
|
|
553
|
+
|
|
554
|
+
Formatters::ProgressFormatter.success("Agent '#{name}' resumed")
|
|
555
|
+
puts
|
|
556
|
+
puts 'The agent will now execute according to its schedule.'
|
|
557
|
+
puts
|
|
558
|
+
puts 'View next execution time with:'
|
|
559
|
+
puts " aictl agent inspect #{name}"
|
|
560
|
+
rescue StandardError => e
|
|
561
|
+
Formatters::ProgressFormatter.error("Failed to resume agent: #{e.message}")
|
|
562
|
+
raise if ENV['DEBUG']
|
|
563
|
+
|
|
564
|
+
exit 1
|
|
565
|
+
end
|
|
566
|
+
|
|
567
|
+
desc 'workspace NAME', 'Browse agent workspace files'
|
|
568
|
+
long_desc <<-DESC
|
|
569
|
+
Browse and manage the workspace files for an agent.
|
|
570
|
+
|
|
571
|
+
Workspaces provide persistent storage for agents to maintain state,
|
|
572
|
+
cache data, and remember information across executions.
|
|
573
|
+
|
|
574
|
+
Examples:
|
|
575
|
+
aictl agent workspace my-agent # List all files
|
|
576
|
+
aictl agent workspace my-agent --path /workspace/state.json # View specific file
|
|
577
|
+
aictl agent workspace my-agent --clean # Clear workspace
|
|
578
|
+
DESC
|
|
579
|
+
option :cluster, type: :string, desc: 'Override current cluster context'
|
|
580
|
+
option :path, type: :string, desc: 'View specific file contents'
|
|
581
|
+
option :clean, type: :boolean, desc: 'Clear workspace (with confirmation)'
|
|
582
|
+
def workspace(name)
|
|
583
|
+
ctx = Helpers::ClusterContext.from_options(options)
|
|
584
|
+
|
|
585
|
+
# Get agent to verify it exists
|
|
586
|
+
begin
|
|
587
|
+
agent = ctx.client.get_resource('LanguageAgent', name, ctx.namespace)
|
|
588
|
+
rescue K8s::Error::NotFound
|
|
589
|
+
Formatters::ProgressFormatter.error("Agent '#{name}' not found in cluster '#{ctx.name}'")
|
|
590
|
+
exit 1
|
|
591
|
+
end
|
|
592
|
+
|
|
593
|
+
# Check if workspace is enabled
|
|
594
|
+
workspace_enabled = agent.dig('spec', 'workspace', 'enabled')
|
|
595
|
+
unless workspace_enabled
|
|
596
|
+
Formatters::ProgressFormatter.warn("Workspace is not enabled for agent '#{name}'")
|
|
597
|
+
puts
|
|
598
|
+
puts 'Enable workspace in agent configuration:'
|
|
599
|
+
puts ' spec:'
|
|
600
|
+
puts ' workspace:'
|
|
601
|
+
puts ' enabled: true'
|
|
602
|
+
puts ' size: 10Gi'
|
|
603
|
+
exit 1
|
|
604
|
+
end
|
|
605
|
+
|
|
606
|
+
if options[:path]
|
|
607
|
+
view_workspace_file(ctx, name, options[:path])
|
|
608
|
+
elsif options[:clean]
|
|
609
|
+
clean_workspace(ctx, name)
|
|
610
|
+
else
|
|
611
|
+
list_workspace_files(ctx, name)
|
|
612
|
+
end
|
|
613
|
+
rescue StandardError => e
|
|
614
|
+
Formatters::ProgressFormatter.error("Failed to access workspace: #{e.message}")
|
|
615
|
+
raise if ENV['DEBUG']
|
|
616
|
+
|
|
617
|
+
exit 1
|
|
618
|
+
end
|
|
619
|
+
|
|
620
|
+
private
|
|
621
|
+
|
|
622
|
+
def handle_agent_not_found(name, cluster, k8s, cluster_config)
|
|
623
|
+
# Get available agents for fuzzy matching
|
|
624
|
+
agents = k8s.list_resources('LanguageAgent', namespace: cluster_config[:namespace])
|
|
625
|
+
available_names = agents.map { |a| a.dig('metadata', 'name') }
|
|
626
|
+
|
|
627
|
+
error = K8s::Error::NotFound.new(404, 'Not Found', 'LanguageAgent')
|
|
628
|
+
Errors::Handler.handle_not_found(error,
|
|
629
|
+
resource_type: 'LanguageAgent',
|
|
630
|
+
resource_name: name,
|
|
631
|
+
cluster: cluster,
|
|
632
|
+
available_resources: available_names)
|
|
633
|
+
end
|
|
634
|
+
|
|
635
|
+
def display_agent_created(agent, cluster, _description, synthesis_result)
|
|
636
|
+
require 'pastel'
|
|
637
|
+
require_relative '../formatters/code_formatter'
|
|
638
|
+
|
|
639
|
+
pastel = Pastel.new
|
|
640
|
+
agent_name = agent.dig('metadata', 'name')
|
|
641
|
+
|
|
642
|
+
puts
|
|
643
|
+
Formatters::ProgressFormatter.success("Agent '#{agent_name}' created and deployed!")
|
|
644
|
+
puts
|
|
645
|
+
|
|
646
|
+
# Get synthesized code if available
|
|
647
|
+
begin
|
|
648
|
+
cluster_config = Helpers::ClusterValidator.get_cluster_config(cluster)
|
|
649
|
+
k8s = Helpers::ClusterValidator.kubernetes_client(cluster)
|
|
650
|
+
configmap_name = "#{agent_name}-code"
|
|
651
|
+
configmap = k8s.get_resource('ConfigMap', configmap_name, cluster_config[:namespace])
|
|
652
|
+
code_content = configmap.dig('data', 'agent.rb')
|
|
653
|
+
|
|
654
|
+
if code_content
|
|
655
|
+
# Display code preview (first 20 lines)
|
|
656
|
+
Formatters::CodeFormatter.display_ruby_code(
|
|
657
|
+
code_content,
|
|
658
|
+
title: 'Synthesized Code Preview:',
|
|
659
|
+
max_lines: 20
|
|
660
|
+
)
|
|
661
|
+
puts
|
|
662
|
+
end
|
|
663
|
+
rescue StandardError
|
|
664
|
+
# Code not available yet, skip preview
|
|
665
|
+
end
|
|
666
|
+
|
|
667
|
+
# Display agent configuration
|
|
668
|
+
puts pastel.cyan('Agent Configuration:')
|
|
669
|
+
puts " Name: #{agent_name}"
|
|
670
|
+
puts " Cluster: #{cluster}"
|
|
671
|
+
|
|
672
|
+
# Schedule information
|
|
673
|
+
schedule = agent.dig('spec', 'schedule')
|
|
674
|
+
mode = agent.dig('spec', 'mode') || 'autonomous'
|
|
675
|
+
if schedule
|
|
676
|
+
human_schedule = parse_schedule(schedule)
|
|
677
|
+
puts " Schedule: #{human_schedule} (#{schedule})"
|
|
678
|
+
|
|
679
|
+
# Calculate next run
|
|
680
|
+
next_run = agent.dig('status', 'nextRun')
|
|
681
|
+
if next_run
|
|
682
|
+
begin
|
|
683
|
+
next_run_time = Time.parse(next_run)
|
|
684
|
+
time_until = format_time_until(next_run_time)
|
|
685
|
+
puts " Next run: #{next_run} (#{time_until})"
|
|
686
|
+
rescue StandardError
|
|
687
|
+
puts " Next run: #{next_run}"
|
|
688
|
+
end
|
|
689
|
+
end
|
|
690
|
+
else
|
|
691
|
+
puts " Mode: #{mode}"
|
|
692
|
+
end
|
|
693
|
+
|
|
694
|
+
# Persona
|
|
695
|
+
persona = agent.dig('spec', 'persona')
|
|
696
|
+
puts " Persona: #{persona || '(auto-selected)'}"
|
|
697
|
+
|
|
698
|
+
# Tools
|
|
699
|
+
tools = agent.dig('spec', 'tools') || []
|
|
700
|
+
puts " Tools: #{tools.join(', ')}" if tools.any?
|
|
701
|
+
|
|
702
|
+
# Models
|
|
703
|
+
model_refs = agent.dig('spec', 'modelRefs') || []
|
|
704
|
+
if model_refs.any?
|
|
705
|
+
model_names = model_refs.map { |ref| ref['name'] }
|
|
706
|
+
puts " Models: #{model_names.join(', ')}"
|
|
707
|
+
end
|
|
708
|
+
|
|
709
|
+
puts
|
|
710
|
+
|
|
711
|
+
# Synthesis stats
|
|
712
|
+
if synthesis_result[:duration]
|
|
713
|
+
puts pastel.dim("Synthesis completed in #{format_duration(synthesis_result[:duration])}")
|
|
714
|
+
puts pastel.dim("Model: #{synthesis_result[:model]}") if synthesis_result[:model]
|
|
715
|
+
puts
|
|
716
|
+
end
|
|
717
|
+
|
|
718
|
+
# Next steps
|
|
719
|
+
puts pastel.cyan('Next Steps:')
|
|
720
|
+
puts " aictl agent logs #{agent_name} -f # Follow agent execution logs"
|
|
721
|
+
puts " aictl agent code #{agent_name} # View full synthesized code"
|
|
722
|
+
puts " aictl agent inspect #{agent_name} # View detailed agent status"
|
|
723
|
+
puts
|
|
724
|
+
end
|
|
725
|
+
|
|
726
|
+
def parse_schedule(cron_expr)
|
|
727
|
+
# Simple cron to human-readable conversion
|
|
728
|
+
# Format: minute hour day month weekday
|
|
729
|
+
parts = cron_expr.split
|
|
730
|
+
|
|
731
|
+
return cron_expr if parts.length != 5
|
|
732
|
+
|
|
733
|
+
minute, hour, day, month, weekday = parts
|
|
734
|
+
|
|
735
|
+
# Common patterns
|
|
736
|
+
if minute == '0' && hour != '*' && day == '*' && month == '*' && weekday == '*'
|
|
737
|
+
# Daily at specific hour
|
|
738
|
+
hour12 = hour.to_i % 12
|
|
739
|
+
hour12 = 12 if hour12.zero?
|
|
740
|
+
period = hour.to_i < 12 ? 'AM' : 'PM'
|
|
741
|
+
return "Daily at #{hour12}:00 #{period}"
|
|
742
|
+
elsif minute != '*' && hour != '*' && day == '*' && month == '*' && weekday == '*'
|
|
743
|
+
# Daily at specific time
|
|
744
|
+
hour12 = hour.to_i % 12
|
|
745
|
+
hour12 = 12 if hour12.zero?
|
|
746
|
+
period = hour.to_i < 12 ? 'AM' : 'PM'
|
|
747
|
+
return "Daily at #{hour12}:#{minute.rjust(2, '0')} #{period}"
|
|
748
|
+
elsif minute.start_with?('*/') && hour == '*'
|
|
749
|
+
# Every N minutes
|
|
750
|
+
interval = minute[2..].to_i
|
|
751
|
+
return "Every #{interval} minutes"
|
|
752
|
+
elsif minute == '*' && hour.start_with?('*/')
|
|
753
|
+
# Every N hours
|
|
754
|
+
interval = hour[2..].to_i
|
|
755
|
+
return "Every #{interval} hours"
|
|
756
|
+
end
|
|
757
|
+
|
|
758
|
+
# Fallback to cron expression
|
|
759
|
+
cron_expr
|
|
760
|
+
end
|
|
761
|
+
|
|
762
|
+
def format_time_until(future_time)
|
|
763
|
+
Formatters::ValueFormatter.time_until(future_time)
|
|
764
|
+
end
|
|
765
|
+
|
|
766
|
+
def display_dry_run_preview(agent_resource, cluster, description)
|
|
767
|
+
require 'yaml'
|
|
768
|
+
|
|
769
|
+
puts
|
|
770
|
+
puts '=' * 80
|
|
771
|
+
puts ' DRY RUN: Agent Creation Preview'
|
|
772
|
+
puts '=' * 80
|
|
773
|
+
puts
|
|
774
|
+
|
|
775
|
+
# Extract key information
|
|
776
|
+
name = agent_resource.dig('metadata', 'name')
|
|
777
|
+
namespace = agent_resource.dig('metadata', 'namespace')
|
|
778
|
+
persona = agent_resource.dig('spec', 'persona')
|
|
779
|
+
tools = agent_resource.dig('spec', 'tools') || []
|
|
780
|
+
model_refs = agent_resource.dig('spec', 'modelRefs') || []
|
|
781
|
+
models = model_refs.map { |ref| ref['name'] }
|
|
782
|
+
mode = agent_resource.dig('spec', 'mode') || 'autonomous'
|
|
783
|
+
schedule = agent_resource.dig('spec', 'schedule')
|
|
784
|
+
|
|
785
|
+
# Display summary
|
|
786
|
+
puts 'Agent Summary:'
|
|
787
|
+
puts " Name: #{name}"
|
|
788
|
+
puts " Cluster: #{cluster}"
|
|
789
|
+
puts " Namespace: #{namespace}"
|
|
790
|
+
puts " Mode: #{mode}"
|
|
791
|
+
puts " Schedule: #{schedule || 'N/A'}" if schedule
|
|
792
|
+
puts " Instructions: #{description}"
|
|
793
|
+
puts
|
|
794
|
+
|
|
795
|
+
# Show detected configuration
|
|
796
|
+
if persona
|
|
797
|
+
puts 'Detected Configuration:'
|
|
798
|
+
puts " Persona: #{persona}"
|
|
799
|
+
end
|
|
800
|
+
|
|
801
|
+
puts " Tools: #{tools.join(', ')}" if tools.any?
|
|
802
|
+
|
|
803
|
+
puts " Models: #{models.join(', ')}" if models.any?
|
|
804
|
+
|
|
805
|
+
puts if persona || tools.any? || models.any?
|
|
806
|
+
|
|
807
|
+
# Show full YAML
|
|
808
|
+
puts 'Generated YAML:'
|
|
809
|
+
puts '─' * 80
|
|
810
|
+
puts YAML.dump(agent_resource)
|
|
811
|
+
puts '─' * 80
|
|
812
|
+
puts
|
|
813
|
+
|
|
814
|
+
# Show what would happen
|
|
815
|
+
puts 'What would happen:'
|
|
816
|
+
puts ' 1. Agent resource would be created in the cluster'
|
|
817
|
+
puts ' 2. Operator would synthesize Ruby code from instructions'
|
|
818
|
+
puts ' 3. Agent would be deployed and start running'
|
|
819
|
+
puts
|
|
820
|
+
|
|
821
|
+
# Show how to actually create
|
|
822
|
+
Formatters::ProgressFormatter.info('No changes made (dry-run mode)')
|
|
823
|
+
puts
|
|
824
|
+
puts 'To create this agent for real, run:'
|
|
825
|
+
cmd_parts = ["aictl agent create \"#{description}\""]
|
|
826
|
+
cmd_parts << "--name #{name}" if options[:name]
|
|
827
|
+
cmd_parts << "--persona #{persona}" if persona
|
|
828
|
+
cmd_parts << "--tools #{tools.join(' ')}" if tools.any?
|
|
829
|
+
cmd_parts << "--models #{models.join(' ')}" if models.any?
|
|
830
|
+
cmd_parts << "--cluster #{cluster}" if options[:cluster]
|
|
831
|
+
puts " #{cmd_parts.join(' ')}"
|
|
832
|
+
end
|
|
833
|
+
|
|
834
|
+
def format_status(status)
|
|
835
|
+
require 'pastel'
|
|
836
|
+
pastel = Pastel.new
|
|
837
|
+
|
|
838
|
+
case status.downcase
|
|
839
|
+
when 'ready', 'running', 'active'
|
|
840
|
+
"#{pastel.green('●')} #{status}"
|
|
841
|
+
when 'pending', 'creating', 'synthesizing'
|
|
842
|
+
"#{pastel.yellow('●')} #{status}"
|
|
843
|
+
when 'failed', 'error'
|
|
844
|
+
"#{pastel.red('●')} #{status}"
|
|
845
|
+
when 'paused', 'stopped'
|
|
846
|
+
"#{pastel.dim('●')} #{status}"
|
|
847
|
+
else
|
|
848
|
+
"#{pastel.dim('●')} #{status}"
|
|
849
|
+
end
|
|
850
|
+
end
|
|
851
|
+
|
|
852
|
+
def generate_agent_name(description)
|
|
853
|
+
# Simple name generation from description
|
|
854
|
+
# Take first few words, lowercase, hyphenate
|
|
855
|
+
words = description.downcase.gsub(/[^a-z0-9\s]/, '').split[0..2]
|
|
856
|
+
name = words.join('-')
|
|
857
|
+
# Add random suffix to avoid collisions
|
|
858
|
+
"#{name}-#{Time.now.to_i.to_s[-4..]}"
|
|
859
|
+
end
|
|
860
|
+
|
|
861
|
+
def watch_synthesis_status(k8s, agent_name, namespace)
|
|
862
|
+
# Start with analyzing description
|
|
863
|
+
puts
|
|
864
|
+
Formatters::ProgressFormatter.info('Synthesizing agent code...')
|
|
865
|
+
puts
|
|
866
|
+
|
|
867
|
+
max_wait = 600 # Wait up to 10 minutes (local models can be slow)
|
|
868
|
+
interval = 2 # Check every 2 seconds
|
|
869
|
+
elapsed = 0
|
|
870
|
+
start_time = Time.now
|
|
871
|
+
synthesis_data = {}
|
|
872
|
+
|
|
873
|
+
result = Formatters::ProgressFormatter.with_spinner('Analyzing description and generating code') do
|
|
874
|
+
loop do
|
|
875
|
+
status = check_synthesis_status(k8s, agent_name, namespace, synthesis_data, start_time)
|
|
876
|
+
return status if status
|
|
877
|
+
|
|
878
|
+
# Timeout check
|
|
879
|
+
if elapsed >= max_wait
|
|
880
|
+
Formatters::ProgressFormatter.warn('Synthesis taking longer than expected, continuing in background...')
|
|
881
|
+
puts
|
|
882
|
+
puts 'Check synthesis status with:'
|
|
883
|
+
puts " aictl agent inspect #{agent_name}"
|
|
884
|
+
return { success: true, timeout: true }
|
|
885
|
+
end
|
|
886
|
+
|
|
887
|
+
sleep interval
|
|
888
|
+
elapsed += interval
|
|
889
|
+
end
|
|
890
|
+
rescue K8s::Error::NotFound
|
|
891
|
+
# Agent not found yet, keep waiting
|
|
892
|
+
sleep interval
|
|
893
|
+
elapsed += interval
|
|
894
|
+
retry if elapsed < max_wait
|
|
895
|
+
|
|
896
|
+
Formatters::ProgressFormatter.error('Agent resource not found')
|
|
897
|
+
return { success: false }
|
|
898
|
+
rescue StandardError => e
|
|
899
|
+
Formatters::ProgressFormatter.warn("Could not watch synthesis: #{e.message}")
|
|
900
|
+
return { success: true } # Continue anyway
|
|
901
|
+
end
|
|
902
|
+
|
|
903
|
+
# Show synthesis details after spinner completes
|
|
904
|
+
if result[:success] && !result[:timeout]
|
|
905
|
+
duration = result[:duration]
|
|
906
|
+
Formatters::ProgressFormatter.success("Code synthesis completed in #{format_duration(duration)}")
|
|
907
|
+
puts " Model: #{synthesis_data[:model]}" if synthesis_data[:model]
|
|
908
|
+
puts " Tokens: #{synthesis_data[:token_count]}" if synthesis_data[:token_count]
|
|
909
|
+
end
|
|
910
|
+
|
|
911
|
+
result
|
|
912
|
+
end
|
|
913
|
+
|
|
914
|
+
def check_synthesis_status(k8s, agent_name, namespace, synthesis_data, start_time)
|
|
915
|
+
agent = k8s.get_resource('LanguageAgent', agent_name, namespace)
|
|
916
|
+
conditions = agent.dig('status', 'conditions') || []
|
|
917
|
+
synthesis_status = agent.dig('status', 'synthesis')
|
|
918
|
+
|
|
919
|
+
# Capture synthesis metadata
|
|
920
|
+
if synthesis_status
|
|
921
|
+
synthesis_data[:model] = synthesis_status['model']
|
|
922
|
+
synthesis_data[:token_count] = synthesis_status['tokenCount']
|
|
923
|
+
end
|
|
924
|
+
|
|
925
|
+
# Check for synthesis completion
|
|
926
|
+
synthesized = conditions.find { |c| c['type'] == 'Synthesized' }
|
|
927
|
+
return nil unless synthesized
|
|
928
|
+
|
|
929
|
+
if synthesized['status'] == 'True'
|
|
930
|
+
duration = Time.now - start_time
|
|
931
|
+
{ success: true, duration: duration, **synthesis_data }
|
|
932
|
+
elsif synthesized['status'] == 'False'
|
|
933
|
+
Formatters::ProgressFormatter.error("Synthesis failed: #{synthesized['message']}")
|
|
934
|
+
{ success: false }
|
|
935
|
+
end
|
|
936
|
+
end
|
|
937
|
+
|
|
938
|
+
def format_duration(seconds)
|
|
939
|
+
Formatters::ValueFormatter.duration(seconds)
|
|
940
|
+
end
|
|
941
|
+
|
|
942
|
+
def list_cluster_agents(cluster)
|
|
943
|
+
cluster_config = Helpers::ClusterValidator.get_cluster_config(cluster)
|
|
944
|
+
|
|
945
|
+
Formatters::ProgressFormatter.info("Agents in cluster '#{cluster}'")
|
|
946
|
+
|
|
947
|
+
k8s = Helpers::ClusterValidator.kubernetes_client(cluster)
|
|
948
|
+
|
|
949
|
+
agents = k8s.list_resources('LanguageAgent', namespace: cluster_config[:namespace])
|
|
950
|
+
|
|
951
|
+
table_data = agents.map do |agent|
|
|
952
|
+
{
|
|
953
|
+
name: agent.dig('metadata', 'name'),
|
|
954
|
+
mode: agent.dig('spec', 'mode') || 'autonomous',
|
|
955
|
+
status: agent.dig('status', 'phase') || 'Unknown',
|
|
956
|
+
next_run: agent.dig('status', 'nextRun') || 'N/A',
|
|
957
|
+
executions: agent.dig('status', 'executionCount') || 0
|
|
958
|
+
}
|
|
959
|
+
end
|
|
960
|
+
|
|
961
|
+
Formatters::TableFormatter.agents(table_data)
|
|
962
|
+
|
|
963
|
+
return unless agents.empty?
|
|
964
|
+
|
|
965
|
+
puts
|
|
966
|
+
puts 'Create an agent with:'
|
|
967
|
+
puts ' aictl agent create "<description>"'
|
|
968
|
+
end
|
|
969
|
+
|
|
970
|
+
def list_all_clusters
|
|
971
|
+
clusters = Config::ClusterConfig.list_clusters
|
|
972
|
+
|
|
973
|
+
if clusters.empty?
|
|
974
|
+
Formatters::ProgressFormatter.info('No clusters found')
|
|
975
|
+
puts
|
|
976
|
+
puts 'Create a cluster first:'
|
|
977
|
+
puts ' aictl cluster create <name>'
|
|
978
|
+
return
|
|
979
|
+
end
|
|
980
|
+
|
|
981
|
+
all_agents = []
|
|
982
|
+
|
|
983
|
+
clusters.each do |cluster|
|
|
984
|
+
k8s = Helpers::ClusterValidator.kubernetes_client(cluster[:name])
|
|
985
|
+
|
|
986
|
+
agents = k8s.list_resources('LanguageAgent', namespace: cluster[:namespace])
|
|
987
|
+
|
|
988
|
+
agents.each do |agent|
|
|
989
|
+
all_agents << {
|
|
990
|
+
cluster: cluster[:name],
|
|
991
|
+
name: agent.dig('metadata', 'name'),
|
|
992
|
+
mode: agent.dig('spec', 'mode') || 'autonomous',
|
|
993
|
+
status: agent.dig('status', 'phase') || 'Unknown',
|
|
994
|
+
next_run: agent.dig('status', 'nextRun') || 'N/A',
|
|
995
|
+
executions: agent.dig('status', 'executionCount') || 0
|
|
996
|
+
}
|
|
997
|
+
end
|
|
998
|
+
rescue StandardError => e
|
|
999
|
+
Formatters::ProgressFormatter.warn("Failed to get agents from cluster '#{cluster[:name]}': #{e.message}")
|
|
1000
|
+
end
|
|
1001
|
+
|
|
1002
|
+
# Group agents by cluster for formatted display
|
|
1003
|
+
agents_by_cluster = all_agents.group_by { |agent| agent[:cluster] }
|
|
1004
|
+
.transform_values { |agents| agents.map { |a| a.except(:cluster) } }
|
|
1005
|
+
|
|
1006
|
+
Formatters::TableFormatter.all_agents(agents_by_cluster)
|
|
1007
|
+
end
|
|
1008
|
+
|
|
1009
|
+
# Workspace-related helper methods
|
|
1010
|
+
|
|
1011
|
+
def get_agent_pod(ctx, agent_name)
|
|
1012
|
+
# Find pod for this agent using label selector
|
|
1013
|
+
label_selector = "app.kubernetes.io/name=#{agent_name}"
|
|
1014
|
+
pods = ctx.client.list_resources('Pod', namespace: ctx.namespace, label_selector: label_selector)
|
|
1015
|
+
|
|
1016
|
+
if pods.empty?
|
|
1017
|
+
Formatters::ProgressFormatter.error("No running pods found for agent '#{agent_name}'")
|
|
1018
|
+
puts
|
|
1019
|
+
puts 'Possible reasons:'
|
|
1020
|
+
puts ' - Agent pod has not started yet'
|
|
1021
|
+
puts ' - Agent is paused or stopped'
|
|
1022
|
+
puts ' - Agent failed to deploy'
|
|
1023
|
+
puts
|
|
1024
|
+
puts 'Check agent status with:'
|
|
1025
|
+
puts " aictl agent inspect #{agent_name}"
|
|
1026
|
+
exit 1
|
|
1027
|
+
end
|
|
1028
|
+
|
|
1029
|
+
# Find a running pod
|
|
1030
|
+
running_pod = pods.find do |pod|
|
|
1031
|
+
pod.dig('status', 'phase') == 'Running'
|
|
1032
|
+
end
|
|
1033
|
+
|
|
1034
|
+
unless running_pod
|
|
1035
|
+
Formatters::ProgressFormatter.error('Agent pod exists but is not running')
|
|
1036
|
+
puts
|
|
1037
|
+
puts "Current pod status: #{pods.first.dig('status', 'phase')}"
|
|
1038
|
+
puts
|
|
1039
|
+
puts 'Check pod logs with:'
|
|
1040
|
+
puts " aictl agent logs #{agent_name}"
|
|
1041
|
+
exit 1
|
|
1042
|
+
end
|
|
1043
|
+
|
|
1044
|
+
running_pod.dig('metadata', 'name')
|
|
1045
|
+
end
|
|
1046
|
+
|
|
1047
|
+
def exec_in_pod(ctx, pod_name, command)
|
|
1048
|
+
# Properly escape command for shell
|
|
1049
|
+
cmd_str = command.is_a?(Array) ? command.join(' ') : command
|
|
1050
|
+
kubectl_cmd = "#{ctx.kubectl_prefix} exec #{pod_name} -- #{cmd_str}"
|
|
1051
|
+
|
|
1052
|
+
# Execute and capture output
|
|
1053
|
+
require 'open3'
|
|
1054
|
+
stdout, stderr, status = Open3.capture3(kubectl_cmd)
|
|
1055
|
+
|
|
1056
|
+
raise "Command failed: #{stderr}" unless status.success?
|
|
1057
|
+
|
|
1058
|
+
stdout
|
|
1059
|
+
end
|
|
1060
|
+
|
|
1061
|
+
def list_workspace_files(ctx, agent_name)
|
|
1062
|
+
require 'pastel'
|
|
1063
|
+
pastel = Pastel.new
|
|
1064
|
+
|
|
1065
|
+
pod_name = get_agent_pod(ctx, agent_name)
|
|
1066
|
+
|
|
1067
|
+
# Check if workspace directory exists
|
|
1068
|
+
begin
|
|
1069
|
+
exec_in_pod(ctx, pod_name, 'test -d /workspace')
|
|
1070
|
+
rescue StandardError
|
|
1071
|
+
Formatters::ProgressFormatter.error('Workspace directory not found in agent pod')
|
|
1072
|
+
puts
|
|
1073
|
+
puts 'The /workspace directory does not exist in the agent pod.'
|
|
1074
|
+
puts 'This agent may not have workspace support enabled.'
|
|
1075
|
+
exit 1
|
|
1076
|
+
end
|
|
1077
|
+
|
|
1078
|
+
# Get workspace usage
|
|
1079
|
+
usage_output = exec_in_pod(
|
|
1080
|
+
ctx,
|
|
1081
|
+
pod_name,
|
|
1082
|
+
'du -sh /workspace 2>/dev/null || echo "0\t/workspace"'
|
|
1083
|
+
)
|
|
1084
|
+
workspace_size = usage_output.split("\t").first.strip
|
|
1085
|
+
|
|
1086
|
+
# List files with details
|
|
1087
|
+
file_list = exec_in_pod(
|
|
1088
|
+
ctx,
|
|
1089
|
+
pod_name,
|
|
1090
|
+
'find /workspace -ls 2>/dev/null | tail -n +2'
|
|
1091
|
+
)
|
|
1092
|
+
|
|
1093
|
+
puts
|
|
1094
|
+
puts pastel.cyan("Workspace for agent '#{agent_name}' (#{workspace_size})")
|
|
1095
|
+
puts '=' * 60
|
|
1096
|
+
puts
|
|
1097
|
+
|
|
1098
|
+
if file_list.strip.empty?
|
|
1099
|
+
puts pastel.dim('Workspace is empty')
|
|
1100
|
+
puts
|
|
1101
|
+
puts 'The agent will create files here as it runs.'
|
|
1102
|
+
puts
|
|
1103
|
+
return
|
|
1104
|
+
end
|
|
1105
|
+
|
|
1106
|
+
# Parse and display file list
|
|
1107
|
+
file_list.each_line do |line|
|
|
1108
|
+
parts = line.strip.split(/\s+/, 11)
|
|
1109
|
+
next if parts.length < 11
|
|
1110
|
+
|
|
1111
|
+
# Extract relevant parts
|
|
1112
|
+
# Format: inode blocks perms links user group size month day time path
|
|
1113
|
+
perms = parts[2]
|
|
1114
|
+
size = parts[6]
|
|
1115
|
+
month = parts[7]
|
|
1116
|
+
day = parts[8]
|
|
1117
|
+
time_or_year = parts[9]
|
|
1118
|
+
path = parts[10]
|
|
1119
|
+
|
|
1120
|
+
# Skip the /workspace directory itself
|
|
1121
|
+
next if path == '/workspace'
|
|
1122
|
+
|
|
1123
|
+
# Determine file type and icon
|
|
1124
|
+
icon = if perms.start_with?('d')
|
|
1125
|
+
pastel.blue('📁')
|
|
1126
|
+
else
|
|
1127
|
+
pastel.white('📄')
|
|
1128
|
+
end
|
|
1129
|
+
|
|
1130
|
+
# Format path relative to workspace
|
|
1131
|
+
relative_path = path.sub('/workspace/', '')
|
|
1132
|
+
indent = ' ' * relative_path.count('/')
|
|
1133
|
+
|
|
1134
|
+
# Format size
|
|
1135
|
+
formatted_size = format_file_size(size.to_i).rjust(8)
|
|
1136
|
+
|
|
1137
|
+
# Format time
|
|
1138
|
+
formatted_time = "#{month} #{day.rjust(2)} #{time_or_year}"
|
|
1139
|
+
|
|
1140
|
+
puts "#{indent}#{icon} #{File.basename(relative_path).ljust(30)} #{pastel.dim(formatted_size)} #{pastel.dim(formatted_time)}"
|
|
1141
|
+
end
|
|
1142
|
+
|
|
1143
|
+
puts
|
|
1144
|
+
puts pastel.dim('Commands:')
|
|
1145
|
+
puts pastel.dim(" aictl agent workspace #{agent_name} --path /workspace/<file> # View file")
|
|
1146
|
+
puts pastel.dim(" aictl agent workspace #{agent_name} --clean # Clear workspace")
|
|
1147
|
+
puts
|
|
1148
|
+
end
|
|
1149
|
+
|
|
1150
|
+
def view_workspace_file(ctx, agent_name, file_path)
|
|
1151
|
+
require 'pastel'
|
|
1152
|
+
pastel = Pastel.new
|
|
1153
|
+
|
|
1154
|
+
pod_name = get_agent_pod(ctx, agent_name)
|
|
1155
|
+
|
|
1156
|
+
# Check if file exists
|
|
1157
|
+
begin
|
|
1158
|
+
exec_in_pod(ctx, pod_name, "test -f #{file_path}")
|
|
1159
|
+
rescue StandardError
|
|
1160
|
+
Formatters::ProgressFormatter.error("File not found: #{file_path}")
|
|
1161
|
+
puts
|
|
1162
|
+
puts 'List available files with:'
|
|
1163
|
+
puts " aictl agent workspace #{agent_name}"
|
|
1164
|
+
exit 1
|
|
1165
|
+
end
|
|
1166
|
+
|
|
1167
|
+
# Get file metadata
|
|
1168
|
+
stat_output = exec_in_pod(
|
|
1169
|
+
ctx,
|
|
1170
|
+
pod_name,
|
|
1171
|
+
"stat -c '%s %Y' #{file_path}"
|
|
1172
|
+
)
|
|
1173
|
+
size, mtime = stat_output.strip.split
|
|
1174
|
+
|
|
1175
|
+
# Get file contents
|
|
1176
|
+
contents = exec_in_pod(
|
|
1177
|
+
ctx,
|
|
1178
|
+
pod_name,
|
|
1179
|
+
"cat #{file_path}"
|
|
1180
|
+
)
|
|
1181
|
+
|
|
1182
|
+
# Display file
|
|
1183
|
+
puts
|
|
1184
|
+
puts pastel.cyan("File: #{file_path}")
|
|
1185
|
+
puts "Size: #{format_file_size(size.to_i)}"
|
|
1186
|
+
puts "Modified: #{format_timestamp(Time.at(mtime.to_i))}"
|
|
1187
|
+
puts '=' * 60
|
|
1188
|
+
puts
|
|
1189
|
+
puts contents
|
|
1190
|
+
puts
|
|
1191
|
+
end
|
|
1192
|
+
|
|
1193
|
+
def clean_workspace(ctx, agent_name)
|
|
1194
|
+
require 'pastel'
|
|
1195
|
+
pastel = Pastel.new
|
|
1196
|
+
|
|
1197
|
+
pod_name = get_agent_pod(ctx, agent_name)
|
|
1198
|
+
|
|
1199
|
+
# Get current workspace usage
|
|
1200
|
+
usage_output = exec_in_pod(
|
|
1201
|
+
ctx,
|
|
1202
|
+
pod_name,
|
|
1203
|
+
'du -sh /workspace 2>/dev/null || echo "0\t/workspace"'
|
|
1204
|
+
)
|
|
1205
|
+
workspace_size = usage_output.split("\t").first.strip
|
|
1206
|
+
|
|
1207
|
+
# Count files
|
|
1208
|
+
file_count = exec_in_pod(
|
|
1209
|
+
ctx,
|
|
1210
|
+
pod_name,
|
|
1211
|
+
'find /workspace -type f | wc -l'
|
|
1212
|
+
).strip.to_i
|
|
1213
|
+
|
|
1214
|
+
puts
|
|
1215
|
+
puts pastel.yellow("This will delete ALL files in the workspace for '#{agent_name}'")
|
|
1216
|
+
puts
|
|
1217
|
+
puts 'The agent will lose:'
|
|
1218
|
+
puts ' • Execution history'
|
|
1219
|
+
puts ' • Cached data'
|
|
1220
|
+
puts ' • State information'
|
|
1221
|
+
puts
|
|
1222
|
+
puts "Current workspace: #{file_count} files, #{workspace_size}"
|
|
1223
|
+
puts
|
|
1224
|
+
|
|
1225
|
+
# Use UserPrompts helper
|
|
1226
|
+
return unless Helpers::UserPrompts.confirm('Are you sure?')
|
|
1227
|
+
|
|
1228
|
+
# Delete all files in workspace
|
|
1229
|
+
Formatters::ProgressFormatter.with_spinner('Cleaning workspace') do
|
|
1230
|
+
exec_in_pod(
|
|
1231
|
+
ctx,
|
|
1232
|
+
pod_name,
|
|
1233
|
+
'find /workspace -mindepth 1 -delete'
|
|
1234
|
+
)
|
|
1235
|
+
end
|
|
1236
|
+
|
|
1237
|
+
Formatters::ProgressFormatter.success("Workspace cleared (freed #{workspace_size})")
|
|
1238
|
+
puts
|
|
1239
|
+
puts 'The agent will start fresh on its next execution.'
|
|
1240
|
+
end
|
|
1241
|
+
|
|
1242
|
+
def format_file_size(bytes)
|
|
1243
|
+
Formatters::ValueFormatter.file_size(bytes)
|
|
1244
|
+
end
|
|
1245
|
+
|
|
1246
|
+
def format_timestamp(time)
|
|
1247
|
+
Formatters::ValueFormatter.timestamp(time)
|
|
1248
|
+
end
|
|
1249
|
+
end
|
|
1250
|
+
end
|
|
1251
|
+
end
|
|
1252
|
+
end
|