language-operator 0.1.30 → 0.1.31

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 (41) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +35 -0
  3. data/Gemfile.lock +1 -1
  4. data/Makefile +7 -2
  5. data/Rakefile +29 -0
  6. data/docs/dsl/SCHEMA_VERSION.md +250 -0
  7. data/docs/dsl/agent-reference.md +13 -0
  8. data/lib/language_operator/agent/safety/safe_executor.rb +12 -0
  9. data/lib/language_operator/cli/commands/agent.rb +54 -101
  10. data/lib/language_operator/cli/commands/cluster.rb +37 -1
  11. data/lib/language_operator/cli/commands/persona.rb +2 -5
  12. data/lib/language_operator/cli/commands/status.rb +5 -18
  13. data/lib/language_operator/cli/commands/system.rb +772 -0
  14. data/lib/language_operator/cli/formatters/code_formatter.rb +3 -7
  15. data/lib/language_operator/cli/formatters/log_formatter.rb +3 -5
  16. data/lib/language_operator/cli/formatters/progress_formatter.rb +3 -7
  17. data/lib/language_operator/cli/formatters/status_formatter.rb +37 -0
  18. data/lib/language_operator/cli/formatters/table_formatter.rb +10 -26
  19. data/lib/language_operator/cli/helpers/pastel_helper.rb +24 -0
  20. data/lib/language_operator/cli/main.rb +4 -0
  21. data/lib/language_operator/dsl/schema.rb +1102 -0
  22. data/lib/language_operator/dsl.rb +1 -0
  23. data/lib/language_operator/logger.rb +4 -4
  24. data/lib/language_operator/templates/README.md +23 -0
  25. data/lib/language_operator/templates/examples/agent_synthesis.tmpl +115 -0
  26. data/lib/language_operator/templates/examples/persona_distillation.tmpl +19 -0
  27. data/lib/language_operator/templates/schema/.gitkeep +0 -0
  28. data/lib/language_operator/templates/schema/CHANGELOG.md +93 -0
  29. data/lib/language_operator/templates/schema/agent_dsl_openapi.yaml +306 -0
  30. data/lib/language_operator/templates/schema/agent_dsl_schema.json +452 -0
  31. data/lib/language_operator/version.rb +1 -1
  32. data/requirements/tasks/iterate.md +2 -2
  33. metadata +13 -9
  34. data/examples/README.md +0 -569
  35. data/examples/agent_example.rb +0 -86
  36. data/examples/chat_endpoint_agent.rb +0 -118
  37. data/examples/github_webhook_agent.rb +0 -171
  38. data/examples/mcp_agent.rb +0 -158
  39. data/examples/oauth_callback_agent.rb +0 -296
  40. data/examples/stripe_webhook_agent.rb +0 -219
  41. data/examples/webhook_agent.rb +0 -80
