Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions client/core/stream.go
Original file line number Diff line number Diff line change
Expand Up @@ -769,6 +769,14 @@ func parseBraceMatchToolCall(text string) (cleanText string, toolCalls []ToolCal
if end <= start {
return text, nil
}
// The JSON object must run to the end of the response. Trailing prose
// after a JSON-shaped snippet is a strong signal the snippet is an
// example or explanation (e.g. inside a code fence, or mid-sentence),
// not an actual tool call — executing such a false positive would be
// surprising and potentially destructive.
if strings.TrimSpace(text[end+1:]) != "" {
return text, nil
}
candidate := text[start : end+1]
if tc, ok := parseHermesCall(candidate, 0); ok {
return strings.TrimSpace(text[:start]), []ToolCall{tc}
Expand Down
40 changes: 40 additions & 0 deletions client/hermes_toolcall_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -181,3 +181,43 @@ func TestParseBraceMatch_NoJSONInText(t *testing.T) {
t.Errorf("clean = %q, want original text", clean)
}
}

func TestParseBraceMatch_JSONExampleInProseNotExecuted(t *testing.T) {
t.Parallel()
// A tool-call-shaped JSON snippet embedded mid-sentence as an example
// must NOT be executed as a tool call (false-positive guard).
text := `The user asked me to look up data. {"name":"search","arguments":{"q":"x"}} is the format I would use.`
clean, calls := ParseInlineToolCalls(text)
if len(calls) != 0 {
t.Errorf("expected 0 calls for JSON example in prose, got %d: %+v", len(calls), calls)
}
if clean != text {
t.Errorf("clean = %q, want original text verbatim", clean)
}
}

func TestParseBraceMatch_FencedJSONExampleNotExecuted(t *testing.T) {
t.Parallel()
// A JSON example inside a code fence must not be executed.
text := "Call tools like this:\n```json\n{\"name\":\"Bash\",\"arguments\":{\"command\":\"ls\"}}\n```"
clean, calls := ParseInlineToolCalls(text)
if len(calls) != 0 {
t.Errorf("expected 0 calls for fenced JSON example, got %d: %+v", len(calls), calls)
}
if clean != text {
t.Errorf("clean = %q, want original text verbatim", clean)
}
}

func TestParseBraceMatch_TrailingWhitespaceStillParsed(t *testing.T) {
t.Parallel()
// A bare JSON call with trailing whitespace/newlines is a real call.
text := "{\"name\":\"search\",\"arguments\":{\"q\":\"golang\"}}\n\n"
clean, calls := ParseInlineToolCalls(text)
if len(calls) != 1 || calls[0].Name != "search" {
t.Fatalf("expected 1 call named search, got %+v", calls)
}
if clean != "" {
t.Errorf("clean = %q, want empty", clean)
}
}
Loading