language-operator 0.1.63 → 0.1.66

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/.plan.md +127 -0
  3. data/.rspec +3 -0
  4. data/Gemfile +2 -0
  5. data/Gemfile.lock +4 -1
  6. data/Makefile +34 -80
  7. data/README.md +20 -1
  8. data/components/agent/Gemfile +1 -1
  9. data/docs/cheat-sheet.md +173 -0
  10. data/docs/observability.md +208 -0
  11. data/lib/language_operator/agent/base.rb +10 -1
  12. data/lib/language_operator/agent/event_config.rb +172 -0
  13. data/lib/language_operator/agent/safety/ast_validator.rb +1 -1
  14. data/lib/language_operator/agent/safety/safe_executor.rb +5 -1
  15. data/lib/language_operator/agent/task_executor.rb +97 -7
  16. data/lib/language_operator/agent/telemetry.rb +25 -3
  17. data/lib/language_operator/agent/web_server.rb +6 -9
  18. data/lib/language_operator/agent.rb +24 -14
  19. data/lib/language_operator/cli/commands/agent/base.rb +155 -64
  20. data/lib/language_operator/cli/commands/agent/code_operations.rb +157 -16
  21. data/lib/language_operator/cli/commands/cluster.rb +2 -2
  22. data/lib/language_operator/cli/commands/status.rb +2 -2
  23. data/lib/language_operator/cli/commands/system/synthesize.rb +1 -1
  24. data/lib/language_operator/cli/errors/suggestions.rb +1 -1
  25. data/lib/language_operator/cli/formatters/value_formatter.rb +1 -1
  26. data/lib/language_operator/cli/helpers/ux_helper.rb +3 -4
  27. data/lib/language_operator/config.rb +3 -3
  28. data/lib/language_operator/constants/kubernetes_labels.rb +2 -2
  29. data/lib/language_operator/constants.rb +1 -0
  30. data/lib/language_operator/dsl/task_definition.rb +18 -7
  31. data/lib/language_operator/instrumentation/task_tracer.rb +44 -3
  32. data/lib/language_operator/kubernetes/client.rb +112 -1
  33. data/lib/language_operator/templates/schema/CHANGELOG.md +28 -0
  34. data/lib/language_operator/templates/schema/agent_dsl_openapi.yaml +1 -1
  35. data/lib/language_operator/templates/schema/agent_dsl_schema.json +1 -1
  36. data/lib/language_operator/type_coercion.rb +22 -8
  37. data/lib/language_operator/version.rb +1 -1
  38. data/synth/002/agent.rb +23 -12
  39. data/synth/002/output.log +88 -15
  40. data/synth/003/Makefile +17 -4
  41. data/synth/003/agent.txt +1 -1
  42. data/synth/004/Makefile +54 -0
  43. data/synth/004/README.md +281 -0
  44. data/synth/004/instructions.txt +1 -0
  45. metadata +11 -6
  46. data/lib/language_operator/cli/commands/agent/learning.rb +0 -289
  47. data/synth/003/agent.optimized.rb +0 -66
  48. data/synth/003/agent.synthesized.rb +0 -41
@@ -58,8 +58,8 @@ module LanguageOperator
58
58
  unless k8s.namespace_exists?(namespace)
59
59
  Formatters::ProgressFormatter.with_spinner("Creating namespace '#{namespace}'") do
60
60
  k8s.create_namespace(namespace, labels: Constants::KubernetesLabels.cluster_management_labels.merge(
61
- Constants::KubernetesLabels::CLUSTER_LABEL => name
62
- ))
61
+ Constants::KubernetesLabels::CLUSTER_LABEL => name
62
+ ))
63
63
  end
64
64
  end
65
65
 
@@ -35,13 +35,13 @@ module LanguageOperator
35
35
 
36
36
  # Format cluster info using UxHelper
37
37
  logo(title: 'cluster status')
38
-
38
+
39
39
  if cluster_resource
40
40
  # Use actual cluster resource data