@@ -0,0 +1,772 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'thor'
4
+ require 'json'
5
+ require 'yaml'
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 < Thor
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 version flag
43
+ if options[:version]
44
+ puts Dsl::Schema.version
45
+ return
46
+ end
47
+
48
+ # Generate schema based on format
49
+ format = options[:format].downcase
50
+ case format
51
+ when 'json'
52
+ output_json_schema
53
+ when 'yaml'
54
+ output_yaml_schema
55
+ when 'openapi'
56
+ output_openapi_schema
57
+ else
58
+ Formatters::ProgressFormatter.error("Invalid format: #{format}")
59
+ puts
60
+ puts 'Supported formats: json, yaml, openapi'
61
+ exit 1
62
+ end
63
+ rescue StandardError => e
64
+ Formatters::ProgressFormatter.error("Failed to generate schema: #{e.message}")
65
+ exit 1
66
+ end
67
+
68
+ no_commands do
69
+ # Output JSON Schema v7
70
+ def output_json_schema
71
+ schema = Dsl::Schema.to_json_schema
72
+ puts JSON.pretty_generate(schema)
73
+ end
74
+
75
+ # Output YAML Schema
76
+ def output_yaml_schema
77
+ schema = Dsl::Schema.to_json_schema
78
+ puts YAML.dump(schema.transform_keys(&:to_s))
79
+ end
80
+
81
+ # Output OpenAPI 3.0 specification
82
+ def output_openapi_schema
83
+ spec = Dsl::Schema.to_openapi
84
+ puts JSON.pretty_generate(spec)
85
+ end
86
+ end
87
+
88
+ desc 'validate_template', 'Validate synthesis template against DSL schema'
89
+ long_desc <<-DESC
90
+ Validate a synthesis template file against the DSL schema.
91
+
92
+ Extracts Ruby code examples from the template and validates each example
93
+ against the Language Operator Agent DSL schema. Checks for dangerous
94
+ methods, syntax errors, and compliance with safe coding practices.
95
+
96
+ Examples:
97
+ # Validate a custom template file
98
+ aictl system validate_template --template /path/to/template.tmpl
99
+
100
+ # Validate the bundled agent template (default)
101
+ aictl system validate_template
102
+
103
+ # Validate the bundled persona template
104
+ aictl system validate_template --type persona
105
+
106
+ # Verbose output with all violations
107
+ aictl system validate_template --template mytemplate.tmpl --verbose
108
+ DESC
109
+ option :template, type: :string, desc: 'Path to template file (defaults to bundled template)'
110
+ option :type, type: :string, default: 'agent', desc: 'Template type if using bundled template (agent, persona)'
111
+ option :verbose, type: :boolean, default: false, desc: 'Show detailed violation information'
112
+ def validate_template
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
+ rescue StandardError => e
199
+ Formatters::ProgressFormatter.error("Validation error: #{e.message}")
200
+ puts e.backtrace.first(5).join("\n") if options[:verbose]
201
+ exit 1
202
+ end
203
+
204
+ desc 'test-synthesis', 'Test agent synthesis from natural language instructions'
205
+ long_desc <<-DESC
206
+ Test the agent synthesis process by converting natural language instructions
207
+ into Ruby DSL code without creating an actual agent.
208
+
209
+ This command helps you validate your instructions and understand how the
210
+ synthesis engine interprets them. Use --dry-run to see the prompt that
211
+ would be sent to the LLM, or run without it to generate actual code.
212
+
213
+ Examples:
214
+ # Test with dry-run (show prompt only)
215
+ aictl system test-synthesis --instructions "Monitor GitHub issues daily" --dry-run
216
+
217
+ # Generate code from instructions
218
+ aictl system test-synthesis --instructions "Send daily reports to Slack"
219
+
220
+ # Specify custom agent name and tools
221
+ aictl system test-synthesis \\
222
+ --instructions "Process webhooks from GitHub" \\
223
+ --agent-name github-processor \\
224
+ --tools github,slack
225
+
226
+ # Specify available models
227
+ aictl system test-synthesis \\
228
+ --instructions "Analyze logs every hour" \\
229
+ --models gpt-4,claude-3-5-sonnet
230
+ DESC
231
+ option :instructions, type: :string, required: true, desc: 'Natural language instructions for the agent'
232
+ option :agent_name, type: :string, default: 'test-agent', desc: 'Name for the test agent'
233
+ option :tools, type: :string, desc: 'Comma-separated list of available tools'
234
+ option :models, type: :string, desc: 'Comma-separated list of available models'
235
+ option :dry_run, type: :boolean, default: false, desc: 'Show prompt without calling LLM'
236
+ def test_synthesis
237
+ # Load synthesis template
238
+ template_content = load_bundled_template('agent')
239
+
240
+ # Detect temporal intent from instructions
241
+ temporal_intent = detect_temporal_intent(options[:instructions])
242
+
243
+ # Prepare template data
244
+ template_data = {
245
+ 'Instructions' => options[:instructions],
246
+ 'AgentName' => options[:agent_name],
247
+ 'ToolsList' => format_tools_list(options[:tools]),
248
+ 'ModelsList' => format_models_list(options[:models]),
249
+ 'TemporalIntent' => temporal_intent,
250
+ 'PersonaSection' => '',
251
+ 'ScheduleSection' => temporal_intent == 'scheduled' ? ' schedule "0 */1 * * *" # Example hourly schedule' : '',
252
+ 'ScheduleRules' => temporal_intent == 'scheduled' ? "\n2. Include schedule with cron expression\n3. Set mode to :scheduled\n4. " : "\n2. ",
253
+ 'ConstraintsSection' => '',
254
+ 'ErrorContext' => nil
255
+ }
256
+
257
+ # Render template (Go-style template syntax)
258
+ rendered_prompt = render_go_template(template_content, template_data)
259
+
260
+ if options[:dry_run]
261
+ # Show the prompt that would be sent
262
+ puts 'Synthesis Prompt Preview'
263
+ puts '=' * 80
264
+ puts
265
+ puts rendered_prompt
266
+ puts
267
+ puts '=' * 80
268
+ Formatters::ProgressFormatter.success('Dry-run complete - prompt displayed above')
269
+ return
270
+ end
271
+
272
+ # Call LLM to generate code
273
+ puts 'Generating agent code from instructions...'
274
+ puts
275
+
276
+ llm_response = call_llm_for_synthesis(rendered_prompt)
277
+
278
+ # Extract Ruby code from response
279
+ generated_code = extract_ruby_code(llm_response)
280
+
281
+ if generated_code.nil?
282
+ Formatters::ProgressFormatter.error('Failed to extract Ruby code from LLM response')
283
+ puts
284
+ puts 'LLM Response:'
285
+ puts llm_response
286
+ exit 1
287
+ end
288
+
289
+ # Display generated code
290
+ puts 'Generated Code:'
291
+ puts '=' * 80
292
+ puts generated_code
293
+ puts '=' * 80
294
+ puts
295
+
296
+ # Validate generated code
297
+ puts 'Validating generated code...'
298
+ validation_result = validate_code_against_schema(generated_code)
299
+
300
+ if validation_result[:valid] && validation_result[:warnings].empty?
301
+ Formatters::ProgressFormatter.success('✅ Code is valid - No issues found')
302
+ elsif validation_result[:valid]
303
+ Formatters::ProgressFormatter.success('✅ Code is valid - With warnings')
304
+ puts
305
+ validation_result[:warnings].each do |warn|
306
+ puts " ⚠ #{warn[:message]}"
307
+ end
308
+ else
309
+ Formatters::ProgressFormatter.error('❌ Code validation failed')
310
+ puts
311
+ validation_result[:errors].each do |err|
312
+ puts " ✗ #{err[:message]}"
313
+ end
314
+ end
315
+
316
+ puts
317
+ rescue StandardError => e
318
+ Formatters::ProgressFormatter.error("Test synthesis failed: #{e.message}")
319
+ puts e.backtrace.first(5).join("\n") if options[:verbose]
320
+ exit 1
321
+ end
322
+
323
+ desc 'synthesis-template', 'Export synthesis templates for agent code generation'
324
+ long_desc <<-DESC
325
+ Export the synthesis templates used by the Language Operator to generate
326
+ agent code from natural language instructions.
327
+
328
+ These templates are used by the operator's synthesis engine to convert
329
+ user instructions into executable Ruby DSL code.
330
+
331
+ Examples:
332
+ # Export agent synthesis template (default)
333
+ aictl system synthesis-template
334
+
335
+ # Export persona distillation template
336
+ aictl system synthesis-template --type persona
337
+
338
+ # Export as JSON with schema included
339
+ aictl system synthesis-template --format json --with-schema
340
+
341
+ # Export as YAML
342
+ aictl system synthesis-template --format yaml
343
+
344
+ # Validate template syntax
345
+ aictl system synthesis-template --validate
346
+
347
+ # Save to file
348
+ aictl system synthesis-template > agent_synthesis.tmpl
349
+ DESC
350
+ option :format, type: :string, default: 'template', desc: 'Output format (template, json, yaml)'
351
+ option :type, type: :string, default: 'agent', desc: 'Template type (agent, persona)'
352
+ option :with_schema, type: :boolean, default: false, desc: 'Include DSL schema in output'
353
+ option :validate, type: :boolean, default: false, desc: 'Validate template syntax'
354
+ def synthesis_template
355
+ # Validate type
356
+ template_type = options[:type].downcase
357
+ unless %w[agent persona].include?(template_type)
358
+ Formatters::ProgressFormatter.error("Invalid template type: #{template_type}")
359
+ puts
360
+ puts 'Supported types: agent, persona'
361
+ exit 1
362
+ end
363
+
364
+ # Load template
365
+ template_content = load_template(template_type)
366
+
367
+ # Validate if requested
368
+ if options[:validate]
369
+ validation_result = validate_template_content(template_content, template_type)
370
+
371
+ # Display warnings if any
372
+ unless validation_result[:warnings].empty?
373
+ Formatters::ProgressFormatter.warn('Template validation warnings:')
374
+ validation_result[:warnings].each do |warning|
375
+ puts " ⚠ #{warning}"
376
+ end
377
+ puts
378
+ end
379
+
380
+ # Display errors and exit if validation failed
381
+ if validation_result[:valid]
382
+ Formatters::ProgressFormatter.success('Template validation passed')
383
+ return
384
+ else
385
+ Formatters::ProgressFormatter.error('Template validation failed:')
386
+ validation_result[:errors].each do |error|
387
+ puts " ✗ #{error}"
388
+ end
389
+ exit 1
390
+ end
391
+ end
392
+
393
+ # Generate output based on format
394
+ format = options[:format].downcase
395
+ case format
396
+ when 'template'
397
+ output_template_format(template_content)
398
+ when 'json'
399
+ output_json_format(template_content, template_type)
400
+ when 'yaml'
401
+ output_yaml_format(template_content, template_type)
402
+ else
403
+ Formatters::ProgressFormatter.error("Invalid format: #{format}")
404
+ puts
405
+ puts 'Supported formats: template, json, yaml'
406
+ exit 1
407
+ end
408
+ rescue StandardError => e
409
+ Formatters::ProgressFormatter.error("Failed to load template: #{e.message}")
410
+ exit 1
411
+ end
412
+
413
+ private
414
+
415
+ # Render Go-style template ({{.Variable}})
416
+ # Simplified implementation for basic variable substitution
417
+ def render_go_template(template, data)
418
+ result = template.dup
419
+
420
+ # Handle {{if .ErrorContext}} - remove this section for test-synthesis
421
+ result.gsub!(/{{if \.ErrorContext}}.*?{{else}}/m, '')
422
+ result.gsub!(/{{end}}/, '')
423
+
424
+ # Replace simple variables {{.Variable}}
425
+ data.each do |key, value|
426
+ result.gsub!("{{.#{key}}}", value.to_s)
427
+ end
428
+
429
+ result
430
+ end
431
+
432
+ # Detect temporal intent from instructions (scheduled vs autonomous)
433
+ def detect_temporal_intent(instructions)
434
+ temporal_keywords = {
435
+ scheduled: %w[daily weekly hourly monthly schedule cron every day week hour minute],
436
+ autonomous: %w[monitor watch continuously constantly always loop]
437
+ }
438
+
439
+ instructions_lower = instructions.downcase
440
+
441
+ # Check for scheduled keywords
442
+ scheduled_matches = temporal_keywords[:scheduled].count { |keyword| instructions_lower.include?(keyword) }
443
+ autonomous_matches = temporal_keywords[:autonomous].count { |keyword| instructions_lower.include?(keyword) }
444
+
445
+ scheduled_matches > autonomous_matches ? 'scheduled' : 'autonomous'
446
+ end
447
+
448
+ # Format tools list for template
449
+ def format_tools_list(tools_str)
450
+ return 'No tools specified' if tools_str.nil? || tools_str.strip.empty?
451
+
452
+ tools = tools_str.split(',').map(&:strip)
453
+ tools.map { |tool| "- #{tool}" }.join("\n")
454
+ end
455
+
456
+ # Format models list for template
457
+ def format_models_list(models_str)
458
+ # If not specified, try to detect from environment
459
+ if models_str.nil? || models_str.strip.empty?
460
+ models = detect_available_models
461
+ return models.map { |model| "- #{model}" }.join("\n") unless models.empty?
462
+
463
+ return 'No models specified (configure ANTHROPIC_API_KEY or OPENAI_API_KEY)'
464
+ end
465
+
466
+ models = models_str.split(',').map(&:strip)
467
+ models.map { |model| "- #{model}" }.join("\n")
468
+ end
469
+
470
+ # Detect available models from environment
471
+ def detect_available_models
472
+ models = []
473
+ models << 'claude-3-5-sonnet-20241022' if ENV['ANTHROPIC_API_KEY']
474
+ models << 'gpt-4-turbo' if ENV['OPENAI_API_KEY']
475
+ models
476
+ end
477
+
478
+ # Call LLM to generate code from synthesis prompt
479
+ def call_llm_for_synthesis(prompt)
480
+ require 'ruby_llm'
481
+
482
+ # Check for API keys
483
+ unless ENV['ANTHROPIC_API_KEY'] || ENV['OPENAI_API_KEY']
484
+ Formatters::ProgressFormatter.error('No LLM credentials found')
485
+ puts
486
+ puts 'Please set one of the following environment variables:'
487
+ puts ' - ANTHROPIC_API_KEY (for Claude models)'
488
+ puts ' - OPENAI_API_KEY (for GPT models)'
489
+ exit 1
490
+ end
491
+
492
+ # Prefer Anthropic if available
493
+ if ENV['ANTHROPIC_API_KEY']
494
+ provider = :anthropic
495
+ api_key = ENV['ANTHROPIC_API_KEY']
496
+ model = 'claude-3-5-sonnet-20241022'
497
+ else
498
+ provider = :openai
499
+ api_key = ENV.fetch('OPENAI_API_KEY', nil)
500
+ model = 'gpt-4-turbo'
501
+ end
502
+
503
+ # Create client and call LLM
504
+ client = RubyLLM.new(provider: provider, api_key: api_key)
505
+ messages = [{ role: 'user', content: prompt }]
506
+
507
+ response = client.chat(messages, model: model, max_tokens: 4000, temperature: 0.3)
508
+
509
+ # Extract content from response
510
+ if response.is_a?(Hash) && response.key?('content')
511
+ response['content']
512
+ elsif response.is_a?(String)
513
+ response
514
+ else
515
+ response.to_s
516
+ end
517
+ rescue StandardError => e
518
+ Formatters::ProgressFormatter.error("LLM call failed: #{e.message}")
519
+ exit 1
520
+ end
521
+
522
+ # Extract Ruby code from LLM response
523
+ # Looks for ```ruby ... ``` blocks
524
+ def extract_ruby_code(response)
525
+ # Match ```ruby ... ``` blocks
526
+ match = response.match(/```ruby\n(.*?)```/m)
527
+ return match[1].strip if match
528
+
529
+ # Try without language specifier
530
+ match = response.match(/```\n(.*?)```/m)
531
+ return match[1].strip if match
532
+
533
+ # If no code blocks, return nil
534
+ nil
535
+ end
536
+
537
+ # Load template from bundled gem or operator ConfigMap
538
+ def load_template(type)
539
+ # Try to fetch from operator ConfigMap first (if kubectl available)
540
+ template = fetch_from_operator(type)
541
+ return template if template
542
+
543
+ # Fall back to bundled template
544
+ load_bundled_template(type)
545
+ end
546
+
547
+ # Fetch template from operator ConfigMap via kubectl
548
+ def fetch_from_operator(type)
549
+ configmap_name = type == 'agent' ? 'agent-synthesis-template' : 'persona-distillation-template'
550
+ result = `kubectl get configmap #{configmap_name} -n language-operator-system -o jsonpath='{.data.template}' 2>/dev/null`
551
+ result.empty? ? nil : result
552
+ rescue StandardError
553
+ nil
554
+ end
555
+
556
+ # Load bundled template from gem
557
+ def load_bundled_template(type)
558
+ filename = type == 'agent' ? 'agent_synthesis.tmpl' : 'persona_distillation.tmpl'
559
+ template_path = File.join(__dir__, '..', '..', 'templates', 'examples', filename)
560
+ File.read(template_path)
561
+ end
562
+
563
+ # Validate template syntax and structure
564
+ def validate_template_content(content, type)
565
+ errors = []
566
+ warnings = []
567
+
568
+ # Check for required placeholders based on type
569
+ required_placeholders = if type == 'agent'
570
+ %w[
571
+ Instructions ToolsList ModelsList AgentName TemporalIntent
572
+ ]
573
+ else
574
+ %w[
575
+ PersonaName PersonaDescription PersonaSystemPrompt
576
+ AgentInstructions AgentTools
577
+ ]
578
+ end
579
+
580
+ required_placeholders.each do |placeholder|
581
+ errors << "Missing required placeholder: {{.#{placeholder}}}" unless content.include?("{{.#{placeholder}}}")
582
+ end
583
+
584
+ # Check for balanced braces
585
+ open_braces = content.scan(/{{/).count
586
+ close_braces = content.scan(/}}/).count
587
+ errors << "Unbalanced template braces ({{ vs }}): #{open_braces} open, #{close_braces} close" if open_braces != close_braces
588
+
589
+ # Extract and validate Ruby code blocks
590
+ code_examples = extract_code_examples(content)
591
+ code_examples.each do |example|
592
+ code_result = validate_code_against_schema(example[:code])
593
+ unless code_result[:valid]
594
+ code_result[:errors].each do |err|
595
+ # Adjust line numbers to be relative to template
596
+ line = example[:start_line] + (err[:location] || 0)
597
+ errors << "Line #{line}: #{err[:message]}"
598
+ end
599
+ end
600
+ code_result[:warnings].each do |warn|
601
+ line = example[:start_line] + (warn[:location] || 0)
602
+ warnings << "Line #{line}: #{warn[:message]}"
603
+ end
604
+ end
605
+
606
+ # Extract method calls and check if they're in the safe list
607
+ method_calls = extract_method_calls(content)
608
+ safe_methods = Dsl::Schema.safe_agent_methods +
609
+ Dsl::Schema.safe_tool_methods +
610
+ Dsl::Schema.safe_helper_methods
611
+ method_calls.each do |method|
612
+ next if safe_methods.include?(method)
613
+
614
+ warnings << "Method '#{method}' not in safe methods list (may be valid Ruby builtin)"
615
+ end
616
+
617
+ {
618
+ valid: errors.empty?,
619
+ errors: errors,
620
+ warnings: warnings
621
+ }
622
+ end
623
+
624
+ # Extract Ruby code examples from template
625
+ # Returns array of {code: String, start_line: Integer}
626
+ def extract_code_examples(template)
627
+ examples = []
628
+ lines = template.split("\n")
629
+ in_code_block = false
630
+ current_code = []
631
+ start_line = 0
632
+
633
+ lines.each_with_index do |line, idx|
634
+ if line.strip.start_with?('```ruby')
635
+ in_code_block = true
636
+ start_line = idx + 2 # idx is 0-based, we want line number (1-based) of first code line
637
+ current_code = []
638
+ elsif line.strip == '```' && in_code_block
639
+ in_code_block = false
640
+ examples << { code: current_code.join("\n"), start_line: start_line } unless current_code.empty?
641
+ elsif in_code_block
642
+ current_code << line
643
+ end
644
+ end
645
+
646
+ examples
647
+ end
648
+
649
+ # Extract method calls from template code
650
+ # Returns array of method name strings
651
+ def extract_method_calls(template)
652
+ require 'parser/current'
653
+
654
+ method_calls = []
655
+ code_examples = extract_code_examples(template)
656
+
657
+ code_examples.each do |example|
658
+ # Parse the code to find method calls
659
+ buffer = Parser::Source::Buffer.new('(template)')
660
+ buffer.source = example[:code]
661
+ parser = Parser::CurrentRuby.new
662
+ ast = parser.parse(buffer)
663
+
664
+ # Walk the AST to find method calls
665
+ extract_methods_from_ast(ast, method_calls)
666
+ rescue Parser::SyntaxError
667
+ # Skip code with syntax errors - they'll be caught by validate_code_against_schema
668
+ next
669
+ end
670
+
671
+ method_calls.uniq
672
+ end
673
+
674
+ # Recursively extract method names from AST
675
+ def extract_methods_from_ast(node, methods)
676
+ return unless node.is_a?(Parser::AST::Node)
677
+
678
+ if node.type == :send
679
+ _, method_name, * = node.children
680
+ methods << method_name.to_s if method_name
681
+ end
682
+
683
+ node.children.each do |child|
684
+ extract_methods_from_ast(child, methods)
685
+ end
686
+ end
687
+
688
+ # Validate Ruby code against DSL schema
689
+ # Returns {valid: Boolean, errors: Array<Hash>, warnings: Array<Hash>}
690
+ def validate_code_against_schema(code)
691
+ require 'language_operator/agent/safety/ast_validator'
692
+
693
+ validator = LanguageOperator::Agent::Safety::ASTValidator.new
694
+ violations = validator.validate(code, '(template)')
695
+
696
+ errors = []
697
+ warnings = []
698
+
699
+ violations.each do |violation|
700
+ case violation[:type]
701
+ when :syntax_error
702
+ errors << {
703
+ type: :syntax_error,
704
+ location: 0,
705
+ message: violation[:message]
706
+ }
707
+ when :dangerous_method, :dangerous_constant, :dangerous_constant_access, :dangerous_global, :backtick_execution
708
+ errors << {
709
+ type: violation[:type],
710
+ location: violation[:location],
711
+ message: violation[:message]
712
+ }
713
+ else
714
+ warnings << {
715
+ type: violation[:type],
716
+ location: violation[:location] || 0,
717
+ message: violation[:message]
718
+ }
719
+ end
720
+ end
721
+
722
+ {
723
+ valid: errors.empty?,
724
+ errors: errors,
725
+ warnings: warnings
726
+ }
727
+ end
728
+
729
+ # Output raw template format
730
+ def output_template_format(content)
731
+ puts content
732
+ end
733
+
734
+ # Output JSON format with metadata
735
+ def output_json_format(content, type)
736
+ data = {
737
+ version: Dsl::Schema.version,
738
+ template_type: type,
739
+ template: content
740
+ }
741
+
742
+ if options[:with_schema]
743
+ data[:schema] = Dsl::Schema.to_json_schema
744
+ data[:safe_agent_methods] = Dsl::Schema.safe_agent_methods
745
+ data[:safe_tool_methods] = Dsl::Schema.safe_tool_methods
746
+ data[:safe_helper_methods] = Dsl::Schema.safe_helper_methods
747
+ end
748
+
749
+ puts JSON.pretty_generate(data)
750
+ end
751
+
752
+ # Output YAML format with metadata
753
+ def output_yaml_format(content, type)
754
+ data = {
755
+ 'version' => Dsl::Schema.version,
756
+ 'template_type' => type,
757
+ 'template' => content
758
+ }
759
+
760
+ if options[:with_schema]
761
+ data['schema'] = Dsl::Schema.to_json_schema.transform_keys(&:to_s)
762
+ data['safe_agent_methods'] = Dsl::Schema.safe_agent_methods
763
+ data['safe_tool_methods'] = Dsl::Schema.safe_tool_methods
764
+ data['safe_helper_methods'] = Dsl::Schema.safe_helper_methods
765
+ end
766
+
767
+ puts YAML.dump(data)
768
+ end
769
+ end
770
+ end
771
+ end
772
+ end