#2116

[BUG] grep_files crashes on quoted numeric arguments: FunctionTool coercion skips primitive annotations, and the wrapped error names no parameter so the model cannot self-correct

Openllanozeroopened 14d ago

What version of eigent are you using?

Desktop 1.0.6; backend dependency pinned as camel-ai[eigent]==0.2.91a7 (backend/pyproject.toml:9, installed version confirmed via camel/init.py:17)

Problem description

Provenance & confidence (please read first)

Produced with an AI-assisted investigation. This one is empirically reproduced: the A/B calls in the "Steps to reproduce" section were executed on our machine against the bundled venv (~/.eigent/venvs/backend-1.0.6/Scripts/python.exe, camel 0.2.91a7) at 2026-10-27 17:21, and the quoted stdout is verbatim. The single-variable difference (head_limit=2 vs "2") is therefore observed, not inferred.

What still needs your verification: the diagnosis — that function_tool.py:97-98 is the right place to fix it rather than each toolkit — rests on code reading, and we did not test the proposed change. The observed behaviour and the call graph are the parts we stand behind.

FileToolkit.grep_files publishes head_limit: int and context_lines: int in the schema it gives the model, but performs bare comparisons/arithmetic on them at runtime:

camel/toolkits/file_toolkit.py:1341-1342   context_lines: int = 0, head_limit: int = 20
camel/toolkits/file_toolkit.py:1382        effective_limit = head_limit if head_limit > 0 else float("inf")
camel/toolkits/file_toolkit.py:1460, 1470  if head_limit > 0:
camel/toolkits/file_toolkit.py:166-167, 184-185   match_index - context_lines / start_line - context_lines

If a model sends these as strings ("3", "10") — which smaller models do routinely, and which any channel that flattens arguments into text will produce — the call dies with:

Grep search failed: '>' not supported between instances of 'str' and 'int'

Crucially, camel already has an argument-coercion layer and it explicitly declines to touch primitive annotations:

camel/toolkits/function_tool.py:86       "Non-model annotations are returned unchanged."
camel/toolkits/function_tool.py:97-98    if origin is None or not args: return value     # int/float/bool land here
called from                              function_tool.py:633 (sync) and :721 (async)

So the type the schema promises is not the type the runtime accepts, and the gap is one early-return wide.

Two things make this worse than a single recoverable tool error:

  1. The message names no parameter. Grep search failed: '>' not supported … gives the model zero information about which key to fix, so it re-sends the same shape and fails again. We observed the identical failure twice in a row on unrelated paths before isolating it.
  2. The failure is self-reinforcing: once one string-typed call appears in the transcript, a small model copies the observed shape rather than the documented rule. Prompting against it does not reliably help; normalising at ingest does.

Steps to reproduce (no model, no UI, deterministic — verified on our machine)

Run with the venv that ships with the app (camel 0.2.91a7). Single-variable A/B: the only difference between the two calls is head_limit=2 vs head_limit="2".

from camel.toolkits import FileToolkit ft = FileToolkit() P = r"<any source directory containing .py files>"

A — real int: returns matches

ft.grep_files(pattern="^import ", path=P, glob_pattern="*.py", output_mode="content", ignore_case=False, context_lines=0, head_limit=2, multiline=False)

B — same call, head_limit as a string (what a small model emits)

ft.grep_files(pattern="^import ", path=P, glob_pattern="*.py", output_mode="content", ignore_case=False, context_lines=0, head_limit="2", multiline=False)

Actual output of our run (2026-10-27 17:21, ~/.eigent/venvs/backend-1.0.6/Scripts/python.exe):

grep_files 参数名: ['path', 'glob_pattern', 'file_type', 'output_mode', 'ignore_case', 'context_lines', 'head_limit', 'multiline'] camel 版本: 0.2.91a7

A (int) → …_diag_campaign.py:18:> import argparse …_diag_campaign.py:19:> import json B (str) → Error: Grep search failed: '>' not supported between instances of 'str' and 'int' C (str) → Error: Grep search failed: unsupported operand type(s) for -: 'int' and 'str'

Probe C is the same call with context_lines="10" instead of head_limit="2", and it produces a different interpreter message for the same class of defect — both wrap into Grep search failed: {exc} at file_toolkit.py:1476, and neither names the offending parameter.

