Class: Brute::Middleware::ToolUseGuard
- Inherits:
-
Object
- Object
- Brute::Middleware::ToolUseGuard
- Defined in:
- lib/brute/middleware/tool_use_guard.rb
Overview
Guards against tool-only LLM responses where the assistant message is dropped from the context buffer.
When the LLM responds with only tool_use blocks (no text), llm.rb’s response adapter produces empty choices. Context#talk appends nil, BufferNilGuard strips it, and the assistant message carrying tool_use blocks is lost. This causes “unexpected tool_use_id” on the next call because tool_result references a tool_use that’s missing from the buffer.
This middleware runs post-call and ensures every pending tool_use ID is covered by an assistant message in the buffer. It handles three cases:
1. ctx.functions is non-empty and the assistant message exists → no-op
2. ctx.functions is non-empty but the assistant message is missing
(or has different IDs) → inject synthetic message
3. ctx.functions is empty (nil-choice bug) but the stream recorded
tool calls → inject synthetic message using stream metadata
Instance Method Summary collapse
- #call(env) ⇒ Object
-
#initialize(app) ⇒ ToolUseGuard
constructor
A new instance of ToolUseGuard.
Constructor Details
#initialize(app) ⇒ ToolUseGuard
Returns a new instance of ToolUseGuard.
25 26 27 |
# File 'lib/brute/middleware/tool_use_guard.rb', line 25 def initialize(app) @app = app end |
Instance Method Details
#call(env) ⇒ Object
29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
# File 'lib/brute/middleware/tool_use_guard.rb', line 29 def call(env) response = @app.call(env) ctx = env[:context] # Collect pending tool data from ctx.functions (primary) or the # stream's recorded metadata (fallback for nil-choice bug). tool_data = collect_tool_data(ctx, env) return response if tool_data.empty? # Find all tool_use IDs already covered by assistant messages. covered_ids = covered_tool_ids(ctx) # Inject a synthetic assistant message for any uncovered tool calls. uncovered = tool_data.reject { |td| covered_ids.include?(td[:id]) } inject_synthetic!(ctx, uncovered) unless uncovered.empty? response end |