Module: LanguageOperator::Agent

Extended by:
Instrumentation
Defined in:
lib/language_operator/agent.rb,
lib/language_operator/agent/base.rb,
lib/language_operator/agent/executor.rb,
lib/language_operator/agent/telemetry.rb,
lib/language_operator/agent/web_server.rb,
lib/language_operator/agent/task_executor.rb,
lib/language_operator/agent/prompt_builder.rb,
lib/language_operator/agent/safety/manager.rb,
lib/language_operator/agent/execution_state.rb,
lib/language_operator/agent/instrumentation.rb,
lib/language_operator/agent/metrics_tracker.rb,
lib/language_operator/agent/metadata_collector.rb,
lib/language_operator/agent/safety/audit_logger.rb,
lib/language_operator/agent/safety/rate_limiter.rb,
lib/language_operator/agent/safety/ast_validator.rb,
lib/language_operator/agent/safety/safe_executor.rb,
lib/language_operator/agent/safety/budget_tracker.rb,
lib/language_operator/agent/safety/content_filter.rb,
lib/language_operator/agent/webhook_authenticator.rb

Overview

Agent Framework

Provides autonomous execution capabilities for language agents. Extends LanguageOperator::Client with agent-specific features like scheduling, goal evaluation, and workspace integration.

rubocop:disable Metrics/ModuleLength

Examples:

Running an agent

config = LanguageOperator::Client::Config.from_env
agent = LanguageOperator::Agent::Base.new(config)
agent.run

Creating a custom agent

agent = LanguageOperator::Agent::Base.new(config)
agent.execute_goal("Summarize daily news")

Defined Under Namespace

Modules: Instrumentation, Safety, Telemetry Classes: Base, ExecutionInProgressError, ExecutionState, Executor, MetadataCollector, MetricsTracker, PromptBuilder, StreamingBody, TaskExecutionError, TaskExecutor, TaskNetworkError, TaskTimeoutError, TaskValidationError, WebServer, WebhookAuthenticator

Class Method Summary collapse

Class Method Details

.build_executor_config(agent_def) ⇒ Hash

Build executor configuration from agent definition constraints

Parameters:

  • The agent definition

Returns:

  • Executor configuration



410
411
412
413
414
415
416
417
418
419
420
421
422
# File 'lib/language_operator/agent.rb', line 410

def self.build_executor_config(agent_def)
  config = {}

  if agent_def.constraints
    if agent_def.constraints[:timeout]
      timeout = agent_def.constraints[:timeout]
      config[:timeout] = timeout.is_a?(String) ? parse_duration(timeout) : timeout
    end
    config[:max_retries] = agent_def.constraints[:max_retries] if agent_def.constraints[:max_retries]
  end

  config
end

.execute_main_block(agent, agent_def) ⇒ void

This method returns an undefined value.

Execute main block (DSL v1) in autonomous mode

Parameters:

  • The agent instance

  • The agent definition



250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
# File 'lib/language_operator/agent.rb', line 250

def self.execute_main_block(agent, agent_def)
  # Build executor config from agent constraints
  config = build_executor_config(agent_def)
  task_executor = LanguageOperator::Agent::TaskExecutor.new(agent, agent_def.tasks, config)

  logger.info('Executing main block',
              agent: agent_def.name,
              task_count: agent_def.tasks.size)

  # Execute main block within agent_executor span for learning system integration
  with_span('agent_executor', attributes: {
              'agent.name' => agent_def.name,
              'agent.task_count' => agent_def.tasks.size,
              'agent.mode' => ENV.fetch('AGENT_MODE', 'unknown')
            }) do
    # Get inputs from environment or default to empty hash
    inputs = {}

    # Execute main block with task executor as context
    result = agent_def.main.call(inputs, task_executor)

    logger.info('Main block execution completed',
                result: result)

    # Call output handler if defined
    if agent_def.output
      logger.debug('Executing output handler', outputs: result)
      execute_output_handler(agent_def, result, task_executor)
    end

    result
  end
end

.execute_main_block_persistent(agent, agent_def) ⇒ void

This method returns an undefined value.

Execute main block (DSL v1) in persistent mode for autonomous agents

Parameters:

  • The agent instance

  • The agent definition



289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
# File 'lib/language_operator/agent.rb', line 289

