Skip to content

agent-scan: recover finish when LLM omits closing tags - #622

Open
chaucerj wants to merge 1 commit into
Tencent:mainfrom
chaucerj:fix/agent-scan-unclosed-finish
Open

agent-scan: recover finish when LLM omits closing tags#622
chaucerj wants to merge 1 commit into
Tencent:mainfrom
chaucerj:fix/agent-scan-unclosed-finish

Conversation

@chaucerj

Copy link
Copy Markdown

agent-scan/agent_scan/utils/parse.py is a copy of mcp-scan's parser, but it is missing the two finish fallbacks the mcp-scan copy already has:

  1. a bare <finish>...</finish> block, and
  2. a truncated <function=finish> whose closing tag the LLM omitted (e.g. output cut off by max_tokens).

finish is the only way the agent loop terminates. When the parser returns None, handle_no_tool() runs instead and the entire final report produced by the scan is silently lost while the loop idles to max_iter.

This ports both fallbacks from mcp-scan (keeping agent-scan's dict | None signature and its combined <parameter= / <parameter name= regex) and hides bare <finish> blocks in clean_content(), matching mcp-scan.

Verified on: normal tool calls, closed finish, truncated finish, bare finish, tag-free and empty inputs — all behave as intended.

Port the two parse fallbacks mcp-scan already has to agent-scan's copy
of parse_tool_invocations: a bare <finish>...</finish> block, and a
truncated <function=finish> whose closing tag the LLM omitted (e.g.
output cut off by max_tokens). Without them the parser returns None,
handle_no_tool() runs instead of finish, and the entire final report
produced by the scan is silently lost while the loop idles to max_iter.

Also hide bare <finish> blocks in clean_content(), matching mcp-scan.

Verified: normal tool calls, closed finish, truncated finish, bare
finish, tag-free and empty inputs all behave as intended.
@boy-hack

Copy link
Copy Markdown
Collaborator

Thanks @chaucerj — this is a clean, well-motivated port. finish being the only loop-termination mechanism means a parser returning None here silently drops the whole final report and idles to max_iter, so handling both the bare <finish> block and the unclosed <function=finish> is exactly right.

I verified html and re are already imported in parse.py, so the html.unescape(...) calls are safe. The chained fallbacks (function tags → bare finish → unclosed finish) are ordered correctly, and hiding bare <finish> in clean_content() keeps the conversation surface consistent with mcp-scan.

Two minor, non-blocking notes:

  • The second fallback uses re.DOTALL | re.IGNORECASE with a non-greedy .*? and a trailing (?:</function|$). On very large content this is O(n) but fine; if you ever see catastrophic backtracking on pathological inputs, anchoring the end helps. Not a concern for normal LLM output.
  • If mcp-scan's copy evolves further, consider whether agent-scan/agent_scan/utils/parse.py and the mcp-scan parser should converge (or at least track each other), so this class of "agent-scan is a stale copy" bug doesn't recur. Out of scope here.

Otherwise correct and ready to merge. Nice catch on the silent-report-loss.

(Note: I'm leaving this as a review comment only — not merging.)

@chaucerj

chaucerj commented Sep 2, 2026

Copy link
Copy Markdown
Author

Thanks @boy-hack for the thorough verification — and for tracing the callers to confirm the silent-report-loss path.

Both notes acknowledged:

  • The regex tail (?:${'<'}/function|$) stays as-is for now; if backtracking ever shows up on pathological inputs I'll anchor the end explicitly.
  • Parser convergence between agent-scan and mcp-scan — agreed it's out of scope here; happy to look at it separately if there's appetite.

Anything else needed from my side? If not, would you mind adding the formal approval so this can merge?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants