fix(antigravity): keep an undelivered agy handoff from failing a completed turn - #414
fix(antigravity): keep an undelivered agy handoff from failing a completed turn#414r-uben wants to merge 3 commits into
Conversation
…leted turn
`agy --print` advertises its full interactive tool surface, including
`send_message`, which needs an inbox print mode never registers. A
subagent-style prompt ("report back to the main agent") is enough for the
model to deliver its reply that way: the call fails, and the CLI marks the
whole run ERROR *after* the answer has already streamed in full.
The translator recorded that as a failed run, so the caller received a
complete response closed by an SSE `error` — which Claude Code renders as
"Server error mid-response. The response above may be incomplete." Nothing
was missing; only the framing was wrong.
Treat a non-SUCCESS terminal result as success when the error has the
`recipient "<name>" not found` shape and assistant text was already
streamed. Both conditions are load-bearing: without the shape check every
late failure would be normalised, including `--print-timeout` expiry and
503s that do leave a partial answer; without the text check a run whose
only output lived inside the undelivered message would be reported as a
successful empty turn.
Fixing this at the spawn site would be better, but agy 1.1.17 has no tool
allow/deny flag and settings.json has no `disabledTools` key, so the
matcher is a shim keyed on an upstream error string. docs/notes records
what to drop once the CLI offers a way to withhold the tool.
Closes pleaseai#413
Greptile SummaryThe PR adds a compatibility shim that treats a specific Antigravity print-mode handoff failure as successful when assistant text has already streamed.
Confidence Score: 5/5The PR appears safe to merge. No blocking failure remains.
|
| Filename | Overview |
|---|---|
| src/adapters/antigravity/stream.rs | Adds narrowly conditioned normalization of an undelivered handoff error after assistant text has streamed. |
| tests/antigravity_translate.rs | Covers normalized handoff errors, unrelated late failures, empty-output failures, and spacing variants. |
| docs/notes/agy-print-mode-tool-surface.md | Documents the upstream print-mode limitation, compatibility behavior, and removal condition. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[Antigravity result event] --> B{Status SUCCESS?}
B -- Yes --> S[Complete turn successfully]
B -- No --> C{Assistant text streamed?}
C -- No --> F[Preserve terminal failure]
C -- Yes --> D{Recipient not found shape?}
D -- No --> F
D -- Yes --> S
Reviews (3): Last reviewed commit: "Merge branch 'main' into fix/413-agy-rec..." | Re-trigger Greptile
There was a problem hiding this comment.
Code Review
This pull request addresses an issue where agy in print mode exposes interactive tools it cannot service (such as send_message), causing terminal errors after a response has already streamed in full. To handle this, a compatibility shim is_undelivered_handoff is introduced in src/adapters/antigravity/stream.rs to treat these specific late handoff failures as successful turns when response text has already been received. Comprehensive tests and documentation are also added. The review feedback suggests improving the robustness of the error string parsing in is_undelivered_handoff by using trim_start() to tolerate potential whitespace variations from upstream updates.
| fn is_undelivered_handoff(message: &str) -> bool { | ||
| let Some((_, rest)) = message.split_once("recipient \"") else { | ||
| return false; | ||
| }; | ||
| rest.split_once('"') | ||
| .is_some_and(|(_, tail)| tail.starts_with(" not found")) | ||
| } |
There was a problem hiding this comment.
[MEDIUM] Improve robustness of error string matching
Problem: The is_undelivered_handoff function relies on an exact space prefix (" not found") after the closing quote of the recipient. If the upstream CLI changes its spacing (e.g., double spaces or no space), the match will fail.
Rationale: Robust string parsing should tolerate minor whitespace variations to prevent fragile compatibility shims from breaking on minor upstream updates.
Suggestion: Use trim_start() on the tail before checking if it starts with "not found".
| fn is_undelivered_handoff(message: &str) -> bool { | |
| let Some((_, rest)) = message.split_once("recipient \"") else { | |
| return false; | |
| }; | |
| rest.split_once('"') | |
| .is_some_and(|(_, tail)| tail.starts_with(" not found")) | |
| } | |
| fn is_undelivered_handoff(message: &str) -> bool { | |
| let Some((_, rest)) = message.split_once("recipient \"") else { | |
| return false; | |
| }; | |
| rest.split_once('"') | |
| .is_some_and(|(_, tail)| tail.trim_start().starts_with("not found")) | |
| } |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f97ca3672e
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if self.text.is_empty() || !is_undelivered_handoff(&message) { | ||
| self.end = Some(AgyEnd::Failed(message)); | ||
| return String::new(); | ||
| } |
There was a problem hiding this comment.
Do not treat any streamed delta as a completed reply
When the model emits even a short preamble such as “I’ll report the findings” and then puts the actual answer in send_message, the handoff fails with this same recipient error while self.text is nonempty. This branch consequently records success, causing streaming callers to receive end_turn and non-streaming callers to receive HTTP 200 with only the preamble, silently losing the real answer. Nonempty assistant text does not establish that the reply streamed in full, so the error should only be suppressed when a stronger completion signal verifies that the delivered text is complete.
Useful? React with 👍 / 👎.
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
No issues found across 3 files
Architecture diagram
sequenceDiagram
participant Client
participant Gateway as Shunt Gateway
participant Translator as Translator (stream.rs)
participant Agy as Agy CLI (--print mode)
Note over Client,Agy: Request Flow
Client->>Gateway: POST /v1/messages
Gateway->>Agy: Spawn agy process with --output-format stream-json
Note over Gateway,Agy: Streaming Response
loop Until result event
Agy->>Gateway: event: step_update (text_delta)
Gateway->>Translator: on_line(json)
Translator->>Translator: Buffer text response
Translator-->>Client: Anthropic SSE (content_block_delta)
end
Note over Gateway,Agy: NEW: Logic for Terminal Result
Agy->>Gateway: event: result (status: ERROR)
Gateway->>Translator: on_line(json)
Translator->>Translator: NEW: is_undelivered_handoff(error_msg)
Note right of Translator: Matches 'recipient "..." not found'
alt NEW: Normalization Case (Fixes #413)
Note over Translator: Condition: error is handoff AND text was streamed
Translator->>Translator: CHANGED: Set AgyEnd::Success
Translator-->>Client: event: message_stop (end_turn)
else Standard Failure
Note over Translator: Condition: other error (e.g. 503) OR no text streamed
Translator->>Translator: Set AgyEnd::Failed(message)
Translator-->>Client: event: error (display server error)
end
Gateway-->>Client: Close Connection
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ce83a8f316
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| `Translator::on_line` (`src/adapters/antigravity/stream.rs`) treats a non-`SUCCESS` | ||
| terminal result as `AgyEnd::Success` when the error has the | ||
| `recipient "<name>" not found` shape **and** assistant text was already | ||
| streamed. Everything else keeps failing the turn. |
There was a problem hiding this comment.
Add the handoff behavior to the provider guide
This changes observable response semantics for antigravity-cli—a terminal upstream error can now become a successful end_turn/HTTP 200—but documents that behavior only in an engineering note. The published provider guide remains unchanged, so users troubleshooting why an agy error was normalized cannot discover the compatibility shim from the user-facing documentation; add it to the affected site/src/content/docs/guides/providers.mdx surface.
AGENTS.md reference: AGENTS.md:L46-L52
Useful? React with 👍 / 👎.
Closes #413
Summary
agy --print --output-format stream-jsonadvertises its full interactive tool surface, includingsend_message— which needs an inbox that print mode never registers. A subagent-style prompt ("report your findings back to the main agent") is enough for the model to deliver its reply that way. The call fails, and the CLI marks the whole runERROR:That arrives after the answer has streamed in full. The translator recorded it as a failed run, so callers received a complete response closed by an SSE
error— which Claude Code renders as "Server error mid-response. The response above may be incomplete." Nothing was missing; only the framing was wrong.Translator::on_linenow treats a non-SUCCESSterminal result asAgyEnd::Successwhen the error has therecipient "<name>" not foundshape and assistant text was already streamed. Recipients observed in the wild:main,team-lead,user, so the match is on shape rather than on a fixed name.Both conditions are load-bearing:
--print-timeoutexpiry,Eligibility check failed: UNAVAILABLE (code 503), andcontext canceled, all of which do leave a genuinely partial answer.Why not fix it at the spawn site
Preferable, but unavailable on agy 1.1.17:
agy --helpexposes no tool allow/deny flag, andsettings.jsoncarries only a permission allowlist — notools/disabledToolskey. Denying permission would leave the tool advertised and convert its use into a different terminal failure.So this is a compatibility shim keyed on an upstream error string.
docs/notes/agy-print-mode-tool-surface.mdrecords the full tool list affected and says to drop the matcher once the CLI can withhold tools at spawn time.Ruled out
Tested against the same symptom and not causes: four concurrent gateway streams (all completed cleanly), heavy tool use, long generations, and quota — the 429s in the local
agylogs are onloadCodeAssist/setUserSettingsmetadata calls, which the CLI recovers from.Test plan
cargo fmt --all --check✅cargo clippy --all-targets --all-features -- -D warnings✅cargo test --all-features --workspace✅ (1690 + suites, 0 failures)Three new tests in
tests/antigravity_translate.rs:test_undelivered_handoff_after_a_full_reply_ends_the_turn— the fix:end_turn, noevent: error, no[agy error]text appended.test_other_failures_after_text_still_fail_the_turn— a 503 after text still reportsAgyEnd::Failed.test_undelivered_handoff_without_text_still_fails— an empty run still reportsAgyEnd::Failed.Manual reproduction against a running gateway, deterministic before the fix:
Docs
docs/notes/agy-print-mode-tool-surface.mdadded. No README /site/change: no config key, endpoint, CLI, or provider/model support changed — this removes a spurious error from an existing route.Summary by cubic
Prevents a fully streamed reply from being marked failed when
agy --printends with an undeliverablesend_messagehandoff. Previously these runs returned an SSE error after a complete answer; now a terminal "recipient '' not found" ends the turn successfully.docs/notes/agy-print-mode-tool-surface.mddescribing the upstream limitation and when to remove the shim.Written for commit 4168f34. Summary will update on new commits.