language-operator 0.1.31 → 0.1.35

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +7 -8
  3. data/CHANGELOG.md +14 -0
  4. data/CI_STATUS.md +56 -0
  5. data/Gemfile.lock +2 -2
  6. data/Makefile +22 -6
  7. data/lib/language_operator/agent/base.rb +10 -6
  8. data/lib/language_operator/agent/executor.rb +19 -97
  9. data/lib/language_operator/agent/safety/ast_validator.rb +62 -43
  10. data/lib/language_operator/agent/safety/safe_executor.rb +27 -2
  11. data/lib/language_operator/agent/scheduler.rb +60 -0
  12. data/lib/language_operator/agent/task_executor.rb +548 -0
  13. data/lib/language_operator/agent.rb +90 -27
  14. data/lib/language_operator/cli/base_command.rb +117 -0
  15. data/lib/language_operator/cli/commands/agent.rb +339 -407
  16. data/lib/language_operator/cli/commands/cluster.rb +274 -290
  17. data/lib/language_operator/cli/commands/install.rb +110 -119
  18. data/lib/language_operator/cli/commands/model.rb +284 -184
  19. data/lib/language_operator/cli/commands/persona.rb +218 -284
  20. data/lib/language_operator/cli/commands/quickstart.rb +4 -5
  21. data/lib/language_operator/cli/commands/status.rb +31 -35
  22. data/lib/language_operator/cli/commands/system.rb +221 -233
  23. data/lib/language_operator/cli/commands/tool.rb +356 -422
  24. data/lib/language_operator/cli/commands/use.rb +19 -22
  25. data/lib/language_operator/cli/helpers/resource_dependency_checker.rb +0 -18
  26. data/lib/language_operator/cli/wizards/quickstart_wizard.rb +0 -1
  27. data/lib/language_operator/client/config.rb +20 -21
  28. data/lib/language_operator/config.rb +115 -3
  29. data/lib/language_operator/constants.rb +54 -0
  30. data/lib/language_operator/dsl/agent_context.rb +7 -7
  31. data/lib/language_operator/dsl/agent_definition.rb +111 -26
  32. data/lib/language_operator/dsl/config.rb +30 -66
  33. data/lib/language_operator/dsl/main_definition.rb +114 -0
  34. data/lib/language_operator/dsl/schema.rb +84 -43
  35. data/lib/language_operator/dsl/task_definition.rb +315 -0
  36. data/lib/language_operator/dsl.rb +0 -1
  37. data/lib/language_operator/instrumentation/task_tracer.rb +285 -0
  38. data/lib/language_operator/logger.rb +4 -4
  39. data/lib/language_operator/synthesis_test_harness.rb +324 -0
  40. data/lib/language_operator/templates/examples/agent_synthesis.tmpl +26 -8
  41. data/lib/language_operator/templates/schema/CHANGELOG.md +26 -0
  42. data/lib/language_operator/templates/schema/agent_dsl_openapi.yaml +1 -1
  43. data/lib/language_operator/templates/schema/agent_dsl_schema.json +84 -42
  44. data/lib/language_operator/type_coercion.rb +250 -0
  45. data/lib/language_operator/ux/base.rb +81 -0
  46. data/lib/language_operator/ux/concerns/README.md +155 -0
  47. data/lib/language_operator/ux/concerns/headings.rb +90 -0
  48. data/lib/language_operator/ux/concerns/input_validation.rb +146 -0
  49. data/lib/language_operator/ux/concerns/provider_helpers.rb +167 -0
  50. data/lib/language_operator/ux/create_agent.rb +252 -0
  51. data/lib/language_operator/ux/create_model.rb +267 -0
  52. data/lib/language_operator/ux/quickstart.rb +594 -0
  53. data/lib/language_operator/version.rb +1 -1
  54. data/lib/language_operator.rb +2 -0
  55. data/requirements/ARCHITECTURE.md +1 -0
  56. data/requirements/SCRATCH.md +153 -0
  57. data/requirements/dsl.md +0 -0
  58. data/requirements/features +1 -0
  59. data/requirements/personas +1 -0
  60. data/requirements/proposals +1 -0
  61. data/requirements/tasks/iterate.md +14 -15
  62. data/requirements/tasks/optimize.md +13 -4
  63. data/synth/001/Makefile +90 -0
  64. data/synth/001/agent.rb +26 -0
  65. data/synth/001/agent.yaml +7 -0
  66. data/synth/001/output.log +44 -0
  67. data/synth/Makefile +39 -0
  68. data/synth/README.md +342 -0
  69. metadata +37 -10
  70. data/lib/language_operator/dsl/workflow_definition.rb +0 -259
  71. data/test_agent_dsl.rb +0 -108
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'thor'
3
+ require_relative '../base_command'
4
4
  require 'pastel'