def self.execute_main_block_persistent(agent, agent_def)
  shutdown_requested = setup_signal_handlers

  logger.info('Starting agent in persistent autonomous mode',
              agent_name: agent_def.name,
              task_count: agent_def.tasks.size)

  # Execute initial main block
  execute_main_block(agent, agent_def)
  logger.info('Initial task completed - entering idle state')

  # Enter waiting loop for additional instructions
  run_idle_loop(shutdown_requested)

  logger.info('Graceful shutdown requested - agent exiting')
end

.execute_output_handler(agent_def, outputs, task_executor) ⇒ void

This method returns an undefined value.

Execute the output handler (neural or symbolic)

Parameters:

  • The agent definition

  • The outputs from main execution

  • Task executor for context



376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
# File 'lib/language_operator/agent.rb', line 376

def self.execute_output_handler(agent_def, outputs, task_executor)
  output_config = agent_def.output

  # If symbolic implementation exists, use it
  if output_config.symbolic?
    logger.debug('Executing symbolic output handler')
    # Call the execute block directly without validation
    # Output blocks don't need input validation since they receive whatever main returns
    block = output_config.execute_block
    if block.arity == 1
      block.call(outputs)
    elsif block.arity == 2
      block.call(outputs, task_executor)
    else
      block.call
    end
  elsif output_config.neural?
    # Neural output - would need LLM access to execute
    # For now, just log the instruction
    logger.info('Neural output handler',
                instruction: output_config.instructions_text,
                outputs: outputs)
    logger.warn('Neural output execution not yet implemented - instruction logged only')
  end
rescue StandardError => e
  logger.error('Output handler failed',
               error: e.message,
               backtrace: e.backtrace[0..5])
end

.handle_new_instructionBoolean

Handle new instruction from environment variable

Returns:

  • True if instruction was processed



355
356
357
358
359
360
361
362
363
364
365
366
367
368
# File 'lib/language_operator/agent.rb', line 355

def self.handle_new_instruction
  new_instruction = ENV.fetch('AGENT_NEW_INSTRUCTION', nil)
  return false unless new_instruction && !new_instruction.strip.empty?

  logger.info('New instruction received',
              instruction: new_instruction[0..100])

  # Clear the environment variable to prevent re-execution
  ENV['AGENT_NEW_INSTRUCTION'] = ''

  # Execute the new instruction (implementation would depend on requirements)
  logger.info('Instruction acknowledged - agent remains in autonomous mode')
  true
end

.load_and_run(agent) ⇒ void

This method returns an undefined value.

Load synthesized agent code and run with definition if available

Parameters:

  • The agent instance



82
83
84
85
86
87
88
89
90
91
92
93
# File 'lib/language_operator/agent.rb', line 82

def self.load_and_run(agent)
  agent_code_path = ENV.fetch('AGENT_CODE_PATH', nil)
  agent_name = ENV.fetch('AGENT_NAME', nil)

  if agent_code_path && File.exist?(agent_code_path)
    load_synthesized_agent(agent, agent_code_path, agent_name)
  else
    logger.info('No synthesized code found, running in standard mode',
                agent_code_path: agent_code_path)
    agent.run
  end
end

.load_and_run_from_file(code_path, agent_name = nil, config_path: nil) ⇒ void

This method returns an undefined value.

Load and run agent from a specific file path

Parameters:

  • Path to agent DSL code file

  • (defaults to: nil)

    Name of the agent definition to run

  • (defaults to: nil)

    Path to configuration file



63
64
65
66
67
68
69
70
71
72
73
74
75
76
# File 'lib/language_operator/agent.rb', line 63

def self.load_and_run_from_file(code_path, agent_name = nil, config_path: nil)
  # Disable stdout buffering for real-time logging in containers
  $stdout.sync = true
  $stderr.sync = true

  config_path ||= ENV.fetch('CONFIG_PATH', 'config.yaml')
  config = LanguageOperator::Client::Config.load_with_fallback(config_path)

  # Create agent instance
  agent = LanguageOperator::Agent::Base.new(config)

  # Load and run the specified agent code
  load_synthesized_agent(agent, code_path, agent_name)
end

.load_synthesized_agent(agent, code_path, agent_name) ⇒ void

This method returns an undefined value.

Load synthesized agent code and execute

Parameters:

  • The agent instance

  • Path to synthesized code

  • Name of agent definition



101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
# File 'lib/language_operator/agent.rb', line 101

