Class: Candle::Agent

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

Constant Summary collapse

MAX_ITERATIONS =
10

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(llm, tools:, system_prompt: nil, max_iterations: MAX_ITERATIONS) ⇒ Agent

Returns a new instance of Agent.



11
12
13
14
15
16
# File 'lib/candle/agent.rb', line 11

def initialize(llm, tools:, system_prompt: nil, max_iterations: MAX_ITERATIONS)
  @llm = llm
  @tools = tools
  @system_prompt = system_prompt
  @max_iterations = max_iterations
end

Instance Attribute Details

#llmObject (readonly)

Returns the value of attribute llm.



9
10
11
# File 'lib/candle/agent.rb', line 9

def llm
  @llm
end

#max_iterationsObject (readonly)

Returns the value of attribute max_iterations.



9
10
11
# File 'lib/candle/agent.rb', line 9

def max_iterations
  @max_iterations
end

#system_promptObject (readonly)

Returns the value of attribute system_prompt.



9
10
11
# File 'lib/candle/agent.rb', line 9

def system_prompt
  @system_prompt
end

#toolsObject (readonly)

Returns the value of attribute tools.



9
10
11
# File 'lib/candle/agent.rb', line 9

def tools
  @tools
end

Instance Method Details

#run(user_message, **options) ⇒ Object



18
19
20
21
22
23
24
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
56
57
58
59
60
61
62
63
# File 'lib/candle/agent.rb', line 18

def run(user_message, **options)
  messages = []
  messages << { role: "system", content: @system_prompt } if @system_prompt
  messages << { role: "user", content: user_message }

  iterations = 0
  loop do
    iterations += 1
    if iterations > @max_iterations
      raise AgentMaxIterationsError,
        "Agent exceeded maximum iterations (#{@max_iterations})"
    end

    result = @llm.chat_with_tools(messages, tools: @tools, execute: true, **options)

    if result.has_tool_calls?
      # If the model produced a substantial text answer alongside tool calls,
      # treat it as a final response (model is done, trailing tool calls are noise).
      # Strip <think> blocks so they don't count toward the length check.
      text_without_thinking = result.text_response&.gsub(/<think>.*?<\/think>/m, "")&.strip
      if text_without_thinking && text_without_thinking.length > 50
        return AgentResult.new(
          response: result.text_response,
          messages: messages,
          iterations: iterations,
          tool_calls_made: messages.count { |m| m[:role] == "tool" }
        )
      end

      messages << { role: "assistant", content: result.raw_response }

      result.tool_results.each do |tr|
        tool_name = tr[:tool_call]&.name || "unknown"
        tool_output = tr[:error] ? "Error: #{tr[:error]}" : JSON.generate(tr[:result])
        messages << { role: "tool", content: "[#{tool_name}] #{tool_output}" }
      end
    else
      return AgentResult.new(
        response: result.text_response || result.raw_response,
        messages: messages,
        iterations: iterations,
        tool_calls_made: messages.count { |m| m[:role] == "tool" }
      )
    end
  end
end