5
5
  require_relative '../formatters/progress_formatter'
6
6
  require_relative '../../config/cluster_config'
@@ -9,37 +9,34 @@ module LanguageOperator
9
9
  module CLI
10
10
  module Commands
11
11
  # Switch cluster context command
12
- class Use < Thor
12
+ class Use < BaseCommand
13
13
  desc 'use CLUSTER', 'Switch to a different cluster context'
14
14
  def self.exit_on_failure?
15
15
  true
16
16
  end
17
17
 
18
18
  def switch(cluster_name)
19
- unless Config::ClusterConfig.cluster_exists?(cluster_name)
20
- Formatters::ProgressFormatter.error("Cluster '#{cluster_name}' not found")
21
- puts "\nAvailable clusters:"
22
- Config::ClusterConfig.list_clusters.each do |cluster|
23
- puts " - #{cluster[:name]}"
19
+ handle_command_error('switch cluster') do
20
+ unless Config::ClusterConfig.cluster_exists?(cluster_name)
21
+ Formatters::ProgressFormatter.error("Cluster '#{cluster_name}' not found")
22
+ puts "\nAvailable clusters:"
23
+ Config::ClusterConfig.list_clusters.each do |cluster|
24
+ puts " - #{cluster[:name]}"
25
+ end
26
+ exit 1
24
27
  end
25
- exit 1
26
- end
27
-
28
- Config::ClusterConfig.set_current_cluster(cluster_name)
29
- cluster = Config::ClusterConfig.get_cluster(cluster_name)
30
28
 
31
- Formatters::ProgressFormatter.success("Switched to cluster '#{cluster_name}'")
29
+ Config::ClusterConfig.set_current_cluster(cluster_name)
30
+ cluster = Config::ClusterConfig.get_cluster(cluster_name)
32
31
 
33
- pastel = Pastel.new
34
- puts "\nCluster Details"
35
- puts '----------------'
36
- puts "Name: #{pastel.bold.white(cluster[:name])}"
37
- puts "Namespace: #{pastel.bold.white(cluster[:namespace])}"
38
- rescue StandardError => e
39
- Formatters::ProgressFormatter.error("Failed to switch cluster: #{e.message}")
40
- raise if ENV['DEBUG']
32
+ Formatters::ProgressFormatter.success("Switched to cluster '#{cluster_name}'")
41
33
 
42
- exit 1
34
+ pastel = Pastel.new
35
+ puts "\nCluster Details"
36
+ puts '----------------'
37
+ puts "Name: #{pastel.bold.white(cluster[:name])}"
38
+ puts "Namespace: #{pastel.bold.white(cluster[:namespace])}"
39
+ end
43
40
  end
44
41
  end
45
42
  end
@@ -50,24 +50,6 @@ module LanguageOperator
50
50
  def self.tool_usage_count(agents, tool_name)
51
51
  agents_using_tool(agents, tool_name).size
52
52
  end
53
-
54
- # Count how many agents use a specific model
55
- #
56
- # @param agents [Array<Hash>] Array of agent resources
57
- # @param model_name [String] Name of the model
58
- # @return [Integer] Count of agents using this model
59
- def self.model_usage_count(agents, model_name)
60
- agents_using_model(agents, model_name).size
61
- end
62
-
63
- # Count how many agents use a specific persona
64
- #
65
- # @param agents [Array<Hash>] Array of agent resources
66
- # @param persona_name [String] Name of the persona
67
- # @return [Integer] Count of agents using this persona
68
- def self.persona_usage_count(agents, persona_name)
69
- agents_using_persona(agents, persona_name).size
70
- end
71
53
  end
72
54
  end
73
55
  end
@@ -413,7 +413,6 @@ module LanguageOperator
413
413
  end
414
414
 
415
415
  def create_model_resource(cluster_info, name, provider, model, api_key = nil, endpoint = nil)
416
- # rubocop:disable Metrics/BlockLength
417
416
  Formatters::ProgressFormatter.with_spinner("Creating model '#{name}'") do
