You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
fix: strip <answer> XML tags before do/finish markers in _parse_response - #414
Fixes an ordering bug in ModelClient._parse_response (phone_agent/model/client.py) that corrupts every action when the model answers in the tagged format requested by the system prompt.
The bug
The system prompt (phone_agent/config/prompts_zh.py / prompts_en.py) instructs the model to output:
<think>{think}</think>
<answer>{action}</answer>
where {action} is a do(...) or finish(...) call. _parse_response checked its rules in this order:
finish(message= marker
do(action= marker
<answer> XML tags (documented as the fallback)
Because a tagged answer always contains a do(action= or finish(message= call inside the <answer> block, Rule 1/2 match before the tags are stripped. For input:
<think>I should tap the search box</think><answer>do(action="Tap", element=[500, 300])</answer>
the old code returned:
thinking = '<think>I should tap the search box</think><answer>' (tags leaked in)
The action string is not valid Python, so the downstream parse_action (phone_agent/actions/handler.py) raises ValueError. In PhoneAgent._execute_step that exception is swallowed and turned into finish(message=<raw string>), so the agent ends the task immediately instead of tapping. As a consequence, Rule 3 (the XML-tag path) was effectively dead code — it could never be reached for a real action.
The fix
Check for the <answer> tag first. When present, strip <think>/</think>/<answer>/</answer> and return the clean thinking/action pair. Tag-less responses (the AutoGLM-Phone-9B happy path) are unchanged: they fall through to the unchanged finish(message=/do(action= marker rules.
This is a pure reordering — no tag-less behaviour changes.
Tests
Adds tests/test_parse_response_order.py:
Tagged do/finish responses now yield a clean action with no </answer> remnant (regression cases).
Tag-less do, finish, finish-precedes-do, and plain-text fallback still behave exactly as before.
Verification
Not executed in CI here. Verified by reading the code path end-to-end and by simulating the reordered _parse_response against all test inputs (all produce the asserted (thinking, action) tuples). Both edited files pass ast.parse.
Reviewed — moving the tag check before the marker checks correctly prevents trailing from leaking into the action string. Good test coverage. Ready for review.
#378 and this PR fix the same bug: the <answer> tag check sits after the do(action= / finish(message= marker checks, so a tagged response leaves a trailing </answer> inside the action string, which then fails parse_action and aborts the step. Only one should merge, so here are the concrete differences.
This PR
Reorders the three rules only. ModelClient._parse_response keeps its name and signature, so no caller breaks. +24/-14, no unrelated edits.
Strips <think>/</think> from everything before <answer>, so the thinking comes back tag-free whether or not the closing tag is present.
104 lines of tests, 7 cases, covering the tagged path and the tag-less paths it must not regress.
Adds a debug print to the request path at phone_agent/model/client.py:147:
print(f"{thinking=}, {action=}")
That writes every parsed model response to stdout on every step.
Moves _parse_response off the class to a module-level parse_response. Any caller using client._parse_response(...) breaks.
Its rule 1 requires both tags (if "<answer>" in content and "</answer>" in content). With only the opening tag present it falls through to the do(action= marker, which leaves the raw tags in the thinking string — and its own last test pins that as the expected result:
content="""<think>先思考</think><answer>do(action="Launch", app="知乎")"""thinking, action=parse_response(content)
assertthinking=="<think>先思考</think>\n<answer>"# tags leak in here
A truncated model response is exactly this shape, so that assertion locks the leak in rather than fixing it. This PR returns 先思考 for the same input.
One thing #378 does better: it truncates the action at the first closing tag (parts[1].split("</answer>", 1)[0]) instead of deleting all of them, which drops any trailing text after </answer>. I'll fold that into rule 1 here if there's interest.
I've left a note on #378 rather than closing it, since it isn't mine.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes an ordering bug in
ModelClient._parse_response(phone_agent/model/client.py) that corrupts every action when the model answers in the tagged format requested by the system prompt.The bug
The system prompt (
phone_agent/config/prompts_zh.py/prompts_en.py) instructs the model to output:where
{action}is ado(...)orfinish(...)call._parse_responsechecked its rules in this order:finish(message=markerdo(action=marker<answer>XML tags (documented as the fallback)Because a tagged answer always contains a
do(action=orfinish(message=call inside the<answer>block, Rule 1/2 match before the tags are stripped. For input:the old code returned:
thinking = '<think>I should tap the search box</think><answer>'(tags leaked in)action = 'do(action="Tap", element=[500, 300])</answer>'(trailing</answer>)The action string is not valid Python, so the downstream
parse_action(phone_agent/actions/handler.py) raisesValueError. InPhoneAgent._execute_stepthat exception is swallowed and turned intofinish(message=<raw string>), so the agent ends the task immediately instead of tapping. As a consequence, Rule 3 (the XML-tag path) was effectively dead code — it could never be reached for a real action.The fix
Check for the
<answer>tag first. When present, strip<think>/</think>/<answer>/</answer>and return the clean thinking/action pair. Tag-less responses (the AutoGLM-Phone-9B happy path) are unchanged: they fall through to the unchangedfinish(message=/do(action=marker rules.This is a pure reordering — no tag-less behaviour changes.
Tests
Adds
tests/test_parse_response_order.py:do/finishresponses now yield a clean action with no</answer>remnant (regression cases).do,finish, finish-precedes-do, and plain-text fallback still behave exactly as before.Verification
Not executed in CI here. Verified by reading the code path end-to-end and by simulating the reordered
_parse_responseagainst all test inputs (all produce the asserted(thinking, action)tuples). Both edited files passast.parse.