def self.load_synthesized_agent(agent, code_path, agent_name)
  logger.info('DSL code loading',
              path: code_path,
              agent_name: agent_name)

  # Load synthesized DSL code
  LanguageOperator::Dsl.load_agent_file(code_path)

  # Get agent definition from registry
  agent_def = LanguageOperator::Dsl.agent_registry.get(agent_name) if agent_name

  if agent_def
    logger.info('Agent definition loaded',
                agent_name: agent_name,
                has_workflow: !agent_def.workflow.nil?)
    run_with_definition(agent, agent_def)
  else
    log_definition_not_found(agent_name)
    agent.run
  end
rescue StandardError => e
  log_load_error(e)
  agent.run
end

.log_definition_not_found(agent_name) ⇒ void

This method returns an undefined value.

Log when agent definition is not found

Parameters:

  • Name of agent



130
131
132
133
134
135
# File 'lib/language_operator/agent.rb', line 130

def self.log_definition_not_found(agent_name)
  logger.warn('Agent definition not found in registry',
              agent_name: agent_name,
              available: LanguageOperator::Dsl.agent_registry.all.map(&:name))
  logger.info('Falling back to autonomous mode')
end

.log_load_error(error) ⇒ void

This method returns an undefined value.

Log agent code loading error

Parameters:

  • The error



141
142
143
144
145
146
# File 'lib/language_operator/agent.rb', line 141

def self.log_load_error(error)
  logger.error('Failed to load agent code',
               error: error.message,
               backtrace: error.backtrace[0..3])
  logger.info('Falling back to autonomous mode')
end

.loggerObject



34
35
36
# File 'lib/language_operator/agent.rb', line 34

def self.logger
  @logger
end

.parse_duration(duration) ⇒ Numeric

Parse duration string to seconds

Parameters:

  • Duration string (e.g., "10m", "2h", "30s")

Returns:

  • Duration in seconds



428
429
430
431
432
433
434
435
436
437
438
439
440
441
# File 'lib/language_operator/agent.rb', line 428

def self.parse_duration(duration)
  case duration
  when /^(\d+)s$/
    ::Regexp.last_match(1).to_i
  when /^(\d+)m$/
    ::Regexp.last_match(1).to_i * 60
  when /^(\d+)h$/
    ::Regexp.last_match(1).to_i * 3600
  when Numeric
    duration
  else
    raise ArgumentError, "Invalid duration format: #{duration}. Use format like '10m', '2h', '30s'"
  end
end

.register_standard_endpoints(web_server, agent, agent_def) ⇒ void

This method returns an undefined value.

Register standard endpoints for web server

Parameters:

  • The web server instance

  • The agent instance

  • The agent definition



239
240
241
242
243
# File 'lib/language_operator/agent.rb', line 239

def self.register_standard_endpoints(web_server, agent, agent_def)
  web_server.register_mcp_tools(agent_def.mcp_server) if agent_def.mcp_server&.tools?
  web_server.register_chat_endpoint(agent)
  web_server.register_workspace_endpoints(agent)
end

.run(config_path: nil) ⇒ void

This method returns an undefined value.

Run the default agent based on environment configuration

Parameters:

  • (defaults to: nil)

    Path to configuration file



42
43
44
45
46
47
48
49
50
51
52
53
54
55
# File 'lib/language_operator/agent.rb', line 42

def self.run(config_path: nil)
  # Disable stdout buffering for real-time logging in containers
  $stdout.sync = true
  $stderr.sync = true

  config_path ||= ENV.fetch('CONFIG_PATH', 'config.yaml')
  config = LanguageOperator::Client::Config.load_with_fallback(config_path)

  # Create agent instance
  agent = LanguageOperator::Agent::Base.new(config)

  # Load and run with synthesized code if available
  load_and_run(agent)
end

.run_idle_loop(shutdown_requested) ⇒ void

This method returns an undefined value.

Run the idle loop waiting for new instructions

Parameters:

  • Closure that returns shutdown state



327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
# File 'lib/language_operator/agent.rb', line 327

def self.run_idle_loop(shutdown_requested)
  idle_timeout = ENV.fetch('AGENT_IDLE_TIMEOUT', '300').to_i # 5 minutes default
  last_activity = Time.now

  until shutdown_requested.call
    begin
      last_activity = Time.now if handle_new_instruction

      sleep 5 # Avoid busy waiting

      # Reset idle timer when timeout reached (stay persistent)
      if idle_timeout.positive? && Time.now - last_activity > idle_timeout
        logger.info('Idle timeout reached - agent remaining available',
                    timeout_seconds: idle_timeout)
        last_activity = Time.now
      end
    rescue StandardError => e
      logger.error('Error in idle loop',
                   error: e.message,
                   backtrace: e.backtrace[0..3])
      sleep 10 # Back off on error
    end
  end