418
417
  k8s = Kubernetes::Client.new(
419
418
  kubeconfig: cluster_info[:kubeconfig],
@@ -34,12 +34,13 @@ module LanguageOperator
34
34
  {
35
35
  'llm' => {
36
36
  'provider' => detect_provider_from_env,
37
- 'model' => ENV.fetch('LLM_MODEL') { default_model_from_env },
37
+ 'model' => LanguageOperator::Config.get('LLM_MODEL', default: default_model_from_env),
38
38
  'endpoint' => parse_model_endpoint_from_env,
39
- 'api_key' => ENV.fetch('OPENAI_API_KEY') { ENV.fetch('ANTHROPIC_API_KEY', 'dummy-key-for-local-proxy') }
39
+ 'api_key' => LanguageOperator::Config.get('OPENAI_API_KEY', 'ANTHROPIC_API_KEY',
40
+ default: 'dummy-key-for-local-proxy')
40
41
  },
41
42
  'mcp_servers' => parse_mcp_servers_from_env,
42
- 'debug' => ENV['DEBUG'] == 'true'
43
+ 'debug' => LanguageOperator::Config.get_bool('DEBUG', default: false)
43
44
  }
44
45
  end
45
46
 
@@ -49,15 +50,12 @@ module LanguageOperator
49
50
  #
50
51
  # @return [String, nil] Model endpoint URL
51
52
  def self.parse_model_endpoint_from_env
52
- # Support MODEL_ENDPOINTS (operator sets this)
53
- endpoints_env = ENV.fetch('MODEL_ENDPOINTS', nil)
54
- if endpoints_env && !endpoints_env.empty?
55
- # Take the first endpoint from comma-separated list
56
- endpoints_env.split(',').first.strip
57
- else
58
- # Fallback to legacy OPENAI_ENDPOINT
59
- ENV.fetch('OPENAI_ENDPOINT', nil)
60
- end
53
+ # Support MODEL_ENDPOINTS (operator sets this) - take first from comma-separated list
54
+ endpoints = LanguageOperator::Config.get_array('MODEL_ENDPOINTS')
55
+ return endpoints.first unless endpoints.empty?
56
+
57
+ # Fallback to legacy OPENAI_ENDPOINT
58
+ LanguageOperator::Config.get('OPENAI_ENDPOINT')
61
59
  end
62
60
 
63
61
  # Load configuration with automatic fallback to environment variables
@@ -79,11 +77,11 @@ module LanguageOperator
79
77
  # @return [String] Provider name (openai_compatible, openai, or anthropic)
80
78
  # @raise [RuntimeError] If no API key or endpoint is found
81
79
  def self.detect_provider_from_env
82
- if ENV['OPENAI_ENDPOINT'] || ENV['MODEL_ENDPOINTS']
80
+ if LanguageOperator::Config.set?('OPENAI_ENDPOINT', 'MODEL_ENDPOINTS')
83
81
  'openai_compatible'
84
- elsif ENV['OPENAI_API_KEY']
82
+ elsif LanguageOperator::Config.set?('OPENAI_API_KEY')
85
83
  'openai'
86
- elsif ENV['ANTHROPIC_API_KEY']
84
+ elsif LanguageOperator::Config.set?('ANTHROPIC_API_KEY')
87
85
  'anthropic'
88
86
  else
89
87
  raise 'No API key or endpoint found. Set OPENAI_ENDPOINT or MODEL_ENDPOINTS for local LLM, ' \
@@ -109,21 +107,22 @@ module LanguageOperator
109
107
  # @return [Array<Hash>] Array of MCP server configurations
110
108
  def self.parse_mcp_servers_from_env
111
109
  # Support both MCP_SERVERS (comma-separated) and legacy MCP_URL
112
- servers_env = ENV.fetch('MCP_SERVERS', nil)
113
- if servers_env && !servers_env.empty?
110
+ servers = LanguageOperator::Config.get_array('MCP_SERVERS')
111
+
112
+ if !servers.empty?
114
113
  # Parse comma-separated URLs
115
- servers_env.split(',').map.with_index do |url, index|
114
+ servers.map.with_index do |url, index|
116
115
  {
117
116
  'name' => "default-tools-#{index}",
118
- 'url' => url.strip,
117
+ 'url' => url,
119
118
  'transport' => 'streamable',
120
119
  'enabled' => true
121
120
  }
122
121
  end
123
- elsif ENV['MCP_URL']
122
+ elsif (url = LanguageOperator::Config.get('MCP_URL'))
124
123
  [{
125
124
  'name' => 'default-tools',
126
- 'url' => ENV['MCP_URL'],
125
+ 'url' => url,
127
126
  'transport' => 'streamable',
128
127
  'enabled' => true
129
128
  }]
@@ -8,8 +8,9 @@ module LanguageOperator
8
8
  # - Key-to-env-var mappings
9
9
  # - Prefixes (e.g., SMTP_HOST from prefix: 'SMTP')
10
10
  # - Default values
11
- # - Type conversion (string, integer, boolean, float)
11
+ # - Type conversion (string, integer, boolean, float, array)
12
12
  # - Required config validation
13
+ # - Multiple fallback keys
13
14
  #
14
15
  # @example Load SMTP configuration
15
16
  # config = LanguageOperator::Config.load(
@@ -75,7 +76,8 @@ module LanguageOperator
75
76
  # Convert a string value to the specified type
76
77
  #
77
78
  # @param value [String, nil] Raw string value from environment
78
- # @param type [Symbol] Target type (:string, :integer, :boolean, :float)
79
+ # @param type [Symbol] Target type (:string, :integer, :boolean, :float, :array)
80
+ # @param separator [String] Separator for array type (default: ',')
79
81
  # @return [Object] Converted value
80
82
  #
81
83
  # @example String conversion
@@ -92,7 +94,10 @@ module LanguageOperator
92
94
  #
93
95
  # @example Float conversion
94
96
  # Config.convert_type('3.14', :float) # => 3.14
95
- def self.convert_type(value, type)
97
+ #
98
+ # @example Array conversion
99
+ # Config.convert_type('a,b,c', :array) # => ["a", "b", "c"]
100
+ def self.convert_type(value, type, separator: ',')
96
101
  return nil if value.nil?
97
102
 
98
103
  case type
@@ -104,6 +109,10 @@ module LanguageOperator
104
109
  value.to_f
105
110
  when :boolean
106
111
  %w[true 1 yes on].include?(value.to_s.downcase)
112
+ when :array
113
+ return [] if value.to_s.empty?
114
+
115
+ value.to_s.split(separator).map(&:strip).reject(&:empty?)
107
116
  else
108
117
  value
109
118
  end
@@ -134,5 +143,108 @@ module LanguageOperator
134
143
  validate_required!(config, required) unless required.empty?
135
144
  config
136
145
  end
146
+
147
+ # Get environment variable with multiple fallback keys
148
+ #
149
+ # @param keys [Array<String>] Environment variable names to try
150
+ # @param default [Object, nil] Default value if none found
151
+ # @return [String, nil] The first non-nil value or default
152
+ #
153
+ # @example
154
+ # Config.get('SMTP_HOST', 'MAIL_HOST', default: 'localhost')
155
+ def self.get(*keys, default: nil)
156
+ keys.each do |key|
157
+ value = ENV.fetch(key.to_s, nil)
158
+ return value if value
159
+ end
160
+ default
161
+ end
162
+
163
+ # Get required environment variable with fallback keys
164
+ #
165
+ # @param keys [Array<String>] Environment variable names to try
166
+ # @return [String] The first non-nil value
167
+ # @raise [ArgumentError] If none of the keys are set
168
+ #
169
+ # @example
170
+ # Config.require('DATABASE_URL', 'DB_URL')
171
+ def self.require(*keys)
172
+ value = get(*keys)
173
+ raise ArgumentError, "Missing required configuration: #{keys.join(' or ')}" unless value
174
+
175
+ value
176
+ end
177
+
178
+ # Get environment variable as integer
179
+ #
180
+ # @param keys [Array<String>] Environment variable names to try
181
+ # @param default [Integer, nil] Default value if none found
182
+ # @return [Integer, nil] The value converted to integer, or default
183
+ #
184
+ # @example
185
+ # Config.get_int('MAX_WORKERS', default: 4)
186
+ def self.get_int(*keys, default: nil)
187
+ value = get(*keys)
188
+ return default if value.nil?
189
+
190
+ value.to_i
191
+ end
192
+
193
+ # Get environment variable as boolean
194
+ #
195
+ # Treats 'true', '1', 'yes', 'on' as true (case insensitive).
196
+ #
197
+ # @param keys [Array<String>] Environment variable names to try
198
+ # @param default [Boolean] Default value if none found
199
+ # @return [Boolean] The value as boolean
200
+ #
201
+ # @example
202
+ # Config.get_bool('USE_TLS', 'ENABLE_TLS', default: true)
203
+ def self.get_bool(*keys, default: false)
204
+ value = get(*keys)
205
+ return default if value.nil?
206
+
207
+ %w[true 1 yes on].include?(value.to_s.downcase)
208
+ end
209
+
210
+ # Get environment variable as array (split by separator)
211
+ #
212
+ # @param keys [Array<String>] Environment variable names to try
213
+ # @param default [Array] Default value if none found
214
+ # @param separator [String] Character to split on (default: ',')
215
+ # @return [Array<String>] The value split into array
216
+ #
217
+ # @example
218
+ # Config.get_array('ALLOWED_HOSTS', separator: ',')
219
+ def self.get_array(*keys, default: [], separator: ',')
220
+ value = get(*keys)
221
+ return default if value.nil? || value.empty?
222
+
223
+ value.split(separator).map(&:strip).reject(&:empty?)
224
+ end
225
+
226
+ # Check if environment variable is set (even if empty string)
227
+ #
228
+ # @param keys [Array<String>] Environment variable names to check
229
+ # @return [Boolean] True if any key is set
230
+ #
231
+ # @example
232
+ # Config.set?('DEBUG', 'VERBOSE')
233
+ def self.set?(*keys)
234
+ keys.any? { |key| ENV.key?(key.to_s) }
235
+ end
236
+
237
+ # Get all environment variables matching a prefix
238
+ #
239
+ # @param prefix [String] Prefix to match
240
+ # @return [Hash<String, String>] Hash with prefix removed from keys
241
+ #
242
+ # @example
243
+ # Config.with_prefix('DATABASE_')
244
+ # # Returns { 'URL' => '...', 'POOL_SIZE' => '5' } for DATABASE_URL and DATABASE_POOL_SIZE
245
+ def self.with_prefix(prefix)
246
+ ENV.select { |key, _| key.start_with?(prefix) }
247
+ .transform_keys { |key| key.sub(prefix, '') }
248
+ end
137
249
  end
138
250
  end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LanguageOperator
4
+ # Shared constants used across the Language Operator gem
5
+ module Constants
6
+ # Agent execution modes with their aliases
7
+ # Primary modes are the canonical forms used in CRD schemas
8
+ # Aliases are accepted for backwards compatibility and convenience
9
+ EXECUTION_MODES = {
10
+ autonomous: %w[autonomous interactive].freeze,
11
+ scheduled: %w[scheduled event-driven].freeze,
12
+ reactive: %w[reactive http webhook].freeze
13
+ }.freeze
14
+
15
+ # Primary (canonical) execution modes for schema definitions
16
+ PRIMARY_MODES = %w[autonomous scheduled reactive].freeze
17
+
18
+ # All valid mode strings (primary + aliases)
19
+ ALL_MODE_ALIASES = EXECUTION_MODES.values.flatten.freeze
20
+
21
+ # Normalizes a mode string to its primary canonical form
22
+ #
23
+ # @param mode_string [String] The mode string to normalize
24
+ # @return [String] The canonical primary mode
25
+ # @raise [ArgumentError] if the mode string is not recognized
26
+ #
27
+ # @example
28
+ # Constants.normalize_mode('interactive') # => 'autonomous'
29
+ # Constants.normalize_mode('webhook') # => 'reactive'
30
+ # Constants.normalize_mode('scheduled') # => 'scheduled'
31
+ def self.normalize_mode(mode_string)
32
+ return nil if mode_string.nil?
33
+
34
+ mode_string = mode_string.to_s.downcase.strip
35
+
36
+ EXECUTION_MODES.each do |primary, aliases|
37
+ return primary.to_s if aliases.include?(mode_string)
38
+ end
39
+
40
+ raise ArgumentError, "Unknown execution mode: #{mode_string}. " \
41
+ "Valid modes: #{ALL_MODE_ALIASES.join(', ')}"
42
+ end
43
+
44
+ # Validates that a mode string is recognized
45
+ #
46
+ # @param mode_string [String] The mode string to validate
47
+ # @return [Boolean] true if valid, false otherwise
48
+ def self.valid_mode?(mode_string)
49
+ return false if mode_string.nil?
50
+
51
+ ALL_MODE_ALIASES.include?(mode_string.to_s.downcase.strip)
52
+ end
53
+ end
54
+ end
@@ -13,14 +13,14 @@ module LanguageOperator
13
13
  #
14
14
  # schedule "0 12 * * *"
15
15
  #
16
- # objectives [
17
- # "Search for recent news",
18
- # "Summarize findings"
19
- # ]
16
+ # task :search,
17
+ # instructions: "search for recent news",
18
+ # inputs: {},
19
+ # outputs: { results: 'array' }
20
20
  #
21
- # workflow do
22
- # step :search, tool: "web_search"
23
- # step :summarize, depends_on: :search
21
+ # main do |inputs|
22
+ # results = execute_task(:search)
23
+ # results
24
24
  # end
25
25
  # end
26
26
  class AgentContext
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'workflow_definition'
3
+ require_relative 'main_definition'
4
+ require_relative 'task_definition'
4
5
  require_relative 'webhook_definition'
5
6
  require_relative 'mcp_server_definition'
6
7
  require_relative 'chat_endpoint_definition'
@@ -11,7 +12,7 @@ module LanguageOperator
11
12
  module Dsl
12
13
  # Agent definition for autonomous agents
13
14
  #
14
- # Defines an agent with objectives, workflow, schedule, and constraints.
15
+ # Defines an agent with objectives, tasks, main execution block, schedule, and constraints.
15
16
  # Used within the DSL to create agents that can be executed standalone
16
17
  # or deployed to Kubernetes.
17
18
  #
@@ -21,14 +22,14 @@ module LanguageOperator
21
22
  #
22
23
  # schedule "0 12 * * *"
23
24
  #
24
- # objectives [
25
- # "Search for recent news",
26
- # "Summarize findings"
27
- # ]
25
+ # task :search,
26
+ # instructions: "search for latest news",
27
+ # inputs: {},
28
+ # outputs: { results: 'array' }
28
29
  #
29
- # workflow do
30
- # step :search, tool: "web_search", params: {query: "latest news"}
31
- # step :summarize, depends_on: :search
30
+ # main do |inputs|
31
+ # results = execute_task(:search)
32
+ # results
32
33
  # end
33
34
  # end
34
35
  #
@@ -47,7 +48,7 @@ module LanguageOperator
47
48
  class AgentDefinition
48
49
  include LanguageOperator::Loggable
49
50
 
50
- attr_reader :name, :description, :persona, :schedule, :objectives, :workflow,
51
+ attr_reader :name, :description, :persona, :schedule, :objectives, :main, :tasks,
51
52
  :constraints, :output_config, :execution_mode, :webhooks, :mcp_server, :chat_endpoint
52
53
 
53
54
  def initialize(name)
@@ -56,7 +57,8 @@ module LanguageOperator
56
57
  @persona = nil
57
58
  @schedule = nil
58
59
  @objectives = []
59
- @workflow = nil
60
+ @main = nil
61
+ @tasks = {}
60
62
  @constraints = {}
61
63
  @output_config = {}
62
64
  @execution_mode = :autonomous
@@ -118,16 +120,95 @@ module LanguageOperator
118
120
  @objectives << text
119
121
  end
120
122
 
121
- # Define workflow with steps
123
+ # Define main execution block (DSL v1)
122
124
  #
123
- # @yield Workflow definition block
124
- # @return [WorkflowDefinition] Current workflow
125
- def workflow(&block)
126
- return @workflow if block.nil?
125
+ # The main block is the imperative entry point for agent execution.
126
+ # It receives agent inputs and returns agent outputs. Use execute_task()
127
+ # to call organic functions (tasks) defined with the task directive.
128
+ #
129
+ # @yield Main execution block
130
+ # @return [MainDefinition] Current main definition
131
+ # @example
132
+ # main do |inputs|
133
+ # result = execute_task(:fetch_data, inputs: inputs)
134
+ # execute_task(:process_data, inputs: result)
135
+ # end
136
+ def main(&block)
137
+ return @main if block.nil?
138
+
139
+ @main = MainDefinition.new
140
+ @main.execute(&block) if block
141
+ @main
142
+ end
143
+
144
+ # Define a task (organic function) - DSL v1
145
+ #
146
+ # Tasks are the core primitive of DSL v1, representing organic functions with
147
+ # stable input/output contracts. Tasks can be neural (instructions-based),
148
+ # symbolic (code-based), or hybrid (both).
149
+ #
150
+ # @param name [Symbol] Task name
151
+ # @param options [Hash] Task configuration
152
+ # @option options [Hash] :inputs Input schema (param => type)
153
+ # @option options [Hash] :outputs Output schema (field => type)
154
+ # @option options [String] :instructions Natural language instructions (neural)
155
+ # @yield [inputs] Symbolic implementation block (optional)
156
+ # @yieldparam inputs [Hash] Validated input parameters
157
+ # @yieldreturn [Hash] Output matching outputs schema
158
+ # @return [TaskDefinition] The task definition
159
+ #
160
+ # @example Neural task
161
+ # task :analyze_data,
162
+ # instructions: "Analyze the data for anomalies",
163
+ # inputs: { data: 'array' },
164
+ # outputs: { issues: 'array', summary: 'string' }
165
+ #
166
+ # @example Symbolic task
167
+ # task :calculate_total,
168
+ # inputs: { items: 'array' },
169
+ # outputs: { total: 'number' }
170
+ # do |inputs|
171
+ # { total: inputs[:items].sum { |i| i['amount'] } }
172
+ # end
173
+ #
174
+ # @example Hybrid task
175
+ # task :fetch_user,
176
+ # instructions: "Fetch user from database",
177
+ # inputs: { user_id: 'integer' },
178
+ # outputs: { user: 'hash' }
179
+ # do |inputs|
180
+ # execute_tool('database', 'get_user', id: inputs[:user_id])
181
+ # end
182
+ def task(name, **options, &block)
183
+ # Create task definition
184
+ task_def = TaskDefinition.new(name)
185
+
186
+ # Configure from options (keyword arguments)
187
+ task_def.inputs(options[:inputs]) if options[:inputs]
188
+ task_def.outputs(options[:outputs]) if options[:outputs]
189
+ task_def.instructions(options[:instructions]) if options[:instructions]
190
+
191
+ # Symbolic implementation (if block provided)
192
+ task_def.execute(&block) if block
193
+
194
+ # Store in tasks collection
195
+ @tasks[name] = task_def
196
+
197
+ task_type = if task_def.neural? && task_def.symbolic?
198
+ 'hybrid'
199
+ elsif task_def.neural?
200
+ 'neural'
201
+ else
202
+ 'symbolic'
203
+ end
204
+
205
+ logger.debug('Task defined',
206
+ name: name,
207
+ type: task_type,
208
+ inputs: options[:inputs]&.keys || [],
209
+ outputs: options[:outputs]&.keys || [])
127
210
 
128
- @workflow = WorkflowDefinition.new
129
- @workflow.instance_eval(&block) if block
130
- @workflow
211
+ task_def
131
212
  end
132
213
 
133
214
  # Define constraints (max_iterations, timeout, etc.)
@@ -213,7 +294,7 @@ module LanguageOperator
213
294
  name: @name,
214
295
  mode: @execution_mode,
215
296
  objectives_count: @objectives.size,
216
- has_workflow: !@workflow.nil?)
297
+ has_main: !@main.nil?)
217
298
 
