Class: Truffle::Agent

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

Overview

A stateful agent: a provider, a system prompt, a running message history, and a toolbox. Calling #run drives the agent loop to completion.

The loop is the port of pi's agent-core runtime:

run(text)
emit :agent_start
append user message
loop:
  emit :turn_start
  response = provider.chat(messages, tools)
  append assistant message; emit :message
  if response has tool calls:
    for each call: emit :tool_call, run tool, append tool result,
                   emit :tool_result
    emit :turn_end ; continue   # feed results back to the model
  else:
    emit :turn_end ; emit :agent_end ; return assistant text

Events let a UI (TUI, web, logs) observe the run without the harness knowing anything about how it is rendered. Subscribe with #on.

Constant Summary collapse

DEFAULT_MAX_TURNS =
12
EVENTS =
%i[agent_start turn_start message tool_call tool_result turn_end agent_end].freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(provider:, system_prompt: nil, tools: [], model: nil, max_turns: DEFAULT_MAX_TURNS) ⇒ Agent

Returns a new instance of Agent.



32
33
34
35
36
37
38
39
40
41
42
43
# File 'lib/truffle/agent.rb', line 32

def initialize(provider:, system_prompt: nil, tools: [], model: nil,
               max_turns: DEFAULT_MAX_TURNS)
  @provider = provider
  @system_prompt = system_prompt
  @model = model
  @max_turns = max_turns
  @toolbox = tools.is_a?(Toolbox) ? tools : Toolbox.new(tools)
  @listeners = Hash.new { |h, k| h[k] = [] }

  @messages = []
  @messages << Message.system(system_prompt) if system_prompt
end

Instance Attribute Details

#max_turnsObject (readonly)

Returns the value of attribute max_turns.



30
31
32
# File 'lib/truffle/agent.rb', line 30

def max_turns
  @max_turns
end

#messagesObject (readonly)

Returns the value of attribute messages.



30
31
32
# File 'lib/truffle/agent.rb', line 30

def messages
  @messages
end

#providerObject (readonly)

Returns the value of attribute provider.



30
31
32
# File 'lib/truffle/agent.rb', line 30

def provider
  @provider
end

#system_promptObject (readonly)

Returns the value of attribute system_prompt.



30
31
32
# File 'lib/truffle/agent.rb', line 30

def system_prompt
  @system_prompt
end

#toolboxObject (readonly)

Returns the value of attribute toolbox.



30
31
32
# File 'lib/truffle/agent.rb', line 30

def toolbox
  @toolbox
end

Instance Method Details

#on(event = nil, &block) ⇒ Object

Register a listener. on(:tool_call) { |payload| ... } for one event, or on { |type, payload| ... } (no event arg) for every event.

Raises:

  • (ArgumentError)


47
48
49
50
51
52
53
54
55
56
57
58
59
60
# File 'lib/truffle/agent.rb', line 47

def on(event = nil, &block)
  raise ArgumentError, "on requires a block" unless block

  if event.nil?
    @listeners[:_all] << block
  else
    event = event.to_sym
    unless EVENTS.include?(event)
      raise ArgumentError, "unknown event #{event.inspect}, expected one of #{EVENTS.inspect}"
    end
    @listeners[event] << block
  end
  self
end

#resetObject

Reset history back to just the system prompt (keeps tools + listeners).



97
98
99
100
101
# File 'lib/truffle/agent.rb', line 97

def reset
  @messages = []
  @messages << Message.system(@system_prompt) if @system_prompt
  self
end

#run(user_input) ⇒ Object

Send a user message and run the loop until the model answers without requesting a tool. Returns the final assistant text.



64
65
66
67
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
# File 'lib/truffle/agent.rb', line 64

def run(user_input)
  emit(:agent_start, input: user_input)
  @messages << Message.user(user_input)

  final_text = nil
  turns = 0

  loop do
    turns += 1
    if turns > max_turns
      raise Error, "exceeded max_turns (#{max_turns}) without a final answer"
    end

    emit(:turn_start, turn: turns)
    response = @provider.chat(messages: @messages, tools: @toolbox.to_schema, model: @model)
    @messages << response.message
    emit(:message, message: response.message, usage: response.usage)

    unless response.tool_calls?
      final_text = response.text
      emit(:turn_end, turn: turns, tool_results: [])
      break
    end

    tool_results = run_tool_calls(response.tool_calls)
    emit(:turn_end, turn: turns, tool_results: tool_results)
  end

  emit(:agent_end, output: final_text, messages: @messages)
  final_text
end