Class: Legate::Agent

Inherits:
Object
  • Object
show all
Defined in:
lib/legate/agent.rb

Overview

Agent class represents an AI agent that can perform tasks using tools and a planner. It operates within the context of a session managed by a SessionService.

Constant Summary collapse

DEFAULT_MODEL =

Default Gemini model (supports structured output)

'gemini-3.5-flash'

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(definition:, session_service: nil, planner_override: nil, sub_agents: nil) ⇒ Agent

Initializes a new agent instance. An agent MUST be initialized with a valid Legate::AgentDefinition object.

Parameters:

  • definition (Legate::AgentDefinition)

    The agent definition object.

  • session_service (Legate::SessionService::Base, nil) (defaults to: nil)

    Optional: Pre-initialized session service.

  • planner_override (Legate::Planner, nil) (defaults to: nil)

    Optional: A specific planner instance to override the default.

  • sub_agents (Array<Legate::Agent>, nil) (defaults to: nil)

    Optional: An array of pre-initialized sub-agent instances. If provided, these will be used instead of instantiating from ‘definition.sub_agent_names`.

Raises:



110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
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
# File 'lib/legate/agent.rb', line 110

def initialize(definition:, session_service: nil, planner_override: nil, sub_agents: nil)
  unless definition.is_a?(Legate::AgentDefinition)
    raise ArgumentError,
          "Agent must be initialized with an Legate::AgentDefinition object. Received: #{definition.class}"
  end
  # Perform a more thorough check if it looks like a definition
  unless definition.respond_to?(:name) && definition.respond_to?(:description) &&
         definition.respond_to?(:instruction) && definition.respond_to?(:tool_names) &&
         definition.respond_to?(:model_name) && definition.respond_to?(:fallback_mode) &&
         definition.respond_to?(:mcp_servers)
    raise ArgumentError,
          'Provided definition object does not appear to be a valid Legate::AgentDefinition (missing required attributes/methods).'
  end

  @definition = definition
  @name = definition.name

  # --- Initialize Callbacks from Definition ---
  @before_agent_callback = definition.before_agent_callback
  @after_agent_callback = definition.after_agent_callback
  @before_model_callback = definition.before_model_callback
  @after_model_callback = definition.after_model_callback
  @before_tool_callback = definition.before_tool_callback
  @after_tool_callback = definition.after_tool_callback
  # --- End Initialize Callbacks ---

  # --- Initialize Authentication Config from Definition ---
  @auth_credential_names = definition.auth_credential_names || Set.new
  @auth_url_mappings = definition.auth_url_mappings || []
  @auth_scheme_assignments = definition.auth_scheme_assignments || {}
  @auth_credential_assignments = definition.auth_credential_assignments || {}
  # --- End Initialize Authentication Config ---

  # Check for direct self-references in the definition's sub_agent_names
  raise Legate::ConfigurationError, "Circular dependency detected: Agent '#{@name}' cannot include itself as a sub-agent" if definition.respond_to?(:sub_agent_names) && definition.sub_agent_names&.any? && definition.sub_agent_names.include?(@name)

  @description = definition.description
  @instruction = definition.instruction
  @model_name = definition.model_name || DEFAULT_MODEL
  @fallback_mode = definition.fallback_mode # Assumes :error is default in AgentDefinition
  @selected_tool_names = definition.tool_names.to_a # Tool names are directly from definition

  # MAS Attributes Initialization
  @parent_agent = nil # Will be set by parent if this is a sub-agent
  @sub_agents = []    # Will be populated if this agent has sub-agents defined

  @session_service = session_service || Legate.config.session_service
  @state = :idle

  Legate.logger.info("Initializing agent '#{@name}' from provided definition object...")

  setup_tool_registry(definition)
  setup_mcp_config(definition)

  @selected_tool_names = @definition.tool_names.to_a
  @mcp_manager = Legate::Mcp::ConnectionManager.new(
    tool_registry: @tool_registry,
    selected_tool_names: @selected_tool_names,
    agent_name: @name
  )
  @plan_executor = Legate::PlanExecutor.new(self)

  @planner = planner_override || Legate::Planner.new(agent: self, model_name: @model_name)

  unless @session_service&.respond_to?(:get_session) && @session_service.respond_to?(:append_event)
    raise ConfigurationError,
          "Agent '#{@name}' requires a valid Session Service (must respond to :get_session, :append_event)."
  end
  raise ConfigurationError, "Agent '#{@name}' requires a valid Planner (must respond to :plan)." unless @planner&.respond_to?(:plan)

  Legate.logger.debug {
    "Agent '#{@name}' initialized with #{@tool_registry.tools.count} tools: [#{@tool_registry.tools.keys.join(', ')}]"
  }

  setup_sub_agents(definition, sub_agents)
end

Instance Attribute Details

#after_agent_callbackObject (readonly)

— Callback Instance Variables —



36
37
38
# File 'lib/legate/agent.rb', line 36

def after_agent_callback
  @after_agent_callback
end

#after_model_callbackObject (readonly)

— Callback Instance Variables —



36
37
38
# File 'lib/legate/agent.rb', line 36

def after_model_callback
  @after_model_callback
end

#after_tool_callbackObject (readonly)

— Callback Instance Variables —



36
37
38
# File 'lib/legate/agent.rb', line 36

def after_tool_callback
  @after_tool_callback
end

#auth_credential_assignmentsObject (readonly)

— Authentication Instance Variables —



43
44
45
# File 'lib/legate/agent.rb', line 43

def auth_credential_assignments
  @auth_credential_assignments
end

#auth_credential_namesObject (readonly)

— Authentication Instance Variables —



43
44
45
# File 'lib/legate/agent.rb', line 43

def auth_credential_names
  @auth_credential_names
end

#auth_scheme_assignmentsObject (readonly)

— Authentication Instance Variables —



43
44
45
# File 'lib/legate/agent.rb', line 43

def auth_scheme_assignments
  @auth_scheme_assignments
end

#auth_url_mappingsObject (readonly)

— Authentication Instance Variables —



43
44
45
# File 'lib/legate/agent.rb', line 43

def auth_url_mappings
  @auth_url_mappings
end

#before_agent_callbackObject (readonly)

— Callback Instance Variables —



36
37
38
# File 'lib/legate/agent.rb', line 36

def before_agent_callback
  @before_agent_callback
end

#before_model_callbackObject (readonly)

— Callback Instance Variables —



36
37
38
# File 'lib/legate/agent.rb', line 36

def before_model_callback
  @before_model_callback
end

#before_tool_callbackObject (readonly)

— Callback Instance Variables —



36
37
38
# File 'lib/legate/agent.rb', line 36

def before_tool_callback
  @before_tool_callback
end

#definitionObject (readonly)

Added session_service to attr_reader



31
32
33
# File 'lib/legate/agent.rb', line 31

def definition
  @definition
end

#descriptionObject (readonly)

Added session_service to attr_reader



31
32
33
# File 'lib/legate/agent.rb', line 31

def description
  @description
end

#fallback_modeObject (readonly)

Added session_service to attr_reader



31
32
33
# File 'lib/legate/agent.rb', line 31

def fallback_mode
  @fallback_mode
end

#instructionObject (readonly)

Added session_service to attr_reader



31
32
33
# File 'lib/legate/agent.rb', line 31

def instruction
  @instruction
end

#loggerObject (readonly)

Added session_service to attr_reader



31
32
33
# File 'lib/legate/agent.rb', line 31

def logger
  @logger
end

#model_nameObject (readonly)

Added session_service to attr_reader



31
32
33
# File 'lib/legate/agent.rb', line 31

def model_name
  @model_name
end

#nameObject (readonly)

Added session_service to attr_reader



31
32
33
# File 'lib/legate/agent.rb', line 31

def name
  @name
end

#parent_agentObject (readonly)

MAS Attributes



33
34
35
# File 'lib/legate/agent.rb', line 33

def parent_agent
  @parent_agent
end

#plannerObject (readonly)

Added session_service to attr_reader



31
32
33
# File 'lib/legate/agent.rb', line 31

def planner
  @planner
end

#session_serviceObject (readonly)

Added session_service to attr_reader



31
32
33
# File 'lib/legate/agent.rb', line 31

def session_service
  @session_service
end

#stateObject (readonly)

Added session_service to attr_reader



31
32
33
# File 'lib/legate/agent.rb', line 31

def state
  @state
end

#sub_agentsObject (readonly)

Added session_service to attr_reader



31
32
33
# File 'lib/legate/agent.rb', line 31

def sub_agents
  @sub_agents
end

#tool_registryObject (readonly)

Added session_service to attr_reader



31
32
33
# File 'lib/legate/agent.rb', line 31

def tool_registry
  @tool_registry
end

Class Method Details

.define {|a| ... } ⇒ Legate::AgentDefinition

— Class Method for Configuration DSL — Provides a block-based DSL for configuring and creating an Agent instance.

The DSL is positional (method-call style), not assignment. The resulting definition is registered globally in GlobalDefinitionRegistry as a side effect, then returned.

Examples:

definition = Legate::Agent.define do |a|
  a.name :news_agent
  a.description 'Summarizes news articles.'
  a.instruction 'Summarize the article the user provides.'
  a.model_name 'gemini-3.5-flash'
  a.use_tool :echo
  a.fallback_mode :echo
end

Yield Parameters:

  • a (Legate::AgentDefinition::DefinitionProxy)

    The proxy object to configure the definition.

Returns:

Raises:

  • (ArgumentError)

    if the block is not provided or required attributes are missing.



68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
# File 'lib/legate/agent.rb', line 68

def self.define(&block)
  raise ArgumentError, 'Legate::Agent.define requires a block.' unless block_given?

  # 1. Create a new AgentDefinition
  definition = Legate::AgentDefinition.new

  # 2. Evaluate the block within the definition's proxy DSL
  # Use the definition instance's define method which takes the block
  # This also handles internal validation via validate!
  begin
    definition.define(&block)
  rescue ArgumentError => e
    # Re-raise DSL validation errors immediately
    raise e
  end

  # 3. Register the validated definition in the GlobalDefinitionRegistry
  begin
    GlobalDefinitionRegistry.register(definition)
    agent_name = definition.instance_variable_get(:@name)
    Legate.logger.info("Agent definition '#{agent_name}' registered in GlobalDefinitionRegistry.")
  rescue ArgumentError => e
    agent_name_for_log = definition.instance_variable_get(:@name) || 'unknown'
    Legate.logger.error("Failed to register definition '#{agent_name_for_log}': #{e.class} - #{e.message}")
    raise e
  rescue StandardError => e
    agent_name_for_log = definition.instance_variable_get(:@name) || 'unknown'
    Legate.logger.error("Unexpected error registering definition '#{agent_name_for_log}': #{e.class} - #{e.message}")
    raise Legate::StoreError, "Unexpected error registering definition '#{agent_name_for_log}': #{e.message}"
  end

  definition # Return the definition instance
end

Instance Method Details

#add_tool(tool) ⇒ Boolean

Adds a tool instance OR class to the agent’s registry

Parameters:

Returns:

  • (Boolean)

    True if the tool was added, false otherwise



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
# File 'lib/legate/agent.rb', line 190

def add_tool(tool)
  # Check if it's a valid tool instance or class
  is_tool_instance = tool.is_a?(Legate::Tool)
  is_tool_class = tool.is_a?(Class) && tool < Legate::Tool

  unless is_tool_instance || is_tool_class
    Legate.logger.error("Agent '#{name}' add_tool: Attempted to add invalid tool: #{tool.inspect}")
    return false
  end

  # Determine the actual tool class
  tool_class = is_tool_class ? tool : tool.class

  # --- Determine Tool Name with Fallbacks --- #
  tool_name = get_tool_name_from_class(tool_class) # Use the new helper
  # --- End Determine Tool Name --- #

  # Validate name was found
  unless tool_name # The helper returns nil if no valid name is found
    Legate.logger.error("Agent '#{name}' add_tool: Could not determine tool name for class #{tool_class}. Cannot add tool.")
    return false # Explicitly return false
  end

  # Check for overwrite
  Legate.logger.warn("Agent '#{name}': Tool '#{tool_name}' already added. Overwriting with class #{tool_class}.") if @tool_registry.find_class(tool_name)

  # Register the class using the determined name
  Legate.logger.debug("Agent '#{name}' add_tool: Registering tool_name=#{tool_name.inspect} with class=#{tool_class.inspect} in registry=#{@tool_registry.object_id}")
  registration_result = @tool_registry.register(tool_name, tool_class)
  Legate.logger.debug("Agent '#{name}' add_tool: Registry after registration for #{tool_name.inspect}: #{@tool_registry.tools.keys.inspect}")

  # Explicitly return the boolean result from the registry
  registration_result
end

#apply_pending_state(callback_context, session_id, session_service, clear: false) ⇒ Object

Flushes a callback’s accumulated state delta into the session via the session service. Optionally clears the delta afterward (when execution continues and the same context will be reused).



497
498
499
500
501
502
503
504
# File 'lib/legate/agent.rb', line 497

def apply_pending_state(callback_context, session_id, session_service, clear: false)
  return if callback_context.pending_state_delta.empty?

  callback_context.pending_state_delta.each do |key, value|
    session_service.set_state(session_id: session_id, key: key, value: value)
  end
  callback_context.clear_pending_state_delta! if clear
end

#ask(user_input, user_id: 'default', session_id: nil) {|event| ... } ⇒ Legate::Event

One-shot convenience runner: starts the agent if needed, creates (or reuses) a session on the agent’s own session service, runs the task, and returns the final event. The friendly path over the explicit start/create_session/run_task/stop dance.

answer = agent.ask('What is 2 + 2?').answer
agent.ask('Search ruby') { |event| puts event.role } # live progress (R3)

Lazy-starts but does NOT auto-stop — stopping tears down MCP connections that are costly to re-establish, and an agent typically answers many asks. Call #stop when done (or let process exit reclaim it).

Parameters:

  • user_input (String)

    the user’s request

  • user_id (String) (defaults to: 'default')

    identity for the auto-created session

  • session_id (String, nil) (defaults to: nil)

    reuse an existing session to continue a conversation

Yield Parameters:

  • event (Legate::Event)

    optional live progress (forwarded to run_task’s on_event)

Returns:

  • (Legate::Event)

    the final agent event (use #answer / #success?)



335
336
337
338
339
340
# File 'lib/legate/agent.rb', line 335

def ask(user_input, user_id: 'default', session_id: nil, &on_event)
  start unless running?
  session_id ||= @session_service.create_session(app_name: name.to_s, user_id: user_id).id
  run_task(session_id: session_id, user_input: user_input,
           session_service: @session_service, on_event: on_event)
end

#available_tools_metadataObject

Returns the list of available tool metadata (names, descriptions, parameters) from the agent’s specific tool registry.



307
308
309
# File 'lib/legate/agent.rb', line 307

def 
  @tool_registry.list_tools
end

#find_agent(name_sym) ⇒ Legate::Agent?

Finds an agent with the given name in the hierarchy using DFS

Parameters:

  • name_sym (Symbol)

    The name of the agent to find (as a symbol)

Returns:

  • (Legate::Agent, nil)

    The agent with the given name, or nil if not found



530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
# File 'lib/legate/agent.rb', line 530

def find_agent(name_sym)
  # Convert to symbol if string provided
  name_sym = name_sym.to_sym if name_sym.is_a?(String)

  # Check if this is the agent we're looking for
  return self if @name.to_sym == name_sym

  # Search sub-agents recursively
  @sub_agents.each do |sub_agent|
    found = sub_agent.find_agent(name_sym)
    return found if found
  end

  # Not found in this branch
  nil
end

#find_sub_agent(name_sym) ⇒ Legate::Agent?

Finds a direct sub-agent with the given name

Parameters:

  • name_sym (Symbol)

    The name of the sub-agent to find

Returns:

  • (Legate::Agent, nil)

    The sub-agent with the given name, or nil if not found



550
551
552
553
554
555
556
557
# File 'lib/legate/agent.rb', line 550

def find_sub_agent(name_sym)
  # Convert to symbol if string provided
  name_sym = name_sym.to_sym if name_sym.is_a?(String)

  return nil unless @sub_agents.is_a?(Array)

  @sub_agents.find { |sub_agent| sub_agent.name.to_sym == name_sym }
end

#find_tool(tool_name) ⇒ Legate::Tool?

Finds a tool instance by name

Parameters:

  • tool_name (Symbol)

    The name of the tool to find

Returns:

  • (Legate::Tool, nil)

    The tool instance if found, nil otherwise



244
245
246
# File 'lib/legate/agent.rb', line 244

def find_tool(tool_name)
  @tool_registry.create_instance(tool_name.to_sym)
end

#find_tool_class(tool_name) ⇒ Class<Legate::Tool>?

Finds a tool class by name from the agent’s specific tool registry.

Parameters:

  • tool_name (Symbol)

Returns:



314
315
316
# File 'lib/legate/agent.rb', line 314

def find_tool_class(tool_name)
  @tool_registry.find_class(tool_name.to_sym)
end

#record_error_event(session_id, session_service, message) ⇒ Legate::Event

Builds an agent error event, records it in the session history (best-effort: a failed append must not mask the original error), and returns it.

Returns:



509
510
511
512
513
514
515
516
517
# File 'lib/legate/agent.rb', line 509

def record_error_event(session_id, session_service, message)
  event = Legate::Event.new(role: :agent, content: { status: :error, error_message: message })
  begin
    session_service.append_event(session_id: session_id, event: event)
  rescue StandardError => e
    Legate.logger.error { "Agent '#{@name}': failed to record error event in session: #{e.message}" }
  end
  event
end

#register_tool_class(tool_class) ⇒ Boolean

Registers a tool class with the agent’s specific registry.

Parameters:

  • tool_class (Class)

    The tool class to register (must inherit from Legate::Tool).

Returns:

  • (Boolean)

    True if registration was successful, false otherwise.



251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
# File 'lib/legate/agent.rb', line 251

def register_tool_class(tool_class)
  Legate.logger.debug("[register_tool_class] Registering class: #{tool_class.inspect} (Object ID: #{tool_class.object_id})")
  # Basic validation
  unless tool_class < Legate::Tool
    Legate.logger.error("Agent '#{name}': Attempted to register invalid object (must inherit from Legate::Tool): #{tool_class.inspect}")
    return false
  end

  # Get name via metadata method
  tool_name = get_tool_name_from_class(tool_class) # Use the new helper
  Legate.logger.debug("[register_tool_class] Determined tool name: #{tool_name.inspect} for class #{tool_class.inspect}")

  unless tool_name # Helper returns nil if no valid name
    # Use logger method, not direct access
    Legate.logger.error("Agent '#{name}': Could not determine tool name for class #{tool_class}. Cannot register.") # Consistent error message
    return false
  end

  Legate.logger.warn("Agent '#{name}': Tool '#{tool_name}' already registered. Overwriting.") if @tool_registry.find_class(tool_name)

  # Register with the instance registry
  @tool_registry.register(tool_name, tool_class)
  true # Return true on success
end

#root_agentLegate::Agent

Returns the root agent in the hierarchy (the topmost agent with no parent)

Returns:



521
522
523
524
525
# File 'lib/legate/agent.rb', line 521

def root_agent
  return self if @parent_agent.nil?

  @parent_agent.root_agent
end

#run_task(session_id:, user_input:, session_service:, on_event: nil) ⇒ Legate::Event

Returns The final agent event.

Parameters:

  • on_event (Proc, nil) (defaults to: nil)

    optional callback invoked with each Legate::Event as it is appended during the run (user, tool_request, tool_result, final agent) — for streaming progress (R3). The final event is still returned; non-streaming callers pass nothing and are unaffected.

Returns:



347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
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
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
# File 'lib/legate/agent.rb', line 347

def run_task(session_id:, user_input:, session_service:, on_event: nil)
  # --- Pre-execution Checks --- #
  unless running?
    err_msg = "Agent '#{name}' is not running. Call agent.start before run_task, " \
              'or use agent.ask (which starts automatically).'
    Legate.logger.error(err_msg)
    return Legate::Event.new(role: :agent, content: { status: :error, error_message: err_msg })
  end

  session = session_service.get_session(session_id: session_id)
  unless session
    err_msg = "Session not found: #{session_id}"
    Legate.logger.error(err_msg)
    # Even if session isn't found, return an event for consistency?
    return Legate::Event.new(role: :agent, content: { status: :error, error_message: err_msg })
  end
  # ----------------- #

  # Generate invocation_id for this run and create callback context
  invocation_id = SecureRandom.uuid
  callback_context = nil

  # R3: stream lifecycle events to the optional on_event callback as they're
  # appended. Torn down in the ensure below so the subscription can't leak.
  event_subscription = subscribe_events(session_service, session_id, on_event)

  begin
    # Create callback context for callbacks to use
    callback_context = Legate::Callbacks::CallbackContext.new(
      agent_name: @name,
      invocation_id: invocation_id,
      session_id: session_id,
      user_id: session.user_id,
      app_name: session.app_name,
      session_service: session_service
    )

    # Execute before_agent_callback if defined
    if @definition.respond_to?(:before_agent_callback) && @definition.before_agent_callback
      Legate.logger.debug { "Agent '#{@name}': Executing before_agent_callback." }

      # Execute the callback and check if it returns a result
      begin
        override_result = @definition.before_agent_callback.call(callback_context)

        # If the callback returns a result (not nil), use it instead of normal execution
        if override_result
          Legate.logger.info { "Agent '#{@name}': before_agent_callback provided an override result." }

          # Apply any pending state changes from the callback
          apply_pending_state(callback_context, session_id, session_service)

          # Create an agent event with the override result
          final_agent_event = Legate::Event.new(role: :agent, content: override_result)
          session_service.append_event(session_id: session_id, event: final_agent_event)

          # Store the output if configured
          _store_output_in_session(final_agent_event, session_id, session_service)

          return final_agent_event
        end
      rescue StandardError => e
        Legate.logger.error { "Agent '#{@name}': Error in before_agent_callback: #{e.message}\n#{e.backtrace.join("\n")}" }
        return record_error_event(session_id, session_service, "Error in before_agent_callback: #{e.message}")
      end

      # Apply any pending state changes from the callback if execution continues
      apply_pending_state(callback_context, session_id, session_service, clear: true)
    end

    # --- Normal Execution Flow --- #
    # Create a user-message event for this turn
    user_message_event = Legate::Event.new(
      role: :user,
      content: user_input
    )
    session_service.append_event(session_id: session_id, event: user_message_event)

    # Produce the result via the configured strategy. :plan (default) asks
    # the planner for one upfront plan and runs it; :react drives an agentic
    # observe->think->act loop. Both return the same { details:, last_result: }
    # shape, so the final-event handling below is strategy-agnostic.
    result_hash =
      if react_strategy?
        run_react_loop(user_input, session, session_service, invocation_id)
      else
        plan = @planner.plan(user_input, invocation_id)
        execute_plan(plan, session, session_service, invocation_id)
      end

    # Create an agent event with the result
    final_agent_event = Legate::Event.new(role: :agent, content: result_hash[:last_result] || result_hash)
    session_service.append_event(session_id: session_id, event: final_agent_event)

    # Execute after_agent_callback if defined
    if @definition.respond_to?(:after_agent_callback) && @definition.after_agent_callback
      Legate.logger.debug { "Agent '#{@name}': Executing after_agent_callback." }

      begin
        # Execute the callback and let it modify the result if needed
        # Pass the actual result (last_result) to the callback, not the full hash with details
        modified_result = @definition.after_agent_callback.call(callback_context, result_hash[:last_result] || result_hash)

        # If the callback returned a modified result, use it
        if modified_result && modified_result != (result_hash[:last_result] || result_hash)
          Legate.logger.info { "Agent '#{@name}': after_agent_callback modified the result." }

          # Create a new agent event with the modified result
          final_agent_event = Legate::Event.new(role: :agent, content: modified_result)
          session_service.append_event(session_id: session_id, event: final_agent_event)
        end
      rescue StandardError => e
        Legate.logger.error { "Agent '#{@name}': Error in after_agent_callback: #{e.message}\n#{e.backtrace.join("\n")}" }
        # Don't override the result completely on error, just log it
      end

      # Apply the callback's pending state changes exactly once (whether or
      # not it modified the result).
      apply_pending_state(callback_context, session_id, session_service)
    end

    # Store the output if configured
    _store_output_in_session(final_agent_event, session_id, session_service)

    # Return the final agent event
    final_agent_event
  rescue StandardError => e
    # Handle any other errors during execution. Record the failure in the
    # session so its history reflects what the caller saw (the success and
    # callback paths already append their events).
    Legate.logger.error { "Agent '#{@name}' runtime error: #{e.message}\n#{e.backtrace.join("\n")}" }
    record_error_event(session_id, session_service, e.message)
  ensure
    session_service.unsubscribe(event_subscription) if event_subscription && session_service.respond_to?(:unsubscribe)
  end
end

#running?Boolean

Returns:

  • (Boolean)


301
302
303
# File 'lib/legate/agent.rb', line 301

def running?
  @state == :running
end

#startObject

— Runtime State Methods (unchanged) —



277
278
279
280
281
282
283
284
285
286
287
# File 'lib/legate/agent.rb', line 277

def start
  return if running? # Prevent starting multiple times

  Legate.logger.info("Starting agent '#{name}' runtime...")
  @state = :running

  # Connect to MCP Servers and register tools
  connect_mcp_servers

  Legate.logger.info("Agent '#{name}' runtime started.")
end

#stopObject



289
290
291
292
293
294
295
296
297
298
299
# File 'lib/legate/agent.rb', line 289

def stop
  return unless running?

  Legate.logger.info("Stopping agent '#{name}' runtime...")
  @state = :stopped

  # Disconnect MCP Clients
  disconnect_mcp_servers

  Legate.logger.info("Agent '#{name}' runtime stopped.")
end

#toolsArray<Legate::Tool>

Returns the list of tools registered with this agent

Returns:



227
228
229
230
231
232
233
234
235
236
237
238
239
# File 'lib/legate/agent.rb', line 227

def tools
  @tool_registry.tools.values.map do |tool_class|
    # Get name reliably using the new helper method
    tool_name = get_tool_name_from_class(tool_class)
    if tool_name
      @tool_registry.create_instance(tool_name)
    else
      # This branch should ideally not be hit frequently if registration robustly requires a name.
      Legate.logger.warn("Agent '#{name}': Skipping tool instance creation for class #{tool_class} as its name could not be determined post-registration.")
      nil
    end
  end.compact
end

#transfer_to(target_agent_name, task, session_id, session_service) ⇒ Hash

Transfers control to another agent, executing a task with the same session context. This is a public version of the private transfer_to method

Parameters:

  • target_agent_name (Symbol)

    The name of the target agent to delegate to

  • task (String)

    The task to delegate to the target agent

  • session_id (String)

    The current session ID

  • session_service (Legate::SessionService::Base)

    The session service instance

Returns:

  • (Hash)

    A standard result hash { status: :success/:error, result/error_message: … }



567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
# File 'lib/legate/agent.rb', line 567

def transfer_to(target_agent_name, task, session_id, session_service)
  # Verify the target agent is in the delegation_targets list if defined
  if @definition.respond_to?(:delegation_targets) && @definition.delegation_targets&.any? && !@definition.delegation_targets.include?(target_agent_name)
    error_msg = "Agent '#{target_agent_name}' is not in the delegation targets for '#{@name}'"
    Legate.logger.error(error_msg)
    return { status: :error, error_message: error_msg, error_class: 'InvalidDelegationTarget' }
  end

  # Find the target agent in the agent hierarchy, starting from the root
  target_agent = root_agent.find_agent(target_agent_name)

  # If not found in hierarchy, try to instantiate from definition store
  unless target_agent
    Legate.logger.info("Target agent '#{target_agent_name}' not found in hierarchy. Attempting to load from definition store.")

    begin
      # Try to find the definition in the global registry
      target_def = Legate::GlobalDefinitionRegistry.find(target_agent_name)

      unless target_def
        error_msg = "Target agent definition '#{target_agent_name}' not found in registry"
        Legate.logger.error(error_msg)
        return { status: :error, error_message: error_msg, error_class: 'AgentDefinitionNotFound' }
      end

      # Create a new agent instance from the definition
      target_agent = Legate::Agent.new(
        definition: target_def,
        session_service: session_service
      )
    rescue StandardError => e
      error_msg = "Failed to instantiate target agent '#{target_agent_name}': #{e.message}"
      Legate.logger.error("#{error_msg}\n#{e.backtrace.join("\n")}")
      return { status: :error, error_message: error_msg, error_class: e.class.name }
    end
  end

  # Verify the target agent exists
  unless target_agent
    error_msg = "Target agent '#{target_agent_name}' not found in hierarchy or definition store"
    Legate.logger.error(error_msg)
    return { status: :error, error_message: error_msg, error_class: 'AgentNotFound' }
  end

  # Start the target agent if it's not already running
  target_agent.start unless target_agent.running?

  # Execute the delegated task
  begin
    Legate.logger.info("Executing delegated task on agent '#{target_agent_name}': #{task}")

    # Call run_task with the same session context
    result_event = target_agent.run_task(
      session_id: session_id,
      user_input: task,
      session_service: session_service
    )

    # Extract and format the result
    result_content = result_event.respond_to?(:content) ? result_event.content : result_event

    {
      status: :success,
      target_agent: target_agent_name.to_s,
      result: result_content
    }
  rescue StandardError => e
    error_msg = "Error executing task on target agent '#{target_agent_name}': #{e.message}"
    Legate.logger.error("#{error_msg}\n#{e.backtrace.join("\n")}")
    { status: :error, error_message: error_msg, error_class: e.class.name }
  end
end