Skip to content

fix(cuga-lite): don't stop mid-task after stating a plan; warn on suspicious tool arguments; correct made-up tool names#416

Merged
sami-marreed merged 12 commits into
mainfrom
feat/cuga-lite-hardening
Jul 12, 2026
Merged

fix(cuga-lite): don't stop mid-task after stating a plan; warn on suspicious tool arguments; correct made-up tool names#416
sami-marreed merged 12 commits into
mainfrom
feat/cuga-lite-hardening

Conversation

@haroldship

@haroldship haroldship commented Jun 30, 2026

Copy link
Copy Markdown
Collaborator

What this PR fixes

Three robustness fixes for CugaLite, described in plain words with real examples. Independent of issue #272.

1. The agent could stop mid-task right after stating a plan

Problem. Sometimes the model replies with a short plan instead of code — and the run treats that plan as the final answer.

Example (observed). The agent replies:

"We need to search student_loan app."

…and stops. That sentence is returned to the user as the answer; the search it announced never happens.

Fix. Planning statements like this are now recognized (a deterministic check for the observed patterns, plus example-guided hardening of the existing auto-continue LLM classifier), and the agent keeps working instead of stopping.

The check is careful about the opposite mistake: a plan that also asks the user for something —

"Ok I will fetch the information, but first I require your ID"

— is not auto-continued, because the user has to reply. (Second-person guard, added after review.)

The broader redesign of this decision (autonomous vs interactive, deferral handling) is tracked on #445.

2. Made-up tool names caused silent retry loops

Problem. Models sometimes invent plausible-looking tool names instead of using the exact names find_tools returned. The resulting NameError carried no guidance, so the agent kept inventing variants until the step limit.

Example (observed, gpt-oss). The agent calls mondial_geo_get_countries_countries_get(...) — a name it constructed. Before: bare NameError, repeated re-fabrication, task dies at the step limit. Now the error says:

[tool-name correction] 'mondial_geo_get_countries_countries_get' is NOT an available tool — … Did you mean one of: mondial_geo_get_top_country_by_gdp_and_agriculture, …? Similarly named tools can do very different things (e.g. delete vs get) …

and the agent recovers in one step.

Precision (after review). The check is AST-based: a name inside a string like var = " agent_1(" can't trigger it, and a helper function the agent wrote itself keeps its plain NameError (the right fix there is repairing the helper, not hunting for a tool). When no similar tool exists, the message covers both possibilities instead of assuming a tool was meant.

Tool-name hallucination itself is tracked in #312.

3. Suspicious tool arguments were invisible

Problem. The sandbox calls tools directly, bypassing schema validation — so a malformed argument reaches the registry with no earlier signal.

Example. The agent passes amount="5" (a string) where the tool wants a number. Now the log shows:

[arg-warning] venmo_send_money: 'amount' looks malformed — stringized number; value='5'. Forwarded unchanged (no coercion; see arg_warning.py).

Fix. Detection-only: the warning logs the suspect shape and always forwards the argument unchanged. Mining of past eval runs showed these shapes don't currently cause agent-visible failures, so auto-fixing was deliberately rejected. Examples and evidence: #452.

Removed during review: require-tool-call-before-final guard

An earlier commit added an optional guard that refused a final answer until at least one code block had executed (aimed at "confident answer with zero tool calls"). Review found it enforces the wrong thing: what can you do?, skills-without-tools, and follow-up questions would loop to the step limit, and any trivial code block (x = 1) satisfied it. It has been removed from this PR; the underlying problem — the agent answering from general LLM knowledge instead of tools — is now tracked comprehensively in #451, including a configurable answer-source policy.

Testing

  • test_nl_auto_continue_classifier.py, test_shared_call_model.py, test_code_executor.py, test_arg_warning.py — all pass.
  • Full cuga_lite + cuga_agent_core suites: 414 passed; the only failures are 5 pre-existing ones unrelated to this PR (3 e2b-package-not-installed, 2 workspace_fs), verified identical with this PR's changes stashed.
  • ruff check and ruff format clean.
  • AppWorld test-easy rerun: pending (after this review round).

Summary by CodeRabbit

  • New Features

    • Added smarter guidance when tool or code execution hits a missing-name error, including hints for likely matches or how to re-check available tools.
    • Added diagnostic warnings for suspicious tool arguments, helping surface likely mismatched inputs without changing how calls run.
    • Improved natural-language auto-continue detection for short planning-style messages, reducing unnecessary model calls.
  • Bug Fixes

    • Improved error messages for tool-call and execution issues.
    • Added safeguards to avoid false correction hints in non-tool, non-call cases.

