language-operator 0.1.63 → 0.1.66
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.plan.md +127 -0
- data/.rspec +3 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +4 -1
- data/Makefile +34 -80
- data/README.md +20 -1
- data/components/agent/Gemfile +1 -1
- data/docs/cheat-sheet.md +173 -0
- data/docs/observability.md +208 -0
- data/lib/language_operator/agent/base.rb +10 -1
- data/lib/language_operator/agent/event_config.rb +172 -0
- data/lib/language_operator/agent/safety/ast_validator.rb +1 -1
- data/lib/language_operator/agent/safety/safe_executor.rb +5 -1
- data/lib/language_operator/agent/task_executor.rb +97 -7
- data/lib/language_operator/agent/telemetry.rb +25 -3
- data/lib/language_operator/agent/web_server.rb +6 -9
- data/lib/language_operator/agent.rb +24 -14
- data/lib/language_operator/cli/commands/agent/base.rb +155 -64
- data/lib/language_operator/cli/commands/agent/code_operations.rb +157 -16
- data/lib/language_operator/cli/commands/cluster.rb +2 -2
- data/lib/language_operator/cli/commands/status.rb +2 -2
- data/lib/language_operator/cli/commands/system/synthesize.rb +1 -1
- data/lib/language_operator/cli/errors/suggestions.rb +1 -1
- data/lib/language_operator/cli/formatters/value_formatter.rb +1 -1
- data/lib/language_operator/cli/helpers/ux_helper.rb +3 -4
- data/lib/language_operator/config.rb +3 -3
- data/lib/language_operator/constants/kubernetes_labels.rb +2 -2
- data/lib/language_operator/constants.rb +1 -0
- data/lib/language_operator/dsl/task_definition.rb +18 -7
- data/lib/language_operator/instrumentation/task_tracer.rb +44 -3
- data/lib/language_operator/kubernetes/client.rb +112 -1
- data/lib/language_operator/templates/schema/CHANGELOG.md +28 -0
- data/lib/language_operator/templates/schema/agent_dsl_openapi.yaml +1 -1
- data/lib/language_operator/templates/schema/agent_dsl_schema.json +1 -1
- data/lib/language_operator/type_coercion.rb +22 -8
- data/lib/language_operator/version.rb +1 -1
- data/synth/002/agent.rb +23 -12
- data/synth/002/output.log +88 -15
- data/synth/003/Makefile +17 -4
- data/synth/003/agent.txt +1 -1
- data/synth/004/Makefile +54 -0
- data/synth/004/README.md +281 -0
- data/synth/004/instructions.txt +1 -0
- metadata +11 -6
- data/lib/language_operator/cli/commands/agent/learning.rb +0 -289
- data/synth/003/agent.optimized.rb +0 -66
- data/synth/003/agent.synthesized.rb +0 -41
|
@@ -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
|
-
|
|
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
|
|
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
|
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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
|
|
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 [
|
|
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
|
-
|
|
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 [
|
|
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
|
-
|
|
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 [
|
|
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
|
|
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(
|
|
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**
|
|
@@ -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.
|
|
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
|
-
#
|
|
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',
|
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
|
|
6
|
-
description
|
|
3
|
+
agent "s002" do
|
|
4
|
+
description "Tell me a fortune every 10 minutes"
|
|
5
|
+
|
|
7
6
|
mode :scheduled
|
|
8
|
-
schedule
|
|
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 :
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
16
|
-
|
|
25
|
+
constraints do
|
|
26
|
+
max_iterations 999999
|
|
27
|
+
timeout "10m"
|
|
17
28
|
end
|
|
18
29
|
|
|
19
30
|
output do |outputs|
|
|
20
|
-
puts outputs[:
|
|
31
|
+
puts outputs[:message]
|
|
21
32
|
end
|
|
22
33
|
end
|