mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-30 13:34:50 +02:00
[eric] mcp: activating a server ends the turn, so the model stops guessing tool names
This commit is contained in:
@@ -53,6 +53,14 @@ class TurnRunner(AgentManagerProtocol):
|
||||
# Per-turn thinking aggregation trackers (added for the "Thought for Ns · M tokens" persisted label). Without nonlocal, the int reassignments at AssistantMessage emission below shadow them as locals and the dict access at content_block_start crashes with UnboundLocalError.
|
||||
# p_stream lets the persistent-client path feed receive_response() through this same consumption loop (one body, two transports).
|
||||
async for message in (p_stream if p_stream is not None else query(prompt=prompt_stream(), options=options)):
|
||||
# MCPActivate tells the model its new tools are not callable yet and to stop. Asking
|
||||
# was not enough: it kept going and guessed names like `send email`, which is what
|
||||
# made every MCP task look broken. The activated tools genuinely do not exist until
|
||||
# the transport is rebuilt, so end the turn here and let the auto-continuation fire
|
||||
# with the real names. Checked before the message is handled, so the model's next
|
||||
# move after activating never runs.
|
||||
if getattr(session, "pending_continuation", False):
|
||||
break
|
||||
if isinstance(message, ResultMessage):
|
||||
turn.current_turn_emitted = False
|
||||
else:
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
"""Activating an MCP server must END the turn, not ask the model nicely to stop.
|
||||
|
||||
MCPActivate returns "its tools are NOT callable in this turn ... Do not attempt any other tool call
|
||||
now" and queues a hidden continuation for afterwards. That was advisory, and models ignored it: the
|
||||
observed behaviour was activate google-workspace, then immediately guess `send email`, send a
|
||||
message with a wrong subject, then narrate "Wrong tool name guess. Let me find the actual Gmail tool
|
||||
names." Every MCP task looked broken, on every install since 1.6.0.
|
||||
|
||||
The activated tools genuinely do not exist until the transport is rebuilt, so anything the model
|
||||
does after activating is guesswork by construction. The loop now breaks on the flag.
|
||||
"""
|
||||
import inspect
|
||||
|
||||
from backend.apps.agents.manager.run import TurnRunner
|
||||
|
||||
|
||||
def p_loop_source() -> str:
|
||||
src = inspect.getsource(TurnRunner)
|
||||
start = src.index("async def p_run_streaming_turn")
|
||||
return src[start:start + 3000]
|
||||
|
||||
|
||||
def test_the_turn_loop_breaks_on_a_pending_continuation():
|
||||
body = p_loop_source()
|
||||
assert "pending_continuation" in body, "nothing ends the turn, so the model keeps guessing"
|
||||
assert "break" in body
|
||||
|
||||
|
||||
def test_the_check_runs_BEFORE_the_message_is_handled():
|
||||
"""A check after handling would still let the model's next tool call execute, which is the
|
||||
whole bug: the wrong-named call already went out."""
|
||||
body = p_loop_source()
|
||||
loop_at = body.index("async for message in")
|
||||
check_at = body.index("pending_continuation")
|
||||
handled_at = body.index("isinstance(message, ResultMessage)")
|
||||
assert loop_at < check_at < handled_at, (
|
||||
"the flag must be read at the top of the iteration, before any message handling"
|
||||
)
|
||||
|
||||
|
||||
def test_the_reason_is_recorded_where_the_next_reader_will_look():
|
||||
body = p_loop_source()
|
||||
assert "guess" in body.lower(), "a bare break invites someone to delete it as dead code"
|
||||
|
||||
|
||||
def test_the_continuation_hook_still_consumes_the_flag():
|
||||
"""Breaking the loop is only half of it. If the end-of-loop hook stopped firing, activation
|
||||
would leave the user with a dead turn and no follow-up at all, which is worse than guessing."""
|
||||
from backend.apps.agents import agent_manager
|
||||
|
||||
src = inspect.getsource(agent_manager)
|
||||
assert "pending_continuation" in src
|
||||
assert "hidden=True" in src, "the continuation must not add a visible user bubble"
|
||||
|
||||
|
||||
def test_the_activation_response_still_tells_the_model_what_happened():
|
||||
"""The hard stop is the enforcement; the words are still what the model reads on the next turn
|
||||
to understand why it was cut off."""
|
||||
from backend.apps.agents import mcp_meta_server
|
||||
|
||||
src = inspect.getsource(mcp_meta_server)
|
||||
assert "NOT callable in this turn" in src
|
||||
assert "continuation turn will fire" in src
|
||||
Reference in New Issue
Block a user