CugaLite occasionally emits a short first-person plan with no code on a
turn where it clearly intends to keep working (e.g. "We need to search
student_loan app."). The LLM-based auto-continue classifier sometimes
misfires on these and finalizes the plan as the answer, producing a turn
with zero tool calls (the "planning-text stall").

Add a deterministic fast-path, looks_like_planning_text(), that detects
short first-person intent statements and forces auto_continue=True before
the model call. It is conservative: length-capped, skips questions, and a
negation guard lets genuine results ("I could not find ...") fall through.
The pre-routing step limit bounds any over-fire against infinite loops.

Adds unit tests covering the real observed stall strings, false-positive
guards, and the LLM short-circuit behaviour.
Default-off guardrail for the NL auto-continue loop, gated by the
advanced_features.require_tool_call_before_final setting (behavior
unchanged when unset):

- When the model would emit a final answer but no tool/code block has
  executed this task, refuse it: inject a directive ("call a tool before
  answering; do not answer from prior knowledge") and continue instead.
  Catches the "0 tool calls -> confident hallucination" pattern.
  Bounded by the existing step limit.

The code-execution counter is carried in the adapter metadata dict (no
graph state schema change; works for lite/supervisor/core). The write is
skipped when the guard is off, so the disabled path is byte-identical to
before.
… guard

Flag suspicious tool arguments without mutating them (a 200-task mining
pass showed the target coercion case never occurs in agent execution, so
no auto-fix is warranted), and surface a tool-name correction listing the
closest real tools when the agent calls a fabricated tool name instead of
a bare NameError.

- arg_warning: cuga_lite_warn_suspect_args (default on) detects-only
- unknown-tool correction in local_executor via difflib close-matches;
  falls back to a find_tools re-query hint when no close match exists
@coderabbitai

coderabbitai Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR adds a detection-only warning layer for suspicious CugaLite tool-call kwargs behind a new settings flag, extends BaseExecutor/LocalExecutor.format_error with tool-name correction hints for fabricated NameErrors during code execution, adds a deterministic planning-text fast-path to the NL auto-continue classifier, and simplifies a metadata-update expression in shared_nodes.py.

Changes

CugaLite argument warning and tool-name correction diagnostics

Layer / File(s) Summary
Suspect-arg detection module and settings
src/cuga/backend/cuga_graph/nodes/cuga_lite/adapter/arg_warning.py, src/cuga/config.py, src/cuga/settings.toml
New arg_warning.py maps Pydantic field types to scalar JSON-schema types, detects malformed kwargs (dict/list-as-scalar, stringified numbers), logs warnings, and provides make_arg_warning_callable to wrap tools without mutating args; adds cuga_lite_warn_suspect_args validator/setting (default True).
Tool wrapping and tests
src/cuga/backend/cuga_graph/nodes/cuga_lite/adapter/prepare_node.py, src/cuga/backend/cuga_graph/nodes/cuga_lite/tests/test_arg_warning.py
prepare_node.py wraps execution tool callables with make_arg_warning_callable gated by the new setting; new test module validates type mapping, suspect detection, warning emission, and pass-through wrapper behavior.
format_error signature and correction logic
src/cuga/backend/cuga_graph/nodes/cuga_lite/executors/base_executor.py, .../executors/local/local_executor.py
format_error gains available_tools/code params; LocalExecutor adds _unknown_tool_correction and _missing_name_usage to append difflib-based "Did you mean" hints or find_tools guidance for fabricated tool-name NameErrors, using AST/regex to avoid false positives.
CodeExecutor wiring and regression tests
.../executors/code_executor.py, .../executors/tests/test_code_executor.py
Exception handlers compute available_tools from callable locals and pass them plus executed code into format_error; new tests cover close-match suggestions, no-match fallback, suppressed hints for defined helpers/string literals, and preserved bare NameError for plain undefined variables.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Planning-text fast-path for NL auto-continue classifier

Layer / File(s) Summary
Deterministic detector and early-exit
src/cuga/backend/cuga_graph/nodes/cuga_lite/nl_auto_continue_classifier.py, .../tests/test_nl_auto_continue_classifier.py
Adds looks_like_planning_text using regexes to detect short first-person planning statements (filtering negations, questions, second-person text); classify_nl_auto_continue returns True immediately on a match, bypassing the LLM call; tests cover positive/negative detection and flag-gated fast-path behavior.

Estimated code review effort: 2 (Simple) | ~12 minutes

Metadata construction cleanup

Layer / File(s) Summary
Inline metadata update
src/cuga/backend/cuga_graph/nodes/cuga_agent_core/graph/shared_nodes.py
call_model's meta_update dict now inlines adapter.build_metadata_update(...) directly instead of via an intermediate variable.

Estimated code review effort: 1 (Trivial) | ~3 minutes

Sequence Diagram(s)

sequenceDiagram
  participant CodeExecutor
  participant LocalExecutor
  participant difflib
  CodeExecutor->>LocalExecutor: format_error(error, available_tools, code)
  LocalExecutor->>difflib: get_close_matches(missing_name, available_tools)
  difflib-->>LocalExecutor: close match or none
  LocalExecutor-->>CodeExecutor: formatted error with correction hint
Loading
sequenceDiagram
  participant classify_nl_auto_continue
  participant looks_like_planning_text
  participant LLM
  classify_nl_auto_continue->>looks_like_planning_text: inspect visible text
  looks_like_planning_text-->>classify_nl_auto_continue: match or no match
  alt planning text matches
    classify_nl_auto_continue-->>classify_nl_auto_continue: return True
  else no match
    classify_nl_auto_continue->>LLM: ainvoke classifier prompt
    LLM-->>classify_nl_auto_continue: JSON result
  end
Loading

Possibly related issues

Possibly related PRs

Suggested labels: readability: good, complexity: medium

Suggested reviewers: sami-marreed, offerakrabi

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 42.42% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the three main cuga-lite robustness changes introduced in the PR.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/cuga-lite-hardening

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot added the complexity: high Large or risky change — needs careful review label Jun 30, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (1)
src/cuga/backend/cuga_graph/nodes/cuga_lite/tests/test_arg_warning.py (1)

109-171: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a synchronous tool case here.

All end-to-end wrapper tests use an async callable, but prepare_tools_and_apps also wraps sync .func/._run tools. A one-test sync fixture would catch the current unconditional-await regression immediately. As per coding guidelines, "Write new unit tests in corresponding tests/ sub-folders ... to verify all new branches and components."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cuga/backend/cuga_graph/nodes/cuga_lite/tests/test_arg_warning.py` around
lines 109 - 171, Add a synchronous end-to-end wrapper test in
test_arg_warning.py to cover the prepare_tools_and_apps sync-tool path, since
make_arg_warning_callable currently only has async fixtures. Create a sync
callable fixture alongside _recorder and verify the wrapped function preserves
args/kwargs without awaiting, using the existing make_arg_warning_callable entry
point and a model like DirectorModel or YearModel to catch the
unconditional-await regression.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/cuga/backend/cuga_graph/nodes/cuga_lite/adapter/arg_warning.py`:
- Around line 120-125: The wrapper in arg_warning.py always awaits tool_func,
but prepare_tools_and_apps also routes sync .func/_run callables through this
path, so the wrapper must support both sync and async tools. Update wrapper to
detect whether tool_func is awaitable (or call it first and then await only when
needed), while keeping the existing warn_suspect_kwargs behavior and preserving
the current wrapper signature and call flow.

In `@src/cuga/backend/cuga_graph/nodes/cuga_lite/nl_auto_continue_classifier.py`:
- Around line 61-63: The `_NEGATION_RE` pattern in
`nl_auto_continue_classifier.py` uses a literal curly apostrophe, which triggers
Ruff’s RUF001 in `cuga_graph/nodes`. Update the regex in `_NEGATION_RE` to avoid
the raw `’` by using an escaped form such as `\u2019` or an ASCII-only
equivalent, then run `ruff format` and `ruff check` for the `cuga_graph/nodes/`
Python changes.

In
`@src/cuga/backend/cuga_graph/nodes/cuga_lite/tests/test_nl_auto_continue_classifier.py`:
- Around line 21-25: Add a regression test in test_nl_auto_continue_classifier
alongside the existing classifier tests that disables cuga_lite_nl_auto_continue
and verifies a planning-style utterance still returns False. Keep the autouse
_enable_auto_continue fixture for the current positive-path tests, but override
the flag off in the new test using the same mod.settings.advanced_features
setting so the fast-path branch is exercised explicitly.

---

Nitpick comments:
In `@src/cuga/backend/cuga_graph/nodes/cuga_lite/tests/test_arg_warning.py`:
- Around line 109-171: Add a synchronous end-to-end wrapper test in
test_arg_warning.py to cover the prepare_tools_and_apps sync-tool path, since
make_arg_warning_callable currently only has async fixtures. Create a sync
callable fixture alongside _recorder and verify the wrapped function preserves
args/kwargs without awaiting, using the existing make_arg_warning_callable entry
point and a model like DirectorModel or YearModel to catch the
unconditional-await regression.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 6ade3e1d-f74b-49ca-9088-684136851dd7

📥 Commits

Reviewing files that changed from the base of the PR and between 09e6b22 and 0506a7f.

📒 Files selected for processing (13)
  • src/cuga/backend/cuga_graph/nodes/cuga_agent_core/graph/shared_nodes.py
  • src/cuga/backend/cuga_graph/nodes/cuga_agent_core/tests/graph/test_shared_call_model.py
  • src/cuga/backend/cuga_graph/nodes/cuga_lite/adapter/arg_warning.py
  • src/cuga/backend/cuga_graph/nodes/cuga_lite/adapter/prepare_node.py
  • src/cuga/backend/cuga_graph/nodes/cuga_lite/executors/base_executor.py
  • src/cuga/backend/cuga_graph/nodes/cuga_lite/executors/code_executor.py
  • src/cuga/backend/cuga_graph/nodes/cuga_lite/executors/local/local_executor.py
  • src/cuga/backend/cuga_graph/nodes/cuga_lite/executors/tests/test_code_executor.py
  • src/cuga/backend/cuga_graph/nodes/cuga_lite/nl_auto_continue_classifier.py
  • src/cuga/backend/cuga_graph/nodes/cuga_lite/tests/test_arg_warning.py
  • src/cuga/backend/cuga_graph/nodes/cuga_lite/tests/test_nl_auto_continue_classifier.py
  • src/cuga/config.py
  • src/cuga/settings.toml

Comment thread src/cuga/backend/cuga_graph/nodes/cuga_lite/adapter/arg_warning.py Outdated
@haroldship
haroldship requested a review from sami-marreed June 30, 2026 14:00
@sami-marreed

Copy link
Copy Markdown
Contributor

requires testing on appworld again and lets get help of @Sergey-Zeltyn on groq

- arg_warning: the warning wrapper unconditionally awaited tool_func, but
  prepare_tools_and_apps also routes sync .func/._run callables through it.
  Call the tool and await only when the result is awaitable; add a sync-tool
  regression test.
- nl_auto_continue_classifier: replace the literal curly apostrophe in
  _NEGATION_RE with ’ to clear Ruff RUF001 (ambiguous unicode).
- tests: add a regression test asserting the classifier finalizes (returns
  False, no LLM call) on planning text when cuga_lite_nl_auto_continue is off.
@coderabbitai coderabbitai Bot added complexity: medium Moderate scope — multiple files or non-trivial logic and removed complexity: high Large or risky change — needs careful review labels Jun 30, 2026
@haroldship

Copy link
Copy Markdown
Collaborator Author

requires testing on appworld again and lets get help of @Sergey-Zeltyn on groq

test-easy results on litellm gpt-oss-120b:

Evaluation Report

Summary

Pass@1 28/43 (65.1%)
Total Tokens 9,222,914
Avg Tokens/Task 214,486.4
Total LLM Calls 470
Avg LLM Calls/Task 10.9
Total Duration 7119.7s
Avg Duration/Task 165.6s

Per-Task Results

Task R Tokens Cost LLM Cache Duration Steps
──────────────────────────────────────────────────────────────────────
e775c78_1 ✓ 134,240 $0.0000 8 0 95.9s 13
07bb666_1 ✗ 219,363 $0.0000 12 0 149.4s 25
9aae7da_1 ✓ 44,881 $0.0000 3 0 26.1s 13
245cb43_1 ✓ 152,499 $0.0000 9 0 135.1s 20
4d12842_1 ✓ 223,018 $0.0000 12 0 204.6s 18
81be677_1 ✓ 46,496 $0.0000 3 0 54.9s 13
365e0a3_1 ✓ 109,608 $0.0000 8 0 157.8s 13
ba46d91_1 ✗ 78,580 $0.0000 7 0 95.5s 13
eb5ad85_1 ✓ 115,750 $0.0000 7 0 80.8s 13
dbc0276_1 ✗ 383,922 $0.0000 18 0 284.3s 20
5e27cd7_1 ✓ 324,745 $0.0000 19 0 264.5s 33
6588a51_1 ✗ 199,099 $0.0000 10 0 143.4s 15
5238afc_1 ✗ 255,649 $0.0000 13 0 266.5s 18
9bf2c8a_1 ✓ 265,688 $0.0000 13 0 213.5s 23
7d26579_1 ✓ 208,271 $0.0000 10 0 124.7s 27
0d22252_1 ✓ 92,696 $0.0000 7 0 95.0s 18
3650990_1 ✗ 100,629 $0.0000 8 0 162.4s 15
2e9b91e_1 ✓ 195,148 $0.0000 11 0 244.9s 25
baeb104_1 ✗ 2,311,692 $0.0000 52 0 1268.2s 120
6a5e690_1 ✓ 13,716 $0.0000 2 0 9.7s 15
f691597_1 ✓ 0 $0.0000 0 0 0.0s 13
16be9ce_1 ✗ 46,825 $0.0000 3 0 36.6s 15
277d81d_1 ✓ 0 $0.0000 0 0 0.0s 20
98d2608_1 ✓ 82,319 $0.0000 9 0 98.0s 40
fd1f8fa_1 ✓ 0 $0.0000 0 0 0.0s 23
29a7b7e_1 ✗ 180,673 $0.0000 13 0 186.0s 35
21abae1_1 ✓ 6,967 $0.0000 1 0 13.7s 13
5a83b05_1 ✓ 76,764 $0.0000 9 0 113.9s 25
cef9191_1 ✓ 204,491 $0.0000 9 0 77.2s 13
afc4005_1 ✓ 220,969 $0.0000 15 0 149.4s 30
425a494_1 ✗ 63,769 $0.0000 4 0 39.7s 13
a30375d_1 ✗ 1,386,987 $0.0000 51 0 699.2s 90
09b0ee6_1 ✓ 144,499 $0.0000 8 0 80.3s 13
7847649_1 ✓ 76,522 $0.0000 17 0 134.9s 28
552869a_1 ✓ 55,175 $0.0000 5 0 86.5s 13
024c982_1 ✓ 301,030 $0.0000 20 0 277.6s 35
13547f5_1 ✓ 76,122 $0.0000 7 0 131.3s 13
1150ed6_1 ✗ 53,924 $0.0000 3 0 39.4s 13
31dc501_1 ✗ 115,179 $0.0000 11 0 127.2s 23
59fae45_1 ✓ 151,453 $0.0000 8 0 239.0s 18
166f4ff_1 ✗ 219,645 $0.0000 13 0 160.6s 28
dac78d9_1 ✓ 51,163 $0.0000 5 0 54.8s 13
f3f60f0_1 ✗ 232,748 $0.0000 27 0 297.1s 45

Difficulty Breakdown

Diff Tasks Pass@1 Tokens Tok/Task LLM Calls LLM/Task Duration Dur/Task
─────────────────────────────────────────────────────────────────────────────────────────
1 43 65.1% 9,222,914 214,486.4 470 10.9 7119.7s 165.6s

Test-Set Breakdown (AppWorld)

Test Set Tasks Pass@1 Tokens Tok/Task LLM Calls LLM/Task Duration Dur/Task
──────────────────────────────────────────────────────────────────────────────────────────────
challenge 24 66.7% 5,604,834 233,534.8 244 10.2 4211.6s 175.5s
normal 19 63.2% 3,618,080 190,425.3 226 11.9 2908.0s 153.1s

@sami-marreed sami-marreed left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code review — overall: merge with fixes. Three independent, well-scoped fixes; defaults are backward-compatible and non-intrusive as claimed; all four new test files pass (78/78). One trivial must-fix plus two should-fix items, left inline.

Strengths

  • Genuinely independent fixes on disjoint code paths, each individually gated.
  • Arg-warning is provably non-mutating (forwards kwargs verbatim; tests assert "NOT coerced").
  • Tests exercise real behavior (real code through eval_with_tools_async, real pydantic models, assert_not_called()), not mocks.
  • Conservative classifier: negation/question guards, 400-char cap, anchored match, bounded by step limit.

Minor (not inlined)

  • PR body says 76 tests; actually 78 passing.
  • meta_value aliases the state metadata dict, so meta_value["code_exec_count"] = ... mutates graph state before Command(update=...) is returned. Works under LangGraph but a dict(meta_value) copy is cleaner.
  • require_tool_call_before_final loops to the step limit on legitimate no-tool tasks (e.g. a greeting), returning a step-limit error instead of an answer. Opt-in, but worth noting in the flag's help text.

# directive and continue instead. Bounded by the step limit.
if require_tool_call and code_exec_count == 0:
logger.warning(
"%s: require_tool_call_before_final — final answer with 0 tool calls; injecting directive and continuing",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Important — loguru %s regression. Loguru formats with {}, not printf %s. This logs the literal %s: ... and silently drops adapter.sender_name. This is exactly the defect class that #389 just removed across 22 files (merged 2 commits before this PR's base). Fix with an f-string:

logger.warning(
    f"{adapter.sender_name}: require_tool_call_before_final — final answer with 0 tool calls; injecting directive and continuing"
)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 2827681 — f-string, matching the #389 sweep.

Comment on lines +226 to +228
if require_tool_call:
# Record that at least one tool/code block has run this task.
meta_value["code_exec_count"] = code_exec_count + 1

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Important — counts emitted code blocks, not successful tool calls. code_exec_count increments whenever the model emits any code block, regardless of whether it executed successfully or actually called a tool. A model can emit x = 1 (or a block that NameErrors) and thereby satisfy require_tool_call_before_final, then still hallucinate a final answer — weaker than the name implies. Default-off mitigates severity. Either increment only after the execute node reports a non-error tool invocation, or tighten the comment/flag wording to "at least one code block executed."

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Took the wording option in 2827681: the code comment and the settings.toml help now state it counts emitted/executed code blocks (a x = 1 block or an erroring block still counts) — a lightweight grounding proxy, not a confirmed-tool-call guarantee. Counting only non-error tool invocations would need the execute node to report back into the adapter metadata; given the flag is default-off I kept the cheap proxy and made the docs honest about it.

Comment on lines +361 to +365
tool_func = make_arg_warning_callable(
tool_func,
getattr(tool, "args_schema", None),
enable=_warn_args,
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Important — default-on warning silently moves sync tools onto the event loop. For a sync-only tool (no .coroutine, has .func/._run) with an args_schema, the old path was sync func → make_tool_awaitablerun_in_executor (worker thread). make_arg_warning_callable returns an async def wrapper that calls the sync tool_func inline; make_tool_awaitable then sees a coroutine function and awaits it directly — so the sync tool now runs in the event loop instead of a worker thread, and a blocking sync tool could stall the loop. Scope is narrow (only sync-only tools with a schema; .coroutine tools are unaffected) but it's a silent, default-on behavior change. Fix: dispatch sync callables via run_in_executor inside the wrapper, or only wrap when inspect.iscoroutinefunction(tool_func).

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 2827681make_arg_warning_callable now preserves the sync/async nature of the wrapped tool: async tools get an async wrapper, sync tools a sync wrapper, so make_tool_awaitable still sees a non-coroutine for sync-only tools and dispatches them via run_in_executor as before. Regression test covers the sync path.

- shared_nodes: fix loguru `%s` placeholder — it logged a literal `%s` and
  dropped `adapter.sender_name` (loguru formats with `{}`). Now an f-string.
  Same defect class swept out in #389.
- shared_nodes: copy the metadata dict before bumping `code_exec_count` so the
  increment updates only the returned Command payload, not live graph state.
- arg_warning: preserve the sync/async nature of the wrapped tool. The wrapper
  was always `async def`, so `make_tool_awaitable` awaited sync tools inline on
  the event loop instead of dispatching them via run_in_executor — a blocking
  sync tool could stall the loop. Sync tools now get a sync wrapper.
- Tighten `require_tool_call_before_final` wording (comments + settings help):
  it counts emitted/executed code blocks, not confirmed successful tool calls,
  and legitimately tool-free tasks loop to the step limit.
@haroldship

Copy link
Copy Markdown
Collaborator Author

@sami-marreed @Sergey-Zeltyn Re-ran appworld easy with increased timeout. Below results for gpt-oss-120b and gpt-4.1

GPT-4.1 is a bit below what Sergey had before (84.7%)

Evaluation Comparison Report
============================

1 run(s) per configuration.


Summary
-------

Configuration                Runs     Pass@1     pass@1     pass^1      maj@1   Cons      Tokens    LLM     Time
────────────────────────────────────────────────────────────────────────────────────────────────────────────────
cuga (GPT-OSS-120B)             1      79.1%      79.1%      79.1%      79.1%   1.00  3,090,341.0  171.0   586.6s
cuga (GPT-4.1)                  1      81.4%      81.4%      81.4%      81.4%   1.00  7,190,672.0  399.0  2090.8s


Cost Summary
------------

Configuration                    Tokens    Avg/Task     LLM   Avg/Task      Time   Avg/Task
───────────────────────────────────────────────────────────────────────────────────────────
cuga (GPT-OSS-120B)          3,090,341.0    71,868.4   171.0        4.0    586.6s      13.6s
cuga (GPT-4.1)               7,190,672.0   167,224.9   399.0        9.3   2090.8s      48.6s

@sami-marreed sami-marreed added the priority: high Important, address soon label Jul 2, 2026
@Sergey-Zeltyn

Copy link
Copy Markdown
Collaborator

There are several changes in PR, here I comment on one of them

Deterministic looks_like_planning_text() short-circuit before the LLM classifier in nl_auto_continue_classifier.py:160. For AppWorld this is a genuine behavioral delta on an already-active path — previously every no-code NL turn was classified by the LLM; now text matching the planning regex is forced to auto-continue without the LLM call.

False positives = wasted steps. Any genuine final answer that matches the regex (starts with we/I/let's/let me + optional discourse marker + a present-tense action/modal verb, ≤400 chars, no ?, no negation) now loops instead of finalizing. The design mitigates this well — past-tense finals ("I found…", "I determined…") don't match because the verb list is present-tense/infinitive with \b boundaries, and questions/negations are excluded. But a correct final phrased as "I'll return the list: …" would match and burn a step. With cuga_lite_max_steps=70 it can't loop forever, but repeated over-fire near the cap could convert a would-be pass into a step-limit error.

My verdict when I consider AppWorld ground truth answers: it should not affect appworld negatively

@Sergey-Zeltyn

Copy link
Copy Markdown
Collaborator

Doing some gpt-oss log analysis to find possible concerns

Concern (misdirection from the unknown-tool correction): The new _unknown_tool_correction in local_executor.py format_error fires on every NameError, but its message assumes the missing name is a fabricated tool. When the missing name is actually an ordinary variable, the injected guidance ("not an available tool… call find_tools… do not retry") is wrong and could steer the model away from the real fix (define/compute the variable) — and it fires even on tasks that currently pass.

Example (real, bundle b6d1104, score 1.0 — a passing task):

Agent runs a block referencing formatted_total, never defined → NameError: name 'formatted_total' is not defined.
Current behavior: bare NameError → agent recovers next step by writing the actual computation → task passes.
With the correction: error also carries "[tool-name correction] 'formatted_total' is NOT an available tool… Call tools by the EXACT name returned by find_tools… Do not retry" (closest matches in my sim: format, total, find_tools) — misleading noise on a variable, injected into a currently-passing trajectory.

Rare (1 exposed passing task in 87; the other 84 NameErrors were genuinely fabricated tool names where the correction helps), but it's the one place the change can actively hurt.

@Sergey-Zeltyn

Copy link
Copy Markdown
Collaborator

Second (and last) concern

loose cutoff=0.4 in difflib.get_close_matches(missing, available_tools, n=5, cutoff=0.4) (_unknown_tool_correction, local_executor.py):

The app-prefix structure usually keeps suggestions in the right app and often exact (e.g. amazon_get_returns_returns_get → amazon_show_returns_returns_get). But the 0.4 cutoff is loose enough to surface two hazards:

  • Same-app but semantically wrong — including destructive tools. phone_get_sms_messages_sms_get(a read) → topsuggestion phone_delete_sms_message_sms_delete` (a delete). The correct read tool appears too, but offering a destructive op first is the sharpest risk if the model follows the head of the list.

  • Cross-app junk when the right tool isn't loaded. simple_note_list_notes_getspotify_create_playlist_post (wrong app entirely). Likely partly a proxy artifact of my reconstructed namespace, and on an already-failing task — but it shows 0.4 lets unrelated names through when no good match is present.

Address PR #416 review (Sergey-Zeltyn) on _unknown_tool_correction:

- suppress the correction when the missing name is a plain variable
  reference — it now fires only when the name is called like a function,
  so an undefined variable keeps its bare NameError instead of getting
  misleading "call find_tools / do not retry" guidance
- raise difflib cutoff 0.4 -> 0.6 so weak cross-app matches (e.g.
  simple_note_* -> spotify_*) are no longer offered as suggestions
- add a caution line to suggestions: lookalike names can perform
  different actions (delete vs get), pick by intent not list order
- pass the executed code through format_error so the call-shape check
  has something to inspect; update BaseExecutor signature accordingly
- add regression tests for all three behaviors (82 tests pass)
@haroldship

Copy link
Copy Markdown
Collaborator Author

@Sergey-Zeltyn addressed in cc2d8a8:

Variable NameError misdirection — the correction now fires only when the missing name is called like a function in the executed code (format_error receives the code and checks for name(...)). A plain variable reference like your formatted_total example keeps the bare NameError, so the passing trajectory you found is no longer disturbed.

Loose cutoff=0.4 — raised to 0.6. Verified against your examples: simple_note_list_notes_get → spotify_create_playlist_post scores 0.481 and is now filtered (falls back to the find_tools re-query hint), while legitimate corrections like amazon_get_returns… → amazon_show_returns… (0.885) survive. The destructive-lookalike case (phone_get_sms… → phone_delete_sms…, 0.831) clears any reasonable cutoff, so the suggestion list now carries an explicit caution: "Similarly named tools can do very different things (e.g. delete vs get) — pick only a suggestion whose action matches your intent."

Regression tests added for all three behaviors (variable NameError stays bare, cross-app junk filtered, caution present) — 82 tests pass.

Planning-text short-circuit — no change; per your analysis the present-tense verb list and question/negation guards keep AppWorld finals out of the match, and the step cap bounds any residual over-fire.

@haroldship

Copy link
Copy Markdown
Collaborator Author

@sami-marreed swept your review — inline threads answered individually; the non-inlined minors:

  • test count: PR body updated to 82 (78 at review time + 4 regression tests from the Sergey-review fixes in cc2d8a8).
  • meta_value aliasing: fixed in 2827681 — the metadata dict is copied before the code_exec_count bump, so only the returned Command payload is mutated, not live graph state.
  • require_tool_call_before_final on tool-free tasks: the settings.toml help text now carries the caveat verbatim (loops to the step limit and returns a step-limit error instead of an answer; flag stays default-off).

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
src/cuga/backend/cuga_graph/nodes/cuga_lite/executors/tests/test_code_executor.py (1)

167-185: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Strengthen the "no close match" assertion.

The "find_tools" in hint check (line 184) is trivially true regardless of available_tools content — the fallback branch's static text always says "Call find_tools again with a different query." It doesn't actually validate that no other weak match slipped through besides the one explicitly excluded. Consider asserting "Did you mean" not in hint (or "No close match" in hint) to directly pin the branch taken.

✅ Suggested strengthened assertion
     assert "tool-name correction" in hint
     assert "spotify_create_playlist_post" not in hint
-    assert "find_tools" in hint
+    assert "Did you mean" not in hint
+    assert "find_tools" in hint
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/cuga/backend/cuga_graph/nodes/cuga_lite/executors/tests/test_code_executor.py`
around lines 167 - 185, The test in
test_correction_cutoff_rejects_cross_app_junk is only checking for the static
fallback text from _unknown_tool_correction, so it does not prove the
no-close-match branch was taken. Strengthen the assertion by validating the
fallback path directly in the hint returned from
LocalExecutor._unknown_tool_correction, for example by checking that “Did you
mean” is absent or that “No close match” is present, while keeping the existing
exclusion of spotify_create_playlist_post.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In
`@src/cuga/backend/cuga_graph/nodes/cuga_lite/executors/tests/test_code_executor.py`:
- Around line 167-185: The test in test_correction_cutoff_rejects_cross_app_junk
is only checking for the static fallback text from _unknown_tool_correction, so
it does not prove the no-close-match branch was taken. Strengthen the assertion
by validating the fallback path directly in the hint returned from
LocalExecutor._unknown_tool_correction, for example by checking that “Did you
mean” is absent or that “No close match” is present, while keeping the existing
exclusion of spotify_create_playlist_post.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: d7e42e29-9a7f-45d9-8260-3dfa55083a58

📥 Commits

Reviewing files that changed from the base of the PR and between 2827681 and cc2d8a8.

📒 Files selected for processing (4)
  • src/cuga/backend/cuga_graph/nodes/cuga_lite/executors/base_executor.py
  • src/cuga/backend/cuga_graph/nodes/cuga_lite/executors/code_executor.py
  • src/cuga/backend/cuga_graph/nodes/cuga_lite/executors/local/local_executor.py
  • src/cuga/backend/cuga_graph/nodes/cuga_lite/executors/tests/test_code_executor.py
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/cuga/backend/cuga_graph/nodes/cuga_lite/executors/base_executor.py
  • src/cuga/backend/cuga_graph/nodes/cuga_lite/executors/code_executor.py
  • src/cuga/backend/cuga_graph/nodes/cuga_lite/executors/local/local_executor.py

@sami-marreed

Copy link
Copy Markdown
Contributor

@coderabbitai can you label this pr readiness ?

@coderabbitai

coderabbitai Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
repo="cuga-project/cuga-agent"

echo "== Existing labels matching 'readiness' =="
gh label list --repo "$repo" --search "readiness" || true

echo "== All labels (for reference) =="
gh label list --repo "$repo" --limit 100

Length of output: 2669


🏁 Script executed:

#!/bin/bash
set -euo pipefail
repo="cuga-project/cuga-agent"
pr=416

gh pr edit "$pr" --repo "$repo" --add-label "ready: maintainer-review"
gh pr view "$pr" --repo "$repo" --json labels --jq '.labels[].name'

Length of output: 633


@sami-marreed, I found there isn't an exact readiness label in this repo, but the closest match is ready: maintainer-review ("Ready for maintainer review (CodeRabbit gate passed)"). However, applying labels via the GitHub CLI is blocked in my sandbox due to workflow restriction settings, so I couldn't add it directly.

Could you apply ready: maintainer-review manually, or let me know if you'd like a different label from the existing set (e.g., readability: good, complexity: medium is already applied)? Once labeling permissions are enabled for this workflow, I'll be able to do this directly in the future.

# Model wants to finalize. Guard: refuse a final answer produced without
# any tool/code execution (e.g. a confident hallucination); inject a
# directive and continue instead. Bounded by the step limit.
if require_tool_call and code_exec_count == 0:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Edge cases when require tool call is true:

  1. Saying to the agent what can you do? --> no tools --> Error
  2. Will affect working with skills and no tools
  3. Follow-up questions on previous threads

Problem: Cuga returns intermediate/hallucinated answers without doing actual computations based on tools
examples:

  1. Let me perform the second phase. ( intermediate )
  2. Cuga answers based on general LLM knowledge instead of tool computation

Open questions:

  1. Does the flag of autonomous mode relate to this flag require_tool_call?

Decision:

  1. Let's remove it and open issue on agent answering from general knowledge instead of tools, use might want to configure that when to answer in general/llm vs based on tools

Comment on lines +338 to +345
# Pre-flight arg WARNING: the sandbox calls tool.coroutine directly,
# bypassing the StructuredTool's args_schema, so nothing flags malformed
# kwargs before the registry. This detector logs (never mutates) suspect
# shapes — the dict-as-string bug and friends. The former coercion layer
# (Wave-1 Change #4) is WONTFIX: a 200-task M3 mining pass found the
# failure does not occur in the current corpus. See arg_warning.py.
_af = getattr(settings, "advanced_features", None)
_warn_args = bool(getattr(_af, "cuga_lite_warn_suspect_args", True))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Provide issue with example input and output. delete M3 logs etc

Comment on lines +132 to +137
def _unknown_tool_correction(
error: Exception,
available_tools: Optional[list[str]],
code: Optional[str] = None,
) -> str:
"""Build a correction hint when the agent calls a non-existent tool.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Add issue for tool name hallucination, we might already have one

if not missing:
return ""

if code is not None and not re.search(rf"\b{re.escape(missing)}\s*\(", code):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This can match many phrases like var = " agent_1"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

sometimes it can also catch a helper function that agent generated with current code

async def helper1(arg1):
pass

res = await helper1(arg1)

Comment on lines 150 to 163
async def classify_nl_auto_continue(
llm: BaseChatModel,
assistant_visible: Any,
reasoning_excerpt: Optional[Any],
) -> bool:
"""Return True if the graph should append a user ``continue`` message and re-invoke the coder model."""
if not getattr(settings.advanced_features, "cuga_lite_nl_auto_continue", True):
return False
visible = normalize_assistant_text(assistant_visible)
reasoning = normalize_assistant_text(reasoning_excerpt)
if looks_like_planning_text(visible):
logger.info("NL auto-continue: planning-text fast-path matched; auto-continuing")
return True
combined = build_combined_content_and_reasoning(visible, reasoning)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Make sure you have issue created on looks like planning text and what's the best approach to solve it

r"explore|examine|check|investigate|figure\s+out|determine|"
r"retrieve|gather|list|enumerate)\b",
re.IGNORECASE,
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@coderabbitai how this will react to Ok i will fetch the infromation, but first i require your ID

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@sami-marreed As written, that example would currently match the fast-path and return True for auto-continue.

Why:

  • it starts with a permitted discourse marker + first person: Ok i
  • then hits a planning/modal phrase: will
  • then an action verb in the allowlist: fetch
  • it does not end with ?
  • it does not contain a negation like not, cannot, couldn't, etc.

So for:

Ok i will fetch the infromation, but first i require your ID

looks_like_planning_text(...) would likely treat it as “planning text”, and classify_nl_auto_continue(...) would short-circuit before the LLM and auto-continue.

The important nuance is that the tail clause (but first i require your ID) is ignored by the current heuristic once the earlier i will fetch shape matches. If the intent is “ask the user for missing info”, a version ending as a question, e.g. ... could you provide your ID?, would not match this fast-path because of the ? check.

So yes — this is a real false-positive shape for the current regex.


🧠 Learnings used
Learnt from: sami-marreed
Repo: cuga-project/cuga-agent PR: 326
File: src/system_tests/load/tests/test_isolation.py:61-61
Timestamp: 2026-06-17T20:57:37.760Z
Learning: In this repository, Ruff rule RUF059 (unused unpacked variable) is intentionally not enabled. When reviewing Python code, do not flag issues of the form “unused unpacked variables” matching RUF059 (e.g., unused variables from tuple unpacking such as `error`). This aligns with the project’s Ruff configuration where `ruff check` succeeds without reporting these cases.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Will this enter endless loop because of this?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n "classify_nl_auto_continue" -C5 --type=py

Length of output: 4372


🏁 Script executed:

#!/bin/bash
rg -n "classify_nl_auto_continue\|nl_auto_continue\|step_limit\|max_steps\|require_tool_call_before_final" -C5 --type py 2>/dev/null
rg -rn "classify_nl_auto_continue" -C8 .
echo "---"
rg -n "nl_auto_continue" -C6 src/cuga/backend/cuga_graph/nodes/cuga_lite/

Length of output: 579


🏁 Script executed:

#!/bin/bash
set -x
fd nl_auto_continue_classifier
echo "---calls---"
grep -rn "classify_nl_auto_continue" --include=*.py .
echo "---step limit---"
grep -rn "step_limit\|max_steps\|MAX_STEPS\|step_count" --include=*.py src/cuga/backend/cuga_graph/nodes/cuga_lite/ | head -50

Length of output: 6696


🏁 Script executed:

#!/bin/bash
sed -n '140,260' src/cuga/backend/cuga_graph/nodes/cuga_lite/adapter/sandbox_node.py
echo "===graph_adapter==="
sed -n '150,200' src/cuga/backend/cuga_graph/nodes/cuga_lite/adapter/graph_adapter.py
echo "===cuga_lite_graph append_with_step_limit==="
sed -n '130,200' src/cuga/backend/cuga_graph/nodes/cuga_lite/cuga_lite_graph.py

Length of output: 855


🏁 Script executed:

#!/bin/bash
awk 'NR>=140 && NR<=260' src/cuga/backend/cuga_graph/nodes/cuga_lite/adapter/sandbox_node.py
echo "===graph_adapter==="
awk 'NR>=150 && NR<=200' src/cuga/backend/cuga_graph/nodes/cuga_lite/adapter/graph_adapter.py
echo "===cuga_lite_graph==="
awk 'NR>=100 && NR<=200' src/cuga/backend/cuga_graph/nodes/cuga_lite/cuga_lite_graph.py

Length of output: 11542


🏁 Script executed:

#!/bin/bash
grep -n "cuga_lite_max_steps" -r src/cuga/config.py src/cuga/settings.toml

Length of output: 356


@sami-marreed No — it won't hang forever, but it also isn't fully safe from wasting steps.

The auto-continue loop is bounded by the graph's step counter, not by this classifier. Here's the chain:

  • classify_nl_auto_continue is invoked from AgentGraphAdapter.classify_auto_continue in src/cuga/backend/cuga_graph/nodes/cuga_lite/adapter/graph_adapter.py.
  • Every continue cycle (call_model + sandbox) increments state.step_count through append_chat_messages_with_step_limit in src/cuga/backend/cuga_graph/nodes/cuga_lite/cuga_lite_graph.py.
  • Once step_count exceeds settings.advanced_features.cuga_lite_max_steps (default 70, set in src/cuga/settings.toml Line 51), core_create_error_command returns a Command to END with an error message — the graph terminates.

So in the worst case where the false-positive fast-path keeps matching turn after turn (e.g., the model keeps repeating a variant of "Ok I will fetch the information, but first I require your ID"), the run doesn't hang indefinitely — it stops at the step limit (default 70 iterations) and surfaces an error. That's a real cost (burning most of the step budget with a stalled conversation instead of finalizing quickly on turn 1), but not an unbounded/endless loop.

Given the false-positive you identified earlier, it may still be worth tightening _PLANNING_INTENT_RE/looks_like_planning_text (e.g., bail out when a clause like "but first I require/need ..." follows), so genuine "I need info from you" turns finalize immediately rather than consuming step budget before hitting the cap.


🧬 Code Graph Analysis Results

Relevant snippets

src/cuga/backend/cuga_graph/nodes/cuga_lite/nl_auto_continue_classifier.py

Line 39-53 (comment about looping / step limit)

# This path is intentionally conservative: it only flips False -> True for short
# text that opens with a first-person intent ("we"/"I"/"let's"/"let me"),
# optionally behind a discourse marker, followed by a forward-looking action or
# modal verb. A genuine final answer rarely matches, and the surrounding graph
# already enforces a step limit before auto-continuing, so an over-fire cannot
# loop forever.

Line 83-101 (looks_like_planning_text)

def looks_like_planning_text(visible: str) -> bool:
    """True for a short first-person intent statement that signals more work to come.

    Conservative deterministic detector for the planning-text stall. Returns
    False for empty text, anything longer than a couple of sentences, text
    that reads as a question (clarifying questions should finalize, not loop),
    or text that addresses the user in the second person (it may be requesting
    input the user must supply).
    """
    t = (visible or "").strip()
    if not t or len(t) > _PLANNING_MAX_LEN:
        return False
    if t.rstrip().endswith("?"):
        return False
    if _NEGATION_RE.search(t):
        return False
    if _SECOND_PERSON_RE.search(t):
        return False
    return bool(_PLANNING_INTENT_RE.match(t))

Line 144-165 (parse_auto_continue_json)

def parse_auto_continue_json(raw: str) -> Optional[bool]:
    t = (raw or "").strip()
    if not t:
        return None
    if t.startswith("```"):
        t = re.sub(r"^```(?:json)?\s*", "", t, flags=re.IGNORECASE).strip()
        t = re.sub(r"\s*```\s*$", "", t).strip()
    start, end = t.find("{"), t.rfind("}")
    if start == -1 or end == -1 or end <= start:
        return None
    try:
        obj = json.loads(t[start : end + 1])
    except json.JSONDecodeError:
        return None
    if not isinstance(obj, dict):
        return None
    v = obj.get("auto_continue")
    if isinstance(v, bool):
        return v
    if isinstance(v, str) and v.lower() in ("true", "false"):
        return v.lower() == "true"
    return None

Line 168-206 (classify_nl_auto_continue)

async def classify_nl_auto_continue(
    llm: BaseChatModel,
    assistant_visible: Any,
    reasoning_excerpt: Optional[Any],
) -> bool:
    """Return True if the graph should append a user ``continue`` message and re-invoke the coder model."""
    if not getattr(settings.advanced_features, "cuga_lite_nl_auto_continue", True):
        return False
    visible = normalize_assistant_text(assistant_visible)
    reasoning = normalize_assistant_text(reasoning_excerpt)
    if looks_like_planning_text(visible):
        logger.info("NL auto-continue: planning-text fast-path matched; auto-continuing")
        return True
    combined = build_combined_content_and_reasoning(visible, reasoning)
    if not combined.strip():
        return False
    user_block = (
        "Classify this assistant output (content + reasoning below).\n\n"
        f"{combined}\n\n"
        'Respond with JSON only: {"auto_continue": true} or {"auto_continue": false}'
    )
    try:
        from cuga.backend.cuga_graph.utils.langfuse_tracing import get_langfuse_invoke_config

        resp = await llm.ainvoke(
            [
                {"role": "system", "content": CLASSIFIER_SYSTEM_PROMPT},
                {"role": "user", "content": user_block},
            ],
            config=get_langfuse_invoke_config(),
        )
        parsed = parse_auto_continue_json(getattr(resp, "content", "") or "")
        if parsed is None:
            logger.warning("NL auto-continue classifier returned unparsable output; treating as finalize")
            return False
        return parsed
    except Exception as e:
        logger.warning(f"NL auto-continue classifier failed: {e}")
        return False

Review decision on PR #416: the guard enforces a proxy (a code block was
emitted) rather than the real property (the answer is grounded in observed
data), and breaks legitimate tool-free turns — 'what can you do?', skills
without tools, follow-ups on previous threads — by looping them to the step
limit. Superseded by #451, which tracks the underlying problem (answering
from general LLM knowledge instead of tools) with general candidate designs
and a configurable answer-source policy.
Address PR #416 review (Sami) on _unknown_tool_correction:

- the call-shape check was a text search (\bname\s*\() that could match
  inside string literals like var = " agent_1(" — now only real ast.Call
  nodes count
- a name the agent's own code defines (def / async def / class / assignment /
  import) keeps its bare NameError: calling a self-written helper before its
  definition used to get 'call find_tools' guidance, steering the agent away
  from the real fix
- the no-close-match hint no longer assumes a tool was meant — it covers the
  lost-helper possibility (re-include the definition) alongside the find_tools
  re-query, with the lookalike-action caution retained
- regression tests for all three behaviors
Address PR #416 review (Sami): 'Ok i will fetch the infromation, but first i
require your ID' matched the planning fast-path and would auto-continue —
answering the agent's request with a synthetic 'continue' instead of the
user's reply. A planning statement describes the agent's own next actions,
so any second-person text (you/your) now falls through to the LLM classifier.

Also hardens CLASSIFIER_SYSTEM_PROMPT with the recorded decision examples
(the Slack follow-up): interim plans -> continue; input requests, results,
and clarifying questions -> finalize. The mode-aware redesign of this
decision is tracked on #445.
Per PR #416 review, internal eval-bundle names (M3 mining pass) do not
belong in code comments. The detection-only rationale stays, stated
generically; the concrete example inputs/outputs and the mining evidence
now live in #452.
@haroldship haroldship changed the title feat(cuga-lite): hardening — planning-stall fix, require-tool-call guard, detection-only arg warning + unknown-tool correction fix(cuga-lite): don't stop mid-task after stating a plan; warn on suspicious tool arguments; correct made-up tool names Jul 8, 2026
@coderabbitai coderabbitai Bot added the readability: good Clear PR goal and description; easy to review label Jul 8, 2026
Keep both PR unknown-tool correction tests and main's syntax-error
guard tests in test_code_executor.py.
@haroldship

Copy link
Copy Markdown
Collaborator Author

AppWorld results

Litellm/gpt4.1 Number of tasks 120 sec Average@1 Average duration sec Average Input Tokens Average output tokens Average cached token
Test easy 43 0.814 55.1 137132 5497 80714
Test medium 40 0.575 105 376932 11096 218349

@haroldship

Copy link
Copy Markdown
Collaborator Author

Vakra results

Litellm/gpt-oss-120b Number of tasks 120 sec Average@1 Average duration sec Average Input Tokens Average output tokens Average cached token
Capability 2 100 0.410 88.4 228854 7771 0
Capability 3 100 0.150 102.7 218459 10998 0
Total 200 0.280 95.5 223657 9385 0

Notes:

  • Bundle 20260709_115720_compare_gpt-oss-120b_no-policies (M3, cuga v0.2.20, no policies, 10 domains × 10 tasks per capability, 1 run).
  • Token splits come from the Langfuse traces; output tokens include gpt-oss reasoning tokens.
  • The endpoint reported input_cache_read = 0 on every generation, so average cached tokens is 0.
  • 2 capability-2 tasks (1.25M and 1.21M total tokens) are missing Langfuse traces; their input/output split was imputed from the capability-2 traced input fraction (96.7%) applied to their recorded totals.

r"explore|examine|check|investigate|figure\s+out|determine|"
r"retrieve|gather|list|enumerate)\b",
re.IGNORECASE,
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Will this enter endless loop because of this?

@sami-marreed

Copy link
Copy Markdown
Contributor

will merge after ido's

@sami-marreed
sami-marreed merged commit 4e823e1 into main Jul 12, 2026
16 checks passed
@sami-marreed
sami-marreed deleted the feat/cuga-lite-hardening branch July 12, 2026 09:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

complexity: medium Moderate scope — multiple files or non-trivial logic priority: high Important, address soon readability: good Clear PR goal and description; easy to review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants