From eba7c869bb713f5e40ae5f3ad5e314fcbef3ace3 Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Mon, 20 Apr 2026 03:08:04 -0700 Subject: [PATCH] fix(steer): drain /steer between individual tool calls, not at batch end (#12959) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, /steer text was only injected after an entire tool batch completed (_execute_tool_calls_sequential/concurrent returned). If the batch had a long-running tool (delegate_task, terminal build), the steer waited for ALL tools to finish before landing — functionally identical to /queue from the user's perspective. Now _apply_pending_steer_to_tool_results() is called after EACH individual tool result is appended to messages, in both the sequential and concurrent paths. A steer arriving during Tool 1 lands in Tool 1's result before Tool 2 starts executing. Also handles leftover steers in the gateway: if a steer arrives during the final API call (no tool batch to drain into), it's now delivered as the next user turn instead of being silently dropped. Fixes user report from Utku. --- gateway/run.py | 10 ++++++++++ run_agent.py | 11 +++++++++++ 2 files changed, 21 insertions(+) diff --git a/gateway/run.py b/gateway/run.py index 8f35d157..50f33aa3 100644 --- a/gateway/run.py +++ b/gateway/run.py @@ -10384,6 +10384,16 @@ class GatewayRunner: pending = pending_event.text or _build_media_placeholder(pending_event) logger.debug("Processing queued message after agent completion: '%s...'", pending[:40]) + # Leftover /steer: if a steer arrived after the last tool batch + # (e.g. during the final API call), the agent couldn't inject it + # and returned it in result["pending_steer"]. Deliver it as the + # next user turn so it isn't silently dropped. + if result and not pending and not pending_event: + _leftover_steer = result.get("pending_steer") + if _leftover_steer: + pending = _leftover_steer + logger.debug("Delivering leftover /steer as next turn: '%s...'", pending[:40]) + # Safety net: if the pending text is a slash command (e.g. "/stop", # "/new"), discard it — commands should never be passed to the agent # as user input. The primary fix is in base.py (commands bypass the diff --git a/run_agent.py b/run_agent.py index f8b0423b..007cb1a6 100644 --- a/run_agent.py +++ b/run_agent.py @@ -8404,6 +8404,11 @@ class AIAgent: } messages.append(tool_msg) + # ── Per-tool /steer drain ─────────────────────────────────── + # Same as the sequential path: drain between each collected + # result so the steer lands as early as possible. + self._apply_pending_steer_to_tool_results(messages, 1) + # ── Per-turn aggregate budget enforcement ───────────────────────── num_tools = len(parsed_calls) if num_tools > 0: @@ -8767,6 +8772,12 @@ class AIAgent: } messages.append(tool_msg) + # ── Per-tool /steer drain ─────────────────────────────────── + # Drain pending steer BETWEEN individual tool calls so the + # injection lands as soon as a tool finishes — not after the + # entire batch. The model sees it on the next API iteration. + self._apply_pending_steer_to_tool_results(messages, 1) + if not self.quiet_mode: if self.verbose_logging: print(f" ✅ Tool {i} completed in {tool_duration:.2f}s")