Class: ToolExecutionJob

Inherits:
ApplicationJob show all
Defined in:
app/jobs/tool_execution_job.rb

Overview

Runs a single tool on behalf of the session and reports the outcome.

Queued by Events::Subscribers::LLMResponseHandler when the LLM returns a tool_use block. The session is already in the :executing state (transition owned by the response handler). This job:

  1. Dispatches the tool via Tools::Registry.

  2. Truncates and formats the result.

  3. Emits Events::ToolExecuted.

The job does not release the session or create the tool_response PendingMessage — that’s Events::Subscribers::ToolResponseCreator‘s job. Event emission is the final act that hands control off.

Instance Method Summary collapse

Instance Method Details

#perform(session_id, tool_use_id:, tool_name:, tool_input:) ⇒ Object

Parameters:

  • session_id (Integer)
  • tool_use_id (String)

    Anthropic-assigned pairing ID

  • tool_name (String)
  • tool_input (Hash)


25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# File 'app/jobs/tool_execution_job.rb', line 25

def perform(session_id, tool_use_id:, tool_name:, tool_input:)
  session = Session.find(session_id)
  # ShellSession.for_session returns the conversation's persistent shell
  # — spawned on first use, reused on every subsequent tool call so the
  # agent's cd's and exported env vars survive between calls. We do NOT
  # finalize it here; the shell's lifetime is the Session's lifetime.
  shell_session = ShellSession.for_session(session)
  registry = Tools::Registry.build(session: session, shell_session: shell_session)

  content, success = execute(registry, tool_name, tool_input, tool_use_id)

  Events::Bus.emit(Events::ToolExecuted.new(
    session_id: session_id,
    tool_use_id: tool_use_id,
    tool_name: tool_name,
    content: content,
    success: success
  ))
rescue => error
  # A missing {Events::ToolExecuted} would leave the session in +:executing+
  # forever. Always emit a synthetic failure event so
  # {Events::Subscribers::ToolResponseCreator} runs and releases the claim.
  Rails.logger.error("ToolExecutionJob crashed: #{error.class}: #{error.message}")
  Events::Bus.emit(Events::ToolExecuted.new(
    session_id: session_id,
    tool_use_id: tool_use_id,
    tool_name: tool_name,
    content: "#{error.class}: #{error.message}",
    success: false
  ))
end