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_linesIf 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:
- 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. - 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_limitcrashes atfile_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_linescrashes 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 errorControl 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)
- Honour primitive annotations in the existing coercion hook. In
function_tool.py:_coerce_arg, before theorigin is Noneearly return, coerceint/float/boolwhen 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: avoidisinstance(True, int)confusion, and do not silently turn arbitrary strings into numbers. - 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. - 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
0No activity yet.