language-operator 0.1.63 → 0.1.66
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/.plan.md +127 -0
- data/.rspec +3 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +4 -1
- data/Makefile +34 -80
- data/README.md +20 -1
- data/components/agent/Gemfile +1 -1
- data/docs/cheat-sheet.md +173 -0
- data/docs/observability.md +208 -0
- data/lib/language_operator/agent/base.rb +10 -1
- data/lib/language_operator/agent/event_config.rb +172 -0
- data/lib/language_operator/agent/safety/ast_validator.rb +1 -1
- data/lib/language_operator/agent/safety/safe_executor.rb +5 -1
- data/lib/language_operator/agent/task_executor.rb +97 -7
- data/lib/language_operator/agent/telemetry.rb +25 -3
- data/lib/language_operator/agent/web_server.rb +6 -9
- data/lib/language_operator/agent.rb +24 -14
- data/lib/language_operator/cli/commands/agent/base.rb +155 -64
- data/lib/language_operator/cli/commands/agent/code_operations.rb +157 -16
- data/lib/language_operator/cli/commands/cluster.rb +2 -2
- data/lib/language_operator/cli/commands/status.rb +2 -2
- data/lib/language_operator/cli/commands/system/synthesize.rb +1 -1
- data/lib/language_operator/cli/errors/suggestions.rb +1 -1
- data/lib/language_operator/cli/formatters/value_formatter.rb +1 -1
- data/lib/language_operator/cli/helpers/ux_helper.rb +3 -4
- data/lib/language_operator/config.rb +3 -3
- data/lib/language_operator/constants/kubernetes_labels.rb +2 -2
- data/lib/language_operator/constants.rb +1 -0
- data/lib/language_operator/dsl/task_definition.rb +18 -7
- data/lib/language_operator/instrumentation/task_tracer.rb +44 -3
- data/lib/language_operator/kubernetes/client.rb +112 -1
- data/lib/language_operator/templates/schema/CHANGELOG.md +28 -0
- 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 +22 -8
- data/lib/language_operator/version.rb +1 -1
- data/synth/002/agent.rb +23 -12
- data/synth/002/output.log +88 -15
- data/synth/003/Makefile +17 -4
- data/synth/003/agent.txt +1 -1
- data/synth/004/Makefile +54 -0
- data/synth/004/README.md +281 -0
- data/synth/004/instructions.txt +1 -0
- metadata +11 -6
- data/lib/language_operator/cli/commands/agent/learning.rb +0 -289
- data/synth/003/agent.optimized.rb +0 -66
- data/synth/003/agent.synthesized.rb +0 -41
|
@@ -4,6 +4,7 @@ require_relative 'agent/base'
|
|
|
4
4
|
require_relative 'agent/executor'
|
|
5
5
|
require_relative 'agent/task_executor'
|
|
6
6
|
require_relative 'agent/web_server'
|
|
7
|
+
require_relative 'agent/instrumentation'
|
|
7
8
|
require_relative 'dsl'
|
|
8
9
|
require_relative 'logger'
|
|
9
10
|
|
|
@@ -24,6 +25,8 @@ module LanguageOperator
|
|
|
24
25
|
# agent.execute_goal("Summarize daily news")
|
|
25
26
|
# rubocop:disable Metrics/ModuleLength
|
|
26
27
|
module Agent
|
|
28
|
+
extend LanguageOperator::Agent::Instrumentation
|
|
29
|
+
|
|
27
30
|
# Module-level logger for Agent framework
|
|
28
31
|
@logger = LanguageOperator::Logger.new(component: 'Agent')
|
|
29
32
|
|
|
@@ -215,22 +218,29 @@ module LanguageOperator
|
|
|
215
218
|
agent: agent_def.name,
|
|
216
219
|
task_count: agent_def.tasks.size)
|
|
217
220
|
|
|
218
|
-
#
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
221
|
+
# Execute main block within agent_executor span for learning system integration
|
|
222
|
+
with_span('agent_executor', attributes: {
|
|
223
|
+
'agent.name' => agent_def.name,
|
|
224
|
+
'agent.task_count' => agent_def.tasks.size,
|
|
225
|
+
'agent.mode' => ENV.fetch('AGENT_MODE', 'unknown')
|
|
226
|
+
}) do
|
|
227
|
+
# Get inputs from environment or default to empty hash
|
|
228
|
+
inputs = {}
|
|
229
|
+
|
|
230
|
+
# Execute main block with task executor as context
|
|
231
|
+
result = agent_def.main.call(inputs, task_executor)
|
|
232
|
+
|
|
233
|
+
logger.info('Main block execution completed',
|
|
234
|
+
result: result)
|
|
235
|
+
|
|
236
|
+
# Call output handler if defined
|
|
237
|
+
if agent_def.output
|
|
238
|
+
logger.debug('Executing output handler', outputs: result)
|
|
239
|
+
execute_output_handler(agent_def, result, task_executor)
|
|
240
|
+
end
|
|
226
241
|
|
|
227
|
-
|
|
228
|
-
if agent_def.output
|
|
229
|
-
logger.debug('Executing output handler', outputs: result)
|
|
230
|
-
execute_output_handler(agent_def, result, task_executor)
|
|
242
|
+
result
|
|
231
243
|
end
|
|
232
|
-
|
|
233
|
-
result
|
|
234
244
|
end
|
|
235
245
|
|
|
236
246
|
# Execute main block (DSL v1) in persistent mode for autonomous agents
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'thor'
|
|
4
|
+
require 'json'
|
|
4
5
|
require_relative '../../command_loader'
|
|
5
6
|
require_relative '../../wizards/agent_wizard'
|
|
6
7
|
|
|
@@ -9,7 +10,6 @@ require_relative 'workspace'
|
|
|
9
10
|
require_relative 'code_operations'
|
|
10
11
|
require_relative 'logs'
|
|
11
12
|
require_relative 'lifecycle'
|
|
12
|
-
require_relative 'learning'
|
|
13
13
|
|
|
14
14
|
# Include helper modules
|
|
15
15
|
require_relative 'helpers/cluster_llm_client'
|
|
@@ -35,7 +35,6 @@ module LanguageOperator
|
|
|
35
35
|
include CodeOperations
|
|
36
36
|
include Logs
|
|
37
37
|
include Lifecycle
|
|
38
|
-
include Learning
|
|
39
38
|
|
|
40
39
|
# NOTE: Core commands (create, list, inspect, delete) will be added below
|
|
41
40
|
# This file is a placeholder for the refactoring process
|
|
@@ -173,6 +172,9 @@ module LanguageOperator
|
|
|
173
172
|
# Main agent information
|
|
174
173
|
puts
|
|
175
174
|
status = agent.dig('status', 'phase') || 'Unknown'
|
|
175
|
+
creation_timestamp = agent.dig('metadata', 'creationTimestamp')
|
|
176
|
+
formatted_created = creation_timestamp ? Formatters::ValueFormatter.time_ago(Time.parse(creation_timestamp)) : nil
|
|
177
|
+
|
|
176
178
|
format_agent_details(
|
|
177
179
|
name: name,
|
|
178
180
|
namespace: ctx.namespace,
|
|
@@ -180,8 +182,8 @@ module LanguageOperator
|
|
|
180
182
|
status: format_status(status),
|
|
181
183
|
mode: agent.dig('spec', 'executionMode') || 'autonomous',
|
|
182
184
|
schedule: agent.dig('spec', 'schedule'),
|
|
183
|
-
persona: agent.dig('spec', 'persona'),
|
|
184
|
-
created:
|
|
185
|
+
persona: agent.dig('spec', 'persona') || 'None',
|
|
186
|
+
created: formatted_created
|
|
185
187
|
)
|
|
186
188
|
puts
|
|
187
189
|
|
|
@@ -189,9 +191,8 @@ module LanguageOperator
|
|
|
189
191
|
mode = agent.dig('spec', 'executionMode') || 'autonomous'
|
|
190
192
|
if mode == 'scheduled'
|
|
191
193
|
exec_data = get_execution_data(name, ctx)
|
|
192
|
-
|
|
194
|
+
|
|
193
195
|
exec_rows = {
|
|
194
|
-
'Total Runs' => exec_data[:total_runs],
|
|
195
196
|
'Last Run' => exec_data[:last_run] || 'Never'
|
|
196
197
|
}
|
|
197
198
|
exec_rows['Next Run'] = exec_data[:next_run] || 'N/A' if agent.dig('spec', 'schedule')
|
|
@@ -200,6 +201,10 @@ module LanguageOperator
|
|
|
200
201
|
puts
|
|
201
202
|
end
|
|
202
203
|
|
|
204
|
+
# Learning status
|
|
205
|
+
display_learning_section(agent, name, ctx)
|
|
206
|
+
puts
|
|
207
|
+
|
|
203
208
|
# Resources
|
|
204
209
|
resources = agent.dig('spec', 'resources')
|
|
205
210
|
if resources
|
|
@@ -302,62 +307,71 @@ module LanguageOperator
|
|
|
302
307
|
Formatters::ProgressFormatter.with_spinner("Deleting agent '#{name}'") do
|
|
303
308
|
ctx.client.delete_resource(RESOURCE_AGENT, name, ctx.namespace)
|
|
304
309
|
end
|
|
310
|
+
|
|
311
|
+
# Verify deletion completed
|
|
312
|
+
verify_agent_deletion(ctx, name)
|
|
305
313
|
end
|
|
306
314
|
end
|
|
307
315
|
|
|
308
|
-
|
|
309
|
-
long_desc <<-DESC
|
|
310
|
-
List the versioned ConfigMaps created by the operator for an agent.
|
|
316
|
+
private
|
|
311
317
|
|
|
312
|
-
|
|
318
|
+
# Display learning status section in agent inspect
|
|
319
|
+
def display_learning_section(agent, _name, _ctx)
|
|
320
|
+
annotations = agent.dig('metadata', 'annotations')
|
|
321
|
+
annotations = annotations.respond_to?(:to_h) ? annotations.to_h : (annotations || {})
|
|
313
322
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
aictl agent versions my-agent --cluster production
|
|
317
|
-
DESC
|
|
318
|
-
option :cluster, type: :string, desc: 'Override current cluster context'
|
|
319
|
-
def versions(name)
|
|
320
|
-
handle_command_error('list agent versions') do
|
|
321
|
-
ctx = CLI::Helpers::ClusterContext.from_options(options)
|
|
323
|
+
# Determine learning state
|
|
324
|
+
learning_enabled = !annotations.key?(Constants::KubernetesLabels::LEARNING_DISABLED_LABEL)
|
|
322
325
|
|
|
323
|
-
|
|
324
|
-
|
|
326
|
+
# Get runs pending learning from agent status
|
|
327
|
+
runs_pending_learning = agent.dig('status', 'runsPendingLearning') || 0
|
|
328
|
+
learning_threshold = 10 # Standard threshold
|
|
325
329
|
|
|
326
|
-
|
|
327
|
-
|
|
330
|
+
# Calculate progress percentage
|
|
331
|
+
progress_percent = [(runs_pending_learning.to_f / learning_threshold * 100).round, 100].min
|
|
332
|
+
runs_display = if runs_pending_learning >= learning_threshold
|
|
333
|
+
"#{runs_pending_learning}/#{learning_threshold} #{pastel.green('(Ready)')}"
|
|
334
|
+
else
|
|
335
|
+
"#{runs_pending_learning}/#{learning_threshold} (#{progress_percent}%)"
|
|
336
|
+
end
|
|
328
337
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
labels = cm.dig('metadata', 'labels') || {}
|
|
332
|
-
labels['agent'] == name && labels['version']
|
|
333
|
-
end
|
|
338
|
+
status_color = learning_enabled ? :green : :yellow
|
|
339
|
+
status_text = learning_enabled ? 'Enabled' : 'Disabled'
|
|
334
340
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
341
|
+
highlighted_box(
|
|
342
|
+
title: 'Learning',
|
|
343
|
+
color: :cyan,
|
|
344
|
+
rows: {
|
|
345
|
+
'Status' => pastel.send(status_color).bold(status_text),
|
|
346
|
+
'Threshold' => "#{pastel.cyan('10 successful runs')} (auto-learning trigger)",
|
|
347
|
+
'Confidence Target' => "#{pastel.cyan('85%')} (pattern detection)",
|
|
348
|
+
'Runs Recorded' => runs_display
|
|
349
|
+
}
|
|
350
|
+
)
|
|
344
351
|
end
|
|
345
352
|
|
|
346
|
-
private
|
|
347
|
-
|
|
348
353
|
# Shared helper methods that are used across multiple commands
|
|
349
354
|
# These will be extracted from the original agent.rb
|
|
350
355
|
|
|
351
|
-
def handle_agent_not_found(name, ctx, error)
|
|
356
|
+
def handle_agent_not_found(name, ctx, error = nil)
|
|
352
357
|
# Get available agents for fuzzy matching
|
|
353
358
|
agents = ctx.client.list_resources(RESOURCE_AGENT, namespace: ctx.namespace)
|
|
354
359
|
available_names = agents.map { |a| a.dig('metadata', 'name') }
|
|
355
360
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
+
# Create error if not provided
|
|
362
|
+
error ||= K8s::Error::NotFound.new('GET', "/apis/langop.io/v1alpha1/namespaces/#{ctx.namespace}/languageagents/#{name}", 404, 'Not Found')
|
|
363
|
+
|
|
364
|
+
begin
|
|
365
|
+
CLI::Errors::Handler.handle_not_found(error, {
|
|
366
|
+
resource_type: RESOURCE_AGENT,
|
|
367
|
+
resource_name: name,
|
|
368
|
+
cluster: ctx.name,
|
|
369
|
+
available_resources: available_names
|
|
370
|
+
})
|
|
371
|
+
rescue CLI::Errors::NotFoundError
|
|
372
|
+
# Error message already displayed by handler, just exit gracefully
|
|
373
|
+
exit 1
|
|
374
|
+
end
|
|
361
375
|
end
|
|
362
376
|
|
|
363
377
|
def display_agent_created(agent, ctx, _description, _synthesis_result)
|
|
@@ -372,8 +386,8 @@ module LanguageOperator
|
|
|
372
386
|
status: format_status(status),
|
|
373
387
|
mode: agent.dig('spec', 'executionMode') || 'autonomous',
|
|
374
388
|
schedule: agent.dig('spec', 'schedule'),
|
|
375
|
-
persona: agent.dig('spec', 'persona') || '
|
|
376
|
-
created:
|
|
389
|
+
persona: agent.dig('spec', 'persona') || 'None',
|
|
390
|
+
created: 'just now'
|
|
377
391
|
)
|
|
378
392
|
|
|
379
393
|
puts
|
|
@@ -526,11 +540,17 @@ module LanguageOperator
|
|
|
526
540
|
end
|
|
527
541
|
|
|
528
542
|
table_data = agents.map do |agent|
|
|
543
|
+
status = if agent.dig('metadata', 'deletionTimestamp')
|
|
544
|
+
'Pending Deletion'
|
|
545
|
+
else
|
|
546
|
+
agent.dig('status', 'phase') || 'Unknown'
|
|
547
|
+
end
|
|
548
|
+
|
|
529
549
|
{
|
|
530
550
|
name: agent.dig('metadata', 'name'),
|
|
531
551
|
namespace: agent.dig('metadata', 'namespace') || context.namespace,
|
|
532
552
|
mode: agent.dig('spec', 'executionMode') || 'autonomous',
|
|
533
|
-
status:
|
|
553
|
+
status: status
|
|
534
554
|
}
|
|
535
555
|
end
|
|
536
556
|
|
|
@@ -556,11 +576,17 @@ module LanguageOperator
|
|
|
556
576
|
agents = ctx.client.list_resources(RESOURCE_AGENT, namespace: ctx.namespace)
|
|
557
577
|
|
|
558
578
|
agents.each do |agent|
|
|
579
|
+
status = if agent.dig('metadata', 'deletionTimestamp')
|
|
580
|
+
'Pending Deletion'
|
|
581
|
+
else
|
|
582
|
+
agent.dig('status', 'phase') || 'Unknown'
|
|
583
|
+
end
|
|
584
|
+
|
|
559
585
|
all_agents << {
|
|
560
586
|
cluster: cluster[:name],
|
|
561
587
|
name: agent.dig('metadata', 'name'),
|
|
562
588
|
mode: agent.dig('spec', 'executionMode') || 'autonomous',
|
|
563
|
-
status:
|
|
589
|
+
status: status,
|
|
564
590
|
next_run: agent.dig('status', 'nextRun') || 'N/A',
|
|
565
591
|
executions: agent.dig('status', 'executionCount') || 0
|
|
566
592
|
}
|
|
@@ -740,7 +766,7 @@ module LanguageOperator
|
|
|
740
766
|
begin
|
|
741
767
|
# Get CronJob to find last execution time and next run
|
|
742
768
|
cronjob = ctx.client.get_resource('CronJob', agent_name, ctx.namespace)
|
|
743
|
-
|
|
769
|
+
|
|
744
770
|
# Get last successful execution time
|
|
745
771
|
last_successful = cronjob.dig('status', 'lastSuccessfulTime')
|
|
746
772
|
if last_successful
|
|
@@ -750,9 +776,7 @@ module LanguageOperator
|
|
|
750
776
|
|
|
751
777
|
# Calculate next run time from schedule
|
|
752
778
|
schedule = cronjob.dig('spec', 'schedule')
|
|
753
|
-
if schedule
|
|
754
|
-
execution_data[:next_run] = calculate_next_run(schedule)
|
|
755
|
-
end
|
|
779
|
+
execution_data[:next_run] = calculate_next_run(schedule) if schedule
|
|
756
780
|
rescue K8s::Error::NotFound, StandardError
|
|
757
781
|
# CronJob not found or parsing error, continue with job counting
|
|
758
782
|
end
|
|
@@ -761,7 +785,7 @@ module LanguageOperator
|
|
|
761
785
|
begin
|
|
762
786
|
# Count total completed jobs for this agent
|
|
763
787
|
jobs = ctx.client.list_resources('Job', namespace: ctx.namespace)
|
|
764
|
-
|
|
788
|
+
|
|
765
789
|
agent_jobs = jobs.select do |job|
|
|
766
790
|
labels = job.dig('metadata', 'labels') || {}
|
|
767
791
|
labels['app.kubernetes.io/name'] == agent_name
|
|
@@ -772,7 +796,7 @@ module LanguageOperator
|
|
|
772
796
|
conditions = job.dig('status', 'conditions') || []
|
|
773
797
|
conditions.any? { |c| c['type'] == 'Complete' && c['status'] == 'True' }
|
|
774
798
|
end
|
|
775
|
-
|
|
799
|
+
|
|
776
800
|
execution_data[:total_runs] = successful_jobs.length
|
|
777
801
|
rescue StandardError
|
|
778
802
|
# If job listing fails, keep default count of 0
|
|
@@ -784,32 +808,32 @@ module LanguageOperator
|
|
|
784
808
|
def calculate_next_run(schedule)
|
|
785
809
|
# Simple next run calculation for common cron patterns
|
|
786
810
|
# Handle the most common case: */N * * * * (every N minutes)
|
|
787
|
-
|
|
811
|
+
|
|
788
812
|
parts = schedule.split
|
|
789
813
|
return schedule unless parts.length == 5 # Not a valid cron expression
|
|
790
|
-
|
|
814
|
+
|
|
791
815
|
minute, hour, day, month, weekday = parts
|
|
792
816
|
current_time = Time.now
|
|
793
|
-
|
|
817
|
+
|
|
794
818
|
# Handle every-N-minutes pattern: */10 * * * *
|
|
795
819
|
if minute.start_with?('*/') && hour == '*' && day == '*' && month == '*' && weekday == '*'
|
|
796
820
|
interval = minute[2..].to_i
|
|
797
821
|
if interval > 0 && interval < 60
|
|
798
822
|
current_minute = current_time.min
|
|
799
|
-
|
|
800
|
-
|
|
823
|
+
current_time.sec
|
|
824
|
+
|
|
801
825
|
# Find the next occurrence
|
|
802
826
|
next_minute_mark = ((current_minute / interval) + 1) * interval
|
|
803
|
-
|
|
827
|
+
|
|
804
828
|
if next_minute_mark < 60
|
|
805
829
|
# Same hour
|
|
806
|
-
next_time = Time.new(current_time.year, current_time.month, current_time.day,
|
|
830
|
+
next_time = Time.new(current_time.year, current_time.month, current_time.day,
|
|
807
831
|
current_time.hour, next_minute_mark, 0)
|
|
808
832
|
else
|
|
809
833
|
# Next hour
|
|
810
834
|
next_hour = current_time.hour + 1
|
|
811
835
|
next_minute = next_minute_mark - 60
|
|
812
|
-
|
|
836
|
+
|
|
813
837
|
if next_hour < 24
|
|
814
838
|
next_time = Time.new(current_time.year, current_time.month, current_time.day,
|
|
815
839
|
next_hour, next_minute, 0)
|
|
@@ -820,16 +844,83 @@ module LanguageOperator
|
|
|
820
844
|
0, next_minute, 0)
|
|
821
845
|
end
|
|
822
846
|
end
|
|
823
|
-
|
|
847
|
+
|
|
824
848
|
return Formatters::ValueFormatter.time_until(next_time)
|
|
825
849
|
end
|
|
826
850
|
end
|
|
827
|
-
|
|
851
|
+
|
|
828
852
|
# For other patterns, show the schedule (could add more patterns later)
|
|
829
853
|
schedule
|
|
830
854
|
rescue StandardError
|
|
831
855
|
schedule
|
|
832
856
|
end
|
|
857
|
+
|
|
858
|
+
def verify_agent_deletion(ctx, name)
|
|
859
|
+
max_wait = 30 # Wait up to 30 seconds
|
|
860
|
+
interval = 2 # Check every 2 seconds
|
|
861
|
+
elapsed = 0
|
|
862
|
+
|
|
863
|
+
Formatters::ProgressFormatter.with_spinner('Verifying deletion') do
|
|
864
|
+
loop do
|
|
865
|
+
begin
|
|
866
|
+
agent = ctx.client.get_resource(RESOURCE_AGENT, name, ctx.namespace)
|
|
867
|
+
|
|
868
|
+
# Check if deletion is stuck on finalizers
|
|
869
|
+
deletion_timestamp = agent.dig('metadata', 'deletionTimestamp')
|
|
870
|
+
if deletion_timestamp
|
|
871
|
+
finalizers = agent.dig('metadata', 'finalizers') || []
|
|
872
|
+
if finalizers.any?
|
|
873
|
+
if elapsed >= max_wait
|
|
874
|
+
deletion_stuck_error(name, finalizers)
|
|
875
|
+
return
|
|
876
|
+
end
|
|
877
|
+
end
|
|
878
|
+
end
|
|
879
|
+
rescue K8s::Error::NotFound
|
|
880
|
+
# Agent successfully deleted
|
|
881
|
+
break
|
|
882
|
+
end
|
|
883
|
+
|
|
884
|
+
if elapsed >= max_wait
|
|
885
|
+
deletion_timeout_error(name)
|
|
886
|
+
return
|
|
887
|
+
end
|
|
888
|
+
|
|
889
|
+
sleep interval
|
|
890
|
+
elapsed += interval
|
|
891
|
+
end
|
|
892
|
+
end
|
|
893
|
+
|
|
894
|
+
# Deletion verified - no additional success message needed
|
|
895
|
+
end
|
|
896
|
+
|
|
897
|
+
def deletion_stuck_error(name, finalizers)
|
|
898
|
+
puts
|
|
899
|
+
Formatters::ProgressFormatter.error("Deletion of agent '#{name}' is stuck")
|
|
900
|
+
puts
|
|
901
|
+
puts "The agent has the following finalizers preventing deletion:"
|
|
902
|
+
finalizers.each { |f| puts " - #{pastel.yellow(f)}" }
|
|
903
|
+
puts
|
|
904
|
+
puts "This usually indicates the operator is not running properly."
|
|
905
|
+
puts
|
|
906
|
+
puts "To diagnose:"
|
|
907
|
+
puts " kubectl get pods -n kube-system | grep language-operator"
|
|
908
|
+
puts " kubectl logs -n kube-system -l app.kubernetes.io/name=language-operator"
|
|
909
|
+
puts
|
|
910
|
+
puts "Emergency cleanup (advanced users only):"
|
|
911
|
+
puts " kubectl patch languageagent #{name} -p '{\"metadata\":{\"finalizers\":null}}' --type=merge"
|
|
912
|
+
end
|
|
913
|
+
|
|
914
|
+
def deletion_timeout_error(name)
|
|
915
|
+
puts
|
|
916
|
+
Formatters::ProgressFormatter.warn("Could not verify deletion of agent '#{name}' within 30 seconds")
|
|
917
|
+
puts
|
|
918
|
+
puts "Check deletion status with:"
|
|
919
|
+
puts " aictl agent list"
|
|
920
|
+
puts " kubectl get languageagent #{name}"
|
|
921
|
+
puts
|
|
922
|
+
puts "If the agent shows 'Unknown' status, it may be pending deletion."
|
|
923
|
+
end
|
|
833
924
|
end
|
|
834
925
|
end
|
|
835
926
|
end
|
|
@@ -4,42 +4,57 @@ module LanguageOperator
|
|
|
4
4
|
module CLI
|
|
5
5
|
module Commands
|
|
6
6
|
module Agent
|
|
7
|
-
# Code viewing and editing for agents
|
|
7
|
+
# Code viewing and editing for agents using LanguageAgentVersion CRD
|
|
8
8
|
module CodeOperations
|
|
9
9
|
def self.included(base)
|
|
10
10
|
base.class_eval do
|
|
11
|
-
desc 'code NAME', 'Display
|
|
11
|
+
desc 'code NAME', 'Display agent code from LanguageAgentVersion'
|
|
12
12
|
option :cluster, type: :string, desc: 'Override current cluster context'
|
|
13
13
|
option :raw, type: :boolean, default: false, desc: 'Output raw code without formatting'
|
|
14
|
+
option :version, type: :string, desc: 'Display specific version (e.g., --version=2)'
|
|
14
15
|
def code(name)
|
|
15
16
|
handle_command_error('get code') do
|
|
16
17
|
require_relative '../../formatters/code_formatter'
|
|
17
18
|
|
|
18
19
|
ctx = CLI::Helpers::ClusterContext.from_options(options)
|
|
19
20
|
|
|
20
|
-
# Get the
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
Formatters::ProgressFormatter.error("Synthesized code not found for agent '#{name}'")
|
|
21
|
+
# Get the LanguageAgentVersion resource
|
|
22
|
+
version_resource = get_agent_version_resource(ctx, name, options[:version])
|
|
23
|
+
|
|
24
|
+
unless version_resource
|
|
25
|
+
Formatters::ProgressFormatter.error("No code versions found for agent '#{name}'")
|
|
26
26
|
puts
|
|
27
|
-
puts '
|
|
28
|
-
puts ' - Agent
|
|
27
|
+
puts 'This may indicate:'
|
|
28
|
+
puts ' - Agent has not been synthesized yet'
|
|
29
29
|
puts ' - Agent synthesis failed'
|
|
30
30
|
puts
|
|
31
|
-
puts 'Check
|
|
31
|
+
puts 'Check agent status with:'
|
|
32
32
|
puts " aictl agent inspect #{name}"
|
|
33
33
|
exit 1
|
|
34
34
|
end
|
|
35
35
|
|
|
36
|
-
# Get
|
|
37
|
-
code_content =
|
|
36
|
+
# Get code from LanguageAgentVersion spec
|
|
37
|
+
code_content = version_resource.dig('spec', 'code')
|
|
38
38
|
unless code_content
|
|
39
|
-
Formatters::ProgressFormatter.error('Code content not found in
|
|
39
|
+
Formatters::ProgressFormatter.error('Code content not found in LanguageAgentVersion')
|
|
40
40
|
exit 1
|
|
41
41
|
end
|
|
42
42
|
|
|
43
|
+
# Determine version info for title
|
|
44
|
+
version_num = version_resource.dig('spec', 'version')
|
|
45
|
+
source_type = version_resource.dig('spec', 'sourceType') || 'manual'
|
|
46
|
+
|
|
47
|
+
title = if options[:version]
|
|
48
|
+
"Code for Agent: #{name} (Version #{version_num})"
|
|
49
|
+
else
|
|
50
|
+
active_version = get_active_version(ctx, name)
|
|
51
|
+
if version_num.to_s == active_version
|
|
52
|
+
"Current Code for Agent: #{name} (Version #{version_num} - #{source_type})"
|
|
53
|
+
else
|
|
54
|
+
"Code for Agent: #{name} (Version #{version_num} - #{source_type})"
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
43
58
|
# Raw output mode - just print the code
|
|
44
59
|
if options[:raw]
|
|
45
60
|
puts code_content
|
|
@@ -49,11 +64,137 @@ module LanguageOperator
|
|
|
49
64
|
# Display with syntax highlighting
|
|
50
65
|
Formatters::CodeFormatter.display_ruby_code(
|
|
51
66
|
code_content,
|
|
52
|
-
title:
|
|
67
|
+
title: title
|
|
53
68
|
)
|
|
54
69
|
end
|
|
55
70
|
end
|
|
56
71
|
|
|
72
|
+
desc 'versions NAME', 'List available LanguageAgentVersion resources'
|
|
73
|
+
option :cluster, type: :string, desc: 'Override current cluster context'
|
|
74
|
+
def versions(name)
|
|
75
|
+
handle_command_error('list versions') do
|
|
76
|
+
ctx = CLI::Helpers::ClusterContext.from_options(options)
|
|
77
|
+
|
|
78
|
+
# Get agent to verify it exists
|
|
79
|
+
get_resource_or_exit(Constants::RESOURCE_AGENT, name)
|
|
80
|
+
|
|
81
|
+
# Find all LanguageAgentVersion resources for this agent
|
|
82
|
+
versions = ctx.client.list_resources(Constants::RESOURCE_AGENT_VERSION, namespace: ctx.namespace)
|
|
83
|
+
.select { |v| v.dig('spec', 'agentRef', 'name') == name }
|
|
84
|
+
.sort_by { |v| v.dig('spec', 'version').to_i }
|
|
85
|
+
|
|
86
|
+
if versions.empty?
|
|
87
|
+
puts
|
|
88
|
+
puts "No LanguageAgentVersion resources found for agent: #{pastel.bold(name)}"
|
|
89
|
+
puts
|
|
90
|
+
puts pastel.yellow('No versions have been created yet.')
|
|
91
|
+
puts
|
|
92
|
+
puts 'Versions are created automatically during agent synthesis.'
|
|
93
|
+
puts "Check agent status: #{pastel.dim("aictl agent inspect #{name}")}"
|
|
94
|
+
return
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Get current active version
|
|
98
|
+
active_version = get_active_version(ctx, name)
|
|
99
|
+
|
|
100
|
+
# Build table data
|
|
101
|
+
table_data = versions.map do |version_resource|
|
|
102
|
+
version_num = version_resource.dig('spec', 'version').to_s
|
|
103
|
+
status = version_num == active_version ? 'current' : 'available'
|
|
104
|
+
source_type = version_resource.dig('spec', 'sourceType') || 'manual'
|
|
105
|
+
created = version_resource.dig('metadata', 'creationTimestamp')
|
|
106
|
+
created = created ? Time.parse(created).strftime('%Y-%m-%d %H:%M') : 'Unknown'
|
|
107
|
+
|
|
108
|
+
{
|
|
109
|
+
version: "v#{version_num}",
|
|
110
|
+
status: status,
|
|
111
|
+
type: source_type.capitalize,
|
|
112
|
+
created: created
|
|
113
|
+
}
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Display table
|
|
117
|
+
headers = %w[VERSION STATUS TYPE CREATED]
|
|
118
|
+
rows = table_data.map do |row|
|
|
119
|
+
status_indicator = row[:status] == 'current' ? pastel.green('●') : pastel.dim('○')
|
|
120
|
+
status_text = row[:status] == 'current' ? pastel.green('current') : 'available'
|
|
121
|
+
|
|
122
|
+
[
|
|
123
|
+
row[:version],
|
|
124
|
+
"#{status_indicator} #{status_text}",
|
|
125
|
+
row[:type],
|
|
126
|
+
row[:created]
|
|
127
|
+
]
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
puts
|
|
131
|
+
puts "Code versions for agent: #{pastel.bold(name)}"
|
|
132
|
+
puts
|
|
133
|
+
puts table(headers, rows)
|
|
134
|
+
puts
|
|
135
|
+
puts 'Usage:'
|
|
136
|
+
puts " #{pastel.dim("aictl agent code #{name}")}"
|
|
137
|
+
puts " #{pastel.dim("aictl agent code #{name} --version=X")}"
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
private
|
|
142
|
+
|
|
143
|
+
def get_active_version(ctx, agent_name)
|
|
144
|
+
begin
|
|
145
|
+
agent = ctx.client.get_resource(Constants::RESOURCE_AGENT, agent_name, ctx.namespace)
|
|
146
|
+
version_ref = agent.dig('spec', 'agentVersionRef', 'name')
|
|
147
|
+
return nil unless version_ref
|
|
148
|
+
|
|
149
|
+
# Extract version number from "agent-name-vX" format
|
|
150
|
+
version_ref.split('-v').last
|
|
151
|
+
rescue K8s::Error::NotFound
|
|
152
|
+
nil
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def get_agent_version_resource(ctx, agent_name, requested_version = nil)
|
|
157
|
+
# Query LanguageAgentVersion resources for this agent
|
|
158
|
+
versions = ctx.client.list_resources(Constants::RESOURCE_AGENT_VERSION, namespace: ctx.namespace)
|
|
159
|
+
.select { |v| v.dig('spec', 'agentRef', 'name') == agent_name }
|
|
160
|
+
|
|
161
|
+
if versions.empty?
|
|
162
|
+
return nil
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
if requested_version
|
|
166
|
+
# Find specific version
|
|
167
|
+
target_version = versions.find { |v| v.dig('spec', 'version').to_s == requested_version }
|
|
168
|
+
|
|
169
|
+
unless target_version
|
|
170
|
+
Formatters::ProgressFormatter.error("Version #{requested_version} not found for agent '#{agent_name}'")
|
|
171
|
+
puts
|
|
172
|
+
puts 'Available versions:'
|
|
173
|
+
versions.each do |v|
|
|
174
|
+
version_num = v.dig('spec', 'version')
|
|
175
|
+
puts " v#{version_num}"
|
|
176
|
+
end
|
|
177
|
+
puts
|
|
178
|
+
puts "Use 'aictl agent versions #{agent_name}' to see all available versions"
|
|
179
|
+
exit 1
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
return target_version
|
|
183
|
+
else
|
|
184
|
+
# Get currently active version
|
|
185
|
+
active_version = get_active_version(ctx, agent_name)
|
|
186
|
+
|
|
187
|
+
if active_version
|
|
188
|
+
# Find the currently active version resource
|
|
189
|
+
active_version_resource = versions.find { |v| v.dig('spec', 'version').to_s == active_version }
|
|
190
|
+
return active_version_resource if active_version_resource
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# Fall back to latest version if no active version or active version not found
|
|
194
|
+
return versions.max_by { |v| v.dig('spec', 'version').to_i }
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
57
198
|
desc 'edit NAME', 'Edit agent instructions'
|
|
58
199
|
option :cluster, type: :string, desc: 'Override current cluster context'
|
|
59
200
|
def edit(name)
|
|
@@ -99,4 +240,4 @@ module LanguageOperator
|
|
|
99
240
|
end
|
|
100
241
|
end
|
|
101
242
|
end
|
|
102
|
-
end
|
|
243
|
+
end
|