language-operator 0.0.1 → 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 (120) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +125 -0
  3. data/CHANGELOG.md +88 -0
  4. data/Gemfile +8 -0
  5. data/Gemfile.lock +284 -0
  6. data/LICENSE +229 -21
  7. data/Makefile +82 -0
  8. data/README.md +3 -11
  9. data/Rakefile +63 -0
  10. data/bin/aictl +7 -0
  11. data/completions/_aictl +232 -0
  12. data/completions/aictl.bash +121 -0
  13. data/completions/aictl.fish +114 -0
  14. data/docs/architecture/agent-runtime.md +585 -0
  15. data/docs/dsl/SCHEMA_VERSION.md +250 -0
  16. data/docs/dsl/agent-reference.md +604 -0
  17. data/docs/dsl/best-practices.md +1078 -0
  18. data/docs/dsl/chat-endpoints.md +895 -0
  19. data/docs/dsl/constraints.md +671 -0
  20. data/docs/dsl/mcp-integration.md +1177 -0
  21. data/docs/dsl/webhooks.md +932 -0
  22. data/docs/dsl/workflows.md +744 -0
  23. data/lib/language_operator/agent/base.rb +110 -0
  24. data/lib/language_operator/agent/executor.rb +440 -0
  25. data/lib/language_operator/agent/instrumentation.rb +54 -0
  26. data/lib/language_operator/agent/metrics_tracker.rb +183 -0
  27. data/lib/language_operator/agent/safety/ast_validator.rb +272 -0
  28. data/lib/language_operator/agent/safety/audit_logger.rb +104 -0
  29. data/lib/language_operator/agent/safety/budget_tracker.rb +175 -0
  30. data/lib/language_operator/agent/safety/content_filter.rb +93 -0
  31. data/lib/language_operator/agent/safety/manager.rb +207 -0
  32. data/lib/language_operator/agent/safety/rate_limiter.rb +150 -0
  33. data/lib/language_operator/agent/safety/safe_executor.rb +127 -0
  34. data/lib/language_operator/agent/scheduler.rb +183 -0
  35. data/lib/language_operator/agent/telemetry.rb +116 -0
  36. data/lib/language_operator/agent/web_server.rb +610 -0
  37. data/lib/language_operator/agent/webhook_authenticator.rb +226 -0
  38. data/lib/language_operator/agent.rb +149 -0
  39. data/lib/language_operator/cli/commands/agent.rb +1205 -0
  40. data/lib/language_operator/cli/commands/cluster.rb +371 -0
  41. data/lib/language_operator/cli/commands/install.rb +404 -0
  42. data/lib/language_operator/cli/commands/model.rb +266 -0
  43. data/lib/language_operator/cli/commands/persona.rb +393 -0
  44. data/lib/language_operator/cli/commands/quickstart.rb +22 -0
  45. data/lib/language_operator/cli/commands/status.rb +143 -0
  46. data/lib/language_operator/cli/commands/system.rb +772 -0
  47. data/lib/language_operator/cli/commands/tool.rb +537 -0
  48. data/lib/language_operator/cli/commands/use.rb +47 -0
  49. data/lib/language_operator/cli/errors/handler.rb +180 -0
  50. data/lib/language_operator/cli/errors/suggestions.rb +176 -0
  51. data/lib/language_operator/cli/formatters/code_formatter.rb +77 -0
  52. data/lib/language_operator/cli/formatters/log_formatter.rb +288 -0
  53. data/lib/language_operator/cli/formatters/progress_formatter.rb +49 -0
  54. data/lib/language_operator/cli/formatters/status_formatter.rb +37 -0
  55. data/lib/language_operator/cli/formatters/table_formatter.rb +163 -0
  56. data/lib/language_operator/cli/formatters/value_formatter.rb +113 -0
  57. data/lib/language_operator/cli/helpers/cluster_context.rb +62 -0
  58. data/lib/language_operator/cli/helpers/cluster_validator.rb +101 -0
  59. data/lib/language_operator/cli/helpers/editor_helper.rb +58 -0
  60. data/lib/language_operator/cli/helpers/kubeconfig_validator.rb +167 -0
  61. data/lib/language_operator/cli/helpers/pastel_helper.rb +24 -0
  62. data/lib/language_operator/cli/helpers/resource_dependency_checker.rb +74 -0
  63. data/lib/language_operator/cli/helpers/schedule_builder.rb +108 -0
  64. data/lib/language_operator/cli/helpers/user_prompts.rb +69 -0
  65. data/lib/language_operator/cli/main.rb +236 -0
  66. data/lib/language_operator/cli/templates/tools/generic.yaml +66 -0
  67. data/lib/language_operator/cli/wizards/agent_wizard.rb +246 -0
  68. data/lib/language_operator/cli/wizards/quickstart_wizard.rb +588 -0
  69. data/lib/language_operator/client/base.rb +214 -0
  70. data/lib/language_operator/client/config.rb +136 -0
  71. data/lib/language_operator/client/cost_calculator.rb +37 -0
  72. data/lib/language_operator/client/mcp_connector.rb +123 -0
  73. data/lib/language_operator/client.rb +19 -0
  74. data/lib/language_operator/config/cluster_config.rb +101 -0
  75. data/lib/language_operator/config/tool_patterns.yaml +57 -0
  76. data/lib/language_operator/config/tool_registry.rb +96 -0
  77. data/lib/language_operator/config.rb +138 -0
  78. data/lib/language_operator/dsl/adapter.rb +124 -0
  79. data/lib/language_operator/dsl/agent_context.rb +90 -0
  80. data/lib/language_operator/dsl/agent_definition.rb +427 -0
  81. data/lib/language_operator/dsl/chat_endpoint_definition.rb +115 -0
  82. data/lib/language_operator/dsl/config.rb +119 -0
  83. data/lib/language_operator/dsl/context.rb +50 -0
  84. data/lib/language_operator/dsl/execution_context.rb +47 -0
  85. data/lib/language_operator/dsl/helpers.rb +109 -0
  86. data/lib/language_operator/dsl/http.rb +184 -0
  87. data/lib/language_operator/dsl/mcp_server_definition.rb +73 -0
  88. data/lib/language_operator/dsl/parameter_definition.rb +124 -0
  89. data/lib/language_operator/dsl/registry.rb +36 -0
  90. data/lib/language_operator/dsl/schema.rb +1102 -0
  91. data/lib/language_operator/dsl/shell.rb +125 -0
  92. data/lib/language_operator/dsl/tool_definition.rb +112 -0
  93. data/lib/language_operator/dsl/webhook_authentication.rb +114 -0
  94. data/lib/language_operator/dsl/webhook_definition.rb +106 -0
  95. data/lib/language_operator/dsl/workflow_definition.rb +259 -0
  96. data/lib/language_operator/dsl.rb +161 -0
  97. data/lib/language_operator/errors.rb +60 -0
  98. data/lib/language_operator/kubernetes/client.rb +279 -0
  99. data/lib/language_operator/kubernetes/resource_builder.rb +194 -0
  100. data/lib/language_operator/loggable.rb +47 -0
  101. data/lib/language_operator/logger.rb +141 -0
  102. data/lib/language_operator/retry.rb +123 -0
  103. data/lib/language_operator/retryable.rb +132 -0
  104. data/lib/language_operator/templates/README.md +23 -0
  105. data/lib/language_operator/templates/examples/agent_synthesis.tmpl +115 -0
  106. data/lib/language_operator/templates/examples/persona_distillation.tmpl +19 -0
  107. data/lib/language_operator/templates/schema/.gitkeep +0 -0
  108. data/lib/language_operator/templates/schema/CHANGELOG.md +93 -0
  109. data/lib/language_operator/templates/schema/agent_dsl_openapi.yaml +306 -0
  110. data/lib/language_operator/templates/schema/agent_dsl_schema.json +452 -0
  111. data/lib/language_operator/tool_loader.rb +242 -0
  112. data/lib/language_operator/validators.rb +170 -0
  113. data/lib/language_operator/version.rb +1 -1
  114. data/lib/language_operator.rb +65 -3
  115. data/requirements/tasks/challenge.md +9 -0
  116. data/requirements/tasks/iterate.md +36 -0
  117. data/requirements/tasks/optimize.md +21 -0
  118. data/requirements/tasks/tag.md +5 -0
  119. data/test_agent_dsl.rb +108 -0
  120. metadata +507 -20
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../client'
4
+ require_relative 'telemetry'
5
+ require_relative 'instrumentation'
6
+
7
+ module LanguageOperator
8
+ module Agent
9
+ # Base Agent Class
10
+ #
11
+ # Extends LanguageOperator::Client::Base with agent-specific functionality including:
12
+ # - Workspace integration
13
+ # - Goal-directed execution
14
+ # - Autonomous operation modes
15
+ #
16
+ # @example Basic agent
17
+ # agent = LanguageOperator::Agent::Base.new(config)
18
+ # agent.connect!
19
+ # agent.execute_goal("Complete the task")
20
+ class Base < LanguageOperator::Client::Base
21
+ include Instrumentation
22
+
23
+ attr_reader :workspace_path, :mode
24
+
25
+ # Initialize the agent
26
+ #
27
+ # @param config [Hash] Configuration hash
28
+ def initialize(config)
29
+ super
30
+
31
+ # Initialize OpenTelemetry
32
+ LanguageOperator::Agent::Telemetry.configure
33
+ otel_enabled = !ENV.fetch('OTEL_EXPORTER_OTLP_ENDPOINT', nil).nil?
34
+ logger.info "OpenTelemetry #{otel_enabled ? 'enabled' : 'disabled'}"
35
+
36
+ @workspace_path = ENV.fetch('WORKSPACE_PATH', '/workspace')
37
+ @mode = ENV.fetch('AGENT_MODE', 'autonomous')
38
+ @executor = nil
39
+ @scheduler = nil
40
+ end
41
+
42
+ # Run the agent in its configured mode
43
+ #
44
+ # @return [void]
45
+ def run
46
+ with_span('agent.run', attributes: {
47
+ 'agent.name' => ENV.fetch('AGENT_NAME', nil),
48
+ 'agent.mode' => @mode,
49
+ 'agent.workspace_available' => workspace_available?
50
+ }) do
51
+ connect!
52
+
53
+ case @mode
54
+ when 'autonomous', 'interactive'
55
+ run_autonomous
56
+ when 'scheduled', 'event-driven'
57
+ run_scheduled
58
+ when 'reactive', 'http', 'webhook'
59
+ run_reactive
60
+ else
61
+ raise "Unknown agent mode: #{@mode}"
62
+ end
63
+ end
64
+ end
65
+
66
+ # Execute a single goal
67
+ #
68
+ # @param goal [String] The goal to achieve
69
+ # @return [String] The result
70
+ def execute_goal(goal)
71
+ @executor ||= Executor.new(self)
72
+ @executor.execute(goal)
73
+ end
74
+
75
+ # Check if workspace is available
76
+ #
77
+ # @return [Boolean]
78
+ def workspace_available?
79
+ File.directory?(@workspace_path) && File.writable?(@workspace_path)
80
+ end
81
+
82
+ private
83
+
84
+ # Run in autonomous mode
85
+ #
86
+ # @return [void]
87
+ def run_autonomous
88
+ @executor = Executor.new(self)
89
+ @executor.run_loop
90
+ end
91
+
92
+ # Run in scheduled mode
93
+ #
94
+ # @return [void]
95
+ def run_scheduled
96
+ @scheduler = Scheduler.new(self)
97
+ @scheduler.start
98
+ end
99
+
100
+ # Run in reactive mode (HTTP server)
101
+ #
102
+ # @return [void]
103
+ def run_reactive
104
+ require_relative 'web_server'
105
+ @web_server = WebServer.new(self)
106
+ @web_server.start
107
+ end
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,440 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../logger'
4
+ require_relative '../loggable'
5
+ require_relative 'metrics_tracker'
6
+ require_relative 'safety/manager'
7
+ require_relative 'instrumentation'
8
+
9
+ module LanguageOperator
10
+ module Agent
11
+ # Task Executor
12
+ #
13
+ # Handles autonomous task execution with retry logic and error handling.
14
+ #
15
+ # @example
16
+ # executor = Executor.new(agent)
17
+ # executor.execute("Complete the task")
18
+ class Executor
19
+ include LanguageOperator::Loggable
20
+ include Instrumentation
21
+
22
+ attr_reader :agent, :iteration_count, :metrics_tracker
23
+
24
+ # Initialize the executor
25
+ #
26
+ # @param agent [LanguageOperator::Agent::Base] The agent instance
27
+ # @param agent_definition [LanguageOperator::Dsl::AgentDefinition, nil] Optional agent definition
28
+ def initialize(agent, agent_definition: nil)
29
+ @agent = agent
30
+ @agent_definition = agent_definition
31
+ @iteration_count = 0
32
+ @max_iterations = 100
33
+ @show_full_responses = ENV.fetch('SHOW_FULL_RESPONSES', 'false') == 'true'
34
+ @metrics_tracker = MetricsTracker.new
35
+
36
+ # Initialize safety manager from agent definition or environment
37
+ @safety_manager = initialize_safety_manager(agent_definition)
38
+
39
+ logger.debug('Executor initialized',
40
+ max_iterations: @max_iterations,
41
+ show_full_responses: @show_full_responses,
42
+ workspace: @agent.workspace_path,
43
+ safety_enabled: @safety_manager&.enabled?)
44
+ end
45
+
46
+ # Execute a task with additional context (for webhooks/HTTP requests)
47
+ #
48
+ # @param instruction [String] The instruction to execute
49
+ # @param context [Hash] Additional context (webhook payload, request data, etc.)
50
+ # @return [String] The result
51
+ def execute_with_context(instruction:, context: {})
52
+ # Build enriched instruction with context
53
+ enriched_instruction = build_instruction_with_context(instruction, context)
54
+
55
+ # Execute with standard logic
56
+ execute(enriched_instruction)
57
+ end
58
+
59
+ # Execute a single task or workflow
60
+ #
61
+ # @param task [String] The task to execute
62
+ # @param agent_definition [LanguageOperator::Dsl::AgentDefinition, nil] Optional agent definition with workflow
63
+ # @return [String] The result
64
+ # rubocop:disable Metrics/BlockLength
65
+ def execute(task, agent_definition: nil)
66
+ with_span('agent.execute_goal', attributes: {
67
+ 'agent.goal_description' => task[0...500]
68
+ }) do
69
+ @iteration_count += 1
70
+
71
+ # Route to workflow execution if agent has a workflow defined
72
+ return execute_workflow(agent_definition) if agent_definition&.workflow
73
+
74
+ # Standard instruction-based execution
75
+ logger.info('Starting iteration',
76
+ iteration: @iteration_count,
77
+ max_iterations: @max_iterations)
78
+ logger.debug('Prompt', prompt: task[0..200])
79
+
80
+ # Safety check before request
81
+ if @safety_manager&.enabled?
82
+ # Estimate cost and tokens (rough estimate)
83
+ estimated_tokens = estimate_tokens(task)
84
+ estimated_cost = estimate_cost(estimated_tokens)
85
+
86
+ @safety_manager.check_request!(
87
+ message: task,
88
+ estimated_cost: estimated_cost,
89
+ estimated_tokens: estimated_tokens
90
+ )
91
+ end
92
+
93
+ logger.info('🤖 LLM request')
94
+ result = logger.timed('LLM response received') do
95
+ @agent.send_message(task)
96
+ end
97
+
98
+ # Record metrics
99
+ model_id = @agent.config.dig('llm', 'model')
100
+ @metrics_tracker.record_request(result, model_id) if model_id
101
+
102
+ # Safety check after response and record spending
103
+ result_text = result.is_a?(String) ? result : result.content
104
+ metrics = @metrics_tracker.cumulative_stats
105
+
106
+ if @safety_manager&.enabled?
107
+ @safety_manager.check_response!(result_text)
108
+ @safety_manager.record_request(
109
+ cost: metrics[:estimatedCost],
110
+ tokens: metrics[:totalTokens]
111
+ )
112
+ end
113
+ logger.info('✓ Iteration completed',
114
+ iteration: @iteration_count,
115
+ response_length: result_text.length,
116
+ total_tokens: metrics[:totalTokens],
117
+ estimated_cost: "$#{metrics[:estimatedCost]}")
118
+ logger.debug('Response preview', response: result_text[0..200])
119
+
120
+ result
121
+ rescue StandardError => e
122
+ handle_error(e)
123
+ end
124
+ end
125
+ # rubocop:enable Metrics/BlockLength
126
+
127
+ # Run continuous execution loop
128
+ #
129
+ # @return [void]
130
+ def run_loop
131
+ start_time = Time.now
132
+
133
+ logger.info('▶ Starting execution')
134
+ logger.info('Configuration',
135
+ workspace: @agent.workspace_path,
136
+ mcp_servers: @agent.servers_info.length,
137
+ max_iterations: @max_iterations)
138
+
139
+ # Log persona loading
140
+ persona = @agent.config.dig('agent', 'persona') || 'default'
141
+ logger.info("👤 Loading persona: #{persona}")
142
+
143
+ # Log MCP server details
144
+ if @agent.servers_info.any?
145
+ @agent.servers_info.each do |server|
146
+ logger.info('◆ MCP server connected', name: server[:name], tool_count: server[:tool_count])
147
+ end
148
+ end
149
+
150
+ # Get initial instructions from config or environment
151
+ instructions = @agent.config.dig('agent', 'instructions') ||
152
+ ENV['AGENT_INSTRUCTIONS'] ||
153
+ 'Monitor workspace and respond to changes'
154
+
155
+ logger.info('Instructions', instructions: instructions[0..200])
156
+ logger.info('Starting autonomous execution loop')
157
+
158
+ loop do
159
+ break if @iteration_count >= @max_iterations
160
+
161
+ progress_pct = ((@iteration_count.to_f / @max_iterations) * 100).round(1)
162
+ logger.debug('Loop progress',
163
+ iteration: @iteration_count,
164
+ max: @max_iterations,
165
+ progress: "#{progress_pct}%")
166
+
167
+ result = execute(instructions)
168
+ result_text = result.is_a?(String) ? result : result.content
169
+
170
+ # Log result based on verbosity settings
171
+ if @show_full_responses
172
+ logger.info('Full iteration result',
173
+ iteration: @iteration_count,
174
+ result: result_text)
175
+ else
176
+ preview = result_text[0..200]
177
+ preview += '...' if result_text.length > 200
178
+ logger.info('Iteration result',
179
+ iteration: @iteration_count,
180
+ preview: preview)
181
+ end
182
+
183
+ # Rate limiting
184
+ logger.debug('Rate limit pause', duration: 5)
185
+ sleep 5
186
+ end
187
+
188
+ # Log execution summary
189
+ total_duration = Time.now - start_time
190
+ metrics = @metrics_tracker.cumulative_stats
191
+ logger.info('✅ Execution complete',
192
+ iterations: @iteration_count,
193
+ duration_s: total_duration.round(2),
194
+ total_requests: metrics[:requestCount],
195
+ total_tokens: metrics[:totalTokens],
196
+ estimated_cost: "$#{metrics[:estimatedCost]}",
197
+ reason: @iteration_count >= @max_iterations ? 'max_iterations' : 'completed')
198
+
199
+ return unless @iteration_count >= @max_iterations
200
+
201
+ logger.warn('Maximum iterations reached',
202
+ iterations: @max_iterations,
203
+ reason: 'Hit max_iterations limit')
204
+ end
205
+
206
+ # Execute a workflow-based agent
207
+ #
208
+ # @param agent_def [LanguageOperator::Dsl::AgentDefinition] The agent definition
209
+ # @return [RubyLLM::Message] The final response
210
+ def execute_workflow(agent_def)
211
+ start_time = Time.now
212
+
213
+ logger.info("▶ Starting workflow execution: #{agent_def.name}")
214
+
215
+ # Log persona if defined
216
+ logger.info("👤 Loading persona: #{agent_def.persona}") if agent_def.persona
217
+
218
+ # Build orchestration prompt from agent definition
219
+ prompt = build_workflow_prompt(agent_def)
220
+ logger.debug('Workflow prompt', prompt: prompt[0..300])
221
+
222
+ # Register workflow steps as tools (placeholder - will implement after tool converter)
223
+ # For now, just execute with instructions
224
+ result = logger.timed('🤖 LLM request') do
225
+ @agent.send_message(prompt)
226
+ end
227
+
228
+ # Record metrics
229
+ model_id = @agent.config.dig('llm', 'model')
230
+ @metrics_tracker.record_request(result, model_id) if model_id
231
+
232
+ # Write output if configured
233
+ write_output(agent_def, result) if agent_def.output_config && result
234
+
235
+ # Log execution summary
236
+ total_duration = Time.now - start_time
237
+ metrics = @metrics_tracker.cumulative_stats
238
+ logger.info('✅ Workflow execution completed',
239
+ duration_s: total_duration.round(2),
240
+ total_tokens: metrics[:totalTokens],
241
+ estimated_cost: "$#{metrics[:estimatedCost]}")
242
+ result
243
+ rescue StandardError => e
244
+ logger.error('❌ Workflow execution failed', error: e.message)
245
+ handle_error(e)
246
+ end
247
+
248
+ # Build orchestration prompt from agent definition
249
+ #
250
+ # @param agent_def [LanguageOperator::Dsl::AgentDefinition] The agent definition
251
+ # @return [String] The prompt
252
+ def build_workflow_prompt(agent_def)
253
+ prompt = "# Task: #{agent_def.description}\n\n"
254
+
255
+ if agent_def.objectives&.any?
256
+ prompt += "## Objectives:\n"
257
+ agent_def.objectives.each { |obj| prompt += "- #{obj}\n" }
258
+ prompt += "\n"
259
+ end
260
+
261
+ if agent_def.workflow&.steps&.any?
262
+ prompt += "## Workflow Steps:\n"
263
+ agent_def.workflow.step_order.each do |step_name|
264
+ step = agent_def.workflow.steps[step_name]
265
+ prompt += step_name.to_s.tr('_', ' ').capitalize.to_s
266
+ prompt += " (using tool: #{step.tool_name})" if step.tool_name
267
+ prompt += " - depends on: #{step.dependencies.join(', ')}" if step.dependencies&.any?
268
+ prompt += "\n"
269
+ end
270
+ prompt += "\n"
271
+ end
272
+
273
+ if agent_def.constraints
274
+ prompt += "## Constraints:\n"
275
+ prompt += "- Maximum iterations: #{agent_def.constraints[:max_iterations]}\n" if agent_def.constraints[:max_iterations]
276
+ prompt += "- Timeout: #{agent_def.constraints[:timeout]}\n" if agent_def.constraints[:timeout]
277
+ prompt += "\n"
278
+ end
279
+
280
+ prompt += 'Please complete this task following the workflow steps.'
281
+ prompt
282
+ end
283
+
284
+ # Write output to configured destinations
285
+ #
286
+ # @param agent_def [LanguageOperator::Dsl::AgentDefinition] The agent definition
287
+ # @param result [RubyLLM::Message] The result to write
288
+ def write_output(agent_def, result)
289
+ return unless agent_def.output_config
290
+
291
+ content = result.is_a?(String) ? result : result.content
292
+
293
+ if (workspace_path = agent_def.output_config[:workspace])
294
+ full_path = File.join(@agent.workspace_path, workspace_path)
295
+
296
+ begin
297
+ FileUtils.mkdir_p(File.dirname(full_path))
298
+ File.write(full_path, content)
299
+ logger.info("📝 Wrote output to #{workspace_path}")
300
+ rescue Errno::EACCES, Errno::EPERM
301
+ # Permission denied - try writing to workspace root
302
+ fallback_path = File.join(@agent.workspace_path, 'output.txt')
303
+ begin
304
+ File.write(fallback_path, content)
305
+ logger.warn("⚠️ Could not write to #{workspace_path}, wrote to output.txt instead")
306
+ rescue StandardError => e2
307
+ logger.warn("⚠️ Could not write output to workspace: #{e2.message}")
308
+ logger.info("📄 Output (first 500 chars): #{content[0..500]}")
309
+ end
310
+ end
311
+ end
312
+
313
+ # Future: Handle Slack, email outputs
314
+ rescue StandardError => e
315
+ logger.warn('Output writing failed', error: e.message)
316
+ end
317
+
318
+ private
319
+
320
+ def logger_component
321
+ 'Agent::Executor'
322
+ end
323
+
324
+ # Build instruction enriched with request context
325
+ #
326
+ # @param instruction [String] Base instruction
327
+ # @param context [Hash] Request context
328
+ # @return [String] Enriched instruction
329
+ def build_instruction_with_context(instruction, context)
330
+ enriched = instruction.dup
331
+ enriched += "\n\n## Request Context\n"
332
+ enriched += "- Method: #{context[:method]}\n" if context[:method]
333
+ enriched += "- Path: #{context[:path]}\n" if context[:path]
334
+
335
+ if context[:params] && !context[:params].empty?
336
+ enriched += "\n### Parameters:\n"
337
+ enriched += "```json\n#{JSON.pretty_generate(context[:params])}\n```\n"
338
+ end
339
+
340
+ if context[:body] && !context[:body].empty?
341
+ enriched += "\n### Request Body:\n"
342
+ enriched += "```\n#{context[:body][0..1000]}\n```\n"
343
+ end
344
+
345
+ if context[:headers] && !context[:headers].empty?
346
+ enriched += "\n### Headers:\n"
347
+ context[:headers].each do |key, value|
348
+ enriched += "- #{key}: #{value}\n"
349
+ end
350
+ end
351
+
352
+ enriched
353
+ end
354
+
355
+ def initialize_safety_manager(agent_definition)
356
+ # Get safety config from agent definition constraints
357
+ config = agent_definition&.constraints || {}
358
+
359
+ # Merge with environment variables
360
+ config = {
361
+ enabled: ENV.fetch('SAFETY_ENABLED', 'true') != 'false',
362
+ daily_budget: config[:daily_budget] || parse_float_env('DAILY_BUDGET'),
363
+ hourly_budget: config[:hourly_budget] || parse_float_env('HOURLY_BUDGET'),
364
+ token_budget: config[:token_budget] || parse_int_env('TOKEN_BUDGET'),
365
+ requests_per_minute: config[:requests_per_minute] || parse_int_env('REQUESTS_PER_MINUTE'),
366
+ requests_per_hour: config[:requests_per_hour] || parse_int_env('REQUESTS_PER_HOUR'),
367
+ requests_per_day: config[:requests_per_day] || parse_int_env('REQUESTS_PER_DAY'),
368
+ blocked_patterns: config[:blocked_patterns] || parse_array_env('BLOCKED_PATTERNS'),
369
+ blocked_topics: config[:blocked_topics] || parse_array_env('BLOCKED_TOPICS'),
370
+ case_sensitive: config[:case_sensitive] || ENV.fetch('CASE_SENSITIVE', 'false') == 'true',
371
+ audit_logging: config[:audit_logging] != false
372
+ }.compact
373
+
374
+ return nil if config[:enabled] == false
375
+
376
+ Safety::Manager.new(config)
377
+ rescue StandardError => e
378
+ logger.warn('Failed to initialize safety manager',
379
+ error: e.message,
380
+ fallback: 'Safety features disabled')
381
+ nil
382
+ end
383
+
384
+ def parse_float_env(key)
385
+ val = ENV.fetch(key, nil)
386
+ return nil unless val
387
+
388
+ val.to_f
389
+ end
390
+
391
+ def parse_int_env(key)
392
+ val = ENV.fetch(key, nil)
393
+ return nil unless val
394
+
395
+ val.to_i
396
+ end
397
+
398
+ def parse_array_env(key)
399
+ val = ENV.fetch(key, nil)
400
+ return nil unless val
401
+
402
+ val.split(',').map(&:strip)
403
+ end
404
+
405
+ def estimate_tokens(text)
406
+ # Rough estimate: ~1.3 tokens per word
407
+ (text.split.length * 1.3).to_i
408
+ end
409
+
410
+ def estimate_cost(tokens)
411
+ # Estimate based on common model pricing
412
+ # Average of ~$3-15 per 1M tokens (using $10 as middle ground)
413
+ # This is a rough estimate; actual cost varies by model
414
+ (tokens / 1_000_000.0) * 10.0
415
+ end
416
+
417
+ def handle_error(error)
418
+ case error
419
+ when Timeout::Error, /timeout/i.match?(error.message)
420
+ logger.error('Request timeout',
421
+ error: error.class.name,
422
+ message: error.message,
423
+ iteration: @iteration_count)
424
+ when /connection refused|operation not permitted/i.match?(error.message)
425
+ logger.error('Connection failed',
426
+ error: error.class.name,
427
+ message: error.message,
428
+ hint: 'Check if model service is healthy and accessible')
429
+ else
430
+ logger.error('Task execution failed',
431
+ error: error.class.name,
432
+ message: error.message)
433
+ logger.debug('Backtrace', trace: error.backtrace[0..5].join("\n")) if error.backtrace
434
+ end
435
+
436
+ "Error executing task: #{error.message}"
437
+ end
438
+ end
439
+ end
440
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'opentelemetry/sdk'
4
+
5
+ module LanguageOperator
6
+ module Agent
7
+ # OpenTelemetry instrumentation helpers for agent methods
8
+ #
9
+ # Provides reusable patterns for tracing agent operations with
10
+ # automatic error handling and span status management.
11
+ #
12
+ # @example Instrument a method
13
+ # include LanguageOperator::Agent::Instrumentation
14
+ #
15
+ # def my_method
16
+ # with_span('my_method', attributes: { 'key' => 'value' }) do
17
+ # # Method implementation
18
+ # end
19
+ # end
20
+ module Instrumentation
21
+ private
22
+
23
+ # Get the configured OpenTelemetry tracer
24
+ #
25
+ # @return [OpenTelemetry::Trace::Tracer]
26
+ def tracer
27
+ @tracer ||= OpenTelemetry.tracer_provider.tracer(
28
+ 'language-operator-agent',
29
+ LanguageOperator::VERSION
30
+ )
31
+ end
32
+
33
+ # Execute block within a traced span with automatic error handling
34
+ #
35
+ # Creates a span with the given name and attributes, executes the block,
36
+ # and automatically records exceptions and sets error status if raised.
37
+ #
38
+ # @param name [String] Span name
39
+ # @param attributes [Hash] Span attributes
40
+ # @yield [OpenTelemetry::Trace::Span] The created span
41
+ # @return [Object] Result of the block
42
+ # @raise Re-raises any exception after recording it on the span
43
+ def with_span(name, attributes: {})
44
+ tracer.in_span(name, attributes: attributes) do |span|
45
+ yield span
46
+ rescue StandardError => e
47
+ span.record_exception(e)
48
+ span.status = OpenTelemetry::Trace::Status.error(e.message)
49
+ raise
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end