language-operator 0.1.31 → 0.1.35
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 +7 -8
- data/CHANGELOG.md +14 -0
- data/CI_STATUS.md +56 -0
- data/Gemfile.lock +2 -2
- data/Makefile +22 -6
- data/lib/language_operator/agent/base.rb +10 -6
- data/lib/language_operator/agent/executor.rb +19 -97
- data/lib/language_operator/agent/safety/ast_validator.rb +62 -43
- data/lib/language_operator/agent/safety/safe_executor.rb +27 -2
- data/lib/language_operator/agent/scheduler.rb +60 -0
- data/lib/language_operator/agent/task_executor.rb +548 -0
- data/lib/language_operator/agent.rb +90 -27
- data/lib/language_operator/cli/base_command.rb +117 -0
- data/lib/language_operator/cli/commands/agent.rb +339 -407
- data/lib/language_operator/cli/commands/cluster.rb +274 -290
- data/lib/language_operator/cli/commands/install.rb +110 -119
- data/lib/language_operator/cli/commands/model.rb +284 -184
- data/lib/language_operator/cli/commands/persona.rb +218 -284
- data/lib/language_operator/cli/commands/quickstart.rb +4 -5
- data/lib/language_operator/cli/commands/status.rb +31 -35
- data/lib/language_operator/cli/commands/system.rb +221 -233
- data/lib/language_operator/cli/commands/tool.rb +356 -422
- data/lib/language_operator/cli/commands/use.rb +19 -22
- data/lib/language_operator/cli/helpers/resource_dependency_checker.rb +0 -18
- data/lib/language_operator/cli/wizards/quickstart_wizard.rb +0 -1
- data/lib/language_operator/client/config.rb +20 -21
- data/lib/language_operator/config.rb +115 -3
- data/lib/language_operator/constants.rb +54 -0
- data/lib/language_operator/dsl/agent_context.rb +7 -7
- data/lib/language_operator/dsl/agent_definition.rb +111 -26
- data/lib/language_operator/dsl/config.rb +30 -66
- data/lib/language_operator/dsl/main_definition.rb +114 -0
- data/lib/language_operator/dsl/schema.rb +84 -43
- data/lib/language_operator/dsl/task_definition.rb +315 -0
- data/lib/language_operator/dsl.rb +0 -1
- data/lib/language_operator/instrumentation/task_tracer.rb +285 -0
- data/lib/language_operator/logger.rb +4 -4
- data/lib/language_operator/synthesis_test_harness.rb +324 -0
- data/lib/language_operator/templates/examples/agent_synthesis.tmpl +26 -8
- data/lib/language_operator/templates/schema/CHANGELOG.md +26 -0
- data/lib/language_operator/templates/schema/agent_dsl_openapi.yaml +1 -1
- data/lib/language_operator/templates/schema/agent_dsl_schema.json +84 -42
- data/lib/language_operator/type_coercion.rb +250 -0
- data/lib/language_operator/ux/base.rb +81 -0
- data/lib/language_operator/ux/concerns/README.md +155 -0
- data/lib/language_operator/ux/concerns/headings.rb +90 -0
- data/lib/language_operator/ux/concerns/input_validation.rb +146 -0
- data/lib/language_operator/ux/concerns/provider_helpers.rb +167 -0
- data/lib/language_operator/ux/create_agent.rb +252 -0
- data/lib/language_operator/ux/create_model.rb +267 -0
- data/lib/language_operator/ux/quickstart.rb +594 -0
- data/lib/language_operator/version.rb +1 -1
- data/lib/language_operator.rb +2 -0
- data/requirements/ARCHITECTURE.md +1 -0
- data/requirements/SCRATCH.md +153 -0
- data/requirements/dsl.md +0 -0
- data/requirements/features +1 -0
- data/requirements/personas +1 -0
- data/requirements/proposals +1 -0
- data/requirements/tasks/iterate.md +14 -15
- data/requirements/tasks/optimize.md +13 -4
- data/synth/001/Makefile +90 -0
- data/synth/001/agent.rb +26 -0
- data/synth/001/agent.yaml +7 -0
- data/synth/001/output.log +44 -0
- data/synth/Makefile +39 -0
- data/synth/README.md +342 -0
- metadata +37 -10
- data/lib/language_operator/dsl/workflow_definition.rb +0 -259
- data/test_agent_dsl.rb +0 -108
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'thor'
|
|
4
|
+
require_relative '../base_command'
|
|
4
5
|
require_relative '../formatters/progress_formatter'
|
|
5
6
|
require_relative '../formatters/table_formatter'
|
|
6
7
|
require_relative '../formatters/value_formatter'
|
|
@@ -15,12 +16,13 @@ require_relative '../errors/handler'
|
|
|
15
16
|
require_relative '../../config/cluster_config'
|
|
16
17
|
require_relative '../../kubernetes/client'
|
|
17
18
|
require_relative '../../kubernetes/resource_builder'
|
|
19
|
+
require_relative '../../ux/create_agent'
|
|
18
20
|
|
|
19
21
|
module LanguageOperator
|
|
20
22
|
module CLI
|
|
21
23
|
module Commands
|
|
22
24
|
# Agent management commands
|
|
23
|
-
class Agent <
|
|
25
|
+
class Agent < BaseCommand
|
|
24
26
|
include Helpers::ClusterValidator
|
|
25
27
|
include Helpers::PastelHelper
|
|
26
28
|
|
|
@@ -45,234 +47,216 @@ module LanguageOperator
|
|
|
45
47
|
option :dry_run, type: :boolean, default: false, desc: 'Preview what would be created without applying'
|
|
46
48
|
option :wizard, type: :boolean, default: false, desc: 'Use interactive wizard mode'
|
|
47
49
|
def create(description = nil)
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
50
|
+
handle_command_error('create agent') do
|
|
51
|
+
# Activate wizard mode if --wizard flag or no description provided
|
|
52
|
+
if options[:wizard] || description.nil?
|
|
53
|
+
description = Ux::CreateAgent.execute(ctx)
|
|
54
|
+
|
|
55
|
+
# User cancelled wizard
|
|
56
|
+
unless description
|
|
57
|
+
Formatters::ProgressFormatter.info('Agent creation cancelled')
|
|
58
|
+
return
|
|
59
|
+
end
|
|
58
60
|
end
|
|
59
|
-
end
|
|
60
61
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
62
|
+
# Handle --create-cluster flag
|
|
63
|
+
if options[:create_cluster]
|
|
64
|
+
cluster_name = options[:create_cluster]
|
|
65
|
+
unless Config::ClusterConfig.cluster_exists?(cluster_name)
|
|
66
|
+
Formatters::ProgressFormatter.info("Creating cluster '#{cluster_name}'...")
|
|
67
|
+
# Delegate to cluster create command
|
|
68
|
+
require_relative 'cluster'
|
|
69
|
+
Cluster.new.invoke(:create, [cluster_name], switch: true)
|
|
70
|
+
end
|
|
71
|
+
cluster = cluster_name
|
|
72
|
+
else
|
|
73
|
+
# Validate cluster selection (this will exit if none selected)
|
|
74
|
+
cluster = Helpers::ClusterValidator.get_cluster(options[:cluster])
|
|
69
75
|
end
|
|
70
|
-
cluster = cluster_name
|
|
71
|
-
else
|
|
72
|
-
# Validate cluster selection (this will exit if none selected)
|
|
73
|
-
cluster = Helpers::ClusterValidator.get_cluster(options[:cluster])
|
|
74
|
-
end
|
|
75
76
|
|
|
76
|
-
|
|
77
|
+
ctx = Helpers::ClusterContext.from_options(options.merge(cluster: cluster))
|
|
77
78
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
# Generate agent name from description if not provided
|
|
82
|
-
agent_name = options[:name] || generate_agent_name(description)
|
|
79
|
+
Formatters::ProgressFormatter.info("Creating agent in cluster '#{ctx.name}'")
|
|
80
|
+
puts
|
|
83
81
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
if models.nil? || models.empty?
|
|
87
|
-
available_models = ctx.client.list_resources('LanguageModel', namespace: ctx.namespace)
|
|
88
|
-
models = available_models.map { |m| m.dig('metadata', 'name') }
|
|
82
|
+
# Generate agent name from description if not provided
|
|
83
|
+
agent_name = options[:name] || generate_agent_name(description)
|
|
89
84
|
|
|
90
|
-
|
|
91
|
-
|
|
85
|
+
# Get models: use specified models, or default to all available models in cluster
|
|
86
|
+
models = options[:models]
|
|
87
|
+
if models.nil? || models.empty?
|
|
88
|
+
available_models = ctx.client.list_resources('LanguageModel', namespace: ctx.namespace)
|
|
89
|
+
models = available_models.map { |m| m.dig('metadata', 'name') }
|
|
92
90
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
agent_name,
|
|
96
|
-
instructions: description,
|
|
97
|
-
cluster: ctx.namespace,
|
|
98
|
-
persona: options[:persona],
|
|
99
|
-
tools: options[:tools] || [],
|
|
100
|
-
models: models
|
|
101
|
-
)
|
|
91
|
+
Errors::Handler.handle_no_models_available(cluster: ctx.name) if models.empty?
|
|
92
|
+
end
|
|
102
93
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
94
|
+
# Build LanguageAgent resource
|
|
95
|
+
agent_resource = Kubernetes::ResourceBuilder.language_agent(
|
|
96
|
+
agent_name,
|
|
97
|
+
instructions: description,
|
|
98
|
+
cluster: ctx.namespace,
|
|
99
|
+
persona: options[:persona],
|
|
100
|
+
tools: options[:tools] || [],
|
|
101
|
+
models: models
|
|
102
|
+
)
|
|
108
103
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
104
|
+
# Dry-run mode: preview without applying
|
|
105
|
+
if options[:dry_run]
|
|
106
|
+
display_dry_run_preview(agent_resource, ctx.name, description)
|
|
107
|
+
return
|
|
108
|
+
end
|
|
113
109
|
|
|
114
|
-
|
|
115
|
-
|
|
110
|
+
# Apply resource to cluster
|
|
111
|
+
Formatters::ProgressFormatter.with_spinner("Creating agent '#{agent_name}'") do
|
|
112
|
+
ctx.client.apply_resource(agent_resource)
|
|
113
|
+
end
|
|
116
114
|
|
|
117
|
-
|
|
118
|
-
|
|
115
|
+
# Watch synthesis status
|
|
116
|
+
synthesis_result = watch_synthesis_status(ctx.client, agent_name, ctx.namespace)
|
|
119
117
|
|
|
120
|
-
|
|
121
|
-
|
|
118
|
+
# Exit if synthesis failed
|
|
119
|
+
exit 1 unless synthesis_result[:success]
|
|
122
120
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
rescue StandardError => e
|
|
126
|
-
Formatters::ProgressFormatter.error("Failed to create agent: #{e.message}")
|
|
127
|
-
raise if ENV['DEBUG']
|
|
121
|
+
# Fetch the updated agent to get complete details
|
|
122
|
+
agent = ctx.client.get_resource('LanguageAgent', agent_name, ctx.namespace)
|
|
128
123
|
|
|
129
|
-
|
|
124
|
+
# Display enhanced success output
|
|
125
|
+
display_agent_created(agent, ctx.name, description, synthesis_result)
|
|
126
|
+
end
|
|
130
127
|
end
|
|
131
128
|
|
|
132
129
|
desc 'list', 'List all agents in current cluster'
|
|
133
130
|
option :cluster, type: :string, desc: 'Override current cluster context'
|
|
134
131
|
option :all_clusters, type: :boolean, default: false, desc: 'Show agents across all clusters'
|
|
135
132
|
def list
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
133
|
+
handle_command_error('list agents') do
|
|
134
|
+
if options[:all_clusters]
|
|
135
|
+
list_all_clusters
|
|
136
|
+
else
|
|
137
|
+
ctx = Helpers::ClusterContext.from_options(options)
|
|
138
|
+
list_cluster_agents(ctx.name)
|
|
139
|
+
end
|
|
141
140
|
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
141
|
end
|
|
148
142
|
|
|
149
143
|
desc 'inspect NAME', 'Show detailed agent information'
|
|
150
144
|
option :cluster, type: :string, desc: 'Override current cluster context'
|
|
151
145
|
def inspect(name)
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
agent = ctx.client.get_resource('LanguageAgent', name, ctx.namespace)
|
|
155
|
-
|
|
156
|
-
puts "Agent: #{name}"
|
|
157
|
-
puts " Cluster: #{ctx.name}"
|
|
158
|
-
puts " Namespace: #{ctx.namespace}"
|
|
159
|
-
puts
|
|
160
|
-
|
|
161
|
-
# Status
|
|
162
|
-
status = agent.dig('status', 'phase') || 'Unknown'
|
|
163
|
-
puts "Status: #{format_status(status)}"
|
|
164
|
-
puts
|
|
146
|
+
handle_command_error('inspect agent') do
|
|
147
|
+
ctx = Helpers::ClusterContext.from_options(options)
|
|
165
148
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
149
|
+
begin
|
|
150
|
+
agent = ctx.client.get_resource('LanguageAgent', name, ctx.namespace)
|
|
151
|
+
rescue K8s::Error::NotFound
|
|
152
|
+
handle_agent_not_found(name, ctx)
|
|
153
|
+
return
|
|
154
|
+
end
|
|
172
155
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
puts 'Instructions:'
|
|
177
|
-
puts " #{instructions}"
|
|
156
|
+
puts "Agent: #{name}"
|
|
157
|
+
puts " Cluster: #{ctx.name}"
|
|
158
|
+
puts " Namespace: #{ctx.namespace}"
|
|
178
159
|
puts
|
|
179
|
-
end
|
|
180
160
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
puts "Tools (#{tools.length}):"
|
|
185
|
-
tools.each { |tool| puts " - #{tool}" }
|
|
161
|
+
# Status
|
|
162
|
+
status = agent.dig('status', 'phase') || 'Unknown'
|
|
163
|
+
puts "Status: #{format_status(status)}"
|
|
186
164
|
puts
|
|
187
|
-
end
|
|
188
165
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
puts "
|
|
193
|
-
|
|
166
|
+
# Spec details
|
|
167
|
+
puts 'Configuration:'
|
|
168
|
+
puts " Mode: #{agent.dig('spec', 'mode') || 'autonomous'}"
|
|
169
|
+
puts " Schedule: #{agent.dig('spec', 'schedule') || 'N/A'}" if agent.dig('spec', 'schedule')
|
|
170
|
+
puts " Persona: #{agent.dig('spec', 'persona') || '(auto-selected)'}"
|
|
194
171
|
puts
|
|
195
|
-
end
|
|
196
172
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
puts " Duration: #{synthesis['duration']}" if synthesis['duration']
|
|
205
|
-
puts " Token Count: #{synthesis['tokenCount']}" if synthesis['tokenCount']
|
|
206
|
-
puts
|
|
207
|
-
end
|
|
173
|
+
# Instructions
|
|
174
|
+
instructions = agent.dig('spec', 'instructions')
|
|
175
|
+
if instructions
|
|
176
|
+
puts 'Instructions:'
|
|
177
|
+
puts " #{instructions}"
|
|
178
|
+
puts
|
|
179
|
+
end
|
|
208
180
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
181
|
+
# Tools
|
|
182
|
+
tools = agent.dig('spec', 'tools') || []
|
|
183
|
+
if tools.any?
|
|
184
|
+
puts "Tools (#{tools.length}):"
|
|
185
|
+
tools.each { |tool| puts " - #{tool}" }
|
|
186
|
+
puts
|
|
187
|
+
end
|
|
213
188
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
189
|
+
# Models
|
|
190
|
+
model_refs = agent.dig('spec', 'modelRefs') || []
|
|
191
|
+
if model_refs.any?
|
|
192
|
+
puts "Models (#{model_refs.length}):"
|
|
193
|
+
model_refs.each { |ref| puts " - #{ref['name']}" }
|
|
194
|
+
puts
|
|
195
|
+
end
|
|
219
196
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
puts " #{
|
|
197
|
+
# Synthesis info
|
|
198
|
+
synthesis = agent.dig('status', 'synthesis')
|
|
199
|
+
if synthesis
|
|
200
|
+
puts 'Synthesis:'
|
|
201
|
+
puts " Status: #{synthesis['status']}"
|
|
202
|
+
puts " Model: #{synthesis['model']}" if synthesis['model']
|
|
203
|
+
puts " Completed: #{synthesis['completedAt']}" if synthesis['completedAt']
|
|
204
|
+
puts " Duration: #{synthesis['duration']}" if synthesis['duration']
|
|
205
|
+
puts " Token Count: #{synthesis['tokenCount']}" if synthesis['tokenCount']
|
|
206
|
+
puts
|
|
227
207
|
end
|
|
208
|
+
|
|
209
|
+
# Execution stats
|
|
210
|
+
execution_count = agent.dig('status', 'executionCount') || 0
|
|
211
|
+
last_execution = agent.dig('status', 'lastExecution')
|
|
212
|
+
next_run = agent.dig('status', 'nextRun')
|
|
213
|
+
|
|
214
|
+
puts 'Execution:'
|
|
215
|
+
puts " Total Runs: #{execution_count}"
|
|
216
|
+
puts " Last Run: #{last_execution || 'Never'}"
|
|
217
|
+
puts " Next Run: #{next_run || 'N/A'}" if agent.dig('spec', 'schedule')
|
|
228
218
|
puts
|
|
229
|
-
end
|
|
230
219
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
220
|
+
# Conditions
|
|
221
|
+
conditions = agent.dig('status', 'conditions') || []
|
|
222
|
+
if conditions.any?
|
|
223
|
+
puts "Conditions (#{conditions.length}):"
|
|
224
|
+
conditions.each do |condition|
|
|
225
|
+
status_icon = condition['status'] == 'True' ? '✓' : '✗'
|
|
226
|
+
puts " #{status_icon} #{condition['type']}: #{condition['message'] || condition['reason']}"
|
|
227
|
+
end
|
|
228
|
+
puts
|
|
229
|
+
end
|
|
238
230
|
|
|
239
|
-
|
|
231
|
+
# Recent events (if available)
|
|
232
|
+
# This would require querying events, which we can add later
|
|
233
|
+
end
|
|
240
234
|
end
|
|
241
235
|
|
|
242
236
|
desc 'delete NAME', 'Delete an agent'
|
|
243
237
|
option :cluster, type: :string, desc: 'Override current cluster context'
|
|
244
238
|
option :force, type: :boolean, default: false, desc: 'Skip confirmation'
|
|
245
239
|
def delete(name)
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
# Get agent to show details before deletion
|
|
249
|
-
begin
|
|
250
|
-
agent = ctx.client.get_resource('LanguageAgent', name, ctx.namespace)
|
|
251
|
-
rescue K8s::Error::NotFound
|
|
252
|
-
Formatters::ProgressFormatter.error("Agent '#{name}' not found in cluster '#{ctx.name}'")
|
|
253
|
-
exit 1
|
|
254
|
-
end
|
|
240
|
+
handle_command_error('delete agent') do
|
|
241
|
+
ctx = Helpers::ClusterContext.from_options(options)
|
|
255
242
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
puts "This will delete agent '#{name}' from cluster '#{ctx.name}':"
|
|
259
|
-
puts " Instructions: #{agent.dig('spec', 'instructions')}"
|
|
260
|
-
puts " Mode: #{agent.dig('spec', 'mode') || 'autonomous'}"
|
|
261
|
-
puts
|
|
262
|
-
return unless Helpers::UserPrompts.confirm('Are you sure?')
|
|
263
|
-
end
|
|
243
|
+
# Get agent to show details before deletion
|
|
244
|
+
agent = get_resource_or_exit('LanguageAgent', name)
|
|
264
245
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
246
|
+
# Confirm deletion
|
|
247
|
+
details = {
|
|
248
|
+
'Instructions' => agent.dig('spec', 'instructions'),
|
|
249
|
+
'Mode' => agent.dig('spec', 'mode') || 'autonomous'
|
|
250
|
+
}
|
|
251
|
+
return unless confirm_deletion('agent', name, ctx.name, details: details, force: options[:force])
|
|
269
252
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
253
|
+
# Delete the agent
|
|
254
|
+
Formatters::ProgressFormatter.with_spinner("Deleting agent '#{name}'") do
|
|
255
|
+
ctx.client.delete_resource('LanguageAgent', name, ctx.namespace)
|
|
256
|
+
end
|
|
274
257
|
|
|
275
|
-
|
|
258
|
+
Formatters::ProgressFormatter.success("Agent '#{name}' deleted successfully")
|
|
259
|
+
end
|
|
276
260
|
end
|
|
277
261
|
|
|
278
262
|
desc 'logs NAME', 'Show agent execution logs'
|
|
@@ -289,261 +273,217 @@ module LanguageOperator
|
|
|
289
273
|
option :follow, type: :boolean, aliases: '-f', default: false, desc: 'Follow logs'
|
|
290
274
|
option :tail, type: :numeric, default: 100, desc: 'Number of lines to show from the end'
|
|
291
275
|
def logs(name)
|
|
292
|
-
|
|
276
|
+
handle_command_error('get logs') do
|
|
277
|
+
ctx = Helpers::ClusterContext.from_options(options)
|
|
293
278
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
agent = ctx.client.get_resource('LanguageAgent', name, ctx.namespace)
|
|
297
|
-
rescue K8s::Error::NotFound
|
|
298
|
-
Formatters::ProgressFormatter.error("Agent '#{name}' not found in cluster '#{ctx.name}'")
|
|
299
|
-
exit 1
|
|
300
|
-
end
|
|
279
|
+
# Get agent to determine the pod name
|
|
280
|
+
agent = get_resource_or_exit('LanguageAgent', name)
|
|
301
281
|
|
|
302
|
-
|
|
282
|
+
mode = agent.dig('spec', 'mode') || 'autonomous'
|
|
303
283
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
namespace_arg = "-n #{ctx.namespace}"
|
|
308
|
-
tail_arg = "--tail=#{options[:tail]}"
|
|
309
|
-
follow_arg = options[:follow] ? '-f' : ''
|
|
310
|
-
|
|
311
|
-
# For scheduled agents, logs come from CronJob pods
|
|
312
|
-
# For autonomous agents, logs come from Deployment pods
|
|
313
|
-
if mode == 'scheduled'
|
|
314
|
-
# Get most recent job from cronjob
|
|
315
|
-
else
|
|
316
|
-
# Get pod from deployment
|
|
317
|
-
end
|
|
318
|
-
label_selector = "app.kubernetes.io/name=#{name}"
|
|
284
|
+
# Build kubectl command for log streaming
|
|
285
|
+
tail_arg = "--tail=#{options[:tail]}"
|
|
286
|
+
follow_arg = options[:follow] ? '-f' : ''
|
|
319
287
|
|
|
320
|
-
|
|
321
|
-
|
|
288
|
+
# For scheduled agents, logs come from CronJob pods
|
|
289
|
+
# For autonomous agents, logs come from Deployment pods
|
|
290
|
+
if mode == 'scheduled'
|
|
291
|
+
# Get most recent job from cronjob
|
|
292
|
+
else
|
|
293
|
+
# Get pod from deployment
|
|
294
|
+
end
|
|
295
|
+
label_selector = "app.kubernetes.io/name=#{name}"
|
|
322
296
|
|
|
323
|
-
|
|
324
|
-
|
|
297
|
+
# Use kubectl logs with label selector
|
|
298
|
+
cmd = "#{ctx.kubectl_prefix} logs -l #{label_selector} #{tail_arg} #{follow_arg} --prefix --all-containers"
|
|
325
299
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
#
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
300
|
+
Formatters::ProgressFormatter.info("Streaming logs for agent '#{name}'...")
|
|
301
|
+
puts
|
|
302
|
+
|
|
303
|
+
# Stream and format logs in real-time
|
|
304
|
+
require 'open3'
|
|
305
|
+
Open3.popen3(cmd) do |_stdin, stdout, stderr, wait_thr|
|
|
306
|
+
# Handle stdout (logs)
|
|
307
|
+
stdout_thread = Thread.new do
|
|
308
|
+
stdout.each_line do |line|
|
|
309
|
+
puts Formatters::LogFormatter.format_line(line.chomp)
|
|
310
|
+
$stdout.flush
|
|
311
|
+
end
|
|
334
312
|
end
|
|
335
|
-
end
|
|
336
313
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
314
|
+
# Handle stderr (errors)
|
|
315
|
+
stderr_thread = Thread.new do
|
|
316
|
+
stderr.each_line do |line|
|
|
317
|
+
warn line
|
|
318
|
+
end
|
|
341
319
|
end
|
|
342
|
-
end
|
|
343
320
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
321
|
+
# Wait for both streams to complete
|
|
322
|
+
stdout_thread.join
|
|
323
|
+
stderr_thread.join
|
|
347
324
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
325
|
+
# Check exit status
|
|
326
|
+
exit_status = wait_thr.value
|
|
327
|
+
exit exit_status.exitstatus unless exit_status.success?
|
|
328
|
+
end
|
|
351
329
|
end
|
|
352
|
-
rescue StandardError => e
|
|
353
|
-
Formatters::ProgressFormatter.error("Failed to get logs: #{e.message}")
|
|
354
|
-
raise if ENV['DEBUG']
|
|
355
|
-
|
|
356
|
-
exit 1
|
|
357
330
|
end
|
|
358
331
|
|
|
359
332
|
desc 'code NAME', 'Display synthesized agent code'
|
|
360
333
|
option :cluster, type: :string, desc: 'Override current cluster context'
|
|
361
334
|
def code(name)
|
|
362
|
-
|
|
335
|
+
handle_command_error('get code') do
|
|
336
|
+
require_relative '../formatters/code_formatter'
|
|
363
337
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
# Get the code ConfigMap for this agent
|
|
367
|
-
configmap_name = "#{name}-code"
|
|
368
|
-
begin
|
|
369
|
-
configmap = ctx.client.get_resource('ConfigMap', configmap_name, ctx.namespace)
|
|
370
|
-
rescue K8s::Error::NotFound
|
|
371
|
-
Formatters::ProgressFormatter.error("Synthesized code not found for agent '#{name}'")
|
|
372
|
-
puts
|
|
373
|
-
puts 'Possible reasons:'
|
|
374
|
-
puts ' - Agent synthesis not yet complete'
|
|
375
|
-
puts ' - Agent synthesis failed'
|
|
376
|
-
puts
|
|
377
|
-
puts 'Check synthesis status with:'
|
|
378
|
-
puts " aictl agent inspect #{name}"
|
|
379
|
-
exit 1
|
|
380
|
-
end
|
|
338
|
+
ctx = Helpers::ClusterContext.from_options(options)
|
|
381
339
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
340
|
+
# Get the code ConfigMap for this agent
|
|
341
|
+
configmap_name = "#{name}-code"
|
|
342
|
+
begin
|
|
343
|
+
configmap = ctx.client.get_resource('ConfigMap', configmap_name, ctx.namespace)
|
|
344
|
+
rescue K8s::Error::NotFound
|
|
345
|
+
Formatters::ProgressFormatter.error("Synthesized code not found for agent '#{name}'")
|
|
346
|
+
puts
|
|
347
|
+
puts 'Possible reasons:'
|
|
348
|
+
puts ' - Agent synthesis not yet complete'
|
|
349
|
+
puts ' - Agent synthesis failed'
|
|
350
|
+
puts
|
|
351
|
+
puts 'Check synthesis status with:'
|
|
352
|
+
puts " aictl agent inspect #{name}"
|
|
353
|
+
exit 1
|
|
354
|
+
end
|
|
388
355
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
code_content
|
|
392
|
-
|
|
393
|
-
|
|
356
|
+
# Get the agent.rb code from the ConfigMap
|
|
357
|
+
code_content = configmap.dig('data', 'agent.rb')
|
|
358
|
+
unless code_content
|
|
359
|
+
Formatters::ProgressFormatter.error('Code content not found in ConfigMap')
|
|
360
|
+
exit 1
|
|
361
|
+
end
|
|
394
362
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
raise if ENV['DEBUG']
|
|
363
|
+
# Display with syntax highlighting
|
|
364
|
+
Formatters::CodeFormatter.display_ruby_code(
|
|
365
|
+
code_content,
|
|
366
|
+
title: "Synthesized Code for Agent: #{name}"
|
|
367
|
+
)
|
|
401
368
|
|
|
402
|
-
|
|
369
|
+
puts
|
|
370
|
+
puts 'This code was automatically synthesized from the agent instructions.'
|
|
371
|
+
puts "View full agent details with: aictl agent inspect #{name}"
|
|
372
|
+
end
|
|
403
373
|
end
|
|
404
374
|
|
|
405
375
|
desc 'edit NAME', 'Edit agent instructions'
|
|
406
376
|
option :cluster, type: :string, desc: 'Override current cluster context'
|
|
407
377
|
def edit(name)
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
# Get current agent
|
|
411
|
-
begin
|
|
412
|
-
agent = ctx.client.get_resource('LanguageAgent', name, ctx.namespace)
|
|
413
|
-
rescue K8s::Error::NotFound
|
|
414
|
-
Formatters::ProgressFormatter.error("Agent '#{name}' not found in cluster '#{ctx.name}'")
|
|
415
|
-
exit 1
|
|
416
|
-
end
|
|
378
|
+
handle_command_error('edit agent') do
|
|
379
|
+
ctx = Helpers::ClusterContext.from_options(options)
|
|
417
380
|
|
|
418
|
-
|
|
381
|
+
# Get current agent
|
|
382
|
+
agent = get_resource_or_exit('LanguageAgent', name)
|
|
419
383
|
|
|
420
|
-
|
|
421
|
-
new_instructions = Helpers::EditorHelper.edit_content(
|
|
422
|
-
current_instructions,
|
|
423
|
-
'agent-instructions-',
|
|
424
|
-
'.txt'
|
|
425
|
-
).strip
|
|
384
|
+
current_instructions = agent.dig('spec', 'instructions')
|
|
426
385
|
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
386
|
+
# Edit instructions in user's editor
|
|
387
|
+
new_instructions = Helpers::EditorHelper.edit_content(
|
|
388
|
+
current_instructions,
|
|
389
|
+
'agent-instructions-',
|
|
390
|
+
'.txt'
|
|
391
|
+
).strip
|
|
432
392
|
|
|
433
|
-
|
|
434
|
-
|
|
393
|
+
# Check if changed
|
|
394
|
+
if new_instructions == current_instructions
|
|
395
|
+
Formatters::ProgressFormatter.info('No changes made')
|
|
396
|
+
return
|
|
397
|
+
end
|
|
435
398
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
end
|
|
399
|
+
# Update agent resource
|
|
400
|
+
agent['spec']['instructions'] = new_instructions
|
|
439
401
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
puts
|
|
444
|
-
puts 'Watch synthesis progress with:'
|
|
445
|
-
puts " aictl agent inspect #{name}"
|
|
446
|
-
rescue StandardError => e
|
|
447
|
-
Formatters::ProgressFormatter.error("Failed to edit agent: #{e.message}")
|
|
448
|
-
raise if ENV['DEBUG']
|
|
402
|
+
Formatters::ProgressFormatter.with_spinner('Updating agent instructions') do
|
|
403
|
+
ctx.client.apply_resource(agent)
|
|
404
|
+
end
|
|
449
405
|
|
|
450
|
-
|
|
406
|
+
Formatters::ProgressFormatter.success('Agent instructions updated')
|
|
407
|
+
puts
|
|
408
|
+
puts 'The operator will automatically re-synthesize the agent code.'
|
|
409
|
+
puts
|
|
410
|
+
puts 'Watch synthesis progress with:'
|
|
411
|
+
puts " aictl agent inspect #{name}"
|
|
412
|
+
end
|
|
451
413
|
end
|
|
452
414
|
|
|
453
415
|
desc 'pause NAME', 'Pause scheduled agent execution'
|
|
454
416
|
option :cluster, type: :string, desc: 'Override current cluster context'
|
|
455
417
|
def pause(name)
|
|
456
|
-
|
|
418
|
+
handle_command_error('pause agent') do
|
|
419
|
+
ctx = Helpers::ClusterContext.from_options(options)
|
|
457
420
|
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
agent = ctx.client.get_resource('LanguageAgent', name, ctx.namespace)
|
|
461
|
-
rescue K8s::Error::NotFound
|
|
462
|
-
Formatters::ProgressFormatter.error("Agent '#{name}' not found in cluster '#{ctx.name}'")
|
|
463
|
-
exit 1
|
|
464
|
-
end
|
|
421
|
+
# Get agent
|
|
422
|
+
agent = get_resource_or_exit('LanguageAgent', name)
|
|
465
423
|
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
424
|
+
mode = agent.dig('spec', 'mode') || 'autonomous'
|
|
425
|
+
unless mode == 'scheduled'
|
|
426
|
+
Formatters::ProgressFormatter.warn("Agent '#{name}' is not a scheduled agent (mode: #{mode})")
|
|
427
|
+
puts
|
|
428
|
+
puts 'Only scheduled agents can be paused.'
|
|
429
|
+
puts 'Autonomous agents can be stopped by deleting them.'
|
|
430
|
+
exit 1
|
|
431
|
+
end
|
|
474
432
|
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
433
|
+
# Suspend the CronJob by setting spec.suspend = true
|
|
434
|
+
# This is done by patching the underlying CronJob resource
|
|
435
|
+
cronjob_name = name
|
|
436
|
+
ctx.namespace
|
|
479
437
|
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
438
|
+
Formatters::ProgressFormatter.with_spinner("Pausing agent '#{name}'") do
|
|
439
|
+
# Use kubectl to patch the cronjob
|
|
440
|
+
cmd = "#{ctx.kubectl_prefix} patch cronjob #{cronjob_name} -p '{\"spec\":{\"suspend\":true}}'"
|
|
441
|
+
system(cmd)
|
|
442
|
+
end
|
|
484
443
|
|
|
485
|
-
|
|
486
|
-
|
|
444
|
+
Formatters::ProgressFormatter.success("Agent '#{name}' paused")
|
|
445
|
+
puts
|
|
446
|
+
puts 'The agent will not execute on its schedule until resumed.'
|
|
447
|
+
puts
|
|
448
|
+
puts 'Resume with:'
|
|
449
|
+
puts " aictl agent resume #{name}"
|
|
487
450
|
end
|
|
488
|
-
|
|
489
|
-
Formatters::ProgressFormatter.success("Agent '#{name}' paused")
|
|
490
|
-
puts
|
|
491
|
-
puts 'The agent will not execute on its schedule until resumed.'
|
|
492
|
-
puts
|
|
493
|
-
puts 'Resume with:'
|
|
494
|
-
puts " aictl agent resume #{name}"
|
|
495
|
-
rescue StandardError => e
|
|
496
|
-
Formatters::ProgressFormatter.error("Failed to pause agent: #{e.message}")
|
|
497
|
-
raise if ENV['DEBUG']
|
|
498
|
-
|
|
499
|
-
exit 1
|
|
500
451
|
end
|
|
501
452
|
|
|
502
453
|
desc 'resume NAME', 'Resume paused agent'
|
|
503
454
|
option :cluster, type: :string, desc: 'Override current cluster context'
|
|
504
455
|
def resume(name)
|
|
505
|
-
|
|
456
|
+
handle_command_error('resume agent') do
|
|
457
|
+
ctx = Helpers::ClusterContext.from_options(options)
|
|
506
458
|
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
agent = ctx.client.get_resource('LanguageAgent', name, ctx.namespace)
|
|
510
|
-
rescue K8s::Error::NotFound
|
|
511
|
-
Formatters::ProgressFormatter.error("Agent '#{name}' not found in cluster '#{ctx.name}'")
|
|
512
|
-
exit 1
|
|
513
|
-
end
|
|
459
|
+
# Get agent
|
|
460
|
+
agent = get_resource_or_exit('LanguageAgent', name)
|
|
514
461
|
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
462
|
+
mode = agent.dig('spec', 'mode') || 'autonomous'
|
|
463
|
+
unless mode == 'scheduled'
|
|
464
|
+
Formatters::ProgressFormatter.warn("Agent '#{name}' is not a scheduled agent (mode: #{mode})")
|
|
465
|
+
puts
|
|
466
|
+
puts 'Only scheduled agents can be resumed.'
|
|
467
|
+
exit 1
|
|
468
|
+
end
|
|
522
469
|
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
470
|
+
# Resume the CronJob by setting spec.suspend = false
|
|
471
|
+
cronjob_name = name
|
|
472
|
+
ctx.namespace
|
|
526
473
|
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
474
|
+
Formatters::ProgressFormatter.with_spinner("Resuming agent '#{name}'") do
|
|
475
|
+
# Use kubectl to patch the cronjob
|
|
476
|
+
cmd = "#{ctx.kubectl_prefix} patch cronjob #{cronjob_name} -p '{\"spec\":{\"suspend\":false}}'"
|
|
477
|
+
system(cmd)
|
|
478
|
+
end
|
|
531
479
|
|
|
532
|
-
|
|
533
|
-
|
|
480
|
+
Formatters::ProgressFormatter.success("Agent '#{name}' resumed")
|
|
481
|
+
puts
|
|
482
|
+
puts 'The agent will now execute according to its schedule.'
|
|
483
|
+
puts
|
|
484
|
+
puts 'View next execution time with:'
|
|
485
|
+
puts " aictl agent inspect #{name}"
|
|
534
486
|
end
|
|
535
|
-
|
|
536
|
-
Formatters::ProgressFormatter.success("Agent '#{name}' resumed")
|
|
537
|
-
puts
|
|
538
|
-
puts 'The agent will now execute according to its schedule.'
|
|
539
|
-
puts
|
|
540
|
-
puts 'View next execution time with:'
|
|
541
|
-
puts " aictl agent inspect #{name}"
|
|
542
|
-
rescue StandardError => e
|
|
543
|
-
Formatters::ProgressFormatter.error("Failed to resume agent: #{e.message}")
|
|
544
|
-
raise if ENV['DEBUG']
|
|
545
|
-
|
|
546
|
-
exit 1
|
|
547
487
|
end
|
|
548
488
|
|
|
549
489
|
desc 'workspace NAME', 'Browse agent workspace files'
|
|
@@ -562,41 +502,33 @@ module LanguageOperator
|
|
|
562
502
|
option :path, type: :string, desc: 'View specific file contents'
|
|
563
503
|
option :clean, type: :boolean, desc: 'Clear workspace (with confirmation)'
|
|
564
504
|
def workspace(name)
|
|
565
|
-
|
|
505
|
+
handle_command_error('access workspace') do
|
|
506
|
+
ctx = Helpers::ClusterContext.from_options(options)
|
|
566
507
|
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
agent = ctx.client.get_resource('LanguageAgent', name, ctx.namespace)
|
|
570
|
-
rescue K8s::Error::NotFound
|
|
571
|
-
Formatters::ProgressFormatter.error("Agent '#{name}' not found in cluster '#{ctx.name}'")
|
|
572
|
-
exit 1
|
|
573
|
-
end
|
|
508
|
+
# Get agent to verify it exists
|
|
509
|
+
agent = get_resource_or_exit('LanguageAgent', name)
|
|
574
510
|
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
511
|
+
# Check if workspace is enabled
|
|
512
|
+
workspace_enabled = agent.dig('spec', 'workspace', 'enabled')
|
|
513
|
+
unless workspace_enabled
|
|
514
|
+
Formatters::ProgressFormatter.warn("Workspace is not enabled for agent '#{name}'")
|
|
515
|
+
puts
|
|
516
|
+
puts 'Enable workspace in agent configuration:'
|
|
517
|
+
puts ' spec:'
|
|
518
|
+
puts ' workspace:'
|
|
519
|
+
puts ' enabled: true'
|
|
520
|
+
puts ' size: 10Gi'
|
|
521
|
+
exit 1
|
|
522
|
+
end
|
|
587
523
|
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
524
|
+
if options[:path]
|
|
525
|
+
view_workspace_file(ctx, name, options[:path])
|
|
526
|
+
elsif options[:clean]
|
|
527
|
+
clean_workspace(ctx, name)
|
|
528
|
+
else
|
|
529
|
+
list_workspace_files(ctx, name)
|
|
530
|
+
end
|
|
594
531
|
end
|
|
595
|
-
rescue StandardError => e
|
|
596
|
-
Formatters::ProgressFormatter.error("Failed to access workspace: #{e.message}")
|
|
597
|
-
raise if ENV['DEBUG']
|
|
598
|
-
|
|
599
|
-
exit 1
|
|
600
532
|
end
|
|
601
533
|
|
|
602
534
|
private
|