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
|
@@ -1,1712 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'thor'
|
|
4
|
-
require_relative '../base_command'
|
|
5
|
-
require_relative '../formatters/progress_formatter'
|
|
6
|
-
require_relative '../formatters/table_formatter'
|
|
7
|
-
require_relative '../formatters/value_formatter'
|
|
8
|
-
require_relative '../formatters/log_formatter'
|
|
9
|
-
require_relative '../formatters/status_formatter'
|
|
10
|
-
require_relative '../helpers/cluster_validator'
|
|
11
|
-
require_relative '../helpers/cluster_context'
|
|
12
|
-
require_relative '../helpers/user_prompts'
|
|
13
|
-
require_relative '../helpers/editor_helper'
|
|
14
|
-
require_relative '../helpers/pastel_helper'
|
|
15
|
-
require_relative '../errors/handler'
|
|
16
|
-
require_relative '../../config/cluster_config'
|
|
17
|
-
require_relative '../../kubernetes/client'
|
|
18
|
-
require_relative '../../kubernetes/resource_builder'
|
|
19
|
-
require_relative '../../ux/create_agent'
|
|
20
|
-
|
|
21
|
-
module LanguageOperator
|
|
22
|
-
module CLI
|
|
23
|
-
module Commands
|
|
24
|
-
# Agent management commands
|
|
25
|
-
class Agent < BaseCommand
|
|
26
|
-
include Helpers::ClusterValidator
|
|
27
|
-
include Helpers::PastelHelper
|
|
28
|
-
|
|
29
|
-
desc 'create [DESCRIPTION]', 'Create a new agent with natural language description'
|
|
30
|
-
long_desc <<-DESC
|
|
31
|
-
Create a new autonomous agent by describing what you want it to do in natural language.
|
|
32
|
-
|
|
33
|
-
The operator will synthesize the agent from your description and deploy it to your cluster.
|
|
34
|
-
|
|
35
|
-
Examples:
|
|
36
|
-
aictl agent create "review my spreadsheet at 4pm daily and email me any errors"
|
|
37
|
-
aictl agent create "summarize Hacker News top stories every morning at 8am"
|
|
38
|
-
aictl agent create "monitor my website uptime and alert me if it goes down"
|
|
39
|
-
aictl agent create --wizard # Interactive wizard mode
|
|
40
|
-
DESC
|
|
41
|
-
option :cluster, type: :string, desc: 'Override current cluster context'
|
|
42
|
-
option :create_cluster, type: :string, desc: 'Create cluster if it doesn\'t exist'
|
|
43
|
-
option :name, type: :string, desc: 'Agent name (generated from description if not provided)'
|
|
44
|
-
option :persona, type: :string, desc: 'Persona to use for the agent'
|
|
45
|
-
option :tools, type: :array, desc: 'Tools to make available to the agent'
|
|
46
|
-
option :models, type: :array, desc: 'Models to make available to the agent'
|
|
47
|
-
option :workspace, type: :boolean, default: true, desc: 'Enable workspace for state persistence'
|
|
48
|
-
option :dry_run, type: :boolean, default: false, desc: 'Preview what would be created without applying'
|
|
49
|
-
option :wizard, type: :boolean, default: false, desc: 'Use interactive wizard mode'
|
|
50
|
-
def create(description = nil)
|
|
51
|
-
handle_command_error('create agent') do
|
|
52
|
-
# Read from stdin if available and no description provided
|
|
53
|
-
description = $stdin.read.strip if description.nil? && !$stdin.tty?
|
|
54
|
-
|
|
55
|
-
# Activate wizard mode if --wizard flag or no description provided
|
|
56
|
-
if options[:wizard] || description.nil? || description.empty?
|
|
57
|
-
description = Ux::CreateAgent.execute(ctx)
|
|
58
|
-
|
|
59
|
-
# User cancelled wizard
|
|
60
|
-
unless description
|
|
61
|
-
Formatters::ProgressFormatter.info('Agent creation cancelled')
|
|
62
|
-
return
|
|
63
|
-
end
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
# Handle --create-cluster flag
|
|
67
|
-
if options[:create_cluster]
|
|
68
|
-
cluster_name = options[:create_cluster]
|
|
69
|
-
unless Config::ClusterConfig.cluster_exists?(cluster_name)
|
|
70
|
-
Formatters::ProgressFormatter.info("Creating cluster '#{cluster_name}'...")
|
|
71
|
-
# Delegate to cluster create command
|
|
72
|
-
require_relative 'cluster'
|
|
73
|
-
Cluster.new.invoke(:create, [cluster_name], switch: true)
|
|
74
|
-
end
|
|
75
|
-
cluster = cluster_name
|
|
76
|
-
else
|
|
77
|
-
# Validate cluster selection (this will exit if none selected)
|
|
78
|
-
cluster = Helpers::ClusterValidator.get_cluster(options[:cluster])
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
ctx = Helpers::ClusterContext.from_options(options.merge(cluster: cluster))
|
|
82
|
-
|
|
83
|
-
# Generate agent name from description if not provided
|
|
84
|
-
agent_name = options[:name] || generate_agent_name(description)
|
|
85
|
-
|
|
86
|
-
# Get models: use specified models, or default to all available models in cluster
|
|
87
|
-
models = options[:models]
|
|
88
|
-
if models.nil? || models.empty?
|
|
89
|
-
available_models = ctx.client.list_resources('LanguageModel', namespace: ctx.namespace)
|
|
90
|
-
models = available_models.map { |m| m.dig('metadata', 'name') }
|
|
91
|
-
|
|
92
|
-
Errors::Handler.handle_no_models_available(cluster: ctx.name) if models.empty?
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
# Build LanguageAgent resource
|
|
96
|
-
agent_resource = Kubernetes::ResourceBuilder.language_agent(
|
|
97
|
-
agent_name,
|
|
98
|
-
instructions: description,
|
|
99
|
-
cluster: ctx.namespace,
|
|
100
|
-
persona: options[:persona],
|
|
101
|
-
tools: options[:tools] || [],
|
|
102
|
-
models: models,
|
|
103
|
-
workspace: options[:workspace]
|
|
104
|
-
)
|
|
105
|
-
|
|
106
|
-
# Dry-run mode: preview without applying
|
|
107
|
-
if options[:dry_run]
|
|
108
|
-
display_dry_run_preview(agent_resource, ctx.name, description)
|
|
109
|
-
return
|
|
110
|
-
end
|
|
111
|
-
|
|
112
|
-
# Apply resource to cluster
|
|
113
|
-
Formatters::ProgressFormatter.with_spinner("Creating agent '#{agent_name}'") do
|
|
114
|
-
ctx.client.apply_resource(agent_resource)
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
# Watch synthesis status
|
|
118
|
-
synthesis_result = watch_synthesis_status(ctx.client, agent_name, ctx.namespace)
|
|
119
|
-
|
|
120
|
-
# Exit if synthesis failed
|
|
121
|
-
exit 1 unless synthesis_result[:success]
|
|
122
|
-
|
|
123
|
-
# Fetch the updated agent to get complete details
|
|
124
|
-
agent = ctx.client.get_resource('LanguageAgent', agent_name, ctx.namespace)
|
|
125
|
-
|
|
126
|
-
# Display enhanced success output
|
|
127
|
-
display_agent_created(agent, ctx.name, description, synthesis_result)
|
|
128
|
-
end
|
|
129
|
-
end
|
|
130
|
-
|
|
131
|
-
desc 'list', 'List all agents in current cluster'
|
|
132
|
-
option :cluster, type: :string, desc: 'Override current cluster context'
|
|
133
|
-
option :all_clusters, type: :boolean, default: false, desc: 'Show agents across all clusters'
|
|
134
|
-
def list
|
|
135
|
-
handle_command_error('list agents') do
|
|
136
|
-
if options[:all_clusters]
|
|
137
|
-
list_all_clusters
|
|
138
|
-
else
|
|
139
|
-
ctx = Helpers::ClusterContext.from_options(options)
|
|
140
|
-
list_cluster_agents(ctx.name)
|
|
141
|
-
end
|
|
142
|
-
end
|
|
143
|
-
end
|
|
144
|
-
|
|
145
|
-
desc 'inspect NAME', 'Show detailed agent information'
|
|
146
|
-
option :cluster, type: :string, desc: 'Override current cluster context'
|
|
147
|
-
def inspect(name)
|
|
148
|
-
handle_command_error('inspect agent') do
|
|
149
|
-
ctx = Helpers::ClusterContext.from_options(options)
|
|
150
|
-
|
|
151
|
-
begin
|
|
152
|
-
agent = ctx.client.get_resource('LanguageAgent', name, ctx.namespace)
|
|
153
|
-
rescue K8s::Error::NotFound
|
|
154
|
-
handle_agent_not_found(name, ctx)
|
|
155
|
-
return
|
|
156
|
-
end
|
|
157
|
-
|
|
158
|
-
puts "Agent: #{name}"
|
|
159
|
-
puts " Cluster: #{ctx.name}"
|
|
160
|
-
puts " Namespace: #{ctx.namespace}"
|
|
161
|
-
puts
|
|
162
|
-
|
|
163
|
-
# Status
|
|
164
|
-
status = agent.dig('status', 'phase') || 'Unknown'
|
|
165
|
-
puts "Status: #{format_status(status)}"
|
|
166
|
-
puts
|
|
167
|
-
|
|
168
|
-
# Spec details
|
|
169
|
-
puts 'Configuration:'
|
|
170
|
-
puts " Mode: #{agent.dig('spec', 'mode') || 'autonomous'}"
|
|
171
|
-
puts " Schedule: #{agent.dig('spec', 'schedule') || 'N/A'}" if agent.dig('spec', 'schedule')
|
|
172
|
-
puts " Persona: #{agent.dig('spec', 'persona') || '(auto-selected)'}"
|
|
173
|
-
puts
|
|
174
|
-
|
|
175
|
-
# Instructions
|
|
176
|
-
instructions = agent.dig('spec', 'instructions')
|
|
177
|
-
if instructions
|
|
178
|
-
puts 'Instructions:'
|
|
179
|
-
puts " #{instructions}"
|
|
180
|
-
puts
|
|
181
|
-
end
|
|
182
|
-
|
|
183
|
-
# Tools
|
|
184
|
-
tools = agent.dig('spec', 'tools') || []
|
|
185
|
-
if tools.any?
|
|
186
|
-
puts "Tools (#{tools.length}):"
|
|
187
|
-
tools.each { |tool| puts " - #{tool}" }
|
|
188
|
-
puts
|
|
189
|
-
end
|
|
190
|
-
|
|
191
|
-
# Models
|
|
192
|
-
model_refs = agent.dig('spec', 'modelRefs') || []
|
|
193
|
-
if model_refs.any?
|
|
194
|
-
puts "Models (#{model_refs.length}):"
|
|
195
|
-
model_refs.each { |ref| puts " - #{ref['name']}" }
|
|
196
|
-
puts
|
|
197
|
-
end
|
|
198
|
-
|
|
199
|
-
# Synthesis info
|
|
200
|
-
synthesis = agent.dig('status', 'synthesis')
|
|
201
|
-
if synthesis
|
|
202
|
-
puts 'Synthesis:'
|
|
203
|
-
puts " Status: #{synthesis['status']}"
|
|
204
|
-
puts " Model: #{synthesis['model']}" if synthesis['model']
|
|
205
|
-
puts " Completed: #{synthesis['completedAt']}" if synthesis['completedAt']
|
|
206
|
-
puts " Duration: #{synthesis['duration']}" if synthesis['duration']
|
|
207
|
-
puts " Token Count: #{synthesis['tokenCount']}" if synthesis['tokenCount']
|
|
208
|
-
puts
|
|
209
|
-
end
|
|
210
|
-
|
|
211
|
-
# Execution stats
|
|
212
|
-
execution_count = agent.dig('status', 'executionCount') || 0
|
|
213
|
-
last_execution = agent.dig('status', 'lastExecution')
|
|
214
|
-
next_run = agent.dig('status', 'nextRun')
|
|
215
|
-
|
|
216
|
-
puts 'Execution:'
|
|
217
|
-
puts " Total Runs: #{execution_count}"
|
|
218
|
-
puts " Last Run: #{last_execution || 'Never'}"
|
|
219
|
-
puts " Next Run: #{next_run || 'N/A'}" if agent.dig('spec', 'schedule')
|
|
220
|
-
puts
|
|
221
|
-
|
|
222
|
-
# Conditions
|
|
223
|
-
conditions = agent.dig('status', 'conditions') || []
|
|
224
|
-
if conditions.any?
|
|
225
|
-
puts "Conditions (#{conditions.length}):"
|
|
226
|
-
conditions.each do |condition|
|
|
227
|
-
status_icon = condition['status'] == 'True' ? '✓' : '✗'
|
|
228
|
-
puts " #{status_icon} #{condition['type']}: #{condition['message'] || condition['reason']}"
|
|
229
|
-
end
|
|
230
|
-
puts
|
|
231
|
-
end
|
|
232
|
-
|
|
233
|
-
# Recent events (if available)
|
|
234
|
-
# This would require querying events, which we can add later
|
|
235
|
-
end
|
|
236
|
-
end
|
|
237
|
-
|
|
238
|
-
desc 'delete NAME', 'Delete an agent'
|
|
239
|
-
option :cluster, type: :string, desc: 'Override current cluster context'
|
|
240
|
-
option :force, type: :boolean, default: false, desc: 'Skip confirmation'
|
|
241
|
-
def delete(name)
|
|
242
|
-
handle_command_error('delete agent') do
|
|
243
|
-
ctx = Helpers::ClusterContext.from_options(options)
|
|
244
|
-
|
|
245
|
-
# Get agent to show details before deletion
|
|
246
|
-
agent = get_resource_or_exit('LanguageAgent', name)
|
|
247
|
-
|
|
248
|
-
# Confirm deletion
|
|
249
|
-
details = {
|
|
250
|
-
'Instructions' => agent.dig('spec', 'instructions'),
|
|
251
|
-
'Mode' => agent.dig('spec', 'mode') || 'autonomous'
|
|
252
|
-
}
|
|
253
|
-
return unless confirm_deletion('agent', name, ctx.name, details: details, force: options[:force])
|
|
254
|
-
|
|
255
|
-
# Delete the agent
|
|
256
|
-
Formatters::ProgressFormatter.with_spinner("Deleting agent '#{name}'") do
|
|
257
|
-
ctx.client.delete_resource('LanguageAgent', name, ctx.namespace)
|
|
258
|
-
end
|
|
259
|
-
|
|
260
|
-
Formatters::ProgressFormatter.success("Agent '#{name}' deleted successfully")
|
|
261
|
-
end
|
|
262
|
-
end
|
|
263
|
-
|
|
264
|
-
desc 'logs NAME', 'Show agent execution logs'
|
|
265
|
-
long_desc <<-DESC
|
|
266
|
-
Stream agent execution logs in real-time.
|
|
267
|
-
|
|
268
|
-
Use -f to follow logs continuously (like tail -f).
|
|
269
|
-
|
|
270
|
-
Examples:
|
|
271
|
-
aictl agent logs my-agent
|
|
272
|
-
aictl agent logs my-agent -f
|
|
273
|
-
DESC
|
|
274
|
-
option :cluster, type: :string, desc: 'Override current cluster context'
|
|
275
|
-
option :follow, type: :boolean, aliases: '-f', default: false, desc: 'Follow logs'
|
|
276
|
-
option :tail, type: :numeric, default: 100, desc: 'Number of lines to show from the end'
|
|
277
|
-
def logs(name)
|
|
278
|
-
handle_command_error('get logs') do
|
|
279
|
-
ctx = Helpers::ClusterContext.from_options(options)
|
|
280
|
-
|
|
281
|
-
# Get agent to determine the pod name
|
|
282
|
-
agent = get_resource_or_exit('LanguageAgent', name)
|
|
283
|
-
|
|
284
|
-
mode = agent.dig('spec', 'mode') || 'autonomous'
|
|
285
|
-
|
|
286
|
-
# Build kubectl command for log streaming
|
|
287
|
-
tail_arg = "--tail=#{options[:tail]}"
|
|
288
|
-
follow_arg = options[:follow] ? '-f' : ''
|
|
289
|
-
|
|
290
|
-
# For scheduled agents, logs come from CronJob pods
|
|
291
|
-
# For autonomous agents, logs come from Deployment pods
|
|
292
|
-
if mode == 'scheduled'
|
|
293
|
-
# Get most recent job from cronjob
|
|
294
|
-
else
|
|
295
|
-
# Get pod from deployment
|
|
296
|
-
end
|
|
297
|
-
label_selector = "app.kubernetes.io/name=#{name}"
|
|
298
|
-
|
|
299
|
-
# Use kubectl logs with label selector
|
|
300
|
-
cmd = "#{ctx.kubectl_prefix} logs -l #{label_selector} #{tail_arg} #{follow_arg} --all-containers"
|
|
301
|
-
|
|
302
|
-
Formatters::ProgressFormatter.info("Streaming logs for agent '#{name}'...")
|
|
303
|
-
puts
|
|
304
|
-
|
|
305
|
-
# Stream raw logs in real-time without formatting
|
|
306
|
-
require 'open3'
|
|
307
|
-
Open3.popen3(cmd) do |_stdin, stdout, stderr, wait_thr|
|
|
308
|
-
# Handle stdout (logs)
|
|
309
|
-
stdout_thread = Thread.new do
|
|
310
|
-
stdout.each_line do |line|
|
|
311
|
-
puts line
|
|
312
|
-
$stdout.flush
|
|
313
|
-
end
|
|
314
|
-
end
|
|
315
|
-
|
|
316
|
-
# Handle stderr (errors)
|
|
317
|
-
stderr_thread = Thread.new do
|
|
318
|
-
stderr.each_line do |line|
|
|
319
|
-
warn line
|
|
320
|
-
end
|
|
321
|
-
end
|
|
322
|
-
|
|
323
|
-
# Wait for both streams to complete
|
|
324
|
-
stdout_thread.join
|
|
325
|
-
stderr_thread.join
|
|
326
|
-
|
|
327
|
-
# Check exit status
|
|
328
|
-
exit_status = wait_thr.value
|
|
329
|
-
exit exit_status.exitstatus unless exit_status.success?
|
|
330
|
-
end
|
|
331
|
-
end
|
|
332
|
-
end
|
|
333
|
-
|
|
334
|
-
desc 'code NAME', 'Display synthesized agent code'
|
|
335
|
-
option :cluster, type: :string, desc: 'Override current cluster context'
|
|
336
|
-
option :raw, type: :boolean, default: false, desc: 'Output raw code without formatting'
|
|
337
|
-
def code(name)
|
|
338
|
-
handle_command_error('get code') do
|
|
339
|
-
require_relative '../formatters/code_formatter'
|
|
340
|
-
|
|
341
|
-
ctx = Helpers::ClusterContext.from_options(options)
|
|
342
|
-
|
|
343
|
-
# Get the code ConfigMap for this agent
|
|
344
|
-
configmap_name = "#{name}-code"
|
|
345
|
-
begin
|
|
346
|
-
configmap = ctx.client.get_resource('ConfigMap', configmap_name, ctx.namespace)
|
|
347
|
-
rescue K8s::Error::NotFound
|
|
348
|
-
Formatters::ProgressFormatter.error("Synthesized code not found for agent '#{name}'")
|
|
349
|
-
puts
|
|
350
|
-
puts 'Possible reasons:'
|
|
351
|
-
puts ' - Agent synthesis not yet complete'
|
|
352
|
-
puts ' - Agent synthesis failed'
|
|
353
|
-
puts
|
|
354
|
-
puts 'Check synthesis status with:'
|
|
355
|
-
puts " aictl agent inspect #{name}"
|
|
356
|
-
exit 1
|
|
357
|
-
end
|
|
358
|
-
|
|
359
|
-
# Get the agent.rb code from the ConfigMap
|
|
360
|
-
code_content = configmap.dig('data', 'agent.rb')
|
|
361
|
-
unless code_content
|
|
362
|
-
Formatters::ProgressFormatter.error('Code content not found in ConfigMap')
|
|
363
|
-
exit 1
|
|
364
|
-
end
|
|
365
|
-
|
|
366
|
-
# Raw output mode - just print the code
|
|
367
|
-
if options[:raw]
|
|
368
|
-
puts code_content
|
|
369
|
-
return
|
|
370
|
-
end
|
|
371
|
-
|
|
372
|
-
# Display with syntax highlighting
|
|
373
|
-
Formatters::CodeFormatter.display_ruby_code(
|
|
374
|
-
code_content,
|
|
375
|
-
title: "Synthesized Code for Agent: #{name}"
|
|
376
|
-
)
|
|
377
|
-
end
|
|
378
|
-
end
|
|
379
|
-
|
|
380
|
-
desc 'edit NAME', 'Edit agent instructions'
|
|
381
|
-
option :cluster, type: :string, desc: 'Override current cluster context'
|
|
382
|
-
def edit(name)
|
|
383
|
-
handle_command_error('edit agent') do
|
|
384
|
-
ctx = Helpers::ClusterContext.from_options(options)
|
|
385
|
-
|
|
386
|
-
# Get current agent
|
|
387
|
-
agent = get_resource_or_exit('LanguageAgent', name)
|
|
388
|
-
|
|
389
|
-
current_instructions = agent.dig('spec', 'instructions')
|
|
390
|
-
|
|
391
|
-
# Edit instructions in user's editor
|
|
392
|
-
new_instructions = Helpers::EditorHelper.edit_content(
|
|
393
|
-
current_instructions,
|
|
394
|
-
'agent-instructions-',
|
|
395
|
-
'.txt'
|
|
396
|
-
).strip
|
|
397
|
-
|
|
398
|
-
# Check if changed
|
|
399
|
-
if new_instructions == current_instructions
|
|
400
|
-
Formatters::ProgressFormatter.info('No changes made')
|
|
401
|
-
return
|
|
402
|
-
end
|
|
403
|
-
|
|
404
|
-
# Update agent resource
|
|
405
|
-
agent['spec']['instructions'] = new_instructions
|
|
406
|
-
|
|
407
|
-
Formatters::ProgressFormatter.with_spinner('Updating agent instructions') do
|
|
408
|
-
ctx.client.apply_resource(agent)
|
|
409
|
-
end
|
|
410
|
-
|
|
411
|
-
Formatters::ProgressFormatter.success('Agent instructions updated')
|
|
412
|
-
puts
|
|
413
|
-
puts 'The operator will automatically re-synthesize the agent code.'
|
|
414
|
-
puts
|
|
415
|
-
puts 'Watch synthesis progress with:'
|
|
416
|
-
puts " aictl agent inspect #{name}"
|
|
417
|
-
end
|
|
418
|
-
end
|
|
419
|
-
|
|
420
|
-
desc 'pause NAME', 'Pause scheduled agent execution'
|
|
421
|
-
option :cluster, type: :string, desc: 'Override current cluster context'
|
|
422
|
-
def pause(name)
|
|
423
|
-
handle_command_error('pause agent') do
|
|
424
|
-
ctx = Helpers::ClusterContext.from_options(options)
|
|
425
|
-
|
|
426
|
-
# Get agent
|
|
427
|
-
agent = get_resource_or_exit('LanguageAgent', name)
|
|
428
|
-
|
|
429
|
-
mode = agent.dig('spec', 'mode') || 'autonomous'
|
|
430
|
-
unless mode == 'scheduled'
|
|
431
|
-
Formatters::ProgressFormatter.warn("Agent '#{name}' is not a scheduled agent (mode: #{mode})")
|
|
432
|
-
puts
|
|
433
|
-
puts 'Only scheduled agents can be paused.'
|
|
434
|
-
puts 'Autonomous agents can be stopped by deleting them.'
|
|
435
|
-
exit 1
|
|
436
|
-
end
|
|
437
|
-
|
|
438
|
-
# Suspend the CronJob by setting spec.suspend = true
|
|
439
|
-
# This is done by patching the underlying CronJob resource
|
|
440
|
-
cronjob_name = name
|
|
441
|
-
ctx.namespace
|
|
442
|
-
|
|
443
|
-
Formatters::ProgressFormatter.with_spinner("Pausing agent '#{name}'") do
|
|
444
|
-
# Use kubectl to patch the cronjob
|
|
445
|
-
cmd = "#{ctx.kubectl_prefix} patch cronjob #{cronjob_name} -p '{\"spec\":{\"suspend\":true}}'"
|
|
446
|
-
system(cmd)
|
|
447
|
-
end
|
|
448
|
-
|
|
449
|
-
Formatters::ProgressFormatter.success("Agent '#{name}' paused")
|
|
450
|
-
puts
|
|
451
|
-
puts 'The agent will not execute on its schedule until resumed.'
|
|
452
|
-
puts
|
|
453
|
-
puts 'Resume with:'
|
|
454
|
-
puts " aictl agent resume #{name}"
|
|
455
|
-
end
|
|
456
|
-
end
|
|
457
|
-
|
|
458
|
-
desc 'resume NAME', 'Resume paused agent'
|
|
459
|
-
option :cluster, type: :string, desc: 'Override current cluster context'
|
|
460
|
-
def resume(name)
|
|
461
|
-
handle_command_error('resume agent') do
|
|
462
|
-
ctx = Helpers::ClusterContext.from_options(options)
|
|
463
|
-
|
|
464
|
-
# Get agent
|
|
465
|
-
agent = get_resource_or_exit('LanguageAgent', name)
|
|
466
|
-
|
|
467
|
-
mode = agent.dig('spec', 'mode') || 'autonomous'
|
|
468
|
-
unless mode == 'scheduled'
|
|
469
|
-
Formatters::ProgressFormatter.warn("Agent '#{name}' is not a scheduled agent (mode: #{mode})")
|
|
470
|
-
puts
|
|
471
|
-
puts 'Only scheduled agents can be resumed.'
|
|
472
|
-
exit 1
|
|
473
|
-
end
|
|
474
|
-
|
|
475
|
-
# Resume the CronJob by setting spec.suspend = false
|
|
476
|
-
cronjob_name = name
|
|
477
|
-
ctx.namespace
|
|
478
|
-
|
|
479
|
-
Formatters::ProgressFormatter.with_spinner("Resuming agent '#{name}'") do
|
|
480
|
-
# Use kubectl to patch the cronjob
|
|
481
|
-
cmd = "#{ctx.kubectl_prefix} patch cronjob #{cronjob_name} -p '{\"spec\":{\"suspend\":false}}'"
|
|
482
|
-
system(cmd)
|
|
483
|
-
end
|
|
484
|
-
|
|
485
|
-
Formatters::ProgressFormatter.success("Agent '#{name}' resumed")
|
|
486
|
-
puts
|
|
487
|
-
puts 'The agent will now execute according to its schedule.'
|
|
488
|
-
puts
|
|
489
|
-
puts 'View next execution time with:'
|
|
490
|
-
puts " aictl agent inspect #{name}"
|
|
491
|
-
end
|
|
492
|
-
end
|
|
493
|
-
|
|
494
|
-
desc 'optimize NAME', 'Optimize neural tasks to symbolic based on learned patterns'
|
|
495
|
-
long_desc <<-DESC
|
|
496
|
-
Analyze agent execution patterns and propose optimizations to convert
|
|
497
|
-
neural (LLM-based) tasks into symbolic (code-based) implementations.
|
|
498
|
-
|
|
499
|
-
This command queries OpenTelemetry traces to detect deterministic patterns
|
|
500
|
-
in task execution, then generates optimized symbolic code that runs faster
|
|
501
|
-
and costs less while maintaining the same behavior.
|
|
502
|
-
|
|
503
|
-
Requirements:
|
|
504
|
-
• OpenTelemetry backend configured (SigNoz, Jaeger, or Tempo)
|
|
505
|
-
• Neural task has executed at least 10 times
|
|
506
|
-
• Execution pattern consistency >= 85%
|
|
507
|
-
|
|
508
|
-
Examples:
|
|
509
|
-
aictl agent optimize my-agent # Analyze and propose optimizations
|
|
510
|
-
aictl agent optimize my-agent --dry-run # Show what would be optimized
|
|
511
|
-
aictl agent optimize my-agent --status-only # Show learning status only
|
|
512
|
-
aictl agent optimize my-agent --auto-accept # Auto-accept high-confidence optimizations
|
|
513
|
-
aictl agent optimize my-agent --tasks task1,task2 # Optimize specific tasks only
|
|
514
|
-
DESC
|
|
515
|
-
option :cluster, type: :string, desc: 'Override current cluster context'
|
|
516
|
-
option :dry_run, type: :boolean, default: false, desc: 'Show what would be optimized without applying'
|
|
517
|
-
option :status_only, type: :boolean, default: false, desc: 'Show learning status without optimizing'
|
|
518
|
-
option :auto_accept, type: :boolean, default: false, desc: 'Auto-accept optimizations above min-confidence'
|
|
519
|
-
option :min_confidence, type: :numeric, default: 0.90, desc: 'Minimum consistency for auto-accept (0.0-1.0)'
|
|
520
|
-
option :tasks, type: :array, desc: 'Only optimize specific tasks'
|
|
521
|
-
option :since, type: :string, desc: 'Only analyze traces since (e.g., "2h", "1d", "7d")'
|
|
522
|
-
option :use_synthesis, type: :boolean, default: false, desc: 'Use LLM synthesis instead of pattern detection'
|
|
523
|
-
option :synthesis_model, type: :string, desc: 'Model to use for synthesis (default: cluster default)'
|
|
524
|
-
def optimize(name)
|
|
525
|
-
handle_command_error('optimize agent') do
|
|
526
|
-
require_relative '../../learning/trace_analyzer'
|
|
527
|
-
require_relative '../../learning/pattern_detector'
|
|
528
|
-
require_relative '../../learning/optimizer'
|
|
529
|
-
require_relative '../../learning/task_synthesizer'
|
|
530
|
-
require_relative '../../agent/safety/ast_validator'
|
|
531
|
-
require_relative '../formatters/optimization_formatter'
|
|
532
|
-
|
|
533
|
-
ctx = Helpers::ClusterContext.from_options(options)
|
|
534
|
-
|
|
535
|
-
# Get agent to verify it exists
|
|
536
|
-
get_resource_or_exit('LanguageAgent', name)
|
|
537
|
-
|
|
538
|
-
# Get agent code/definition
|
|
539
|
-
agent_definition = load_agent_definition(ctx, name)
|
|
540
|
-
unless agent_definition
|
|
541
|
-
Formatters::ProgressFormatter.error("Could not load agent definition for '#{name}'")
|
|
542
|
-
exit 1
|
|
543
|
-
end
|
|
544
|
-
|
|
545
|
-
# Check for OpenTelemetry configuration
|
|
546
|
-
unless ENV['OTEL_QUERY_ENDPOINT']
|
|
547
|
-
Formatters::ProgressFormatter.warn('OpenTelemetry endpoint not configured')
|
|
548
|
-
puts
|
|
549
|
-
puts 'Set OTEL_QUERY_ENDPOINT to enable learning:'
|
|
550
|
-
puts ' export OTEL_QUERY_ENDPOINT=https://your-signoz-instance.com'
|
|
551
|
-
puts ' export OTEL_QUERY_API_KEY=your-api-key # For SigNoz authentication'
|
|
552
|
-
puts ' export OTEL_QUERY_BACKEND=signoz # Optional: signoz, jaeger, or tempo'
|
|
553
|
-
puts
|
|
554
|
-
puts 'Auto-detection tries backends in order: SigNoz → Jaeger → Tempo'
|
|
555
|
-
puts 'Set OTEL_QUERY_BACKEND to skip auto-detection and use a specific backend.'
|
|
556
|
-
puts
|
|
557
|
-
exit 1
|
|
558
|
-
end
|
|
559
|
-
|
|
560
|
-
# Initialize learning components
|
|
561
|
-
trace_analyzer = Learning::TraceAnalyzer.new(
|
|
562
|
-
endpoint: ENV.fetch('OTEL_QUERY_ENDPOINT', nil),
|
|
563
|
-
api_key: ENV.fetch('OTEL_QUERY_API_KEY', nil),
|
|
564
|
-
backend: ENV.fetch('OTEL_QUERY_BACKEND', nil)
|
|
565
|
-
)
|
|
566
|
-
|
|
567
|
-
unless trace_analyzer.available?
|
|
568
|
-
Formatters::ProgressFormatter.error('OpenTelemetry backend not available')
|
|
569
|
-
puts
|
|
570
|
-
puts 'Check your OTEL_QUERY_ENDPOINT configuration and backend status.'
|
|
571
|
-
exit 1
|
|
572
|
-
end
|
|
573
|
-
|
|
574
|
-
validator = LanguageOperator::Agent::Safety::ASTValidator.new
|
|
575
|
-
pattern_detector = LanguageOperator::Learning::PatternDetector.new(
|
|
576
|
-
trace_analyzer: trace_analyzer,
|
|
577
|
-
validator: validator
|
|
578
|
-
)
|
|
579
|
-
|
|
580
|
-
# Create task synthesizer for fallback (or forced via --use-synthesis)
|
|
581
|
-
# Synthesis is used when pattern detection fails OR --use-synthesis is set
|
|
582
|
-
task_synthesizer = nil
|
|
583
|
-
llm_client = create_synthesis_llm_client(ctx, options[:synthesis_model])
|
|
584
|
-
if llm_client
|
|
585
|
-
task_synthesizer = LanguageOperator::Learning::TaskSynthesizer.new(
|
|
586
|
-
llm_client: llm_client,
|
|
587
|
-
validator: validator
|
|
588
|
-
)
|
|
589
|
-
Formatters::ProgressFormatter.info('LLM synthesis mode (forced)') if options[:use_synthesis]
|
|
590
|
-
elsif options[:use_synthesis]
|
|
591
|
-
Formatters::ProgressFormatter.warn('Could not create LLM client for synthesis')
|
|
592
|
-
end
|
|
593
|
-
|
|
594
|
-
optimizer = LanguageOperator::Learning::Optimizer.new(
|
|
595
|
-
agent_name: name,
|
|
596
|
-
agent_definition: agent_definition,
|
|
597
|
-
trace_analyzer: trace_analyzer,
|
|
598
|
-
pattern_detector: pattern_detector,
|
|
599
|
-
task_synthesizer: task_synthesizer
|
|
600
|
-
)
|
|
601
|
-
|
|
602
|
-
formatter = Formatters::OptimizationFormatter.new
|
|
603
|
-
|
|
604
|
-
# Parse --since option into time range
|
|
605
|
-
time_range = parse_since_option(options[:since])
|
|
606
|
-
|
|
607
|
-
# Analyze for opportunities
|
|
608
|
-
opportunities = optimizer.analyze(time_range: time_range)
|
|
609
|
-
|
|
610
|
-
# Display analysis only in status-only mode
|
|
611
|
-
if options[:status_only]
|
|
612
|
-
puts formatter.format_analysis(agent_name: name, opportunities: opportunities)
|
|
613
|
-
return
|
|
614
|
-
end
|
|
615
|
-
|
|
616
|
-
# Exit if no opportunities
|
|
617
|
-
return if opportunities.empty?
|
|
618
|
-
|
|
619
|
-
# Filter opportunities:
|
|
620
|
-
# - If synthesis available: try any task with enough executions
|
|
621
|
-
# - Otherwise: only tasks ready for pattern detection
|
|
622
|
-
candidates = if task_synthesizer
|
|
623
|
-
# With synthesis, try any task that has min executions
|
|
624
|
-
opportunities.select { |opp| opp[:execution_count] >= 10 }
|
|
625
|
-
else
|
|
626
|
-
opportunities.select { |opp| opp[:ready_for_learning] }
|
|
627
|
-
end
|
|
628
|
-
return if candidates.empty?
|
|
629
|
-
|
|
630
|
-
# Process each opportunity
|
|
631
|
-
candidates.each do |opp|
|
|
632
|
-
task_name = opp[:task_name]
|
|
633
|
-
|
|
634
|
-
# Skip if not in requested tasks list
|
|
635
|
-
next if options[:tasks] && !options[:tasks].include?(task_name)
|
|
636
|
-
|
|
637
|
-
# Generate proposal
|
|
638
|
-
begin
|
|
639
|
-
proposal = optimizer.propose(task_name: task_name, use_synthesis: options[:use_synthesis])
|
|
640
|
-
rescue ArgumentError => e
|
|
641
|
-
Formatters::ProgressFormatter.warn("Cannot optimize '#{task_name}': #{e.message}")
|
|
642
|
-
next
|
|
643
|
-
end
|
|
644
|
-
|
|
645
|
-
# Display proposal
|
|
646
|
-
puts formatter.format_proposal(proposal: proposal)
|
|
647
|
-
|
|
648
|
-
# Get user confirmation or auto-accept
|
|
649
|
-
accepted = if options[:auto_accept] && proposal[:consistency_score] >= options[:min_confidence]
|
|
650
|
-
consistency_pct = (proposal[:consistency_score] * 100).round(1)
|
|
651
|
-
threshold_pct = (options[:min_confidence] * 100).round(1)
|
|
652
|
-
puts pastel.green("✓ Auto-accepting (consistency: #{consistency_pct}% >= #{threshold_pct}%)")
|
|
653
|
-
true
|
|
654
|
-
elsif options[:dry_run]
|
|
655
|
-
puts pastel.yellow('[DRY RUN] Would prompt for acceptance')
|
|
656
|
-
false
|
|
657
|
-
else
|
|
658
|
-
prompt_for_optimization_acceptance(proposal)
|
|
659
|
-
end
|
|
660
|
-
|
|
661
|
-
# Apply if accepted
|
|
662
|
-
if accepted && !options[:dry_run]
|
|
663
|
-
result = apply_optimization(ctx, name, proposal)
|
|
664
|
-
puts formatter.format_success(result: result)
|
|
665
|
-
elsif accepted
|
|
666
|
-
puts pastel.yellow('[DRY RUN] Would apply optimization')
|
|
667
|
-
else
|
|
668
|
-
puts pastel.yellow("Skipped optimization for '#{task_name}'")
|
|
669
|
-
end
|
|
670
|
-
|
|
671
|
-
puts
|
|
672
|
-
end
|
|
673
|
-
end
|
|
674
|
-
end
|
|
675
|
-
|
|
676
|
-
desc 'workspace NAME', 'Browse agent workspace files'
|
|
677
|
-
long_desc <<-DESC
|
|
678
|
-
Browse and manage the workspace files for an agent.
|
|
679
|
-
|
|
680
|
-
Workspaces provide persistent storage for agents to maintain state,
|
|
681
|
-
cache data, and remember information across executions.
|
|
682
|
-
|
|
683
|
-
Examples:
|
|
684
|
-
aictl agent workspace my-agent # List all files
|
|
685
|
-
aictl agent workspace my-agent --path /workspace/state.json # View specific file
|
|
686
|
-
aictl agent workspace my-agent --clean # Clear workspace
|
|
687
|
-
DESC
|
|
688
|
-
option :cluster, type: :string, desc: 'Override current cluster context'
|
|
689
|
-
option :path, type: :string, desc: 'View specific file contents'
|
|
690
|
-
option :clean, type: :boolean, desc: 'Clear workspace (with confirmation)'
|
|
691
|
-
def workspace(name)
|
|
692
|
-
handle_command_error('access workspace') do
|
|
693
|
-
ctx = Helpers::ClusterContext.from_options(options)
|
|
694
|
-
|
|
695
|
-
# Get agent to verify it exists
|
|
696
|
-
agent = get_resource_or_exit('LanguageAgent', name)
|
|
697
|
-
|
|
698
|
-
# Check if workspace is enabled
|
|
699
|
-
workspace_enabled = agent.dig('spec', 'workspace', 'enabled')
|
|
700
|
-
unless workspace_enabled
|
|
701
|
-
Formatters::ProgressFormatter.warn("Workspace is not enabled for agent '#{name}'")
|
|
702
|
-
puts
|
|
703
|
-
puts 'Enable workspace in agent configuration:'
|
|
704
|
-
puts ' spec:'
|
|
705
|
-
puts ' workspace:'
|
|
706
|
-
puts ' enabled: true'
|
|
707
|
-
puts ' size: 10Gi'
|
|
708
|
-
exit 1
|
|
709
|
-
end
|
|
710
|
-
|
|
711
|
-
if options[:path]
|
|
712
|
-
view_workspace_file(ctx, name, options[:path])
|
|
713
|
-
elsif options[:clean]
|
|
714
|
-
clean_workspace(ctx, name)
|
|
715
|
-
else
|
|
716
|
-
list_workspace_files(ctx, name)
|
|
717
|
-
end
|
|
718
|
-
end
|
|
719
|
-
end
|
|
720
|
-
|
|
721
|
-
private
|
|
722
|
-
|
|
723
|
-
# Parse --since option into seconds (time range)
|
|
724
|
-
#
|
|
725
|
-
# @param since [String, nil] Duration string (e.g., "2h", "1d", "7d")
|
|
726
|
-
# @return [Integer, nil] Seconds or nil if not specified
|
|
727
|
-
def parse_since_option(since)
|
|
728
|
-
return nil unless since
|
|
729
|
-
|
|
730
|
-
match = since.match(/^(\d+)([hHdDwW])$/)
|
|
731
|
-
unless match
|
|
732
|
-
Formatters::ProgressFormatter.warn("Invalid --since format '#{since}', using default (24h)")
|
|
733
|
-
Formatters::ProgressFormatter.info('Valid formats: 2h (hours), 1d (days), 1w (weeks)')
|
|
734
|
-
return nil
|
|
735
|
-
end
|
|
736
|
-
|
|
737
|
-
value = match[1].to_i
|
|
738
|
-
unit = match[2].downcase
|
|
739
|
-
|
|
740
|
-
case unit
|
|
741
|
-
when 'h' then value * 3600
|
|
742
|
-
when 'd' then value * 86_400
|
|
743
|
-
when 'w' then value * 604_800
|
|
744
|
-
end
|
|
745
|
-
end
|
|
746
|
-
|
|
747
|
-
# Create LLM client for task synthesis using cluster model
|
|
748
|
-
#
|
|
749
|
-
# @param ctx [ClusterContext] Cluster context
|
|
750
|
-
# @param model_name [String, nil] Specific model to use (defaults to first available)
|
|
751
|
-
# @return [Object, nil] LLM client or nil if unavailable
|
|
752
|
-
def create_synthesis_llm_client(ctx, model_name = nil)
|
|
753
|
-
# Get model from cluster
|
|
754
|
-
selected_model = model_name || select_synthesis_model(ctx)
|
|
755
|
-
return nil unless selected_model
|
|
756
|
-
|
|
757
|
-
# Get model resource to extract model ID
|
|
758
|
-
# Always use port-forwarding to deployment (LiteLLM proxy for cost controls)
|
|
759
|
-
begin
|
|
760
|
-
model = ctx.client.get_resource('LanguageModel', selected_model, ctx.namespace)
|
|
761
|
-
model_id = model.dig('spec', 'modelName')
|
|
762
|
-
return nil unless model_id
|
|
763
|
-
|
|
764
|
-
ClusterLLMClient.new(
|
|
765
|
-
ctx: ctx,
|
|
766
|
-
model_name: selected_model,
|
|
767
|
-
model_id: model_id,
|
|
768
|
-
agent_command: self
|
|
769
|
-
)
|
|
770
|
-
rescue StandardError => e
|
|
771
|
-
@logger&.warn("Failed to create cluster LLM client: #{e.message}")
|
|
772
|
-
nil
|
|
773
|
-
end
|
|
774
|
-
end
|
|
775
|
-
|
|
776
|
-
# Select model for synthesis (first available if not specified)
|
|
777
|
-
#
|
|
778
|
-
# @param ctx [ClusterContext] Cluster context
|
|
779
|
-
# @return [String, nil] Model name or nil
|
|
780
|
-
def select_synthesis_model(ctx)
|
|
781
|
-
models = ctx.client.list_resources('LanguageModel', namespace: ctx.namespace)
|
|
782
|
-
return nil if models.empty?
|
|
783
|
-
|
|
784
|
-
models.first.dig('metadata', 'name')
|
|
785
|
-
rescue StandardError
|
|
786
|
-
nil
|
|
787
|
-
end
|
|
788
|
-
|
|
789
|
-
# LLM client that uses port-forwarding to cluster model deployments (LiteLLM proxy)
|
|
790
|
-
class ClusterLLMClient
|
|
791
|
-
def initialize(ctx:, model_name:, model_id:, agent_command:)
|
|
792
|
-
@ctx = ctx
|
|
793
|
-
@model_name = model_name
|
|
794
|
-
@model_id = model_id
|
|
795
|
-
@agent_command = agent_command
|
|
796
|
-
end
|
|
797
|
-
|
|
798
|
-
def chat(prompt)
|
|
799
|
-
require 'faraday'
|
|
800
|
-
require 'json'
|
|
801
|
-
|
|
802
|
-
pod = get_model_pod
|
|
803
|
-
pod_name = pod.dig('metadata', 'name')
|
|
804
|
-
|
|
805
|
-
local_port = find_available_port
|
|
806
|
-
port_forward_pid = nil
|
|
807
|
-
|
|
808
|
-
begin
|
|
809
|
-
port_forward_pid = start_port_forward(pod_name, local_port, 4000)
|
|
810
|
-
wait_for_port(local_port)
|
|
811
|
-
|
|
812
|
-
conn = Faraday.new(url: "http://localhost:#{local_port}") do |f|
|
|
813
|
-
f.request :json
|
|
814
|
-
f.response :json
|
|
815
|
-
f.adapter Faraday.default_adapter
|
|
816
|
-
f.options.timeout = 120
|
|
817
|
-
f.options.open_timeout = 10
|
|
818
|
-
end
|
|
819
|
-
|
|
820
|
-
payload = {
|
|
821
|
-
model: @model_id,
|
|
822
|
-
messages: [{ role: 'user', content: prompt }],
|
|
823
|
-
max_tokens: 4000,
|
|
824
|
-
temperature: 0.3
|
|
825
|
-
}
|
|
826
|
-
|
|
827
|
-
response = conn.post('/v1/chat/completions', payload)
|
|
828
|
-
result = response.body
|
|
829
|
-
|
|
830
|
-
raise "LLM error: #{result['error']['message'] || result['error']}" if result['error']
|
|
831
|
-
|
|
832
|
-
result.dig('choices', 0, 'message', 'content')
|
|
833
|
-
ensure
|
|
834
|
-
cleanup_port_forward(port_forward_pid) if port_forward_pid
|
|
835
|
-
end
|
|
836
|
-
end
|
|
837
|
-
|
|
838
|
-
private
|
|
839
|
-
|
|
840
|
-
def get_model_pod
|
|
841
|
-
# Get the deployment for the model
|
|
842
|
-
deployment = @ctx.client.get_resource('Deployment', @model_name, @ctx.namespace)
|
|
843
|
-
raise "Deployment '#{@model_name}' not found in namespace '#{@ctx.namespace}'" if deployment.nil?
|
|
844
|
-
|
|
845
|
-
labels = deployment.dig('spec', 'selector', 'matchLabels')
|
|
846
|
-
raise "Deployment '#{@model_name}' has no selector labels" if labels.nil?
|
|
847
|
-
|
|
848
|
-
# Convert to hash if needed (K8s API may return K8s::Resource)
|
|
849
|
-
labels_hash = labels.respond_to?(:to_h) ? labels.to_h : labels
|
|
850
|
-
raise "Deployment '#{@model_name}' has empty selector labels" if labels_hash.empty?
|
|
851
|
-
|
|
852
|
-
label_selector = labels_hash.map { |k, v| "#{k}=#{v}" }.join(',')
|
|
853
|
-
|
|
854
|
-
# Find a running pod
|
|
855
|
-
pods = @ctx.client.list_resources('Pod', namespace: @ctx.namespace, label_selector: label_selector)
|
|
856
|
-
raise "No pods found for model '#{@model_name}'" if pods.empty?
|
|
857
|
-
|
|
858
|
-
running_pods = pods.select { |p| p.dig('status', 'phase') == 'Running' }
|
|
859
|
-
raise "No running pods found for model '#{@model_name}'" if running_pods.empty?
|
|
860
|
-
|
|
861
|
-
running_pods.first
|
|
862
|
-
end
|
|
863
|
-
|
|
864
|
-
def find_available_port
|
|
865
|
-
server = TCPServer.new('127.0.0.1', 0)
|
|
866
|
-
port = server.addr[1]
|
|
867
|
-
server.close
|
|
868
|
-
port
|
|
869
|
-
end
|
|
870
|
-
|
|
871
|
-
def start_port_forward(pod_name, local_port, remote_port)
|
|
872
|
-
pid = spawn(
|
|
873
|
-
'kubectl', 'port-forward',
|
|
874
|
-
'-n', @ctx.namespace,
|
|
875
|
-
"pod/#{pod_name}",
|
|
876
|
-
"#{local_port}:#{remote_port}",
|
|
877
|
-
%i[out err] => '/dev/null'
|
|
878
|
-
)
|
|
879
|
-
Process.detach(pid)
|
|
880
|
-
pid
|
|
881
|
-
end
|
|
882
|
-
|
|
883
|
-
def wait_for_port(port, max_attempts: 30)
|
|
884
|
-
max_attempts.times do
|
|
885
|
-
TCPSocket.new('127.0.0.1', port).close
|
|
886
|
-
return true
|
|
887
|
-
rescue Errno::ECONNREFUSED
|
|
888
|
-
sleep 0.1
|
|
889
|
-
end
|
|
890
|
-
raise "Port #{port} not available after #{max_attempts} attempts"
|
|
891
|
-
end
|
|
892
|
-
|
|
893
|
-
def cleanup_port_forward(pid)
|
|
894
|
-
Process.kill('TERM', pid)
|
|
895
|
-
rescue Errno::ESRCH
|
|
896
|
-
# Process already gone
|
|
897
|
-
end
|
|
898
|
-
end
|
|
899
|
-
|
|
900
|
-
def handle_agent_not_found(name, ctx)
|
|
901
|
-
# Get available agents for fuzzy matching
|
|
902
|
-
agents = ctx.client.list_resources('LanguageAgent', namespace: ctx.namespace)
|
|
903
|
-
available_names = agents.map { |a| a.dig('metadata', 'name') }
|
|
904
|
-
|
|
905
|
-
error = K8s::Error::NotFound.new(404, 'Not Found', 'LanguageAgent')
|
|
906
|
-
Errors::Handler.handle_not_found(error,
|
|
907
|
-
resource_type: 'LanguageAgent',
|
|
908
|
-
resource_name: name,
|
|
909
|
-
cluster: ctx.name,
|
|
910
|
-
available_resources: available_names)
|
|
911
|
-
end
|
|
912
|
-
|
|
913
|
-
def display_agent_created(agent, cluster, _description, synthesis_result)
|
|
914
|
-
require_relative '../formatters/code_formatter'
|
|
915
|
-
agent_name = agent.dig('metadata', 'name')
|
|
916
|
-
|
|
917
|
-
puts
|
|
918
|
-
Formatters::ProgressFormatter.success("Agent '#{agent_name}' created and deployed!")
|
|
919
|
-
puts
|
|
920
|
-
|
|
921
|
-
# Get synthesized code if available
|
|
922
|
-
begin
|
|
923
|
-
ctx = Helpers::ClusterContext.from_options(cluster: cluster)
|
|
924
|
-
configmap_name = "#{agent_name}-code"
|
|
925
|
-
configmap = ctx.client.get_resource('ConfigMap', configmap_name, ctx.namespace)
|
|
926
|
-
code_content = configmap.dig('data', 'agent.rb')
|
|
927
|
-
|
|
928
|
-
if code_content
|
|
929
|
-
# Display code preview (first 20 lines)
|
|
930
|
-
Formatters::CodeFormatter.display_ruby_code(
|
|
931
|
-
code_content,
|
|
932
|
-
title: 'Synthesized Code Preview:',
|
|
933
|
-
max_lines: 20
|
|
934
|
-
)
|
|
935
|
-
puts
|
|
936
|
-
end
|
|
937
|
-
rescue StandardError
|
|
938
|
-
# Code not available yet, skip preview
|
|
939
|
-
end
|
|
940
|
-
|
|
941
|
-
# Display agent configuration
|
|
942
|
-
puts pastel.cyan('Agent Configuration:')
|
|
943
|
-
puts " Name: #{agent_name}"
|
|
944
|
-
puts " Cluster: #{cluster}"
|
|
945
|
-
|
|
946
|
-
# Schedule information
|
|
947
|
-
schedule = agent.dig('spec', 'schedule')
|
|
948
|
-
mode = agent.dig('spec', 'mode') || 'autonomous'
|
|
949
|
-
if schedule
|
|
950
|
-
human_schedule = parse_schedule(schedule)
|
|
951
|
-
puts " Schedule: #{human_schedule} (#{schedule})"
|
|
952
|
-
|
|
953
|
-
# Calculate next run
|
|
954
|
-
next_run = agent.dig('status', 'nextRun')
|
|
955
|
-
if next_run
|
|
956
|
-
begin
|
|
957
|
-
next_run_time = Time.parse(next_run)
|
|
958
|
-
time_until = format_time_until(next_run_time)
|
|
959
|
-
puts " Next run: #{next_run} (#{time_until})"
|
|
960
|
-
rescue StandardError
|
|
961
|
-
puts " Next run: #{next_run}"
|
|
962
|
-
end
|
|
963
|
-
end
|
|
964
|
-
else
|
|
965
|
-
puts " Mode: #{mode}"
|
|
966
|
-
end
|
|
967
|
-
|
|
968
|
-
# Persona
|
|
969
|
-
persona = agent.dig('spec', 'persona')
|
|
970
|
-
puts " Persona: #{persona || '(auto-selected)'}"
|
|
971
|
-
|
|
972
|
-
# Tools
|
|
973
|
-
tools = agent.dig('spec', 'tools') || []
|
|
974
|
-
puts " Tools: #{tools.join(', ')}" if tools.any?
|
|
975
|
-
|
|
976
|
-
# Models
|
|
977
|
-
model_refs = agent.dig('spec', 'modelRefs') || []
|
|
978
|
-
if model_refs.any?
|
|
979
|
-
model_names = model_refs.map { |ref| ref['name'] }
|
|
980
|
-
puts " Models: #{model_names.join(', ')}"
|
|
981
|
-
end
|
|
982
|
-
|
|
983
|
-
puts
|
|
984
|
-
|
|
985
|
-
# Synthesis stats
|
|
986
|
-
if synthesis_result[:duration]
|
|
987
|
-
puts pastel.dim("Synthesis completed in #{format_duration(synthesis_result[:duration])}")
|
|
988
|
-
puts pastel.dim("Model: #{synthesis_result[:model]}") if synthesis_result[:model]
|
|
989
|
-
puts
|
|
990
|
-
end
|
|
991
|
-
|
|
992
|
-
# Next steps
|
|
993
|
-
puts pastel.cyan('Next Steps:')
|
|
994
|
-
puts " aictl agent logs #{agent_name} -f # Follow agent execution logs"
|
|
995
|
-
puts " aictl agent code #{agent_name} # View full synthesized code"
|
|
996
|
-
puts " aictl agent inspect #{agent_name} # View detailed agent status"
|
|
997
|
-
puts
|
|
998
|
-
end
|
|
999
|
-
|
|
1000
|
-
def parse_schedule(cron_expr)
|
|
1001
|
-
# Simple cron to human-readable conversion
|
|
1002
|
-
# Format: minute hour day month weekday
|
|
1003
|
-
parts = cron_expr.split
|
|
1004
|
-
|
|
1005
|
-
return cron_expr if parts.length != 5
|
|
1006
|
-
|
|
1007
|
-
minute, hour, day, month, weekday = parts
|
|
1008
|
-
|
|
1009
|
-
# Common patterns
|
|
1010
|
-
if minute == '0' && hour != '*' && day == '*' && month == '*' && weekday == '*'
|
|
1011
|
-
# Daily at specific hour
|
|
1012
|
-
hour12 = hour.to_i % 12
|
|
1013
|
-
hour12 = 12 if hour12.zero?
|
|
1014
|
-
period = hour.to_i < 12 ? 'AM' : 'PM'
|
|
1015
|
-
return "Daily at #{hour12}:00 #{period}"
|
|
1016
|
-
elsif minute != '*' && hour != '*' && day == '*' && month == '*' && weekday == '*'
|
|
1017
|
-
# Daily at specific time
|
|
1018
|
-
hour12 = hour.to_i % 12
|
|
1019
|
-
hour12 = 12 if hour12.zero?
|
|
1020
|
-
period = hour.to_i < 12 ? 'AM' : 'PM'
|
|
1021
|
-
return "Daily at #{hour12}:#{minute.rjust(2, '0')} #{period}"
|
|
1022
|
-
elsif minute.start_with?('*/') && hour == '*'
|
|
1023
|
-
# Every N minutes
|
|
1024
|
-
interval = minute[2..].to_i
|
|
1025
|
-
return "Every #{interval} minutes"
|
|
1026
|
-
elsif minute == '*' && hour.start_with?('*/')
|
|
1027
|
-
# Every N hours
|
|
1028
|
-
interval = hour[2..].to_i
|
|
1029
|
-
return "Every #{interval} hours"
|
|
1030
|
-
end
|
|
1031
|
-
|
|
1032
|
-
# Fallback to cron expression
|
|
1033
|
-
cron_expr
|
|
1034
|
-
end
|
|
1035
|
-
|
|
1036
|
-
def format_time_until(future_time)
|
|
1037
|
-
Formatters::ValueFormatter.time_until(future_time)
|
|
1038
|
-
end
|
|
1039
|
-
|
|
1040
|
-
def display_dry_run_preview(agent_resource, cluster, description)
|
|
1041
|
-
require 'yaml'
|
|
1042
|
-
|
|
1043
|
-
puts
|
|
1044
|
-
puts '=' * 80
|
|
1045
|
-
puts ' DRY RUN: Agent Creation Preview'
|
|
1046
|
-
puts '=' * 80
|
|
1047
|
-
puts
|
|
1048
|
-
|
|
1049
|
-
# Extract key information
|
|
1050
|
-
name = agent_resource.dig('metadata', 'name')
|
|
1051
|
-
namespace = agent_resource.dig('metadata', 'namespace')
|
|
1052
|
-
persona = agent_resource.dig('spec', 'persona')
|
|
1053
|
-
tools = agent_resource.dig('spec', 'tools') || []
|
|
1054
|
-
model_refs = agent_resource.dig('spec', 'modelRefs') || []
|
|
1055
|
-
models = model_refs.map { |ref| ref['name'] }
|
|
1056
|
-
mode = agent_resource.dig('spec', 'mode') || 'autonomous'
|
|
1057
|
-
schedule = agent_resource.dig('spec', 'schedule')
|
|
1058
|
-
|
|
1059
|
-
# Display summary
|
|
1060
|
-
puts 'Agent Summary:'
|
|
1061
|
-
puts " Name: #{name}"
|
|
1062
|
-
puts " Cluster: #{cluster}"
|
|
1063
|
-
puts " Namespace: #{namespace}"
|
|
1064
|
-
puts " Mode: #{mode}"
|
|
1065
|
-
puts " Schedule: #{schedule || 'N/A'}" if schedule
|
|
1066
|
-
puts " Instructions: #{description}"
|
|
1067
|
-
puts
|
|
1068
|
-
|
|
1069
|
-
# Show detected configuration
|
|
1070
|
-
if persona
|
|
1071
|
-
puts 'Detected Configuration:'
|
|
1072
|
-
puts " Persona: #{persona}"
|
|
1073
|
-
end
|
|
1074
|
-
|
|
1075
|
-
puts " Tools: #{tools.join(', ')}" if tools.any?
|
|
1076
|
-
|
|
1077
|
-
puts " Models: #{models.join(', ')}" if models.any?
|
|
1078
|
-
|
|
1079
|
-
puts if persona || tools.any? || models.any?
|
|
1080
|
-
|
|
1081
|
-
# Show full YAML
|
|
1082
|
-
puts 'Generated YAML:'
|
|
1083
|
-
puts '─' * 80
|
|
1084
|
-
puts YAML.dump(agent_resource)
|
|
1085
|
-
puts '─' * 80
|
|
1086
|
-
puts
|
|
1087
|
-
|
|
1088
|
-
# Show what would happen
|
|
1089
|
-
puts 'What would happen:'
|
|
1090
|
-
puts ' 1. Agent resource would be created in the cluster'
|
|
1091
|
-
puts ' 2. Operator would synthesize Ruby code from instructions'
|
|
1092
|
-
puts ' 3. Agent would be deployed and start running'
|
|
1093
|
-
puts
|
|
1094
|
-
|
|
1095
|
-
# Show how to actually create
|
|
1096
|
-
Formatters::ProgressFormatter.info('No changes made (dry-run mode)')
|
|
1097
|
-
puts
|
|
1098
|
-
puts 'To create this agent for real, run:'
|
|
1099
|
-
cmd_parts = ["aictl agent create \"#{description}\""]
|
|
1100
|
-
cmd_parts << "--name #{name}" if options[:name]
|
|
1101
|
-
cmd_parts << "--persona #{persona}" if persona
|
|
1102
|
-
cmd_parts << "--tools #{tools.join(' ')}" if tools.any?
|
|
1103
|
-
cmd_parts << "--models #{models.join(' ')}" if models.any?
|
|
1104
|
-
cmd_parts << "--cluster #{cluster}" if options[:cluster]
|
|
1105
|
-
puts " #{cmd_parts.join(' ')}"
|
|
1106
|
-
end
|
|
1107
|
-
|
|
1108
|
-
def format_status(status)
|
|
1109
|
-
Formatters::StatusFormatter.format(status)
|
|
1110
|
-
end
|
|
1111
|
-
|
|
1112
|
-
def generate_agent_name(description)
|
|
1113
|
-
# Simple name generation from description
|
|
1114
|
-
# Take first few words, lowercase, hyphenate
|
|
1115
|
-
words = description.downcase.gsub(/[^a-z0-9\s]/, '').split[0..2]
|
|
1116
|
-
name = words.join('-')
|
|
1117
|
-
# Add random suffix to avoid collisions
|
|
1118
|
-
"#{name}-#{Time.now.to_i.to_s[-4..]}"
|
|
1119
|
-
end
|
|
1120
|
-
|
|
1121
|
-
def watch_synthesis_status(k8s, agent_name, namespace)
|
|
1122
|
-
max_wait = 600 # Wait up to 10 minutes (local models can be slow)
|
|
1123
|
-
interval = 2 # Check every 2 seconds
|
|
1124
|
-
elapsed = 0
|
|
1125
|
-
start_time = Time.now
|
|
1126
|
-
synthesis_data = {}
|
|
1127
|
-
|
|
1128
|
-
result = Formatters::ProgressFormatter.with_spinner('Synthesizing code from instructions') do
|
|
1129
|
-
loop do
|
|
1130
|
-
status = check_synthesis_status(k8s, agent_name, namespace, synthesis_data, start_time)
|
|
1131
|
-
return status if status
|
|
1132
|
-
|
|
1133
|
-
# Timeout check
|
|
1134
|
-
if elapsed >= max_wait
|
|
1135
|
-
Formatters::ProgressFormatter.warn('Synthesis taking longer than expected, continuing in background...')
|
|
1136
|
-
puts
|
|
1137
|
-
puts 'Check synthesis status with:'
|
|
1138
|
-
puts " aictl agent inspect #{agent_name}"
|
|
1139
|
-
return { success: true, timeout: true }
|
|
1140
|
-
end
|
|
1141
|
-
|
|
1142
|
-
sleep interval
|
|
1143
|
-
elapsed += interval
|
|
1144
|
-
end
|
|
1145
|
-
rescue K8s::Error::NotFound
|
|
1146
|
-
# Agent not found yet, keep waiting
|
|
1147
|
-
sleep interval
|
|
1148
|
-
elapsed += interval
|
|
1149
|
-
retry if elapsed < max_wait
|
|
1150
|
-
|
|
1151
|
-
Formatters::ProgressFormatter.error('Agent resource not found')
|
|
1152
|
-
return { success: false }
|
|
1153
|
-
rescue StandardError => e
|
|
1154
|
-
Formatters::ProgressFormatter.warn("Could not watch synthesis: #{e.message}")
|
|
1155
|
-
return { success: true } # Continue anyway
|
|
1156
|
-
end
|
|
1157
|
-
|
|
1158
|
-
# Show synthesis details after spinner completes
|
|
1159
|
-
if result[:success] && !result[:timeout]
|
|
1160
|
-
duration = result[:duration]
|
|
1161
|
-
Formatters::ProgressFormatter.success("Code synthesis completed in #{format_duration(duration)}")
|
|
1162
|
-
puts " Model: #{synthesis_data[:model]}" if synthesis_data[:model]
|
|
1163
|
-
puts " Tokens: #{synthesis_data[:token_count]}" if synthesis_data[:token_count]
|
|
1164
|
-
end
|
|
1165
|
-
|
|
1166
|
-
result
|
|
1167
|
-
end
|
|
1168
|
-
|
|
1169
|
-
def check_synthesis_status(k8s, agent_name, namespace, synthesis_data, start_time)
|
|
1170
|
-
agent = k8s.get_resource('LanguageAgent', agent_name, namespace)
|
|
1171
|
-
conditions = agent.dig('status', 'conditions') || []
|
|
1172
|
-
synthesis_status = agent.dig('status', 'synthesis')
|
|
1173
|
-
|
|
1174
|
-
# Capture synthesis metadata
|
|
1175
|
-
if synthesis_status
|
|
1176
|
-
synthesis_data[:model] = synthesis_status['model']
|
|
1177
|
-
synthesis_data[:token_count] = synthesis_status['tokenCount']
|
|
1178
|
-
end
|
|
1179
|
-
|
|
1180
|
-
# Check for synthesis completion
|
|
1181
|
-
synthesized = conditions.find { |c| c['type'] == 'Synthesized' }
|
|
1182
|
-
return nil unless synthesized
|
|
1183
|
-
|
|
1184
|
-
if synthesized['status'] == 'True'
|
|
1185
|
-
duration = Time.now - start_time
|
|
1186
|
-
{ success: true, duration: duration, **synthesis_data }
|
|
1187
|
-
elsif synthesized['status'] == 'False'
|
|
1188
|
-
Formatters::ProgressFormatter.error("Synthesis failed: #{synthesized['message']}")
|
|
1189
|
-
{ success: false }
|
|
1190
|
-
end
|
|
1191
|
-
end
|
|
1192
|
-
|
|
1193
|
-
def format_duration(seconds)
|
|
1194
|
-
Formatters::ValueFormatter.duration(seconds)
|
|
1195
|
-
end
|
|
1196
|
-
|
|
1197
|
-
def list_cluster_agents(cluster)
|
|
1198
|
-
ctx = Helpers::ClusterContext.from_options(cluster: cluster)
|
|
1199
|
-
|
|
1200
|
-
Formatters::ProgressFormatter.info("Agents in cluster '#{cluster}'")
|
|
1201
|
-
|
|
1202
|
-
agents = ctx.client.list_resources('LanguageAgent', namespace: ctx.namespace)
|
|
1203
|
-
|
|
1204
|
-
table_data = agents.map do |agent|
|
|
1205
|
-
{
|
|
1206
|
-
name: agent.dig('metadata', 'name'),
|
|
1207
|
-
mode: agent.dig('spec', 'mode') || 'autonomous',
|
|
1208
|
-
status: agent.dig('status', 'phase') || 'Unknown',
|
|
1209
|
-
next_run: agent.dig('status', 'nextRun') || 'N/A',
|
|
1210
|
-
executions: agent.dig('status', 'executionCount') || 0
|
|
1211
|
-
}
|
|
1212
|
-
end
|
|
1213
|
-
|
|
1214
|
-
Formatters::TableFormatter.agents(table_data)
|
|
1215
|
-
|
|
1216
|
-
return unless agents.empty?
|
|
1217
|
-
|
|
1218
|
-
puts
|
|
1219
|
-
puts 'Create an agent with:'
|
|
1220
|
-
puts ' aictl agent create "<description>"'
|
|
1221
|
-
end
|
|
1222
|
-
|
|
1223
|
-
def list_all_clusters
|
|
1224
|
-
clusters = Config::ClusterConfig.list_clusters
|
|
1225
|
-
|
|
1226
|
-
if clusters.empty?
|
|
1227
|
-
Formatters::ProgressFormatter.info('No clusters found')
|
|
1228
|
-
puts
|
|
1229
|
-
puts 'Create a cluster first:'
|
|
1230
|
-
puts ' aictl cluster create <name>'
|
|
1231
|
-
return
|
|
1232
|
-
end
|
|
1233
|
-
|
|
1234
|
-
all_agents = []
|
|
1235
|
-
|
|
1236
|
-
clusters.each do |cluster|
|
|
1237
|
-
ctx = Helpers::ClusterContext.from_options(cluster: cluster[:name])
|
|
1238
|
-
|
|
1239
|
-
agents = ctx.client.list_resources('LanguageAgent', namespace: ctx.namespace)
|
|
1240
|
-
|
|
1241
|
-
agents.each do |agent|
|
|
1242
|
-
all_agents << {
|
|
1243
|
-
cluster: cluster[:name],
|
|
1244
|
-
name: agent.dig('metadata', 'name'),
|
|
1245
|
-
mode: agent.dig('spec', 'mode') || 'autonomous',
|
|
1246
|
-
status: agent.dig('status', 'phase') || 'Unknown',
|
|
1247
|
-
next_run: agent.dig('status', 'nextRun') || 'N/A',
|
|
1248
|
-
executions: agent.dig('status', 'executionCount') || 0
|
|
1249
|
-
}
|
|
1250
|
-
end
|
|
1251
|
-
rescue StandardError => e
|
|
1252
|
-
Formatters::ProgressFormatter.warn("Failed to get agents from cluster '#{cluster[:name]}': #{e.message}")
|
|
1253
|
-
end
|
|
1254
|
-
|
|
1255
|
-
# Group agents by cluster for formatted display
|
|
1256
|
-
agents_by_cluster = all_agents.group_by { |agent| agent[:cluster] }
|
|
1257
|
-
.transform_values { |agents| agents.map { |a| a.except(:cluster) } }
|
|
1258
|
-
|
|
1259
|
-
Formatters::TableFormatter.all_agents(agents_by_cluster)
|
|
1260
|
-
end
|
|
1261
|
-
|
|
1262
|
-
# Workspace-related helper methods
|
|
1263
|
-
|
|
1264
|
-
def get_agent_pod(ctx, agent_name)
|
|
1265
|
-
# Find pod for this agent using label selector
|
|
1266
|
-
label_selector = "app.kubernetes.io/name=#{agent_name}"
|
|
1267
|
-
pods = ctx.client.list_resources('Pod', namespace: ctx.namespace, label_selector: label_selector)
|
|
1268
|
-
|
|
1269
|
-
if pods.empty?
|
|
1270
|
-
Formatters::ProgressFormatter.error("No running pods found for agent '#{agent_name}'")
|
|
1271
|
-
puts
|
|
1272
|
-
puts 'Possible reasons:'
|
|
1273
|
-
puts ' - Agent pod has not started yet'
|
|
1274
|
-
puts ' - Agent is paused or stopped'
|
|
1275
|
-
puts ' - Agent failed to deploy'
|
|
1276
|
-
puts
|
|
1277
|
-
puts 'Check agent status with:'
|
|
1278
|
-
puts " aictl agent inspect #{agent_name}"
|
|
1279
|
-
exit 1
|
|
1280
|
-
end
|
|
1281
|
-
|
|
1282
|
-
# Find a running pod
|
|
1283
|
-
running_pod = pods.find do |pod|
|
|
1284
|
-
pod.dig('status', 'phase') == 'Running'
|
|
1285
|
-
end
|
|
1286
|
-
|
|
1287
|
-
unless running_pod
|
|
1288
|
-
Formatters::ProgressFormatter.error('Agent pod exists but is not running')
|
|
1289
|
-
puts
|
|
1290
|
-
puts "Current pod status: #{pods.first.dig('status', 'phase')}"
|
|
1291
|
-
puts
|
|
1292
|
-
puts 'Check pod logs with:'
|
|
1293
|
-
puts " aictl agent logs #{agent_name}"
|
|
1294
|
-
exit 1
|
|
1295
|
-
end
|
|
1296
|
-
|
|
1297
|
-
running_pod.dig('metadata', 'name')
|
|
1298
|
-
end
|
|
1299
|
-
|
|
1300
|
-
def exec_in_pod(ctx, pod_name, command)
|
|
1301
|
-
# Properly escape command for shell
|
|
1302
|
-
cmd_str = command.is_a?(Array) ? command.join(' ') : command
|
|
1303
|
-
kubectl_cmd = "#{ctx.kubectl_prefix} exec #{pod_name} -- #{cmd_str}"
|
|
1304
|
-
|
|
1305
|
-
# Execute and capture output
|
|
1306
|
-
require 'open3'
|
|
1307
|
-
stdout, stderr, status = Open3.capture3(kubectl_cmd)
|
|
1308
|
-
|
|
1309
|
-
raise "Command failed: #{stderr}" unless status.success?
|
|
1310
|
-
|
|
1311
|
-
stdout
|
|
1312
|
-
end
|
|
1313
|
-
|
|
1314
|
-
def list_workspace_files(ctx, agent_name)
|
|
1315
|
-
pod_name = get_agent_pod(ctx, agent_name)
|
|
1316
|
-
|
|
1317
|
-
# Check if workspace directory exists
|
|
1318
|
-
begin
|
|
1319
|
-
exec_in_pod(ctx, pod_name, 'test -d /workspace')
|
|
1320
|
-
rescue StandardError
|
|
1321
|
-
Formatters::ProgressFormatter.error('Workspace directory not found in agent pod')
|
|
1322
|
-
puts
|
|
1323
|
-
puts 'The /workspace directory does not exist in the agent pod.'
|
|
1324
|
-
puts 'This agent may not have workspace support enabled.'
|
|
1325
|
-
exit 1
|
|
1326
|
-
end
|
|
1327
|
-
|
|
1328
|
-
# Get workspace usage
|
|
1329
|
-
usage_output = exec_in_pod(
|
|
1330
|
-
ctx,
|
|
1331
|
-
pod_name,
|
|
1332
|
-
'du -sh /workspace 2>/dev/null || echo "0\t/workspace"'
|
|
1333
|
-
)
|
|
1334
|
-
workspace_size = usage_output.split("\t").first.strip
|
|
1335
|
-
|
|
1336
|
-
# List files with details
|
|
1337
|
-
file_list = exec_in_pod(
|
|
1338
|
-
ctx,
|
|
1339
|
-
pod_name,
|
|
1340
|
-
'find /workspace -ls 2>/dev/null | tail -n +2'
|
|
1341
|
-
)
|
|
1342
|
-
|
|
1343
|
-
puts
|
|
1344
|
-
puts pastel.cyan("Workspace for agent '#{agent_name}' (#{workspace_size})")
|
|
1345
|
-
puts '=' * 60
|
|
1346
|
-
puts
|
|
1347
|
-
|
|
1348
|
-
if file_list.strip.empty?
|
|
1349
|
-
puts pastel.dim('Workspace is empty')
|
|
1350
|
-
puts
|
|
1351
|
-
puts 'The agent will create files here as it runs.'
|
|
1352
|
-
puts
|
|
1353
|
-
return
|
|
1354
|
-
end
|
|
1355
|
-
|
|
1356
|
-
# Parse and display file list
|
|
1357
|
-
file_list.each_line do |line|
|
|
1358
|
-
parts = line.strip.split(/\s+/, 11)
|
|
1359
|
-
next if parts.length < 11
|
|
1360
|
-
|
|
1361
|
-
# Extract relevant parts
|
|
1362
|
-
# Format: inode blocks perms links user group size month day time path
|
|
1363
|
-
perms = parts[2]
|
|
1364
|
-
size = parts[6]
|
|
1365
|
-
month = parts[7]
|
|
1366
|
-
day = parts[8]
|
|
1367
|
-
time_or_year = parts[9]
|
|
1368
|
-
path = parts[10]
|
|
1369
|
-
|
|
1370
|
-
# Skip the /workspace directory itself
|
|
1371
|
-
next if path == '/workspace'
|
|
1372
|
-
|
|
1373
|
-
# Determine file type and icon
|
|
1374
|
-
icon = if perms.start_with?('d')
|
|
1375
|
-
pastel.blue('📁')
|
|
1376
|
-
else
|
|
1377
|
-
pastel.white('📄')
|
|
1378
|
-
end
|
|
1379
|
-
|
|
1380
|
-
# Format path relative to workspace
|
|
1381
|
-
relative_path = path.sub('/workspace/', '')
|
|
1382
|
-
indent = ' ' * relative_path.count('/')
|
|
1383
|
-
|
|
1384
|
-
# Format size
|
|
1385
|
-
formatted_size = format_file_size(size.to_i).rjust(8)
|
|
1386
|
-
|
|
1387
|
-
# Format time
|
|
1388
|
-
formatted_time = "#{month} #{day.rjust(2)} #{time_or_year}"
|
|
1389
|
-
|
|
1390
|
-
puts "#{indent}#{icon} #{File.basename(relative_path).ljust(30)} #{pastel.dim(formatted_size)} #{pastel.dim(formatted_time)}"
|
|
1391
|
-
end
|
|
1392
|
-
|
|
1393
|
-
puts
|
|
1394
|
-
puts pastel.dim('Commands:')
|
|
1395
|
-
puts pastel.dim(" aictl agent workspace #{agent_name} --path /workspace/<file> # View file")
|
|
1396
|
-
puts pastel.dim(" aictl agent workspace #{agent_name} --clean # Clear workspace")
|
|
1397
|
-
puts
|
|
1398
|
-
end
|
|
1399
|
-
|
|
1400
|
-
def view_workspace_file(ctx, agent_name, file_path)
|
|
1401
|
-
pod_name = get_agent_pod(ctx, agent_name)
|
|
1402
|
-
|
|
1403
|
-
# Check if file exists
|
|
1404
|
-
begin
|
|
1405
|
-
exec_in_pod(ctx, pod_name, "test -f #{file_path}")
|
|
1406
|
-
rescue StandardError
|
|
1407
|
-
Formatters::ProgressFormatter.error("File not found: #{file_path}")
|
|
1408
|
-
puts
|
|
1409
|
-
puts 'List available files with:'
|
|
1410
|
-
puts " aictl agent workspace #{agent_name}"
|
|
1411
|
-
exit 1
|
|
1412
|
-
end
|
|
1413
|
-
|
|
1414
|
-
# Get file metadata
|
|
1415
|
-
stat_output = exec_in_pod(
|
|
1416
|
-
ctx,
|
|
1417
|
-
pod_name,
|
|
1418
|
-
"stat -c '%s %Y' #{file_path}"
|
|
1419
|
-
)
|
|
1420
|
-
size, mtime = stat_output.strip.split
|
|
1421
|
-
|
|
1422
|
-
# Get file contents
|
|
1423
|
-
contents = exec_in_pod(
|
|
1424
|
-
ctx,
|
|
1425
|
-
pod_name,
|
|
1426
|
-
"cat #{file_path}"
|
|
1427
|
-
)
|
|
1428
|
-
|
|
1429
|
-
# Display file
|
|
1430
|
-
puts
|
|
1431
|
-
puts pastel.cyan("File: #{file_path}")
|
|
1432
|
-
puts "Size: #{format_file_size(size.to_i)}"
|
|
1433
|
-
puts "Modified: #{format_timestamp(Time.at(mtime.to_i))}"
|
|
1434
|
-
puts '=' * 60
|
|
1435
|
-
puts
|
|
1436
|
-
puts contents
|
|
1437
|
-
puts
|
|
1438
|
-
end
|
|
1439
|
-
|
|
1440
|
-
def clean_workspace(ctx, agent_name)
|
|
1441
|
-
pod_name = get_agent_pod(ctx, agent_name)
|
|
1442
|
-
|
|
1443
|
-
# Get current workspace usage
|
|
1444
|
-
usage_output = exec_in_pod(
|
|
1445
|
-
ctx,
|
|
1446
|
-
pod_name,
|
|
1447
|
-
'du -sh /workspace 2>/dev/null || echo "0\t/workspace"'
|
|
1448
|
-
)
|
|
1449
|
-
workspace_size = usage_output.split("\t").first.strip
|
|
1450
|
-
|
|
1451
|
-
# Count files
|
|
1452
|
-
file_count = exec_in_pod(
|
|
1453
|
-
ctx,
|
|
1454
|
-
pod_name,
|
|
1455
|
-
'find /workspace -type f | wc -l'
|
|
1456
|
-
).strip.to_i
|
|
1457
|
-
|
|
1458
|
-
puts
|
|
1459
|
-
puts pastel.yellow("This will delete ALL files in the workspace for '#{agent_name}'")
|
|
1460
|
-
puts
|
|
1461
|
-
puts 'The agent will lose:'
|
|
1462
|
-
puts ' • Execution history'
|
|
1463
|
-
puts ' • Cached data'
|
|
1464
|
-
puts ' • State information'
|
|
1465
|
-
puts
|
|
1466
|
-
puts "Current workspace: #{file_count} files, #{workspace_size}"
|
|
1467
|
-
puts
|
|
1468
|
-
|
|
1469
|
-
# Use UserPrompts helper
|
|
1470
|
-
return unless Helpers::UserPrompts.confirm('Are you sure?')
|
|
1471
|
-
|
|
1472
|
-
# Delete all files in workspace
|
|
1473
|
-
Formatters::ProgressFormatter.with_spinner('Cleaning workspace') do
|
|
1474
|
-
exec_in_pod(
|
|
1475
|
-
ctx,
|
|
1476
|
-
pod_name,
|
|
1477
|
-
'find /workspace -mindepth 1 -delete'
|
|
1478
|
-
)
|
|
1479
|
-
end
|
|
1480
|
-
|
|
1481
|
-
Formatters::ProgressFormatter.success("Workspace cleared (freed #{workspace_size})")
|
|
1482
|
-
puts
|
|
1483
|
-
puts 'The agent will start fresh on its next execution.'
|
|
1484
|
-
end
|
|
1485
|
-
|
|
1486
|
-
def format_file_size(bytes)
|
|
1487
|
-
Formatters::ValueFormatter.file_size(bytes)
|
|
1488
|
-
end
|
|
1489
|
-
|
|
1490
|
-
def format_timestamp(time)
|
|
1491
|
-
Formatters::ValueFormatter.timestamp(time)
|
|
1492
|
-
end
|
|
1493
|
-
|
|
1494
|
-
# Load agent definition from ConfigMap
|
|
1495
|
-
def load_agent_definition(ctx, agent_name)
|
|
1496
|
-
# Try to get the agent code ConfigMap
|
|
1497
|
-
configmap_name = "#{agent_name}-code"
|
|
1498
|
-
begin
|
|
1499
|
-
configmap = ctx.client.get_resource('ConfigMap', configmap_name, ctx.namespace)
|
|
1500
|
-
code_content = configmap.dig('data', 'agent.rb')
|
|
1501
|
-
|
|
1502
|
-
return nil unless code_content
|
|
1503
|
-
|
|
1504
|
-
# Parse the code to extract agent definition
|
|
1505
|
-
# For now, we'll create a mock definition with the task structure
|
|
1506
|
-
# In a full implementation, this would eval the code safely
|
|
1507
|
-
parse_agent_code(code_content)
|
|
1508
|
-
rescue K8s::Error::NotFound
|
|
1509
|
-
nil
|
|
1510
|
-
rescue StandardError => e
|
|
1511
|
-
@logger&.error("Failed to load agent definition: #{e.message}")
|
|
1512
|
-
nil
|
|
1513
|
-
end
|
|
1514
|
-
end
|
|
1515
|
-
|
|
1516
|
-
# Parse agent code to extract definition
|
|
1517
|
-
def parse_agent_code(code)
|
|
1518
|
-
require_relative '../../dsl/agent_definition'
|
|
1519
|
-
|
|
1520
|
-
# Create a minimal agent definition structure
|
|
1521
|
-
agent_def = Struct.new(:tasks, :name, :mcp_servers) do
|
|
1522
|
-
def initialize
|
|
1523
|
-
super({}, 'agent', {})
|
|
1524
|
-
end
|
|
1525
|
-
end
|
|
1526
|
-
|
|
1527
|
-
agent = agent_def.new
|
|
1528
|
-
|
|
1529
|
-
# Parse tasks from code - extract full task definitions
|
|
1530
|
-
code.scan(/task\s+:(\w+),?\s*(.*?)(?=\n\s*(?:task\s+:|main\s+do|end\s*$))/m) do |match|
|
|
1531
|
-
task_name = match[0].to_sym
|
|
1532
|
-
task_block = match[1]
|
|
1533
|
-
|
|
1534
|
-
# Check if neural (has instructions but no do block) or symbolic
|
|
1535
|
-
is_neural = task_block.include?('instructions:') && !task_block.match?(/\bdo\s*\|/)
|
|
1536
|
-
|
|
1537
|
-
# Extract instructions
|
|
1538
|
-
instructions = extract_string_value(task_block, 'instructions')
|
|
1539
|
-
|
|
1540
|
-
# Extract inputs hash
|
|
1541
|
-
inputs = extract_hash_value(task_block, 'inputs')
|
|
1542
|
-
|
|
1543
|
-
# Extract outputs hash
|
|
1544
|
-
outputs = extract_hash_value(task_block, 'outputs')
|
|
1545
|
-
|
|
1546
|
-
task = Struct.new(:name, :neural?, :instructions, :inputs, :outputs).new(
|
|
1547
|
-
task_name, is_neural, instructions, inputs, outputs
|
|
1548
|
-
)
|
|
1549
|
-
|
|
1550
|
-
agent.tasks[task_name] = task
|
|
1551
|
-
end
|
|
1552
|
-
|
|
1553
|
-
agent
|
|
1554
|
-
end
|
|
1555
|
-
|
|
1556
|
-
# Extract a string value from DSL code (e.g., instructions: "...")
|
|
1557
|
-
def extract_string_value(code, key)
|
|
1558
|
-
# Match both single and double quoted strings, including multi-line
|
|
1559
|
-
match = code.match(/#{key}:\s*(['"])(.*?)\1/m) ||
|
|
1560
|
-
code.match(/#{key}:\s*(['"])(.+?)\1/m)
|
|
1561
|
-
match ? match[2] : ''
|
|
1562
|
-
end
|
|
1563
|
-
|
|
1564
|
-
# Extract a hash value from DSL code (e.g., inputs: { foo: 'bar' })
|
|
1565
|
-
def extract_hash_value(code, key)
|
|
1566
|
-
match = code.match(/#{key}:\s*\{([^}]*)\}/)
|
|
1567
|
-
return {} unless match
|
|
1568
|
-
|
|
1569
|
-
hash_content = match[1].strip
|
|
1570
|
-
return {} if hash_content.empty?
|
|
1571
|
-
|
|
1572
|
-
# Parse simple key: 'value' or key: "value" pairs
|
|
1573
|
-
result = {}
|
|
1574
|
-
hash_content.scan(/(\w+):\s*(['"])([^'"]*)\2/) do |k, _quote, v|
|
|
1575
|
-
result[k.to_sym] = v
|
|
1576
|
-
end
|
|
1577
|
-
result
|
|
1578
|
-
end
|
|
1579
|
-
|
|
1580
|
-
# Prompt user for optimization acceptance
|
|
1581
|
-
def prompt_for_optimization_acceptance(proposal)
|
|
1582
|
-
require 'tty-prompt'
|
|
1583
|
-
prompt = TTY::Prompt.new
|
|
1584
|
-
|
|
1585
|
-
choices = [
|
|
1586
|
-
{ name: 'Yes - apply this optimization', value: :yes },
|
|
1587
|
-
{ name: 'No - skip this task', value: :no },
|
|
1588
|
-
{ name: 'View full code diff', value: :diff },
|
|
1589
|
-
{ name: 'Skip all remaining', value: :skip_all }
|
|
1590
|
-
]
|
|
1591
|
-
|
|
1592
|
-
loop do
|
|
1593
|
-
choice = prompt.select(
|
|
1594
|
-
"Accept optimization for '#{proposal[:task_name]}'?",
|
|
1595
|
-
choices,
|
|
1596
|
-
per_page: 10
|
|
1597
|
-
)
|
|
1598
|
-
|
|
1599
|
-
case choice
|
|
1600
|
-
when :yes
|
|
1601
|
-
return true
|
|
1602
|
-
when :no
|
|
1603
|
-
return false
|
|
1604
|
-
when :diff
|
|
1605
|
-
show_code_diff(proposal)
|
|
1606
|
-
# Loop to ask again
|
|
1607
|
-
when :skip_all
|
|
1608
|
-
throw :skip_all
|
|
1609
|
-
end
|
|
1610
|
-
end
|
|
1611
|
-
end
|
|
1612
|
-
|
|
1613
|
-
# Show full code diff
|
|
1614
|
-
def show_code_diff(proposal)
|
|
1615
|
-
puts
|
|
1616
|
-
puts pastel.bold('Full Generated Code:')
|
|
1617
|
-
puts pastel.dim('=' * 70)
|
|
1618
|
-
puts proposal[:full_generated_code]
|
|
1619
|
-
puts pastel.dim('=' * 70)
|
|
1620
|
-
puts
|
|
1621
|
-
end
|
|
1622
|
-
|
|
1623
|
-
# Apply optimization by updating ConfigMap and restarting pod
|
|
1624
|
-
def apply_optimization(ctx, agent_name, proposal)
|
|
1625
|
-
configmap_name = "#{agent_name}-code"
|
|
1626
|
-
task_name = proposal[:task_name]
|
|
1627
|
-
|
|
1628
|
-
# Get current ConfigMap
|
|
1629
|
-
configmap = ctx.client.get_resource('ConfigMap', configmap_name, ctx.namespace)
|
|
1630
|
-
current_code = configmap.dig('data', 'agent.rb')
|
|
1631
|
-
|
|
1632
|
-
raise "ConfigMap '#{configmap_name}' does not contain agent.rb" unless current_code
|
|
1633
|
-
|
|
1634
|
-
# Replace the neural task with the symbolic implementation
|
|
1635
|
-
updated_code = replace_task_in_code(current_code, task_name, proposal[:proposed_code])
|
|
1636
|
-
|
|
1637
|
-
# Build updated ConfigMap resource
|
|
1638
|
-
# Add annotation to prevent controller from overwriting optimized code
|
|
1639
|
-
updated_configmap = {
|
|
1640
|
-
'apiVersion' => 'v1',
|
|
1641
|
-
'kind' => 'ConfigMap',
|
|
1642
|
-
'metadata' => {
|
|
1643
|
-
'name' => configmap_name,
|
|
1644
|
-
'namespace' => ctx.namespace,
|
|
1645
|
-
'resourceVersion' => configmap.metadata.resourceVersion,
|
|
1646
|
-
'annotations' => {
|
|
1647
|
-
'langop.io/optimized' => 'true',
|
|
1648
|
-
'langop.io/optimized-at' => Time.now.iso8601,
|
|
1649
|
-
'langop.io/optimized-task' => task_name
|
|
1650
|
-
}
|
|
1651
|
-
},
|
|
1652
|
-
'data' => {
|
|
1653
|
-
'agent.rb' => updated_code
|
|
1654
|
-
}
|
|
1655
|
-
}
|
|
1656
|
-
|
|
1657
|
-
# Update ConfigMap
|
|
1658
|
-
ctx.client.update_resource('ConfigMap', configmap_name, ctx.namespace, updated_configmap, 'v1')
|
|
1659
|
-
|
|
1660
|
-
# Restart the agent pod to pick up changes
|
|
1661
|
-
restart_agent_pod(ctx, agent_name)
|
|
1662
|
-
|
|
1663
|
-
{
|
|
1664
|
-
success: true,
|
|
1665
|
-
task_name: task_name,
|
|
1666
|
-
updated_code: proposal[:proposed_code],
|
|
1667
|
-
action: 'applied',
|
|
1668
|
-
message: "Optimization for '#{task_name}' applied successfully"
|
|
1669
|
-
}
|
|
1670
|
-
rescue StandardError => e
|
|
1671
|
-
{
|
|
1672
|
-
success: false,
|
|
1673
|
-
task_name: task_name,
|
|
1674
|
-
error: e.message,
|
|
1675
|
-
action: 'failed',
|
|
1676
|
-
message: "Failed to apply optimization: #{e.message}"
|
|
1677
|
-
}
|
|
1678
|
-
end
|
|
1679
|
-
|
|
1680
|
-
# Replace a task definition in agent code
|
|
1681
|
-
def replace_task_in_code(code, task_name, new_task_code)
|
|
1682
|
-
# Match the task definition including any trailing do block
|
|
1683
|
-
# Pattern matches: task :name, ... (neural) or task :name, ... do |inputs| ... end (symbolic)
|
|
1684
|
-
task_pattern = /task\s+:#{Regexp.escape(task_name.to_s)},?\s*.*?(?=\n\s*(?:task\s+:|main\s+do|end\s*$))/m
|
|
1685
|
-
|
|
1686
|
-
raise "Could not find task ':#{task_name}' in agent code" unless code.match?(task_pattern)
|
|
1687
|
-
|
|
1688
|
-
# Ensure new_task_code has proper trailing newline
|
|
1689
|
-
new_code = "#{new_task_code.strip}\n\n"
|
|
1690
|
-
|
|
1691
|
-
code.gsub(task_pattern, new_code.strip)
|
|
1692
|
-
end
|
|
1693
|
-
|
|
1694
|
-
# Restart agent pod by deleting it (Deployment will recreate)
|
|
1695
|
-
def restart_agent_pod(ctx, agent_name)
|
|
1696
|
-
# Find pods for this agent
|
|
1697
|
-
pods = ctx.client.list_resources('Pod', namespace: ctx.namespace, label_selector: "app=#{agent_name}")
|
|
1698
|
-
|
|
1699
|
-
pods.each do |pod|
|
|
1700
|
-
pod_name = pod.dig('metadata', 'name')
|
|
1701
|
-
begin
|
|
1702
|
-
ctx.client.delete_resource('Pod', pod_name, ctx.namespace)
|
|
1703
|
-
Formatters::ProgressFormatter.info("Restarting pod '#{pod_name}'")
|
|
1704
|
-
rescue StandardError => e
|
|
1705
|
-
Formatters::ProgressFormatter.warn("Could not delete pod '#{pod_name}': #{e.message}")
|
|
1706
|
-
end
|
|
1707
|
-
end
|
|
1708
|
-
end
|
|
1709
|
-
end
|
|
1710
|
-
end
|
|
1711
|
-
end
|
|
1712
|
-
end
|