feat(agent): surfacer reply dans le writer wire .response.json (D6b) — §17

Ferme le canal fichier de la messagerie inter-agents : un agent qui délègue
via le protocole .ideai/requests reçoit désormais le CONTENU de la réponse,
plus seulement l'ACK de cycle de vie.

- OrchestratorResponse gagne reply: Option<String> (skip_serializing_if
  None, camelCase). success() porte le reply ; failure() => None.
- dispatch_file passe out.reply (de l'OrchestratorOutcome D6) à success.

Champ additif : un .response.json legacy sans reply reste valide ; une
commande non-ask (agent.run, spawn_agent...) => clé reply absente du JSON.

Tests (QA) : +4 verts — ask => reply présent ET detail coexistent ;
non-ask => clé absente (garde anti-always-green sur l'absence littérale) ;
round-trip legacy ; failure => absent. cargo test --workspace : 824 passed.

Orchestration structurée bouclée : in-process (D6) + protocole fichier (D6b).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-10 00:04:13 +02:00
parent 6de4e5a6e0
commit 97daf3fae5
2 changed files with 259 additions and 6 deletions

View File

@ -56,15 +56,22 @@ pub struct OrchestratorResponse {
/// Human-readable failure reason (`ok == false`).
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
/// The queried agent's reply content, when the dispatched command produced one
/// (e.g. `ask` / `agent.message`). Absent for commands that return no content
/// (e.g. `agent.run`), so an existing `*.response.json` without this key stays
/// valid.
#[serde(skip_serializing_if = "Option::is_none")]
pub reply: Option<String>,
}
impl OrchestratorResponse {
fn success(action: String, detail: String) -> Self {
fn success(action: String, detail: String, reply: Option<String>) -> Self {
Self {
ok: true,
action: Some(action),
detail: Some(detail),
error: None,
reply,
}
}
fn failure(action: Option<String>, error: String) -> Self {
@ -73,6 +80,7 @@ impl OrchestratorResponse {
action,
detail: None,
error: Some(error),
reply: None,
}
}
}
@ -258,6 +266,7 @@ async fn dispatch_file(
Ok(out) => OrchestratorResponse::success(
action.unwrap_or_else(|| "unknown".to_owned()),
out.detail,
out.reply,
),
Err(e) => OrchestratorResponse::failure(action, e.to_string()),
}