41
41
  status = cluster_resource.dig('status', 'phase') || 'Unknown'
42
42
  domain = cluster_resource.dig('spec', 'domain')
43
43
  created = cluster_resource.dig('metadata', 'creationTimestamp')
44
-
44
+
45
45
  format_cluster_details(
46
46
  name: current_cluster,
47
47
  namespace: cluster_config[:namespace],
@@ -55,7 +55,7 @@ module LanguageOperator
55
55
  option :model, type: :string, desc: 'Model to use for synthesis (defaults to first available in cluster)'
56
56
  option :dry_run, type: :boolean, default: false, desc: 'Show prompt without calling LLM'
57
57
  option :raw, type: :boolean, default: false, desc: 'Output only the raw code without formatting'
58
-
58
+
59
59
  def synthesize(instructions = nil)
60
60
  handle_command_error('synthesize agent') do
61
61
  # Read instructions from STDIN if not provided as argument
@@ -51,7 +51,7 @@ module LanguageOperator
51
51
  def agent_not_found_suggestions(context)
52
52
  suggestions = []
53
53
  suggestions << "List all agents: #{pastel.dim('aictl agent list')}"
54
- suggestions << "Create a new agent: #{pastel.dim('aictl agent create \"description\"')}"
54
+ suggestions << "Create a new agent: #{pastel.dim('aictl agent create "description"')}"
55
55
  suggestions << "Use the wizard: #{pastel.dim('aictl agent create --wizard')}" if context[:suggest_wizard]
56
56
  suggestions
57
57
  end
@@ -51,7 +51,7 @@ module LanguageOperator
51
51
  # ValueFormatter.time_ago(Time.now - 300) # => "5m ago"
52
52
  def self.time_ago(past_time)
53
53
  diff = Time.now - past_time
54
-
54
+
55
55
  if diff < 0
56
56
  'in the future' # Edge case for clock skew
57
57
  elsif diff < SECONDS_PER_MINUTE
@@ -139,8 +139,7 @@ module LanguageOperator
139
139
  # @example
140
140
  # puts highlight_ruby_code("puts 'Hello, world!'")
141
141
  def highlight_ruby_code(code_content)
142
- highlighted = rouge_formatter.format(rouge_lexer.lex(code_content))
143
- highlighted
142
+ rouge_formatter.format(rouge_lexer.lex(code_content))
144
143
  end
145
144
 
146
145
  def logo(title: nil, sparkle: false)
@@ -409,7 +408,7 @@ module LanguageOperator
409
408
  def format_resource_details(type:, name:, common_fields: {}, optional_fields: {})
410
409
  rows = { 'Name' => pastel.white.bold(name) }
411
410
  rows.merge!(common_fields)
412
-
411
+
413
412
  optional_fields.each do |key, value|
414
413
  case key
415
414
  when 'Domain'
@@ -418,7 +417,7 @@ module LanguageOperator
418
417
  rows[key] = value if value
419
418
  end
420
419
  end
421
-
420
+
422
421
  highlighted_box(
423
422
  title: "Language#{type}",
424
423
  rows: rows.compact
@@ -191,7 +191,7 @@ module LanguageOperator
191
191
  # Config.get_int('MAX_WORKERS', default: 4)
192
192
  def self.get_int(*keys, default: nil)
193
193
  keys.each do |key|
194
- value = ENV[key.to_s]
194
+ value = ENV.fetch(key.to_s, nil)
195
195
  next unless value
196
196
 
197
197
  begin
@@ -220,7 +220,7 @@ module LanguageOperator
220
220
  # Config.get_bool('USE_TLS', 'ENABLE_TLS', default: true)
221
221
  def self.get_bool(*keys, default: false)
222
222
  keys.each do |key|
223
- value = ENV[key.to_s]
223
+ value = ENV.fetch(key.to_s, nil)
224
224
  next unless value
225
225
 
226
226
  return %w[true 1 yes on].include?(value.to_s.downcase)
@@ -240,7 +240,7 @@ module LanguageOperator
240
240
  # Config.get_array('ALLOWED_HOSTS', separator: ',')
241
241
  def self.get_array(*keys, default: [], separator: ',')
242
242
  keys.each do |key|
243
- value = ENV[key.to_s]
243
+ value = ENV.fetch(key.to_s, nil)
244
244
  next unless value
245
245
  next if value.empty?
246
246
 
@@ -61,7 +61,7 @@ module LanguageOperator
61
61
  # Build a label selector string for finding tool pods
62
62
  #
63
63
  # @param tool_name [String] The tool name
64
- # @return [String] Label selector string for kubectl commands
64
+ # @return [String] Label selector string for kubectl commands
65
65
  def tool_selector(tool_name)
66
66
  "#{TOOL_LABEL}=#{tool_name}"
67
67
  end
@@ -77,4 +77,4 @@ module LanguageOperator
77
77
  end
78
78
  end
79
79
  end
80
- end
80
+ end
@@ -60,6 +60,7 @@ module LanguageOperator
60
60
  # Kubernetes Custom Resource Definitions (CRD) kinds
61
61
  # These replace magic strings scattered across CLI commands
62
62
  RESOURCE_AGENT = 'LanguageAgent'
63
+ RESOURCE_AGENT_VERSION = 'LanguageAgentVersion'
63
64
  RESOURCE_MODEL = 'LanguageModel'
64
65
  RESOURCE_TOOL = 'LanguageTool'
65
66
  RESOURCE_PERSONA = 'LanguagePersona'
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative '../loggable'
4
4
  require_relative '../type_coercion'
5
+ require_relative '../agent/task_executor'
5
6
 
6
7
  module LanguageOperator
7
8
  module Dsl
@@ -148,7 +149,7 @@ module LanguageOperator
148
149
  #
149
150
  # @param params [Hash] Input parameters
150
151
  # @return [Hash] Validated and coerced parameters
151
- # @raise [ArgumentError] If validation fails
152
+ # @raise [LanguageOperator::Agent::TaskValidationError] If validation fails
152
153
  def validate_inputs(params)
153
154
  params = params.transform_keys(&:to_sym)
154
155
  validated = {}
@@ -157,7 +158,12 @@ module LanguageOperator
157
158
  key_sym = key.to_sym
158
159
  value = params[key_sym]
159
160
 
160
- raise ArgumentError, "Missing required input parameter: #{key}" if value.nil?
161
+ if value.nil?
162
+ original_error = ArgumentError.new("Missing required input parameter: #{key}")
163
+ raise LanguageOperator::Agent::TaskValidationError.new(@name,
164
+ "Missing required input parameter: #{key}",
165
+ original_error)
166
+ end
161
167
 
162
168
  validated[key_sym] = coerce_value(value, type, "input parameter '#{key}'")
163
169
  end
@@ -173,7 +179,7 @@ module LanguageOperator
173
179
  #
174
180
  # @param result [Hash] Output values
175
181
  # @return [Hash] Validated and coerced outputs
176
- # @raise [ArgumentError] If validation fails
182
+ # @raise [LanguageOperator::Agent::TaskValidationError] If validation fails
177
183
  def validate_outputs(result)
178
184
  return result if @outputs_schema.empty? # No schema = no validation
179
185
 
@@ -184,7 +190,12 @@ module LanguageOperator
184
190
  key_sym = key.to_sym
185
191
  value = result[key_sym]
186
192
 
187
- raise ArgumentError, "Missing required output field: #{key}" if value.nil?
193
+ if value.nil?
194
+ original_error = ArgumentError.new("Missing required output field: #{key}")
195
+ raise LanguageOperator::Agent::TaskValidationError.new(@name,
196
+ "Missing required output field: #{key}",
197
+ original_error)
198
+ end
188
199
 
189
200
  validated[key_sym] = coerce_value(value, type, "output field '#{key}'")
190
201
  end
@@ -275,12 +286,12 @@ module LanguageOperator
275
286
  # @param type [String] Target type
276
287
  # @param context [String] Context for error messages
277
288
  # @return [Object] Coerced value
278
- # @raise [ArgumentError] If coercion fails
289
+ # @raise [LanguageOperator::Agent::TaskValidationError] If coercion fails
279
290
  def coerce_value(value, type, context)
280
291
  TypeCoercion.coerce(value, type)
281
292
  rescue ArgumentError => e
282
- # Re-raise with context added
283
- raise ArgumentError, "#{e.message} for #{context}"
293
+ # Re-raise as TaskValidationError with context added
294
+ raise LanguageOperator::Agent::TaskValidationError.new(@name, "#{e.message} for #{context}", e)
284
295
  end
285
296
 
286
297
  # Convert schema hash to JSON Schema format
@@ -87,13 +87,19 @@ module LanguageOperator
87
87
  # @param prompt [String] The generated prompt
88
88
  # @param validated_inputs [Hash] Validated input parameters
89
89
  # @return [Hash] Span attributes
90
- def neural_task_attributes(_task, prompt, validated_inputs)
90
+ def neural_task_attributes(task, prompt, validated_inputs)
91
91
  attributes = {
92
92
  'gen_ai.operation.name' => 'chat',
93
93
  'gen_ai.system' => determine_genai_system,
94
94
  'gen_ai.prompt.size' => prompt.bytesize
95
95
  }
96
96
 
97
+ # Add agent context (CRITICAL for learning system)
98
+ add_agent_context_attributes(attributes)
99
+
100
+ # Add task identification
101
+ attributes['task.name'] = task.name.to_s if task.respond_to?(:name) && task.name
102
+
97
103
  # Add model if available
98
104
  if @agent.respond_to?(:config) && @agent.config
99
105
  model = @agent.config.dig('llm', 'model') || @agent.config['model']
@@ -130,10 +136,19 @@ module LanguageOperator
130
136
  # @param task [TaskDefinition] The task definition
131
137
  # @return [Hash] Span attributes
132
138
  def symbolic_task_attributes(task)
133
- {
139
+ attributes = {
134
140
  'task.execution.type' => 'symbolic',
135
- 'task.execution.has_block' => task.execute_block ? 'true' : 'false'
141
+ 'task.execution.has_block' => task.execute_block ? 'true' : 'false',
142
+ 'gen_ai.operation.name' => 'execute_task'
136
143
  }
144
+
145
+ # Add agent context (CRITICAL for learning system)
146
+ add_agent_context_attributes(attributes)
147
+
148
+ # Add task identification
149
+ attributes['task.name'] = task.name.to_s if task.respond_to?(:name) && task.name
150
+
151
+ attributes
137
152
  end
138
153
 
139
154
  # Record token usage from LLM response on span
@@ -265,6 +280,9 @@ module LanguageOperator
265
280
  'gen_ai.tool.name' => extract_tool_name(tool_call)
266
281
  }
267
282
 
283
+ # Add agent context (CRITICAL for learning system)
284
+ add_agent_context_attributes(attributes)
285
+
268
286
  # Add tool call ID if available
269
287
  attributes['gen_ai.tool.call.id'] = tool_call.id.to_s if tool_call.respond_to?(:id) && tool_call.id
270
288
 
@@ -341,6 +359,29 @@ module LanguageOperator
341
359
  rescue StandardError => e
342
360
  logger&.warn('Failed to record output metadata', error: e.message)
343
361
  end
362
+
363
+ # Add agent context attributes to span attributes hash
364
+ #
365
+ # Ensures all spans include agent identification required for learning system.
366
+ # This is redundant with resource attributes but provides explicit visibility.
367
+ #
368
+ # @param attributes [Hash] Span attributes hash to modify
369
+ def add_agent_context_attributes(attributes)
370
+ # Agent name is CRITICAL for learning controller to track executions
371
+ if (agent_name = ENV.fetch('AGENT_NAME', nil))
372
+ attributes['agent.name'] = agent_name
373
+ end
374
+
375
+ # Add agent mode for better traceability
376
+ if (agent_mode = ENV.fetch('AGENT_MODE', nil))
377
+ attributes['agent.mode'] = agent_mode
378
+ end
379
+
380
+ # Add cluster context if available
381
+ if (cluster_name = ENV.fetch('AGENT_CLUSTER', nil))
382
+ attributes['agent.cluster'] = cluster_name
383
+ end
384
+ end
344
385
  end
345
386
  # rubocop:enable Metrics/ModuleLength
346
387
  end
@@ -3,6 +3,7 @@
3
3
  require 'k8s-ruby'
4
4
  require 'yaml'
5
5
  require_relative '../utils/secure_path'
6
+ require_relative '../agent/event_config'
6
7
 
7
8
  module LanguageOperator
8
9
  module Kubernetes
@@ -163,6 +164,72 @@ module LanguageOperator
163
164
  create_resource(resource)
164
165
  end
165
166
 
167
+ # Create a Kubernetes Event
168
+ # @param event [Hash] Event resource hash
169
+ # @return [K8s::Resource] Created event resource
170
+ def create_event(event)
171
+ # Ensure proper apiVersion and kind for events
172
+ event = event.merge({
173
+ 'apiVersion' => 'v1',
174
+ 'kind' => 'Event'
175
+ })
176
+ create_resource(event)
177
+ end
178
+
179
+ # Emit an execution event for agent task completion
180
+ # @param task_name [String] Name of the executed task
181
+ # @param success [Boolean] Whether task succeeded
182
+ # @param duration_ms [Float] Execution duration in milliseconds
183
+ # @param metadata [Hash] Additional metadata to include
184
+ # @return [K8s::Resource, nil] Created event resource or nil if disabled
185
+ def emit_execution_event(task_name, success:, duration_ms:, metadata: {})
186
+ # Check if events are enabled based on configuration
187
+ event_config = Agent::EventConfig.load
188
+ event_type = success ? :success : :failure
189
+ return nil unless Agent::EventConfig.should_emit?(event_type, event_config)
190
+
191
+ agent_name = ENV.fetch('AGENT_NAME', nil)
192
+ agent_namespace = ENV.fetch('AGENT_NAMESPACE', current_namespace)
193
+
194
+ return nil unless agent_name && agent_namespace
195
+
196
+ timestamp = Time.now.to_i
197
+ event_name = "#{agent_name}-task-#{task_name}-#{timestamp}"
198
+
199
+ event = {
200
+ 'metadata' => {
201
+ 'name' => event_name,
202
+ 'namespace' => agent_namespace,
203
+ 'labels' => {
204
+ 'app.kubernetes.io/name' => 'language-operator',
205
+ 'app.kubernetes.io/component' => 'agent',
206
+ 'langop.io/agent-name' => agent_name,
207
+ 'langop.io/task-name' => task_name.to_s
208
+ }
209
+ },
210
+ 'involvedObject' => {
211
+ 'kind' => 'LanguageAgent',
212
+ 'name' => agent_name,
213
+ 'namespace' => agent_namespace,
214
+ 'apiVersion' => 'langop.io/v1alpha1'
215
+ },
216
+ 'reason' => success ? 'TaskCompleted' : 'TaskFailed',
217
+ 'message' => build_event_message(task_name, success, duration_ms, metadata, event_config),
218
+ 'type' => success ? 'Normal' : 'Warning',
219
+ 'firstTimestamp' => Time.now.utc.iso8601,
220
+ 'lastTimestamp' => Time.now.utc.iso8601,
221
+ 'source' => {
222
+ 'component' => 'language-operator-agent'
223
+ }
224
+ }
225
+
226
+ create_event(event)
227
+ rescue StandardError => e
228
+ # Log error but don't fail task execution
229
+ warn "Failed to emit execution event: #{e.message}"
230
+ nil
231
+ end
232
+
166
233
  # Check if operator is installed
167
234
  def operator_installed?
168
235
  # Check if LanguageCluster CRD exists
@@ -184,6 +251,48 @@ module LanguageOperator
184
251
 
185
252
  private
186
253
 
254
+ # Build event message for task execution
255
+ # @param task_name [String] Task name
256
+ # @param success [Boolean] Whether task succeeded
257
+ # @param duration_ms [Float] Execution duration in milliseconds
258
+ # @param metadata [Hash] Additional metadata
259
+ # @param event_config [Hash] Event configuration
260
+ # @return [String] Formatted event message
261
+ def build_event_message(task_name, success, duration_ms, metadata = {}, event_config = nil)
262
+ event_config ||= Agent::EventConfig.load
263
+ content_config = Agent::EventConfig.content_config(event_config)
264
+
265
+ status = success ? 'completed successfully' : 'failed'
266
+ message = "Task '#{task_name}' #{status} in #{duration_ms.round(2)}ms"
267
+
268
+ # Include metadata if configured
269
+ if content_config[:include_task_metadata] && metadata.any?
270
+ # Filter metadata based on configuration
271
+ filtered_metadata = metadata.dup
272
+
273
+ # Remove error details if not configured to include them
274
+ unless content_config[:include_error_details]
275
+ filtered_metadata.delete('error_type')
276
+ filtered_metadata.delete('error_category')
277
+ end
278
+
279
+ if filtered_metadata.any?
280
+ details = filtered_metadata.map { |k, v| "#{k}: #{v}" }.join(', ')
281
+ message += " (#{details})"
282
+ end
283
+ end
284
+
285
+ # Truncate if configured and message is too long
286
+ if content_config[:truncate_long_messages] &&
287
+ message.length > content_config[:max_message_length]
288
+ max_length = content_config[:max_message_length]
289
+ truncated_length = message.length - max_length
290
+ message = message[0...max_length] + "... (truncated #{truncated_length} chars)"
291
+ end
292
+
293
+ message
294
+ end
295
+
187
296
  # Build singleton instance with automatic config detection
188
297
  def self.build_singleton
189
298
  if in_cluster?
@@ -266,6 +375,8 @@ module LanguageOperator
266
375
  'statefulsets'
267
376
  when 'cronjob'
268
377
  'cronjobs'
378
+ when 'event'
379
+ 'events'
269
380
  else
270
381
  # Generic pluralization - add 's'
271
382
  "#{kind.downcase}s"
@@ -274,7 +385,7 @@ module LanguageOperator
274
385
 
275
386
  def default_api_version(kind)
276
387
  case kind.downcase
277
- when 'languagecluster', 'languageagent', 'languagetool', 'languagemodel', 'languageclient', 'languagepersona'
388
+ when 'languagecluster', 'languageagent', 'languageagentversion', 'languagetool', 'languagemodel', 'languageclient', 'languagepersona'
278
389
  'langop.io/v1alpha1'
279
390
  when 'namespace', 'configmap', 'secret', 'service'
280
391
  'v1'
@@ -12,6 +12,34 @@ The schema version is tied directly to the gem version and follows [Semantic Ver
12
12
 
13
13
  ## Version History
14
14
 
15
+ ### 0.1.64 (2025-11-27)
16
+
17
+ **Learning Status & Observability Improvements**
18
+
19
+ This release enhances the learning status tracking and observability features for agents.
20
+
21
+ **New Features:**
22
+ - Added semantic OpenTelemetry attributes for learning status tracking
23
+ - Implemented Kubernetes event emission for agent-operator communication
24
+ - Real execution metrics from learning status ConfigMap integration
25
+
26
+ **Improvements:**
27
+ - Enhanced `aictl agent learning-status` command with color-coded boxes and better clarity
28
+ - Changed terminology from "Runs Completed" to "Runs Processed"
29
+ - Improved learning status display formatting with cyan highlighted boxes
30
+ - Restructured learning status into two clear informational boxes
31
+
32
+ **Bug Fixes:**
33
+ - Handle empty string `lastExecution` in ConfigMap data
34
+ - Fixed SigNoz Query Builder v5 select fields usage
35
+ - Fixed K8s::Resource annotations handling in learning status command
36
+ - Resolved hanging tests and improved test output visibility
37
+
38
+ **Test Improvements:**
39
+ - Added comprehensive aictl smoke test playbook
40
+ - Removed obsolete learning adapter specs
41
+ - Fixed OpenTelemetry mocking in task executor event emission test
42
+
15
43
  ### 0.1.34 (2025-11-14)
16
44
 
17
45
  **DSL v1: Task/Main Primitives Added**
@@ -2,7 +2,7 @@
2
2
  :openapi: 3.0.3
3
3
  :info:
4
4
  :title: Language Operator Agent API
5
- :version: 0.1.63
5
+ :version: 0.1.65
6
6
  :description: HTTP API endpoints exposed by Language Operator reactive agents
7
7
  :contact:
8
8
  :name: Language Operator
@@ -3,7 +3,7 @@
3
3
  "$id": "https://github.com/language-operator/language-operator-gem/schema/agent-dsl.json",
4
4
  "title": "Language Operator Agent DSL",
5
5
  "description": "Schema for defining autonomous AI agents using the Language Operator DSL",
6
- "version": "0.1.63",
6
+ "version": "0.1.65",
7
7
  "type": "object",
8
8
  "properties": {
9
9
  "name": {
@@ -19,7 +19,7 @@ module LanguageOperator
19
19
  # - integer: Coerces String, Integer, Float to Integer
20
20
  # - number: Coerces String, Integer, Float to Float
21
21
  # - string: Coerces any value to String via to_s
22
- # - boolean: Coerces String, Boolean to Boolean (explicit values only)
22
+ # - boolean: Coerces String, Integer (0 or 1), Boolean to Boolean (explicit values only)
23
23
  # - array: Strict validation (no coercion)
24
24
  # - hash: Strict validation (no coercion)
25
25
  # - any: No coercion, passes through any value
@@ -32,8 +32,11 @@ module LanguageOperator
32
32
  # @example Boolean coercion
33
33
  # TypeCoercion.coerce("true", "boolean") # => true
34
34
  # TypeCoercion.coerce("1", "boolean") # => true
35
+ # TypeCoercion.coerce(1, "boolean") # => true
36
+ # TypeCoercion.coerce(0, "boolean") # => false
35
37
  # TypeCoercion.coerce("false", "boolean") # => false
36
38
  # TypeCoercion.coerce("maybe", "boolean") # raises ArgumentError
39
+ # TypeCoercion.coerce(2, "boolean") # raises ArgumentError
37
40
  #
38
41
  # @example String coercion (never fails)
39
42
  # TypeCoercion.coerce(:symbol, "string") # => "symbol"
@@ -213,11 +216,11 @@ module LanguageOperator
213
216
 
214
217
  # Coerce value to Boolean
215
218
  #
216
- # Accepts: Boolean, String (explicit values only)
219
+ # Accepts: Boolean, Integer (0 or 1), String (explicit values only)
217
220
  # Coercion: Case-insensitive string matching with optimized pattern lookup
218
- # Truthy: "true", "1", "yes", "t", "y"
219
- # Falsy: "false", "0", "no", "f", "n"
220
- # Errors: Ambiguous values (e.g., "maybe", "unknown")
221
+ # Truthy: "true", "1", "yes", "t", "y", 1 (integer)
222
+ # Falsy: "false", "0", "no", "f", "n", 0 (integer)
223
+ # Errors: Ambiguous values (e.g., "maybe", "unknown"), integers other than 0 or 1
221
224
  #
222
225
  # @param value [Object] Value to coerce
223
226
  # @return [Boolean] Coerced boolean
@@ -227,17 +230,28 @@ module LanguageOperator
227
230
  # coerce_boolean(true) # => true
228
231
  # coerce_boolean("true") # => true
229
232
  # coerce_boolean("1") # => true
233
+ # coerce_boolean(1) # => true
230
234
  # coerce_boolean("yes") # => true
231
235
  # coerce_boolean(false) # => false
232
236
  # coerce_boolean("false") # => false
233
237
  # coerce_boolean("0") # => false
238
+ # coerce_boolean(0) # => false
234
239
  # coerce_boolean("no") # => false
235
240
  # coerce_boolean("maybe") # raises ArgumentError
241
+ # coerce_boolean(2) # raises ArgumentError
236
242
  def self.coerce_boolean(value)
237
243
  # Fast path for already-correct types
238
244
  return value if value.is_a?(TrueClass) || value.is_a?(FalseClass)
239
245
 
240
- # Only allow string values for coercion (not integers or other types)
246
+ # Handle integer 0 and 1 (common in many programming contexts)
247
+ if value.is_a?(Integer)
248
+ return true if value == 1
249
+ return false if value.zero?
250
+
251
+ raise ArgumentError, "Cannot coerce #{value.inspect} to boolean (only 0 and 1 are valid integers)"
252
+ end
253
+
254
+ # Only allow string values for coercion (not floats, symbols, or other types)
241
255
  raise ArgumentError, "Cannot coerce #{value.inspect} to boolean" unless value.is_a?(String)
242
256
 
243
257
  # Optimized pattern matching using pre-compiled arrays
@@ -308,9 +322,9 @@ module LanguageOperator
308
322
  errors: 'Never (everything has to_s)'
309
323
  },
310
324
  'boolean' => {
311
- accepts: 'Boolean, String (explicit values)',
325
+ accepts: 'Boolean, Integer (0 or 1), String (explicit values)',
312
326
  method: 'Pattern matching (true/1/yes/t/y or false/0/no/f/n)',
313
- errors: 'Ambiguous values'
327
+ errors: 'Ambiguous values, integers other than 0 or 1'
314
328
  },
315
329
  'array' => {
316
330
  accepts: 'Array only',
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LanguageOperator
4
- VERSION = '0.1.63'
4
+ VERSION = '0.1.66'
5
5
  end
data/synth/002/agent.rb CHANGED
@@ -1,22 +1,33 @@
1
- # frozen_string_literal: true
2
-
3
1
  require 'language_operator'
4
2
 
5
- agent 'test-agent' do
6
- description 'Tell a fortune every 10 minutes'
3
+ agent "s002" do
4
+ description "Tell me a fortune every 10 minutes"
5
+
7
6
  mode :scheduled
8
- schedule '*/10 * * * *'
7
+ schedule "*/10 * * * *"
8
+
9
+ task :generate_fortune,
10
+ instructions: "Generate a short, positive fortune message. Keep it under 100 words. Make it inspiring and uplifting.",
11
+ inputs: {},
12
+ outputs: { fortune: 'string' }
9
13
 
10
- task :tell_fortune,
11
- instructions: 'Generate a random fortune message',
12
- inputs: {},
13
- outputs: { fortune: 'string' }
14
+ task :format_output,
15
+ instructions: "Format the fortune message into a readable output string with a title 'Your Fortune:'",
16
+ inputs: { fortune: 'string' },
17
+ outputs: { message: 'string' }
18
+
19
+ main do |inputs|
20
+ fortune = execute_task(:generate_fortune)
21
+ output = execute_task(:format_output, inputs: fortune)
22
+ output
23
+ end
14
24
 
15
- main do |_inputs|
16
- execute_task(:tell_fortune)
25
+ constraints do
26
+ max_iterations 999999
27
+ timeout "10m"
17
28
  end
18
29
 
19
30
  output do |outputs|
20
- puts outputs[:fortune]
31
+ puts outputs[:message]
21
32
  end
22
33
  end