Skip to content

fix(apicalls): a failed httpcall tool returned "{}" — the model could not know it failed - #684

Merged
ginccc merged 3 commits into
chore/remove-agent-fatherfrom
fix/tool-result-contract
Aug 15, 2026
Merged

fix(apicalls): a failed httpcall tool returned "{}" — the model could not know it failed#684
ginccc merged 3 commits into
chore/remove-agent-fatherfrom
fix/tool-result-contract

Conversation

@ginccc

@ginccc ginccc commented Aug 15, 2026

Copy link
Copy Markdown
Member

The missing piece of "after approval, nothing happened": the human approved setupAgent, the call went out, got a 400 — and the model was handed {} as its tool result.

HttpCallToolsProvider serializes ApiCallExecutor.execute()'s returned map verbatim, and that map was only populated inside if (isResponseSuccessful && call.getSaveResponse()). On a non-2xx it stayed empty; on a 2xx with saveResponse=false, empty too. So the model could neither report a failure nor confirm a success — a human-approved call that 400'd looked exactly like one that worked. (Live evidence: Httpcall tool 'setupAgent' result: keys=[] size=2 right after the 400 in this morning's log.)

Contract now

case tool result
non-2xx {"httpCode": 400, "body": "<error body, truncated to 2000>"} — status message when the body is blank
2xx, saveResponse=false {"httpCode": 204} — body stays out, that is what the flag means
2xx, saveResponse=true unchanged (body + httpCode)

Same keys as the success path rather than a new error namespace: ApiCallsTask merges this map into template data, where that vocabulary is already established. Response object semantics are untouched — an error body still never lands under responseObjectName, and memory still sees it only under the *Error key.

Tests

Five new tests pin the contract (error body + code, blank-body fallback, truncation in the result, code-only on quiet success, and a mutation-verified regression for the retried-failure leak — a 503's error body must not survive into a succeeding retry's quiet-success result; the map is cleared per attempt, final attempt wins). Four existing tests asserted the empty map and were updated — they were pinning the bug. 488 tests across the apicalls/LlmTask suites green.

… not know it failed

The human approved setupAgent, the call went out and got a 400 - and the
model was handed "{}" as the tool result. HttpCallToolsProvider serializes
ApiCallExecutor.execute()'s returned map verbatim, and that map was only
populated inside "if (isResponseSuccessful && call.getSaveResponse())".
On a non-2xx it stayed EMPTY; on a 2xx with saveResponse=false it stayed
empty too. The model could neither report a failure nor confirm a success:
a human-approved call that 400'd looked exactly like one that worked.

- non-2xx -> {"httpCode", "body": error body truncated to 2000 chars}
  (status message when blank). Same keys as the success path, not a new
  "error" namespace: ApiCallsTask merges this map into template data,
  where that vocabulary is already established.
- 2xx with saveResponse=false -> {"httpCode"}. The body stays out (that is
  what the flag means), but a model whose tool returned "{}" cannot tell a
  204 from a crash.
- Response OBJECT semantics untouched: an error body still never lands
  under responseObjectName; memory sees it only under the *Error key.

Four new tests pin the contract; four existing tests that asserted the
empty map were updated - they were pinning the bug.
@ginccc
ginccc requested a review from rolandpickl as a code owner August 15, 2026 07:34
@coderabbitai

coderabbitai Bot commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: d7e9d670-d7c2-4dd7-8547-a54e5168f4bb

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

…'s quiet success

Caught in self-review of the tool-result contract: the failure branch
populates body/httpCode on its pass through the retry loop, and a
succeeding retry with saveResponse=false writes only httpCode - so the
model would have been handed {"httpCode": 200, "body": "<the 503's
error>"}, a self-contradictory tool result. The result map is now cleared
at the top of each attempt: the final attempt wins, entirely.

Test stubs two distinct responses switched per attempt via send() -
sequential stubbing on one mock would flip mid-attempt, since the code
reads getHttpCode() several times per iteration. Verified by mutation:
removing the clear() fails it.

Copilot AI 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.

Pull request overview

Ensures HTTP-call tools return actionable status information to the LLM.

Changes:

  • Returns HTTP status and truncated error bodies for non-2xx responses.
  • Returns status codes for successful calls with saveResponse=false.
  • Adds regression coverage for retries and response handling.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
ApiCallExecutor.java Implements the revised result contract.
ApiCallExecutorTest.java Tests tool results and retries.
ApiCallExecutorValidationErrorTest.java Verifies truncated errors reach results.
ApiCallExecutorBranchCoverageTest.java Updates branch expectations.
docs/changelog.md Documents the fix.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread docs/changelog.md Outdated
Copilot's review caught it: the retried-failure-leak regression added a
fifth test after the entry was written. It is the most load-bearing of the
five, so it is now named rather than merely counted.
@ginccc
ginccc merged commit a9ee9ba into chore/remove-agent-father Aug 15, 2026
3 checks passed
@ginccc
ginccc deleted the fix/tool-result-contract branch August 15, 2026 08:35
pull Bot pushed a commit to Stars1233/EDDI that referenced this pull request Aug 17, 2026
… live guard

A four-reviewer audit of labsai#679-labsai#689 plus an end-to-end operator-path trace,
then a second adversarial round over this fix itself. Everything confirmed
is addressed here; docs/changelog.md carries the full account.

- Cross-version placeholder stranding: dropPendingApprovalPlaceholder now
  recognises the previous builds' default wordings (legacy constant,
  suffix-less tool-named), so the first post-upgrade resume of an in-flight
  pause no longer renders [stale placeholder, answer]. Two upgrade-boundary
  tests simulate a pre-upgrade pause.
- Self-conversation guard is now enforced on the LIVE path, including the
  mixed-batch pause branch the second review round caught (ungated calls
  execute before the pause is thrown and are never rechecked). Shared core
  extracted; same NOT_EXECUTED envelope and trace everywhere.
- labsai#684 contract narrowed: failed results stay out of ApiCallsTask's
  cross-call template merge and out of the RAG system prompt; error bodies
  (and the status-message fallback) are redacted before reaching the model.
  Memory-side *Error keys unchanged.
- Test-drive read-back: a blank returningFields entry means NO filter -
  [""] no longer nulls steps/outputs/properties out of the snapshot.
- Generated tools: body $refs resolve one level (schemas namespace only),
  so descriptions name real fields - a guessed say-body bound to InputData
  defaults and silently sent an empty message; enum values and defaults now
  reach parameter descriptions (the environment typo->production trap).
- padDataLines normalises bare CR; RFC 7615 headers join the credential
  response deny-list; labsai#688's shared-path stripping disclosed in changelog.

264 tests across the affected suites, including mutation-informed pins:
same-tool ordinal drop, refused-mid-batch pairing, blank-filter recovery,
redaction survival of the failure reason.
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.

2 participants