Skip to content

fix: tighten tool markup parse and harden custom-model safety#9

Merged
involvex merged 1 commit into
mainfrom
fix/tool-parser-custom-safety
Jul 19, 2026
Merged

fix: tighten tool markup parse and harden custom-model safety#9
involvex merged 1 commit into
mainfrom
fix/tool-parser-custom-safety

Conversation

@involvex

@involvex involvex commented Jul 19, 2026

Copy link
Copy Markdown
Owner

Summary

  • Require ChatML tool_call delimiters so bare \call:open_app{...}\ in prose is not executed
  • Drop overly broad aliases (\search, \ ime, \weather)
  • Port generation safety, Stop abort, and final sanitize into the custom-model stream path

Follow-up to review feedback on #8 (already released as v0.4.1).

Test plan

  • Unit tests for false-positive prose tool markup
  • Custom imported model: Stop cancels; long loop hits truncate/repetition note
  • Real <|tool_call>call:google_search{...}\ still maps to search_web

Made with Cursor

Summary by Sourcery

Harden tool-call parsing and custom-model streaming safety.

New Features:

  • Enforce ChatML tool_call delimiters so inline call:... markup in prose is not treated as executable tool calls.

Bug Fixes:

  • Prevent short tool names like search and time from being aliased to built-in tools, reducing false-positive tool execution from narrated prose.

Enhancements:

  • Apply generation safety stop messaging and final assistant text sanitization to the custom-model streaming path, ensuring long outputs are truncated and cleaned consistently.

Tests:

  • Add tests covering bare call: markup being ignored without tool_call delimiters and ensuring search/time are no longer normalized to other tools.

@cursor

cursor Bot commented Jul 19, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@changeset-bot

changeset-bot Bot commented Jul 19, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 7a6ada8

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@sourcery-ai

sourcery-ai Bot commented Jul 19, 2026

Copy link
Copy Markdown

Reviewer's Guide

Tightens tool-call parsing to avoid executing bare call: markup, narrows ambiguous tool name aliases, and ports generation safety (stop/truncation/sanitize) into the custom-model streaming path.

Sequence diagram for custom-model streaming with GenerationSafety and tool parsing

sequenceDiagram
  participant ModelOrchestrator
  participant GenerationSafety
  participant ToolCallParser

  ModelOrchestrator->>GenerationSafety: maxOutputCharsForCustom()
  GenerationSafety-->>ModelOrchestrator: maxOutputChars

  loop onTokenReceived
    ModelOrchestrator->>GenerationSafety: safetyStopMessage(fullResponse, maxOutputChars)
    alt safetyStopMessage returns safetyMsg
      GenerationSafety-->>ModelOrchestrator: safetyMsg
      ModelOrchestrator->>ModelOrchestrator: _sanitizeAssistantText(fullResponse + safetyMsg)
      ModelOrchestrator->>ModelOrchestrator: stopGeneration()
    else safetyStopMessage returns null
      GenerationSafety-->>ModelOrchestrator: null
      ModelOrchestrator->>ModelOrchestrator: _tryParseFunctionCalls(textBuffer.toString())
      ModelOrchestrator-->>ToolCallParser: _tryParseFunctionCalls(textBuffer.toString())
    end
  end
Loading

File-Level Changes

Change Details Files
Add generation safety stop/truncation and final sanitize into the custom-model streaming inference loop.
  • Track a safetyStopped flag and compute maxOutputChars for custom models via GenerationSafety.
  • On each streamed token, compute a safetyStopMessage; if present, append it to the response, yield a sanitized streaming InferenceResult, call stopGeneration(), and break the loop.
  • Ensure streaming results (ThinkingResponse and final result) use _sanitizeAssistantText(fullResponse).
  • Break out of the streaming loop if safetyStopped or the streaming completer is already completed.
lib/services/model_orchestrator.dart
Tighten tool markup parsing to require explicit < tool_call> delimiters and avoid executing bare call: markup in prose.
  • Remove regex that strips call:...{...} patterns from sanitized output, so prose mentioning tool markup is preserved.
  • Update _parseMarkupCalls() regex to require a <
Narrow tool alias mappings to avoid short, ambiguous names that cause false positives when models narrate tool usage.
  • Remove broad aliases mapping short names like search/time/weather/screenshot directly to built-in tools.
  • Add more specific aliases such as duckduckgo_search, take_a_screenshot, and get_current_weather pointing to the appropriate tool names.
  • Add unit test verifying that normalizeCall keeps short names search and time unchanged rather than aliasing them.
lib/utils/tool_call_parser.dart
test/utils/tool_call_parser_test.dart

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Co-authored-by: Cursor <cursoragent@cursor.com>

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've left some high level feedback:

  • The safety stop check runs on every token against the full fullResponse string, which may become expensive for long generations; consider basing the check on the buffer length or an incremental counter to avoid repeated work on growing strings.
  • After appending the safety stop message and breaking, fullResponse is sanitized before yielding but textBuffer and any subsequent processing still use the unsanitized buffer; if downstream consumers rely on textBuffer, consider aligning its contents with the sanitized output or documenting the expected divergence.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The safety stop check runs on every token against the full `fullResponse` string, which may become expensive for long generations; consider basing the check on the buffer length or an incremental counter to avoid repeated work on growing strings.
