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.
Files changed (143) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/commands/persona.md +9 -0
  3. data/.claude/commands/task.md +46 -1
  4. data/.rubocop.yml +13 -0
  5. data/.rubocop_custom/use_ux_helper.rb +44 -0
  6. data/CHANGELOG.md +8 -0
  7. data/Gemfile.lock +12 -1
  8. data/Makefile +26 -7
  9. data/Makefile.common +50 -0
  10. data/bin/aictl +8 -1
  11. data/components/agent/Gemfile +1 -1
  12. data/components/agent/bin/langop-agent +7 -0
  13. data/docs/README.md +58 -0
  14. data/docs/{dsl/best-practices.md → best-practices.md} +4 -4
  15. data/docs/cli-reference.md +274 -0
  16. data/docs/{dsl/constraints.md → constraints.md} +5 -5
  17. data/docs/how-agents-work.md +156 -0
  18. data/docs/installation.md +218 -0
  19. data/docs/quickstart.md +299 -0
  20. data/docs/understanding-generated-code.md +265 -0
  21. data/docs/using-tools.md +457 -0
  22. data/docs/webhooks.md +509 -0
  23. data/examples/ux_helpers_demo.rb +296 -0
  24. data/lib/language_operator/agent/base.rb +11 -1
  25. data/lib/language_operator/agent/executor.rb +23 -6
  26. data/lib/language_operator/agent/safety/safe_executor.rb +41 -39
  27. data/lib/language_operator/agent/task_executor.rb +346 -63
  28. data/lib/language_operator/agent/web_server.rb +110 -14
  29. data/lib/language_operator/agent/webhook_authenticator.rb +39 -5
  30. data/lib/language_operator/agent.rb +88 -2
  31. data/lib/language_operator/cli/base_command.rb +17 -11
  32. data/lib/language_operator/cli/command_loader.rb +72 -0
  33. data/lib/language_operator/cli/commands/agent/base.rb +837 -0
  34. data/lib/language_operator/cli/commands/agent/code_operations.rb +102 -0
  35. data/lib/language_operator/cli/commands/agent/helpers/cluster_llm_client.rb +116 -0
  36. data/lib/language_operator/cli/commands/agent/helpers/code_parser.rb +115 -0
  37. data/lib/language_operator/cli/commands/agent/helpers/synthesis_watcher.rb +96 -0
  38. data/lib/language_operator/cli/commands/agent/learning.rb +289 -0
  39. data/lib/language_operator/cli/commands/agent/lifecycle.rb +102 -0
  40. data/lib/language_operator/cli/commands/agent/logs.rb +125 -0
  41. data/lib/language_operator/cli/commands/agent/workspace.rb +327 -0
  42. data/lib/language_operator/cli/commands/cluster.rb +129 -84
  43. data/lib/language_operator/cli/commands/install.rb +1 -1
  44. data/lib/language_operator/cli/commands/model/base.rb +215 -0
  45. data/lib/language_operator/cli/commands/model/test.rb +165 -0
  46. data/lib/language_operator/cli/commands/persona.rb +16 -34
  47. data/lib/language_operator/cli/commands/quickstart.rb +3 -2
  48. data/lib/language_operator/cli/commands/status.rb +40 -67
  49. data/lib/language_operator/cli/commands/system/base.rb +44 -0
  50. data/lib/language_operator/cli/commands/system/exec.rb +147 -0
  51. data/lib/language_operator/cli/commands/system/helpers/llm_synthesis.rb +183 -0
  52. data/lib/language_operator/cli/commands/system/helpers/pod_manager.rb +212 -0
  53. data/lib/language_operator/cli/commands/system/helpers/template_loader.rb +57 -0
  54. data/lib/language_operator/cli/commands/system/helpers/template_validator.rb +174 -0
  55. data/lib/language_operator/cli/commands/system/schema.rb +92 -0
  56. data/lib/language_operator/cli/commands/system/synthesis_template.rb +151 -0
  57. data/lib/language_operator/cli/commands/system/synthesize.rb +224 -0
  58. data/lib/language_operator/cli/commands/system/validate_template.rb +130 -0
  59. data/lib/language_operator/cli/commands/tool/base.rb +271 -0
  60. data/lib/language_operator/cli/commands/tool/install.rb +255 -0
  61. data/lib/language_operator/cli/commands/tool/search.rb +69 -0
  62. data/lib/language_operator/cli/commands/tool/test.rb +115 -0
  63. data/lib/language_operator/cli/commands/use.rb +29 -6
  64. data/lib/language_operator/cli/errors/handler.rb +20 -17
  65. data/lib/language_operator/cli/errors/suggestions.rb +3 -5
  66. data/lib/language_operator/cli/errors/thor_errors.rb +55 -0
  67. data/lib/language_operator/cli/formatters/code_formatter.rb +4 -11
  68. data/lib/language_operator/cli/formatters/log_formatter.rb +8 -15
  69. data/lib/language_operator/cli/formatters/progress_formatter.rb +6 -8
  70. data/lib/language_operator/cli/formatters/status_formatter.rb +26 -7
  71. data/lib/language_operator/cli/formatters/table_formatter.rb +47 -36
  72. data/lib/language_operator/cli/formatters/value_formatter.rb +75 -0
  73. data/lib/language_operator/cli/helpers/cluster_context.rb +5 -3
  74. data/lib/language_operator/cli/helpers/kubeconfig_validator.rb +2 -1
  75. data/lib/language_operator/cli/helpers/label_utils.rb +97 -0
  76. data/lib/language_operator/{ux/concerns/provider_helpers.rb → cli/helpers/provider_helper.rb} +10 -29
  77. data/lib/language_operator/cli/helpers/schedule_builder.rb +21 -1
  78. data/lib/language_operator/cli/helpers/user_prompts.rb +19 -11
  79. data/lib/language_operator/cli/helpers/ux_helper.rb +538 -0
  80. data/lib/language_operator/{ux/concerns/input_validation.rb → cli/helpers/validation_helper.rb} +13 -66
  81. data/lib/language_operator/cli/main.rb +50 -40
  82. data/lib/language_operator/cli/templates/tools/generic.yaml +3 -0
  83. data/lib/language_operator/cli/wizards/agent_wizard.rb +12 -20
  84. data/lib/language_operator/cli/wizards/model_wizard.rb +271 -0
  85. data/lib/language_operator/cli/wizards/quickstart_wizard.rb +8 -34
  86. data/lib/language_operator/client/base.rb +28 -0
  87. data/lib/language_operator/client/config.rb +4 -1
  88. data/lib/language_operator/client/mcp_connector.rb +1 -1
  89. data/lib/language_operator/config/cluster_config.rb +3 -2
  90. data/lib/language_operator/config.rb +38 -11
  91. data/lib/language_operator/constants/kubernetes_labels.rb +80 -0
  92. data/lib/language_operator/constants.rb +13 -0
  93. data/lib/language_operator/dsl/http.rb +127 -10
  94. data/lib/language_operator/dsl.rb +153 -6
  95. data/lib/language_operator/errors.rb +50 -0
  96. data/lib/language_operator/kubernetes/client.rb +11 -6
  97. data/lib/language_operator/kubernetes/resource_builder.rb +58 -84
  98. data/lib/language_operator/templates/schema/agent_dsl_openapi.yaml +1 -1
  99. data/lib/language_operator/templates/schema/agent_dsl_schema.json +1 -1
  100. data/lib/language_operator/type_coercion.rb +118 -34
  101. data/lib/language_operator/utils/secure_path.rb +74 -0
  102. data/lib/language_operator/utils.rb +7 -0
  103. data/lib/language_operator/validators.rb +54 -2
  104. data/lib/language_operator/version.rb +1 -1
  105. data/synth/001/Makefile +10 -2
  106. data/synth/001/agent.rb +16 -15
  107. data/synth/001/output.log +27 -10
  108. data/synth/002/Makefile +10 -2
  109. data/synth/003/Makefile +1 -1
  110. data/synth/003/README.md +205 -133
  111. data/synth/003/agent.optimized.rb +66 -0
  112. data/synth/003/agent.synthesized.rb +41 -0
  113. metadata +111 -35
  114. data/docs/dsl/agent-reference.md +0 -604
  115. data/docs/dsl/mcp-integration.md +0 -1177
  116. data/docs/dsl/webhooks.md +0 -932
  117. data/docs/dsl/workflows.md +0 -744
  118. data/lib/language_operator/cli/commands/agent.rb +0 -1712
  119. data/lib/language_operator/cli/commands/model.rb +0 -366
  120. data/lib/language_operator/cli/commands/system.rb +0 -1259
  121. data/lib/language_operator/cli/commands/tool.rb +0 -654
  122. data/lib/language_operator/cli/formatters/optimization_formatter.rb +0 -226
  123. data/lib/language_operator/cli/helpers/pastel_helper.rb +0 -24
  124. data/lib/language_operator/learning/adapters/base_adapter.rb +0 -149
  125. data/lib/language_operator/learning/adapters/jaeger_adapter.rb +0 -221
  126. data/lib/language_operator/learning/adapters/signoz_adapter.rb +0 -435
  127. data/lib/language_operator/learning/adapters/tempo_adapter.rb +0 -239
  128. data/lib/language_operator/learning/optimizer.rb +0 -319
  129. data/lib/language_operator/learning/pattern_detector.rb +0 -260
  130. data/lib/language_operator/learning/task_synthesizer.rb +0 -288
  131. data/lib/language_operator/learning/trace_analyzer.rb +0 -285
  132. data/lib/language_operator/templates/task_synthesis.tmpl +0 -98
  133. data/lib/language_operator/ux/base.rb +0 -81
  134. data/lib/language_operator/ux/concerns/README.md +0 -155
  135. data/lib/language_operator/ux/concerns/headings.rb +0 -90
  136. data/lib/language_operator/ux/create_agent.rb +0 -255
  137. data/lib/language_operator/ux/create_model.rb +0 -267
  138. data/lib/language_operator/ux/quickstart.rb +0 -594
  139. data/synth/003/agent.rb +0 -41
  140. data/synth/003/output.log +0 -68
  141. /data/docs/{architecture/agent-runtime.md → agent-internals.md} +0 -0
  142. /data/docs/{dsl/chat-endpoints.md → chat-endpoints.md} +0 -0
  143. /data/docs/{dsl/SCHEMA_VERSION.md → schema-versioning.md} +0 -0
@@ -1,1259 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'json'
4
- require 'yaml'
5
- require_relative '../base_command'
6
- require_relative '../formatters/progress_formatter'
7
- require_relative '../../dsl/schema'
8
-
9
- module LanguageOperator
10
- module CLI
11
- module Commands
12
- # System commands for schema introspection and metadata
13
- class System < BaseCommand
14
- desc 'schema', 'Export the DSL schema in various formats'
15
- long_desc <<-DESC
16
- Export the Language Operator Agent DSL schema in various formats.
17
-
18
- The schema documents all available DSL methods, parameters, validation
19
- patterns, and structure. Useful for template validation, documentation
20
- generation, and IDE autocomplete.
21
-
22
- Examples:
23
- # Export JSON schema (default)
24
- aictl system schema
25
-
26
- # Export as YAML
27
- aictl system schema --format yaml
28
-
29
- # Export OpenAPI 3.0 specification
30
- aictl system schema --format openapi
31
-
32
- # Show schema version only
33
- aictl system schema --version
34
-
35
- # Save to file
36
- aictl system schema > schema.json
37
- aictl system schema --format openapi > openapi.json
38
- DESC
39
- option :format, type: :string, default: 'json', desc: 'Output format (json, yaml, openapi)'
40
- option :version, type: :boolean, default: false, desc: 'Show schema version only'
41
- def schema
42
- handle_command_error('generate schema') do
43
- # Handle version flag
44
- if options[:version]
45
- puts Dsl::Schema.version
46
- return
47
- end
48
-
49
- # Generate schema based on format
50
- format = options[:format].downcase
51
- case format
52
- when 'json'
53
- output_json_schema
54
- when 'yaml'
55
- output_yaml_schema
56
- when 'openapi'
57
- output_openapi_schema
58
- else
59
- Formatters::ProgressFormatter.error("Invalid format: #{format}")
60
- puts
61
- puts 'Supported formats: json, yaml, openapi'
62
- exit 1
63
- end
64
- end
65
- end
66
-
67
- no_commands do
68
- # Output JSON Schema v7
69
- def output_json_schema
70
- schema = Dsl::Schema.to_json_schema
71
- puts JSON.pretty_generate(schema)
72
- end
73
-
74
- # Output YAML Schema
75
- def output_yaml_schema
76
- schema = Dsl::Schema.to_json_schema
77
- puts YAML.dump(schema.transform_keys(&:to_s))
78
- end
79
-
80
- # Output OpenAPI 3.0 specification
81
- def output_openapi_schema
82
- spec = Dsl::Schema.to_openapi
83
- puts JSON.pretty_generate(spec)
84
- end
85
- end
86
-
87
- desc 'validate_template', 'Validate synthesis template against DSL schema'
88
- long_desc <<-DESC
89
- Validate a synthesis template file against the DSL schema.
90
-
91
- Extracts Ruby code examples from the template and validates each example
92
- against the Language Operator Agent DSL schema. Checks for dangerous
93
- methods, syntax errors, and compliance with safe coding practices.
94
-
95
- Examples:
96
- # Validate a custom template file
97
- aictl system validate_template --template /path/to/template.tmpl
98
-
99
- # Validate the bundled agent template (default)
100
- aictl system validate_template
101
-
102
- # Validate the bundled persona template
103
- aictl system validate_template --type persona
104
-
105
- # Verbose output with all violations
106
- aictl system validate_template --template mytemplate.tmpl --verbose
107
- DESC
108
- option :template, type: :string, desc: 'Path to template file (defaults to bundled template)'
109
- option :type, type: :string, default: 'agent', desc: 'Template type if using bundled template (agent, persona)'
110
- option :verbose, type: :boolean, default: false, desc: 'Show detailed violation information'
111
- def validate_template
112
- handle_command_error('validate template') do
113
- # Determine template source
114
- if options[:template]
115
- # Load custom template from file
116
- unless File.exist?(options[:template])
117
- Formatters::ProgressFormatter.error("Template file not found: #{options[:template]}")
118
- exit 1
119
- end
120
- template_content = File.read(options[:template])
121
- template_name = File.basename(options[:template])
122
- else
123
- # Load bundled template
124
- template_type = options[:type].downcase
125
- unless %w[agent persona].include?(template_type)
126
- Formatters::ProgressFormatter.error("Invalid template type: #{template_type}")
127
- puts
128
- puts 'Supported types: agent, persona'
129
- exit 1
130
- end
131
- template_content = load_bundled_template(template_type)
132
- template_name = "bundled #{template_type} template"
133
- end
134
-
135
- # Display header
136
- puts "Validating template: #{template_name}"
137
- puts '=' * 60
138
- puts
139
-
140
- # Extract code examples
141
- code_examples = extract_code_examples(template_content)
142
-
143
- if code_examples.empty?
144
- Formatters::ProgressFormatter.warn('No Ruby code examples found in template')
145
- puts
146
- puts 'Templates should contain Ruby code blocks like:'
147
- puts '```ruby'
148
- puts 'agent "my-agent" do'
149
- puts ' # ...'
150
- puts 'end'
151
- puts '```'
152
- exit 1
153
- end
154
-
155
- puts "Found #{code_examples.size} code example(s)"
156
- puts
157
-
158
- # Validate each example
159
- all_valid = true
160
- code_examples.each_with_index do |example, idx|
161
- puts "Example #{idx + 1} (starting at line #{example[:start_line]}):"
162
- puts '-' * 40
163
-
164
- result = validate_code_against_schema(example[:code])
165
-
166
- if result[:valid] && result[:warnings].empty?
167
- Formatters::ProgressFormatter.success('Valid - No issues found')
168
- elsif result[:valid]
169
- Formatters::ProgressFormatter.success('Valid - With warnings')
170
- result[:warnings].each do |warn|
171
- line = example[:start_line] + (warn[:location] || 0)
172
- puts " ⚠ Line #{line}: #{warn[:message]}"
173
- end
174
- else
175
- Formatters::ProgressFormatter.error('Invalid - Violations detected')
176
- result[:errors].each do |err|
177
- line = example[:start_line] + (err[:location] || 0)
178
- puts " ✗ Line #{line}: #{err[:message]}"
179
- puts " Type: #{err[:type]}" if options[:verbose]
180
- end
181
- all_valid = false
182
- end
183
-
184
- puts
185
- end
186
-
187
- # Final summary
188
- puts '=' * 60
189
- if all_valid
190
- Formatters::ProgressFormatter.success('All examples are valid')
191
- exit 0
192
- else
193
- Formatters::ProgressFormatter.error('Validation failed')
194
- puts
195
- puts 'Fix the violations above and run validation again.'
196
- exit 1
197
- end
198
- end
199
- end
200
-
201
- desc 'synthesize [INSTRUCTIONS]', 'Synthesize agent code from natural language instructions'
202
- long_desc <<-DESC
203
- Synthesize agent code by converting natural language instructions
204
- into Ruby DSL code without creating an actual agent.
205
-
206
- This command uses a LanguageModel resource from your cluster to generate
207
- agent code. If --model is not specified, the first available model will
208
- be auto-selected.
209
-
210
- Instructions can be provided either as a command argument or via STDIN.
211
- If no argument is provided, the command will read from STDIN.
212
-
213
- This command helps you validate your instructions and understand how the
214
- synthesis engine interprets them. Use --dry-run to see the prompt that
215
- would be sent to the LLM, or run without it to generate actual code.
216
-
217
- Examples:
218
- # Test with dry-run (show prompt only)
219
- aictl system synthesize "Monitor GitHub issues daily" --dry-run
220
-
221
- # Generate code from instructions (auto-selects first available model)
222
- aictl system synthesize "Send daily reports to Slack"
223
-
224
- # Use a specific cluster model
225
- aictl system synthesize "Process webhooks from GitHub" --model my-claude
226
-
227
- # Output raw code without formatting (useful for piping to files)
228
- aictl system synthesize "Monitor logs" --raw > agent.rb
229
-
230
- # Read instructions from STDIN
231
- cat instructions.txt | aictl system synthesize > agent.rb
232
-
233
- # Read from STDIN with pipe
234
- echo "Monitor GitHub issues" | aictl system synthesize --raw
235
-
236
- # Specify custom agent name and tools
237
- aictl system synthesize "Process webhooks from GitHub" \\
238
- --agent-name github-processor \\
239
- --tools github,slack \\
240
- --model my-gpt4
241
- DESC
242
- option :agent_name, type: :string, default: 'test-agent', desc: 'Name for the test agent'
243
- option :tools, type: :string, desc: 'Comma-separated list of available tools'
244
- option :models, type: :string, desc: 'Comma-separated list of available models (from cluster)'
245
- option :model, type: :string, desc: 'Model to use for synthesis (defaults to first available in cluster)'
246
- option :dry_run, type: :boolean, default: false, desc: 'Show prompt without calling LLM'
247
- option :raw, type: :boolean, default: false, desc: 'Output only the raw code without formatting'
248
- def synthesize(instructions = nil)
249
- handle_command_error('synthesize agent') do
250
- # Read instructions from STDIN if not provided as argument
251
- if instructions.nil? || instructions.strip.empty?
252
- if $stdin.tty?
253
- Formatters::ProgressFormatter.error('No instructions provided')
254
- puts
255
- puts 'Provide instructions either as an argument or via STDIN:'
256
- puts ' aictl system synthesize "Your instructions here"'
257
- puts ' cat instructions.txt | aictl system synthesize'
258
- exit 1
259
- else
260
- instructions = $stdin.read.strip
261
- if instructions.empty?
262
- Formatters::ProgressFormatter.error('No instructions provided')
263
- puts
264
- puts 'Provide instructions either as an argument or via STDIN:'
265
- puts ' aictl system synthesize "Your instructions here"'
266
- puts ' cat instructions.txt | aictl system synthesize'
267
- exit 1
268
- end
269
- end
270
- end
271
- # Select model to use for synthesis
272
- selected_model = select_synthesis_model
273
-
274
- # Load synthesis template
275
- template_content = load_bundled_template('agent')
276
-
277
- # Detect temporal intent from instructions
278
- temporal_intent = detect_temporal_intent(instructions)
279
-
280
- # Prepare template data
281
- template_data = {
282
- 'Instructions' => instructions,
283
- 'AgentName' => options[:agent_name],
284
- 'ToolsList' => format_tools_list(options[:tools]),
285
- 'ModelsList' => format_models_list(options[:models]),
286
- 'TemporalIntent' => temporal_intent,
287
- 'PersonaSection' => '',
288
- 'ScheduleSection' => temporal_intent == 'scheduled' ? ' schedule "0 */1 * * *" # Example hourly schedule' : '',
289
- 'ScheduleRules' => temporal_intent == 'scheduled' ? "\n2. Include schedule with cron expression\n3. Set mode to :scheduled\n4. " : "\n2. ",
290
- 'ConstraintsSection' => '',
291
- 'ErrorContext' => nil
292
- }
293
-
294
- # Render template (Go-style template syntax)
295
- rendered_prompt = render_go_template(template_content, template_data)
296
-
297
- if options[:dry_run]
298
- # Show the prompt that would be sent
299
- puts 'Synthesis Prompt Preview'
300
- puts '=' * 80
301
- puts
302
- puts rendered_prompt
303
- puts
304
- puts '=' * 80
305
- Formatters::ProgressFormatter.success('Dry-run complete - prompt displayed above')
306
- return
307
- end
308
-
309
- # Call LLM to generate code (no output - just do it)
310
- llm_response = call_llm_for_synthesis(rendered_prompt, selected_model)
311
-
312
- # Extract Ruby code from response
313
- generated_code = extract_ruby_code(llm_response)
314
-
315
- if generated_code.nil?
316
- Formatters::ProgressFormatter.error('Failed to extract Ruby code from LLM response')
317
- puts
318
- puts 'LLM Response:'
319
- puts llm_response
320
- exit 1
321
- end
322
-
323
- # Handle raw output
324
- if options[:raw]
325
- puts generated_code
326
- return
327
- end
328
-
329
- # Display formatted code
330
- require 'rouge'
331
- formatter = Rouge::Formatters::Terminal256.new
332
- lexer = Rouge::Lexers::Ruby.new
333
- highlighted_code = formatter.format(lexer.lex(generated_code))
334
-
335
- puts highlighted_code
336
- end
337
- end
338
-
339
- desc 'exec [AGENT_FILE]', 'Execute an agent file in a test pod on the cluster'
340
- long_desc <<-DESC
341
- Deploy and execute an agent file in a temporary test pod on the Kubernetes cluster.
342
-
343
- This command creates a ConfigMap with the agent code, deploys a test pod,
344
- streams the logs until completion, and cleans up all resources.
345
-
346
- The agent code is mounted at /etc/agent/code/agent.rb as expected by the agent runtime.
347
-
348
- Agent code can be provided either as a file path or via STDIN.
349
- If no file path is provided, the command will read from STDIN.
350
-
351
- Examples:
352
- # Execute a synthesized agent file
353
- aictl system exec agent.rb
354
-
355
- # Execute with a custom agent name
356
- aictl system exec agent.rb --agent-name my-test
357
-
358
- # Keep the pod after execution for debugging
359
- aictl system exec agent.rb --keep-pod
360
-
361
- # Use a different agent image
362
- aictl system exec agent.rb --image ghcr.io/language-operator/agent:v0.1.0
363
-
364
- # Read agent code from STDIN
365
- cat agent.rb | aictl system exec
366
-
367
- # Pipe synthesized code directly to execution
368
- cat agent.txt | aictl system synthesize | aictl system exec
369
- DESC
370
- option :agent_name, type: :string, default: 'test-agent', desc: 'Name for the test agent pod'
371
- option :keep_pod, type: :boolean, default: false, desc: 'Keep the pod after execution (for debugging)'
372
- option :image, type: :string, default: 'ghcr.io/language-operator/agent:latest', desc: 'Agent container image'
373
- option :timeout, type: :numeric, default: 300, desc: 'Timeout in seconds for agent execution'
374
- def exec(agent_file = nil)
375
- handle_command_error('exec agent') do
376
- # Verify cluster is selected
377
- unless ctx.client
378
- Formatters::ProgressFormatter.error('No cluster context available')
379
- puts
380
- puts 'Please configure kubectl with a valid cluster context:'
381
- puts ' kubectl config get-contexts'
382
- puts ' kubectl config use-context <context-name>'
383
- exit 1
384
- end
385
-
386
- # Read agent code from file or STDIN
387
- agent_code = if agent_file && !agent_file.strip.empty?
388
- # Read from file
389
- unless File.exist?(agent_file)
390
- Formatters::ProgressFormatter.error("Agent file not found: #{agent_file}")
391
- exit 1
392
- end
393
- File.read(agent_file)
394
- elsif $stdin.tty?
395
- # Read from STDIN
396
- Formatters::ProgressFormatter.error('No agent code provided')
397
- puts
398
- puts 'Provide agent code either as a file or via STDIN:'
399
- puts ' aictl system exec agent.rb'
400
- puts ' cat agent.rb | aictl system exec'
401
- exit 1
402
- else
403
- code = $stdin.read.strip
404
- if code.empty?
405
- Formatters::ProgressFormatter.error('No agent code provided')
406
- puts
407
- puts 'Provide agent code either as a file or via STDIN:'
408
- puts ' aictl system exec agent.rb'
409
- puts ' cat agent.rb | aictl system exec'
410
- exit 1
411
- end
412
- code
413
- end
414
-
415
- # Generate unique names
416
- timestamp = Time.now.to_i
417
- configmap_name = "#{options[:agent_name]}-code-#{timestamp}"
418
- pod_name = "#{options[:agent_name]}-#{timestamp}"
419
-
420
- begin
421
- # Create ConfigMap with agent code
422
- Formatters::ProgressFormatter.with_spinner('Creating ConfigMap with agent code') do
423
- create_agent_configmap(configmap_name, agent_code)
424
- end
425
-
426
- # Create test pod
427
- Formatters::ProgressFormatter.with_spinner('Creating test pod') do
428
- create_test_pod(pod_name, configmap_name, options[:image])
429
- end
430
-
431
- # Wait for pod to be ready or running
432
- Formatters::ProgressFormatter.with_spinner('Waiting for pod to start') do
433
- wait_for_pod_start(pod_name, timeout: 60)
434
- end
435
-
436
- # Stream logs until pod completes
437
- stream_pod_logs(pod_name, timeout: options[:timeout])
438
-
439
- # Wait for pod to fully terminate and get final status
440
- exit_code = wait_for_pod_termination(pod_name)
441
-
442
- if exit_code&.zero?
443
- Formatters::ProgressFormatter.success('Agent completed successfully')
444
- elsif exit_code
445
- Formatters::ProgressFormatter.error("Agent failed with exit code: #{exit_code}")
446
- else
447
- Formatters::ProgressFormatter.warn('Unable to determine pod exit status')
448
- end
449
- ensure
450
- # Clean up resources unless --keep-pod
451
- puts
452
- puts
453
- if options[:keep_pod]
454
- Formatters::ProgressFormatter.info('Resources kept for debugging:')
455
- puts " Pod: #{pod_name}"
456
- puts " ConfigMap: #{configmap_name}"
457
- puts
458
- puts "To view logs: kubectl logs -n #{ctx.namespace} #{pod_name}"
459
- puts "To delete: kubectl delete pod,configmap -n #{ctx.namespace} #{pod_name} #{configmap_name}"
460
- else
461
- Formatters::ProgressFormatter.with_spinner('Cleaning up resources') do
462
- delete_pod(pod_name)
463
- delete_configmap(configmap_name)
464
- end
465
- end
466
- end
467
- end
468
- end
469
-
470
- desc 'synthesis-template', 'Export synthesis templates for agent code generation'
471
- long_desc <<-DESC
472
- Export the synthesis templates used by the Language Operator to generate
473
- agent code from natural language instructions.
474
-
475
- These templates are used by the operator's synthesis engine to convert
476
- user instructions into executable Ruby DSL code.
477
-
478
- Examples:
479
- # Export agent synthesis template (default)
480
- aictl system synthesis-template
481
-
482
- # Export persona distillation template
483
- aictl system synthesis-template --type persona
484
-
485
- # Export as JSON with schema included
486
- aictl system synthesis-template --format json --with-schema
487
-
488
- # Export as YAML
489
- aictl system synthesis-template --format yaml
490
-
491
- # Validate template syntax
492
- aictl system synthesis-template --validate
493
-
494
- # Save to file
495
- aictl system synthesis-template > agent_synthesis.tmpl
496
- DESC
497
- option :format, type: :string, default: 'template', desc: 'Output format (template, json, yaml)'
498
- option :type, type: :string, default: 'agent', desc: 'Template type (agent, persona)'
499
- option :with_schema, type: :boolean, default: false, desc: 'Include DSL schema in output'
500
- option :validate, type: :boolean, default: false, desc: 'Validate template syntax'
501
- def synthesis_template
502
- handle_command_error('load template') do
503
- # Validate type
504
- template_type = options[:type].downcase
505
- unless %w[agent persona].include?(template_type)
506
- Formatters::ProgressFormatter.error("Invalid template type: #{template_type}")
507
- puts
508
- puts 'Supported types: agent, persona'
509
- exit 1
510
- end
511
-
512
- # Load template
513
- template_content = load_template(template_type)
514
-
515
- # Validate if requested
516
- if options[:validate]
517
- validation_result = validate_template_content(template_content, template_type)
518
-
519
- # Display warnings if any
520
- unless validation_result[:warnings].empty?
521
- Formatters::ProgressFormatter.warn('Template validation warnings:')
522
- validation_result[:warnings].each do |warning|
523
- puts " ⚠ #{warning}"
524
- end
525
- puts
526
- end
527
-
528
- # Display errors and exit if validation failed
529
- if validation_result[:valid]
530
- Formatters::ProgressFormatter.success('Template validation passed')
531
- return
532
- else
533
- Formatters::ProgressFormatter.error('Template validation failed:')
534
- validation_result[:errors].each do |error|
535
- puts " ✗ #{error}"
536
- end
537
- exit 1
538
- end
539
- end
540
-
541
- # Generate output based on format
542
- format = options[:format].downcase
543
- case format
544
- when 'template'
545
- output_template_format(template_content)
546
- when 'json'
547
- output_json_format(template_content, template_type)
548
- when 'yaml'
549
- output_yaml_format(template_content, template_type)
550
- else
551
- Formatters::ProgressFormatter.error("Invalid format: #{format}")
552
- puts
553
- puts 'Supported formats: template, json, yaml'
554
- exit 1
555
- end
556
- end
557
- end
558
-
559
- private
560
-
561
- # Render Go-style template ({{.Variable}})
562
- # Simplified implementation for basic variable substitution
563
- def render_go_template(template, data)
564
- result = template.dup
565
-
566
- # Handle {{if .ErrorContext}} - remove this section for test-synthesis
567
- result.gsub!(/{{if \.ErrorContext}}.*?{{else}}/m, '')
568
- result.gsub!(/{{end}}/, '')
569
-
570
- # Replace simple variables {{.Variable}}
571
- data.each do |key, value|
572
- result.gsub!("{{.#{key}}}", value.to_s)
573
- end
574
-
575
- result
576
- end
577
-
578
- # Detect temporal intent from instructions (scheduled vs autonomous)
579
- def detect_temporal_intent(instructions)
580
- temporal_keywords = {
581
- scheduled: %w[daily weekly hourly monthly schedule cron every day week hour minute],
582
- autonomous: %w[monitor watch continuously constantly always loop]
583
- }
584
-
585
- instructions_lower = instructions.downcase
586
-
587
- # Check for scheduled keywords
588
- scheduled_matches = temporal_keywords[:scheduled].count { |keyword| instructions_lower.include?(keyword) }
589
- autonomous_matches = temporal_keywords[:autonomous].count { |keyword| instructions_lower.include?(keyword) }
590
-
591
- scheduled_matches > autonomous_matches ? 'scheduled' : 'autonomous'
592
- end
593
-
594
- # Format tools list for template
595
- def format_tools_list(tools_str)
596
- return 'No tools specified' if tools_str.nil? || tools_str.strip.empty?
597
-
598
- tools = tools_str.split(',').map(&:strip)
599
- tools.map { |tool| "- #{tool}" }.join("\n")
600
- end
601
-
602
- # Format models list for template
603
- def format_models_list(models_str)
604
- # If not specified, try to detect from cluster
605
- if models_str.nil? || models_str.strip.empty?
606
- models = detect_available_models
607
- return models.map { |model| "- #{model}" }.join("\n") unless models.empty?
608
-
609
- return 'No models available (run: aictl model list)'
610
- end
611
-
612
- models = models_str.split(',').map(&:strip)
613
- models.map { |model| "- #{model}" }.join("\n")
614
- end
615
-
616
- # Detect available models from cluster
617
- def detect_available_models
618
- models = ctx.client.list_resources('LanguageModel', namespace: ctx.namespace)
619
- models.map { |m| m.dig('metadata', 'name') }
620
- rescue StandardError => e
621
- Formatters::ProgressFormatter.error("Failed to list models from cluster: #{e.message}")
622
- []
623
- end
624
-
625
- # Select model to use for synthesis
626
- def select_synthesis_model
627
- # If --model option specified, use it
628
- return options[:model] if options[:model]
629
-
630
- # Otherwise, auto-select from available cluster models
631
- available_models = detect_available_models
632
-
633
- if available_models.empty?
634
- Formatters::ProgressFormatter.error('No models available in cluster')
635
- puts
636
- puts 'Please create a model first:'
637
- puts ' aictl model create'
638
- puts
639
- puts 'Or list existing models:'
640
- puts ' aictl model list'
641
- exit 1
642
- end
643
-
644
- # Auto-select first available model (silently)
645
- available_models.first
646
- end
647
-
648
- # Get endpoint for a cluster model
649
- def get_model_endpoint(model_name)
650
- # For cluster models, we use the service endpoint
651
- # The service is typically named the same as the model and listens on port 4000
652
- "http://#{model_name}.#{ctx.namespace}.svc.cluster.local:4000/v1"
653
- end
654
-
655
- # Call LLM to generate code from synthesis prompt using cluster model
656
- def call_llm_for_synthesis(prompt, model_name)
657
- require 'json'
658
- require 'faraday'
659
-
660
- # Get model resource
661
- model = get_resource_or_exit('LanguageModel', model_name)
662
- model_id = model.dig('spec', 'modelName')
663
-
664
- # Get the model's pod
665
- pod = get_model_pod(model_name)
666
- pod_name = pod.dig('metadata', 'name')
667
-
668
- # Set up port-forward to access the model pod
669
- port_forward_pid = nil
670
- local_port = find_available_port
671
-
672
- begin
673
- # Start kubectl port-forward in background
674
- port_forward_pid = start_port_forward(pod_name, local_port, 4000)
675
-
676
- # Wait for port-forward to be ready
677
- wait_for_port(local_port)
678
-
679
- # Build the JSON payload for the chat completion request
680
- payload = {
681
- model: model_id,
682
- messages: [{ role: 'user', content: prompt }],
683
- max_tokens: 4000,
684
- temperature: 0.3
685
- }
686
-
687
- # Make HTTP request using Faraday
688
- conn = Faraday.new(url: "http://localhost:#{local_port}") do |f|
689
- f.request :json
690
- f.response :json
691
- f.adapter Faraday.default_adapter
692
- f.options.timeout = 120
693
- f.options.open_timeout = 10
694
- end
695
-
696
- response = conn.post('/v1/chat/completions', payload)
697
-
698
- # Parse response
699
- result = response.body
700
-
701
- if result['error']
702
- error_msg = result['error']['message'] || result['error']
703
- raise "Model error: #{error_msg}"
704
- elsif !result['choices'] || result['choices'].empty?
705
- raise "Unexpected response format: #{result.inspect}"
706
- end
707
-
708
- # Extract the content from the first choice
709
- result.dig('choices', 0, 'message', 'content')
710
- rescue Faraday::TimeoutError
711
- raise 'LLM request timed out after 120 seconds'
712
- rescue Faraday::ConnectionFailed => e
713
- raise "Failed to connect to model: #{e.message}"
714
- rescue StandardError => e
715
- Formatters::ProgressFormatter.error("LLM call failed: #{e.message}")
716
- puts
717
- puts "Make sure the model '#{model_name}' is running: kubectl get pods -n #{ctx.namespace}"
718
- exit 1
719
- ensure
720
- # Clean up port-forward process
721
- cleanup_port_forward(port_forward_pid) if port_forward_pid
722
- end
723
- end
724
-
725
- # Get the pod for a model
726
- def get_model_pod(model_name)
727
- # Get the deployment for the model
728
- deployment = ctx.client.get_resource('Deployment', model_name, ctx.namespace)
729
- labels = deployment.dig('spec', 'selector', 'matchLabels')
730
-
731
- raise "Deployment '#{model_name}' has no selector labels" if labels.nil?
732
-
733
- # Convert to hash if needed
734
- labels_hash = labels.respond_to?(:to_h) ? labels.to_h : labels
735
- raise "Deployment '#{model_name}' has empty selector labels" if labels_hash.empty?
736
-
737
- label_selector = labels_hash.map { |k, v| "#{k}=#{v}" }.join(',')
738
-
739
- # Find a running pod
740
- pods = ctx.client.list_resources('Pod', namespace: ctx.namespace, label_selector: label_selector)
741
- raise "No pods found for model '#{model_name}'" if pods.empty?
742
-
743
- running_pod = pods.find do |pod|
744
- pod.dig('status', 'phase') == 'Running' &&
745
- pod.dig('status', 'conditions')&.any? { |c| c['type'] == 'Ready' && c['status'] == 'True' }
746
- end
747
-
748
- if running_pod.nil?
749
- pod_phases = pods.map { |p| p.dig('status', 'phase') }.join(', ')
750
- raise "No running pods found. Pod phases: #{pod_phases}"
751
- end
752
-
753
- running_pod
754
- rescue K8s::Error::NotFound
755
- raise "Model deployment '#{model_name}' not found"
756
- end
757
-
758
- # Find an available local port for port-forwarding
759
- def find_available_port
760
- require 'socket'
761
-
762
- # Try ports in the range 14000-14999
763
- (14_000..14_999).each do |port|
764
- server = TCPServer.new('127.0.0.1', port)
765
- server.close
766
- return port
767
- rescue Errno::EADDRINUSE
768
- # Port in use, try next
769
- next
770
- end
771
-
772
- raise 'No available ports found in range 14000-14999'
773
- end
774
-
775
- # Start kubectl port-forward in background
776
- def start_port_forward(pod_name, local_port, remote_port)
777
- require 'English'
778
-
779
- cmd = "kubectl port-forward -n #{ctx.namespace} #{pod_name} #{local_port}:#{remote_port}"
780
- pid = spawn(cmd, out: '/dev/null', err: '/dev/null')
781
-
782
- # Detach so it runs in background
783
- Process.detach(pid)
784
-
785
- pid
786
- end
787
-
788
- # Wait for port-forward to be ready
789
- def wait_for_port(port, max_attempts: 30)
790
- require 'socket'
791
-
792
- max_attempts.times do
793
- socket = TCPSocket.new('127.0.0.1', port)
794
- socket.close
795
- return true
796
- rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH
797
- sleep 0.1
798
- end
799
-
800
- raise "Port-forward to localhost:#{port} failed to become ready after #{max_attempts} attempts"
801
- end
802
-
803
- # Clean up port-forward process
804
- def cleanup_port_forward(pid)
805
- return unless pid
806
-
807
- begin
808
- Process.kill('TERM', pid)
809
- Process.wait(pid, Process::WNOHANG)
810
- rescue Errno::ESRCH
811
- # Process already gone
812
- rescue Errno::ECHILD
813
- # Process already reaped
814
- end
815
- end
816
-
817
- # Extract Ruby code from LLM response
818
- # Looks for ```ruby ... ``` blocks
819
- def extract_ruby_code(response)
820
- # Match ```ruby ... ``` blocks
821
- match = response.match(/```ruby\n(.*?)```/m)
822
- return match[1].strip if match
823
-
824
- # Try without language specifier
825
- match = response.match(/```\n(.*?)```/m)
826
- return match[1].strip if match
827
-
828
- # If no code blocks, return nil
829
- nil
830
- end
831
-
832
- # Load template from bundled gem or operator ConfigMap
833
- def load_template(type)
834
- # Try to fetch from operator ConfigMap first (if kubectl available)
835
- template = fetch_from_operator(type)
836
- return template if template
837
-
838
- # Fall back to bundled template
839
- load_bundled_template(type)
840
- end
841
-
842
- # Fetch template from operator ConfigMap via kubectl
843
- def fetch_from_operator(type)
844
- configmap_name = type == 'agent' ? 'agent-synthesis-template' : 'persona-distillation-template'
845
- result = `kubectl get configmap #{configmap_name} -n language-operator-system -o jsonpath='{.data.template}' 2>/dev/null`
846
- result.empty? ? nil : result
847
- rescue StandardError
848
- nil
849
- end
850
-
851
- # Load bundled template from gem
852
- def load_bundled_template(type)
853
- filename = type == 'agent' ? 'agent_synthesis.tmpl' : 'persona_distillation.tmpl'
854
- template_path = File.join(__dir__, '..', '..', 'templates', filename)
855
- File.read(template_path)
856
- end
857
-
858
- # Validate template syntax and structure
859
- def validate_template_content(content, type)
860
- errors = []
861
- warnings = []
862
-
863
- # Check for required placeholders based on type
864
- required_placeholders = if type == 'agent'
865
- %w[
866
- Instructions ToolsList ModelsList AgentName TemporalIntent
867
- ]
868
- else
869
- %w[
870
- PersonaName PersonaDescription PersonaSystemPrompt
871
- AgentInstructions AgentTools
872
- ]
873
- end
874
-
875
- required_placeholders.each do |placeholder|
876
- errors << "Missing required placeholder: {{.#{placeholder}}}" unless content.include?("{{.#{placeholder}}}")
877
- end
878
-
879
- # Check for balanced braces
880
- open_braces = content.scan(/{{/).count
881
- close_braces = content.scan(/}}/).count
882
- errors << "Unbalanced template braces ({{ vs }}): #{open_braces} open, #{close_braces} close" if open_braces != close_braces
883
-
884
- # Extract and validate Ruby code blocks
885
- code_examples = extract_code_examples(content)
886
- code_examples.each do |example|
887
- code_result = validate_code_against_schema(example[:code])
888
- unless code_result[:valid]
889
- code_result[:errors].each do |err|
890
- # Adjust line numbers to be relative to template
891
- line = example[:start_line] + (err[:location] || 0)
892
- errors << "Line #{line}: #{err[:message]}"
893
- end
894
- end
895
- code_result[:warnings].each do |warn|
896
- line = example[:start_line] + (warn[:location] || 0)
897
- warnings << "Line #{line}: #{warn[:message]}"
898
- end
899
- end
900
-
901
- # Extract method calls and check if they're in the safe list
902
- method_calls = extract_method_calls(content)
903
- safe_methods = Dsl::Schema.safe_agent_methods +
904
- Dsl::Schema.safe_tool_methods +
905
- Dsl::Schema.safe_helper_methods
906
- method_calls.each do |method|
907
- next if safe_methods.include?(method)
908
-
909
- warnings << "Method '#{method}' not in safe methods list (may be valid Ruby builtin)"
910
- end
911
-
912
- {
913
- valid: errors.empty?,
914
- errors: errors,
915
- warnings: warnings
916
- }
917
- end
918
-
919
- # Extract Ruby code examples from template
920
- # Returns array of {code: String, start_line: Integer}
921
- def extract_code_examples(template)
922
- examples = []
923
- lines = template.split("\n")
924
- in_code_block = false
925
- current_code = []
926
- start_line = 0
927
-
928
- lines.each_with_index do |line, idx|
929
- if line.strip.start_with?('```ruby')
930
- in_code_block = true
931
- start_line = idx + 2 # idx is 0-based, we want line number (1-based) of first code line
932
- current_code = []
933
- elsif line.strip == '```' && in_code_block
934
- in_code_block = false
935
- examples << { code: current_code.join("\n"), start_line: start_line } unless current_code.empty?
936
- elsif in_code_block
937
- current_code << line
938
- end
939
- end
940
-
941
- examples
942
- end
943
-
944
- # Extract method calls from template code
945
- # Returns array of method name strings
946
- def extract_method_calls(template)
947
- require 'prism'
948
-
949
- method_calls = []
950
- code_examples = extract_code_examples(template)
951
-
952
- code_examples.each do |example|
953
- # Parse the code to find method calls
954
- result = Prism.parse(example[:code])
955
-
956
- # Walk the AST to find method calls
957
- extract_methods_from_ast(result.value, method_calls) if result.success?
958
- rescue Prism::ParseError
959
- # Skip code with syntax errors - they'll be caught by validate_code_against_schema
960
- next
961
- end
962
-
963
- method_calls.uniq
964
- end
965
-
966
- # Recursively extract method names from AST
967
- def extract_methods_from_ast(node, methods)
968
- return unless node
969
-
970
- methods << node.name.to_s if node.is_a?(Prism::CallNode)
971
-
972
- node.compact_child_nodes.each do |child|
973
- extract_methods_from_ast(child, methods)
974
- end
975
- end
976
-
977
- # Validate Ruby code against DSL schema
978
- # Returns {valid: Boolean, errors: Array<Hash>, warnings: Array<Hash>}
979
- def validate_code_against_schema(code)
980
- require 'language_operator/agent/safety/ast_validator'
981
-
982
- validator = LanguageOperator::Agent::Safety::ASTValidator.new
983
- violations = validator.validate(code, '(template)')
984
-
985
- errors = []
986
- warnings = []
987
-
988
- violations.each do |violation|
989
- case violation[:type]
990
- when :syntax_error
991
- errors << {
992
- type: :syntax_error,
993
- location: 0,
994
- message: violation[:message]
995
- }
996
- when :dangerous_method, :dangerous_constant, :dangerous_constant_access, :dangerous_global, :backtick_execution
997
- errors << {
998
- type: violation[:type],
999
- location: violation[:location],
1000
- message: violation[:message]
1001
- }
1002
- else
1003
- warnings << {
1004
- type: violation[:type],
1005
- location: violation[:location] || 0,
1006
- message: violation[:message]
1007
- }
1008
- end
1009
- end
1010
-
1011
- {
1012
- valid: errors.empty?,
1013
- errors: errors,
1014
- warnings: warnings
1015
- }
1016
- end
1017
-
1018
- # Output raw template format
1019
- def output_template_format(content)
1020
- puts content
1021
- end
1022
-
1023
- # Output JSON format with metadata
1024
- def output_json_format(content, type)
1025
- data = {
1026
- version: Dsl::Schema.version,
1027
- template_type: type,
1028
- template: content
1029
- }
1030
-
1031
- if options[:with_schema]
1032
- data[:schema] = Dsl::Schema.to_json_schema
1033
- data[:safe_agent_methods] = Dsl::Schema.safe_agent_methods
1034
- data[:safe_tool_methods] = Dsl::Schema.safe_tool_methods
1035
- data[:safe_helper_methods] = Dsl::Schema.safe_helper_methods
1036
- end
1037
-
1038
- puts JSON.pretty_generate(data)
1039
- end
1040
-
1041
- # Output YAML format with metadata
1042
- def output_yaml_format(content, type)
1043
- data = {
1044
- 'version' => Dsl::Schema.version,
1045
- 'template_type' => type,
1046
- 'template' => content
1047
- }
1048
-
1049
- if options[:with_schema]
1050
- data['schema'] = Dsl::Schema.to_json_schema.transform_keys(&:to_s)
1051
- data['safe_agent_methods'] = Dsl::Schema.safe_agent_methods
1052
- data['safe_tool_methods'] = Dsl::Schema.safe_tool_methods
1053
- data['safe_helper_methods'] = Dsl::Schema.safe_helper_methods
1054
- end
1055
-
1056
- puts YAML.dump(data)
1057
- end
1058
-
1059
- # Create a ConfigMap with agent code
1060
- def create_agent_configmap(name, code)
1061
- configmap = {
1062
- 'apiVersion' => 'v1',
1063
- 'kind' => 'ConfigMap',
1064
- 'metadata' => {
1065
- 'name' => name,
1066
- 'namespace' => ctx.namespace
1067
- },
1068
- 'data' => {
1069
- 'agent.rb' => code
1070
- }
1071
- }
1072
-
1073
- ctx.client.create_resource(configmap)
1074
- end
1075
-
1076
- # Create a test pod for running the agent
1077
- def create_test_pod(name, configmap_name, image)
1078
- # Detect available models in the cluster
1079
- model_env = detect_model_config
1080
-
1081
- if model_env.nil?
1082
- Formatters::ProgressFormatter.warn('Could not detect model configuration from cluster')
1083
- Formatters::ProgressFormatter.warn('Agent may fail without MODEL_ENDPOINTS configured')
1084
- end
1085
-
1086
- env_vars = [
1087
- { 'name' => 'AGENT_NAME', 'value' => name },
1088
- { 'name' => 'AGENT_MODE', 'value' => 'autonomous' },
1089
- { 'name' => 'AGENT_CODE_PATH', 'value' => '/etc/agent/code/agent.rb' },
1090
- { 'name' => 'CONFIG_PATH', 'value' => '/nonexistent/config.yaml' }
1091
- ]
1092
-
1093
- # Add model configuration if available
1094
- env_vars += model_env if model_env
1095
-
1096
- pod = {
1097
- 'apiVersion' => 'v1',
1098
- 'kind' => 'Pod',
1099
- 'metadata' => {
1100
- 'name' => name,
1101
- 'namespace' => ctx.namespace,
1102
- 'labels' => {
1103
- 'app.kubernetes.io/name' => name,
1104
- 'app.kubernetes.io/component' => 'test-agent',
1105
- 'langop.io/kind' => 'LanguageAgent'
1106
- }
1107
- },
1108
- 'spec' => {
1109
- 'restartPolicy' => 'Never',
1110
- 'containers' => [
1111
- {
1112
- 'name' => 'agent',
1113
- 'image' => image,
1114
- 'imagePullPolicy' => 'Always',
1115
- 'env' => env_vars,
1116
- 'volumeMounts' => [
1117
- {
1118
- 'name' => 'agent-code',
1119
- 'mountPath' => '/etc/agent/code',
1120
- 'readOnly' => true
1121
- }
1122
- ]
1123
- }
1124
- ],
1125
- 'volumes' => [
1126
- {
1127
- 'name' => 'agent-code',
1128
- 'configMap' => {
1129
- 'name' => configmap_name
1130
- }
1131
- }
1132
- ]
1133
- }
1134
- }
1135
-
1136
- ctx.client.create_resource(pod)
1137
- end
1138
-
1139
- # Detect model configuration from the cluster
1140
- def detect_model_config
1141
- models = ctx.client.list_resources('LanguageModel', namespace: ctx.namespace)
1142
- return nil if models.empty?
1143
-
1144
- # Use first available model
1145
- model = models.first
1146
- model_name = model.dig('metadata', 'name')
1147
- model_id = model.dig('spec', 'modelName')
1148
-
1149
- # Build endpoint URL (port 8000 is the model service port)
1150
- endpoint = "http://#{model_name}.#{ctx.namespace}.svc.cluster.local:8000"
1151
-
1152
- [
1153
- { 'name' => 'MODEL_ENDPOINTS', 'value' => endpoint },
1154
- { 'name' => 'LLM_MODEL', 'value' => model_id },
1155
- { 'name' => 'OPENAI_API_KEY', 'value' => 'sk-dummy-key-for-local-proxy' }
1156
- ]
1157
- rescue StandardError => e
1158
- Formatters::ProgressFormatter.error("Failed to detect model configuration: #{e.message}")
1159
- nil
1160
- end
1161
-
1162
- # Wait for pod to start (running or terminated)
1163
- def wait_for_pod_start(name, timeout: 60)
1164
- start_time = Time.now
1165
- loop do
1166
- pod = ctx.client.get_resource('Pod', name, ctx.namespace)
1167
- phase = pod.dig('status', 'phase')
1168
-
1169
- return if %w[Running Succeeded Failed].include?(phase)
1170
-
1171
- raise "Pod #{name} did not start within #{timeout} seconds" if Time.now - start_time > timeout
1172
-
1173
- sleep 1
1174
- end
1175
- end
1176
-
1177
- # Stream pod logs until completion
1178
- def stream_pod_logs(name, timeout: 300)
1179
- require 'open3'
1180
-
1181
- cmd = "kubectl logs -f -n #{ctx.namespace} #{name} 2>&1"
1182
- Open3.popen3(cmd) do |_stdin, stdout, _stderr, wait_thr|
1183
- # Set up timeout
1184
- start_time = Time.now
1185
-
1186
- # Stream logs
1187
- stdout.each_line do |line|
1188
- puts line
1189
-
1190
- # Check timeout
1191
- if Time.now - start_time > timeout
1192
- Process.kill('TERM', wait_thr.pid)
1193
- raise "Log streaming timed out after #{timeout} seconds"
1194
- end
1195
- end
1196
-
1197
- # Wait for process to complete
1198
- wait_thr.value
1199
- end
1200
- rescue Errno::EPIPE
1201
- # Pod terminated, logs finished
1202
- end
1203
-
1204
- # Wait for pod to terminate and get exit code
1205
- def wait_for_pod_termination(name, timeout: 10)
1206
- # Give the pod a moment to fully transition after logs complete
1207
- sleep 2
1208
-
1209
- start_time = Time.now
1210
- loop do
1211
- pod = ctx.client.get_resource('Pod', name, ctx.namespace)
1212
- phase = pod.dig('status', 'phase')
1213
- container_status = pod.dig('status', 'containerStatuses', 0)
1214
-
1215
- # Pod completed successfully or failed
1216
- if %w[Succeeded Failed].include?(phase) && container_status && (terminated = container_status.dig('state', 'terminated'))
1217
- return terminated['exitCode']
1218
- end
1219
-
1220
- # Check timeout
1221
- if Time.now - start_time > timeout
1222
- # Try one last time
1223
- if container_status && (terminated = container_status.dig('state', 'terminated'))
1224
- return terminated['exitCode']
1225
- end
1226
-
1227
- return nil
1228
- end
1229
-
1230
- sleep 0.5
1231
- rescue K8s::Error::NotFound
1232
- # Pod was deleted before we could get status
1233
- return nil
1234
- end
1235
- end
1236
-
1237
- # Get pod status
1238
- def get_pod_status(name)
1239
- pod = ctx.client.get_resource('Pod', name, ctx.namespace)
1240
- pod.to_h.fetch('status', {})
1241
- end
1242
-
1243
- # Delete a pod
1244
- def delete_pod(name)
1245
- ctx.client.delete_resource('Pod', name, ctx.namespace)
1246
- rescue K8s::Error::NotFound
1247
- # Already deleted
1248
- end
1249
-
1250
- # Delete a ConfigMap
1251
- def delete_configmap(name)
1252
- ctx.client.delete_resource('ConfigMap', name, ctx.namespace)
1253
- rescue K8s::Error::NotFound
1254
- # Already deleted
1255
- end
1256
- end
1257
- end
1258
- end
1259
- end