end

.run_with_definition(agent, agent_def) ⇒ void

This method returns an undefined value.

Run agent with a loaded definition

rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/PerceivedComplexity

Parameters:

  • The agent instance

  • The agent definition



154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
# File 'lib/language_operator/agent.rb', line 154

def self.run_with_definition(agent, agent_def)
  agent.connect!

  # Check if agent uses DSL v1 (task/main) or v0 (workflow/step)
  uses_dsl_v1 = agent_def.main&.defined?
  uses_dsl_v0 = agent_def.respond_to?(:workflow) && agent_def.workflow

  case agent.mode
  when 'autonomous', 'interactive'
    # Hybrid mode: All agents now run main work AND web server (chat endpoints always enabled)
    logger.info('Starting hybrid agent (autonomous + web server)',
                agent_name: agent_def.name,
                chat_endpoint_enabled: true, # Always true now
                has_webhooks: agent_def.webhooks.any?,
                has_mcp_tools: !!agent_def.mcp_server&.tools?)

    # Start web server in background thread
    web_server = LanguageOperator::Agent::WebServer.new(agent)
    agent_def.webhooks.each { |webhook_def| webhook_def.register(web_server) }
    register_standard_endpoints(web_server, agent, agent_def)

    web_thread = Thread.new do
      web_server.start
    rescue StandardError => e
      logger.error('Web server error', error: e.message, backtrace: e.backtrace[0..5])
      raise
    end

    # Set up signal handlers for graceful shutdown
    %w[INT TERM].each do |signal|
      Signal.trap(signal) do
        logger.info('Received shutdown signal, stopping hybrid agent')
        web_server.cleanup if web_server.respond_to?(:cleanup)
        web_thread.kill if web_thread&.alive?
        exit 0
      end
    end

    # Run main work in foreground
    if uses_dsl_v1
      execute_main_block_persistent(agent, agent_def)
    elsif uses_dsl_v0
      executor = LanguageOperator::Agent::Executor.new(agent)
      executor.execute_workflow(agent_def)
    else
      raise 'Agent definition must have either main block (DSL v1) or workflow (DSL v0)'
    end
  when 'scheduled', 'event-driven'
    # Standby mode - web server only, wait for /api/v1/execute triggers
    logger.info('Starting agent in standby mode (scheduled) - waiting for HTTP triggers',
                agent_name: agent_def.name,
                chat_endpoint_enabled: true,
                has_webhooks: agent_def.webhooks.any?,
                has_mcp_tools: !!agent_def.mcp_server&.tools?)

    web_server = LanguageOperator::Agent::WebServer.new(agent)
    agent_def.webhooks.each { |webhook_def| webhook_def.register(web_server) }
    register_standard_endpoints(web_server, agent, agent_def)
    web_server.register_execute_endpoint(agent, agent_def)

    web_server.start # Blocks here, waiting for requests
  when 'reactive', 'http', 'webhook'
    # Standby mode - web server with webhooks, MCP tools, chat, and execute endpoint
    logger.info('Starting agent in standby mode (reactive)',
                agent_name: agent_def.name,
                has_webhooks: agent_def.webhooks.any?)

    web_server = LanguageOperator::Agent::WebServer.new(agent)
    agent_def.webhooks.each { |webhook_def| webhook_def.register(web_server) }
    register_standard_endpoints(web_server, agent, agent_def)
    web_server.register_execute_endpoint(agent, agent_def)

    web_server.start
  else
    raise "Unknown agent mode: #{agent.mode}"
  end
end

.setup_signal_handlersProc

Setup signal handlers for graceful shutdown

Returns:

  • shutdown_requested flag wrapped in a closure



309
310
311
312
313
314
315
316
317
318
319
320
321
# File 'lib/language_operator/agent.rb', line 309

def self.setup_signal_handlers
  shutdown_requested = false
  trap('TERM') do
    logger.info('SIGTERM received - requesting graceful shutdown')
    shutdown_requested = true
  end
  trap('INT') do
    logger.info('SIGINT received - requesting graceful shutdown')
    shutdown_requested = true
  end

  -> { shutdown_requested }
end