- After appending the safety stop message and breaking, `fullResponse` is sanitized before yielding but `textBuffer` and any subsequent processing still use the unsanitized buffer; if downstream consumers rely on `textBuffer`, consider aligning its contents with the sanitized output or documenting the expected divergence.

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces safety-stop checks during custom model generation in ModelOrchestrator and refactors ToolCallParser to prevent false positives by requiring explicit delimiters and removing short aliases. Feedback highlights three key issues: appending the safety message before sanitizing the text can cause the message to be stripped, the custom model stream generation loop lacks error handling for raw exceptions, and the tool call regex parser fails to handle nested JSON arguments due to lazy matching.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +1534 to +1551
final safetyMsg = GenerationSafety.safetyStopMessage(
fullResponse,
maxOutputChars,
);
if (safetyMsg != null) {
safetyStopped = true;
fullResponse = '$fullResponse$safetyMsg';
yield InferenceResult(
text: _sanitizeAssistantText(fullResponse),
model: selector.primaryHeavy,
isStreaming: true,
thinking: currentThinking,
toolCalls: allToolCalls.isNotEmpty ? allToolCalls : null,
inferenceTimeMs: inferenceStopwatch.elapsedMilliseconds,
);
unawaited(stopGeneration());
break;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

If the model is safety-stopped (due to length limit or repetition) while in the middle of generating a tool call, fullResponse will end with an incomplete tool call markup (e.g., <|tool_call>call:open_app{). Appending safetyMsg directly to fullResponse and then calling _sanitizeAssistantText(fullResponse) will cause the safety message to be matched by the lazy stripMarkup regex (which strips everything from <|tool_call> to the end of the string $). As a result, the safety message is completely stripped and never shown to the user.

Sanitizing the text before appending the safety message prevents it from being stripped. Note that the same issue exists in the standard processMessage path.

Suggested change
final safetyMsg = GenerationSafety.safetyStopMessage(
fullResponse,
maxOutputChars,
);
if (safetyMsg != null) {
safetyStopped = true;
fullResponse = '$fullResponse$safetyMsg';
yield InferenceResult(
text: _sanitizeAssistantText(fullResponse),
model: selector.primaryHeavy,
isStreaming: true,
thinking: currentThinking,
toolCalls: allToolCalls.isNotEmpty ? allToolCalls : null,
inferenceTimeMs: inferenceStopwatch.elapsedMilliseconds,
);
unawaited(stopGeneration());
break;
}
final safetyMsg = GenerationSafety.safetyStopMessage(
fullResponse,
maxOutputChars,
);
if (safetyMsg != null) {
safetyStopped = true;
final sanitized = _sanitizeAssistantText(fullResponse);
fullResponse = '$sanitized$safetyMsg';
yield InferenceResult(
text: fullResponse,
model: selector.primaryHeavy,
isStreaming: true,
thinking: currentThinking,
toolCalls: allToolCalls.isNotEmpty ? allToolCalls : null,
inferenceTimeMs: inferenceStopwatch.elapsedMilliseconds,
);
unawaited(stopGeneration());
break;
}

@@ -1623,6 +1648,7 @@ class ModelOrchestrator {
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Unlike the standard processMessage stream generation path, _processWithCustomModel does not have any catch blocks around its stream generation loop. If chat.generateChatResponseAsync() or any tool execution throws a PlatformException or Exception, the stream will terminate abruptly and propagate the raw exception to the UI instead of yielding a user-friendly error message (e.g., ⚠️ Inference failed...). Consider wrapping the loop in a try-catch block to handle exceptions gracefully, similar to how it is done in processMessage.

Comment on lines 150 to 154
final pattern = RegExp(
r'(?:<\|?tool_call\|?>)?\s*call:\s*([a-zA-Z0-9_]+)\s*(\{[\s\S]*?\})\s*'
r'<\|?tool_call\|?>\s*call:\s*([a-zA-Z0-9_]+)\s*(\{[\s\S]*?\})\s*'
r'(?:</?\|?tool_call\|?>|<tool_call\|>)?',
caseSensitive: false,
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The lazy match (\\{[\\s\\S]*?\\}) in the regular expression will stop at the first closing brace }. If any tool call arguments contain nested JSON objects (e.g., {"outer": {"inner": "value"}}), this pattern will truncate the arguments and fail to parse them correctly. While current tools may only use flat arguments, consider using a more robust parser or noting this limitation if nested arguments are expected in the future.

@kilo-code-bot

kilo-code-bot Bot commented Jul 19, 2026

Copy link
Copy Markdown

Code Review Summary

Status: No Issues Found | Recommendation: Merge

Files Reviewed (3 files)
  • lib/services/model_orchestrator.dart
  • lib/utils/tool_call_parser.dart
  • test/utils/tool_call_parser_test.dart

Reviewed by step-3.7-flash · Input: 215.4K · Output: 60.4K · Cached: 3.5M

@involvex
involvex merged commit 7ea3d81 into main Jul 19, 2026
6 checks passed
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.

1 participant