Skip to content

Core::Base#execute overrides BaseAgent#execute without setting thread-local context for tools #22

@adham90

Description

@adham90

Bug Description

Core::Base#execute overrides BaseAgent#execute but does not set Thread.current[:ruby_llm_agents_caller_context], so tools never receive the execution context. This means context is always nil inside any RubyLLM::Agents::Tool subclass, and agent params (like workspace_path) are inaccessible.

Versions

  • ruby_llm-agents 3.11.0 (also present in 3.10.0)
  • ruby_llm 1.13.2
  • Ruby 4.0.1

Root Cause

BaseAgent#execute (base_agent.rb:810) correctly wraps the LLM call:

def execute(context)
  previous_context = Thread.current[:ruby_llm_agents_caller_context]
  Thread.current[:ruby_llm_agents_caller_context] = context
  # ... build_client, execute_llm_call ...
ensure
  Thread.current[:ruby_llm_agents_caller_context] = previous_context
end

Core::Base#execute (core/base.rb:74) overrides this without calling super and without setting the thread-local:

def execute(context)
  @execution_started_at = context.started_at || Time.current
  run_callbacks(:before, context)
  client = build_client(context)
  response = execute_llm_call(client, context)
  # ... no Thread.current assignment anywhere ...
end

Since RubyLLM::Agents::Base includes Core::Base, method resolution picks Core::Base#execute over BaseAgent#execute. The thread-local is never set.

Impact

In RubyLLM::Agents::Tool#call (tool.rb:69):

pipeline_context = Thread.current[:ruby_llm_agents_caller_context]
@context = pipeline_context ? ToolContext.new(pipeline_context) : nil

pipeline_context is always nil, so:

  • context is nil in all tools
  • context.workspace_path (or any agent param) raises or returns nil
  • Tool execution tracking (start_tool_tracking) is skipped (no execution_id)

Reproduction

class MyTool < RubyLLM::Agents::Tool
  description "Debug tool"
  param :path, desc: "A path", required: true

  def execute(path:)
    puts "context: #{context.inspect}"  # => nil
    puts "context.workspace_path: #{context&.workspace_path}"  # => nil
    "done"
  end
end

class MyAgent < RubyLLM::Agents::Base
  param :workspace_path, required: true
  param :query, required: true
  tools [MyTool]
  system "You are a helpful assistant."
  user "{query}"
end

MyAgent.call(query: "Use MyTool with path 'test'", workspace_path: "/tmp/test")
# => MyTool#execute runs but context is nil

Workaround

Override execute in your ApplicationAgent to set the thread-local:

class ApplicationAgent < RubyLLM::Agents::Base
  def execute(context)
    previous = Thread.current[:ruby_llm_agents_caller_context]
    Thread.current[:ruby_llm_agents_caller_context] = context
    super
  ensure
    Thread.current[:ruby_llm_agents_caller_context] = previous
  end
end

Suggested Fix

Add the thread-local context assignment to Core::Base#execute, or have it call super to inherit the wrapping from BaseAgent#execute.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions