fix(orchestrator): corrige la delegation inter-agent GLM/OpenCode sans reponse Final

La detection d'absence de reponse structuree finale (structured_no_reply_error)
ne couvrait pas AgentSessionError::Decode ni le message "aucun final textuel
exploitable" emis par l'adaptateur OpenCode, ce qui laissait idea_ask_agent
planter silencieusement sur les profils GLM/OpenCode au lieu de retomber sur
le chemin de secours prevu.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-04 09:50:02 +02:00
parent 11c405efea
commit b88d500d89
3 changed files with 301 additions and 58 deletions

View File

@ -1170,6 +1170,7 @@ impl TestMailbox {
enum TestCompletion {
Replied(String),
NoReply,
DecodeNoFinal,
Cancelled,
}
@ -1467,6 +1468,9 @@ impl AgentSession for CompletionSession {
TestCompletion::NoReply => Err(AgentSessionError::Io(
"target returned without a structured final".to_owned(),
)),
TestCompletion::DecodeNoFinal => Err(AgentSessionError::Decode(
"OpenCode n'a produit aucun final textuel exploitable".to_owned(),
)),
TestCompletion::Cancelled => Err(AgentSessionError::Io(
"structured test turn cancelled".to_owned(),
)),
@ -2134,6 +2138,28 @@ async fn ask_target_returns_to_prompt_without_reply_is_a_typed_error() {
assert_eq!(fx.mailbox.pending(&aid(1)), 0, "head retired by completion");
}
#[tokio::test]
async fn structured_opencode_decode_without_final_is_a_typed_no_reply_error() {
let agent = scratch_agent(aid(1), "architect", "agents/architect.md");
let fx = ask_fixture(FakeContexts::with_agent(&agent, "# persona"));
seed_live_pty(&fx.sessions, aid(1), sid(800));
let svc = Arc::clone(&fx.service);
let ask = tokio::spawn(async move { svc.dispatch(&project(), cmd(ASK_JSON)).await });
await_until(|| fx.mailbox.pending(&aid(1)) == 1).await;
fx.mailbox
.completions()
.push(aid(1), TestCompletion::DecodeNoFinal);
let err = timeout(TEST_GUARD, ask)
.await
.expect("ask completes promptly, not after the long timeout")
.expect("join ok")
.expect_err("OpenCode no-final decode is a typed no-reply error");
assert_eq!(err.code(), "TARGET_RETURNED_NO_REPLY", "got {err:?}");
}
/// An `idea_reply` that lands **first** still wins over a (later) no-reply completion:
/// the completion then finds the head gone and is a no-op (idempotent race).
#[tokio::test]