This isolates the defect to argument handling: path, pattern and glob_pattern are identical in both calls. The two probes crash at different points, which matters for the fix:

  • head_limit crashes at file_toolkit.py:1382 (effective_limit = head_limit if head_limit > 0 …), which is reached before the traversal begins at :1395 — no candidate file is even enumerated.
  • context_lines crashes later, inside the render helpers (:166-167, :184-185), i.e. after files have been opened and matched. So a guard placed only at the top of the function fixes one probe, not both; coercion at the argument boundary fixes both.

Note that the same function already validates one argument by name — file_toolkit.py:1370-1374 returns output_mode must be one of [...] for a bad enum. So the pattern for a parameter-specific message already exists in this code path; numeric types are simply the ones left unhandled.

Expected: the declared schema type is honoured at the boundary (or the call fails with a parameter-specific message). Actual: an internal Python comparison error is surfaced as tool output.

log

Two real calls captured from the agent transcript (identical shape, both failed):

context_lines='0',  glob_pattern='init.py', head_limit='100', ignore_case='false',
multiline='false',  output_mode='content',  pattern='^'
→ Error: Grep search failed: '>' not supported between instances of 'str' and 'int'

context_lines='10', glob_pattern='*.py',    head_limit='3',    ignore_case='false',
multiline='false',  output_mode='content',  pattern='class WorkspaceBundleInstaller'
→ same error

Control call, same tool, same path, real ints:

pattern="class WorkspaceBundle\w*" glob="*.py" head_limit=5 context_lines=3
→ returns matches (authoring.py:39 class WorkspaceBundleAuthoringService)

Additional context

Suggested fixes (pick 1 or 2; 3 is worth doing regardless)

  1. Honour primitive annotations in the existing coercion hook. In function_tool.py:_coerce_arg, before the origin is None early return, coerce int / float / bool when the incoming value is a string that is unambiguously a spelling of that type ("100", "3.5", "true"/"false"), and leave anything ambiguous untouched. This is a handful of lines plus tests, and it fixes every toolkit in one place rather than each tool individually. Two cautions: avoid isinstance(True, int) confusion, and do not silently turn arbitrary strings into numbers.
  2. Or validate against the tool's published OpenAI schema before invocation and return a retryable parameter-specific error, e.g. head_limit must be an integer, got '100'. Error-message quality is itself part of the prompting surface: a message that names the offending key lets the model fix it on the next turn, which is the cheapest possible mitigation.
  3. Don't let internal Python exceptions reach the model as tool output. _tool_error(f"Grep search failed: {exc}") (file_toolkit.py:1476) leaks an interpreter message with no parameter identity.

Related, same root shape

Issue B (write_to_file: required title shadowed by injected message_title) is the same class of problem — the published schema and the accepted arguments disagree, and the resulting Python-level error is opaque. A single schema-driven validation step at the dispatch boundary would address both.

Disclosure / offer

Our installed copy carries a local pre-dispatch patch (app/tool_validation.py) that does option (1) narrowly: it reads the tool's own published properties, coerces quoted strings that are faithful spellings of a declared integer/number/boolean, and leaves anything it does not fully understand untouched on the reasoning that "a wrong refusal would add a failure mode to a call that already worked". It is a workaround in the application layer; we would much rather see the coercion live in _coerce_arg, where it protects all toolkits. To set expectations: we will not be opening a pull request for this — the local patch exists to keep our own setup running, and the fix belongs in your codebase rather than in a fork of ours. Consider this report the evidence, not a pending contribution.

For completeness, and to avoid a wrong impression: the trigger here was Qwen3.8-Flash reached through an OpenAI-compatible API endpoint via a local gateway — an API-served model, not one running on this machine (a local variant of the same family exists; it was not used here). A model of this tier quotes scalars more often than a frontier model, which we read as an argument for the fix rather than against it: quoted scalars are simply part of the input distribution that smaller and cheaper API models produce, and a desktop app cannot control which model a user connects.

Equally important for triage: the provider and the gateway are not part of the defect. The deterministic repro above drives camel directly — no model, no network, no gateway — and the gateway forwards tool-call arguments verbatim (our journalled request_json shows the string-typed value exactly as the model emitted it). The model only decides whether a malformed call is ever sent; the crash lives entirely inside argument handling.

Activity

0

No activity yet.