Skip to content

Commit 4de1bcd

Browse files
committed
context: always emit on_tool_return
Before this change: in order for `on_tool_return` to be triggered, you had to schedule a tool via `LLM::Stream#queue`. After this change: all tool calls go through `on_tool_return`, regardless of how they were scheduled.
1 parent 00d6fb3 commit 4de1bcd

5 files changed

Lines changed: 138 additions & 12 deletions

File tree

mrblib/mruby-llm/agent.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -437,6 +437,7 @@ def call_functions
437437
results = confirmables.map do |tool|
438438
send(:on_tool_confirmation, tool, strategy)
439439
end
440+
@ctx.method(:emit_tool_returns).call(confirmables, results)
440441
@ctx.functions? ? [*results, *wait(strategy)] : results
441442
end
442443

mrblib/mruby-llm/context.rb

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -272,8 +272,11 @@ def wait(strategy)
272272
@queue.wait
273273
else
274274
return guarded_returns if guarded_returns
275-
@queue = functions.spawn(strategy)
276-
@queue.wait
275+
tools = functions
276+
@queue = tools.spawn(strategy)
277+
returns = @queue.wait
278+
emit_tool_returns(tools, returns)
279+
returns
277280
end
278281
ensure
279282
@queue = nil
@@ -529,6 +532,14 @@ def guarded_return_for(function, warning)
529532
})
530533
end
531534

535+
##
536+
# Emits tool return callbacks for directly waited function work.
537+
# @api private
538+
def emit_tool_returns(tools, returns)
539+
return unless LLM::Stream === stream
540+
returns.each_with_index { |result, index| stream.on_tool_return(tools[index], result) }
541+
end
542+
532543
##
533544
# Closes assistant tool-call messages that do not have matching tool
534545
# responses. This can happen when a turn is interrupted while a tool call is

mrblib/mruby-llm/version.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# frozen_string_literal: true
22

33
module LLM
4-
VERSION = "0.1.0.beta.7"
4+
VERSION = "0.1.0.beta.8"
55
end

spec/agent_spec.rb

Lines changed: 106 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -248,14 +248,6 @@ def configured_model
248248
end
249249

250250
describe "#talk" do
251-
before do
252-
transport.stub(
253-
"POST", "/v1/chat/completions",
254-
fixture: "openai/chat_completions.sse",
255-
headers: {"content-type" => "text/event-stream"}
256-
)
257-
end
258-
259251
context "when configured with a class-level stream object" do
260252
let(:stream) { StringIO.new }
261253
let(:agent_class) do
@@ -267,6 +259,11 @@ def configured_model
267259
let(:agent) { agent_class.new(llm, model: "gpt-4.1") }
268260

269261
before do
262+
transport.stub(
263+
"POST", "/v1/chat/completions",
264+
fixture: "openai/chat_completions.sse",
265+
headers: {"content-type" => "text/event-stream"}
266+
)
270267
agent.talk("Say hello")
271268
end
272269

@@ -287,6 +284,11 @@ def configured_model
287284
let(:agent) { agent_class.new(llm, model: "gpt-4.1", stream: override_stream) }
288285

289286
before do
287+
transport.stub(
288+
"POST", "/v1/chat/completions",
289+
fixture: "openai/chat_completions.sse",
290+
headers: {"content-type" => "text/event-stream"}
291+
)
290292
agent.talk("Say hello")
291293
end
292294

@@ -298,6 +300,102 @@ def configured_model
298300
expect(default_stream.string).must_equal ""
299301
end
300302
end
303+
304+
context "when auto-executing streamed tool calls" do
305+
let(:stream) do
306+
Class.new(LLM::Stream) do
307+
attr_reader :returns
308+
def initialize
309+
@returns = []
310+
end
311+
def on_tool_return(tool, result)
312+
@returns << [tool.name, result.name, result.value]
313+
end
314+
end.new
315+
end
316+
let(:tool) do
317+
Class.new(LLM::Tool) do
318+
name "system"
319+
parameter :command, String, "The command"
320+
required %i[command]
321+
def call(command:)
322+
{"success" => command == "date" ? "2025-08-24" : false}
323+
end
324+
end
325+
end
326+
let(:agent) { LLM::Agent.new(llm, model: "gpt-4.1", stream:, tools: [tool]) }
327+
328+
before do
329+
transport
330+
.stub(
331+
"POST", "/v1/chat/completions",
332+
fixture: "openai/chat_completions_tool.sse",
333+
headers: {"content-type" => "text/event-stream"}
334+
)
335+
.stub(
336+
"POST", "/v1/chat/completions",
337+
fixture: "openai/chat_completions_tool_result.sse",
338+
headers: {"content-type" => "text/event-stream"}
339+
)
340+
agent.talk("Run date")
341+
end
342+
343+
it "emits tool returns without manual stream queueing" do
344+
expect(stream.returns).must_equal [["system", "system", {"success" => "2025-08-24"}]]
345+
end
346+
end
347+
348+
context "when confirming streamed tool calls" do
349+
let(:stream) do
350+
Class.new(LLM::Stream) do
351+
attr_reader :returns
352+
def initialize
353+
@returns = []
354+
end
355+
def on_tool_return(tool, result)
356+
@returns << [tool.name, result.name, result.value]
357+
end
358+
end.new
359+
end
360+
let(:tool) do
361+
Class.new(LLM::Tool) do
362+
name "system"
363+
parameter :command, String, "The command"
364+
required %i[command]
365+
def call(command:)
366+
{"success" => command == "date" ? "2025-08-24" : false}
367+
end
368+
end
369+
end
370+
let(:agent_class) do
371+
Class.new(LLM::Agent) do
372+
confirm "system"
373+
def on_tool_confirmation(fn, strategy)
374+
fn.spawn(strategy).wait
375+
end
376+
end
377+
end
378+
let(:agent) { agent_class.new(llm, model: "gpt-4.1", stream:, tools: [tool]) }
379+
380+
before do
381+
transport
382+
.stub(
383+
"POST", "/v1/chat/completions",
384+
fixture: "openai/chat_completions_tool.sse",
385+
headers: {"content-type" => "text/event-stream"}
386+
)
387+
.stub(
388+
"POST", "/v1/chat/completions",
389+
fixture: "openai/chat_completions_tool_result.sse",
390+
headers: {"content-type" => "text/event-stream"}
391+
)
392+
agent.talk("Run date")
393+
end
394+
395+
it "emits confirmed tool returns" do
396+
expect(stream.returns).must_equal [["system", "system", {"success" => "2025-08-24"}]]
397+
end
398+
end
301399
end
302400
end
303401

spec/context_spec.rb

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,17 @@ def call(command:)
145145
end
146146

147147
context "when given a streamed tool call" do
148+
let(:stream) do
149+
Class.new(LLM::Stream) do
150+
attr_reader :returns
151+
def initialize
152+
@returns = []
153+
end
154+
def on_tool_return(tool, result)
155+
@returns << [tool.name, result.name, result.value]
156+
end
157+
end.new
158+
end
148159
let(:tool) do
149160
Class.new(LLM::Tool) do
150161
name "system"
@@ -157,7 +168,7 @@ def call(command:)
157168
end
158169
end
159170
end
160-
let(:params) { {model: "gpt-4.1", stream: true, tools: [tool]} }
171+
let(:params) { {model: "gpt-4.1", stream:, tools: [tool]} }
161172
let(:prompt) do
162173
ctx.build_prompt do
163174
_1.talk "You are a bot that can run UNIX system commands"
@@ -186,6 +197,11 @@ def call(command:)
186197
expect(res.content).must_equal "Today's date is August 24, 2025."
187198
expect(ctx.messages.last.content).must_equal "Today's date is August 24, 2025."
188199
end
200+
201+
it "emits tool returns from direct waits" do
202+
ctx.wait(:call)
203+
expect(stream.returns).must_equal [["system", "system", {"success" => "2025-08-24"}]]
204+
end
189205
end
190206

191207
context "when resuming a serialized conversation" do

0 commit comments

Comments
 (0)