218
299
  case @execution_mode
219
300
  when :scheduled
@@ -317,7 +398,7 @@ module LanguageOperator
317
398
  def execute_objectives
318
399
  logger.info('Executing objectives',
319
400
  total: @objectives.size,
320
- has_workflow: !@workflow.nil?)
401
+ has_main: !@main.nil?)
321
402
 
322
403
  @objectives.each_with_index do |objective, index|
323
404
  logger.info('Executing objective',
@@ -325,13 +406,13 @@ module LanguageOperator
325
406
  total: @objectives.size,
326
407
  objective: objective[0..100])
327
408
 
328
- # If workflow defined, execute it; otherwise just log
329
- if @workflow
330
- logger.timed('Objective workflow execution') do
331
- @workflow.execute(objective)
409
+ # If main defined, execute it; otherwise just log
410
+ if @main
411
+ logger.timed('Objective main execution') do
412
+ @main.call({ objective: objective })
332
413
  end
333
414
  else
334
- logger.warn('No workflow defined, skipping execution')
415
+ logger.warn('No main block defined, skipping execution')
335
416
  end
336
417
  end
337
418
 
@@ -353,6 +434,10 @@ module LanguageOperator
353
434
  @constraints[:timeout] = value
354
435
  end
355
436
 
437
+ def max_retries(value)
438
+ @constraints[:max_retries] = value
439
+ end
440
+
356
441
  def memory(value)
357
442
  @constraints[:memory] = value
358
443
  end