feat(agents): pont Codex inter-agents + readiness/heartbeat lot 1
Deux chantiers livrés au vert (workspace entier : domain+application+
infrastructure 42 + app-tauri --lib 128, 0 échec).
## Codex inter-agents
- domaine: McpConfigStrategy::TomlConfigHome { target, home_env } +
toml_config_home(...); AgentProfile::materializes_idea_bridge()
(whitelist Claude/ConfigFile + Codex/TomlConfigHome); McpServerWiring
+ encodeur TOML.
- application: lifecycle apply_mcp_config bras TomlConfigHome (écrit
{runDir}/<target>, pousse (home_env, parent) dans spec.env);
guard_mcp_bridge_supported ré-exprimée via materializes_idea_bridge();
catalogue Codex porte toml_config_home(".codex/config.toml","CODEX_HOME").
- app-tauri: is_codex_mcp_profile, migrate_codex_run_dir,
mcp_server_entry_toml.
- tests: matrice domaine TomlConfigHome + round-trip dual Claude/Codex
sur loopback réel (fakes, zéro token).
## Readiness/heartbeat lot 1
- domaine: readiness.rs — ReadinessPolicy::classify (Final => TurnEnded),
variantes ReplyEvent::Heartbeat / ToolActivity.
- application: drain_with_readiness consulte la policy et appelle
mark_idle sur le signal déterministe; branché dans ask_agent.
Corrige la cause racine: une cible qui ne renvoie qu'un Final (sans
idea_reply) débloque désormais sa file Busy.
- infrastructure: adapters de session émettent Heartbeat/ToolActivity.
- tests: drain_with_readiness_lot1 (points QA 5 & 6) verts.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -80,6 +80,29 @@ pub enum AgentBusyState {
|
||||
},
|
||||
}
|
||||
|
||||
/// Whether an agent currently shows **signs of life** during a turn (lot 2,
|
||||
/// chantier readiness/heartbeat — détection « Stalled »).
|
||||
///
|
||||
/// Orthogonal to [`AgentBusyState`]: an agent can be `Busy` **and** `Alive` (it is
|
||||
/// working, producing deltas/heartbeats) or `Busy` **and** `Stalled` (no proof of
|
||||
/// liveness for longer than its profile's `stall_after_ms`). Only meaningful while
|
||||
/// `Busy`; an `Idle` agent is considered `Alive` (its turn ended cleanly).
|
||||
///
|
||||
/// Published to the front (`AgentLivenessChanged`) **once per transition** so the UI
|
||||
/// can badge a frozen agent without event spam. Derived from the per-agent
|
||||
/// `last_seen_ms` heartbeat, never from parsing the model output.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase", tag = "liveness")]
|
||||
pub enum AgentLiveness {
|
||||
/// The agent is producing proof of liveness (deltas / tool activity /
|
||||
/// heartbeats) within its `stall_after_ms` window — or is idle.
|
||||
Alive,
|
||||
/// No proof of liveness for longer than the profile's `stall_after_ms`: the
|
||||
/// agent is presumed frozen. Advisory only — the FIFO and the turn keep running
|
||||
/// (a late heartbeat flips it back to [`Self::Alive`]).
|
||||
Stalled,
|
||||
}
|
||||
|
||||
impl AgentBusyState {
|
||||
/// Whether a turn is currently in flight.
|
||||
#[must_use]
|
||||
@ -195,6 +218,29 @@ pub trait InputMediator: Send + Sync {
|
||||
/// Marks `agent` free (prompt-ready or explicit signal) so its FIFO advances.
|
||||
fn mark_idle(&self, agent: AgentId);
|
||||
|
||||
/// Records a **proof of liveness** (« battement ») for `agent` — called on every
|
||||
/// non-terminal turn event (text delta, tool activity, [`crate::ports::ReplyEvent::Heartbeat`])
|
||||
/// by the drain loop. Refreshes the per-agent `last_seen` timestamp so the stall
|
||||
/// detector ([`crate::events::DomainEvent::AgentLivenessChanged`], lot 2) knows the
|
||||
/// agent is still working; a battement arriving after a `Stalled` transition flips it
|
||||
/// back to [`AgentLiveness::Alive`].
|
||||
///
|
||||
/// Default: no-op (a mediator that does not track liveness). The infra adapter
|
||||
/// `MediatedInbox` overrides it to refresh `last_seen` and publish an
|
||||
/// `AgentLivenessChanged{Stalled→Alive}` recovery on the first late battement.
|
||||
fn mark_alive(&self, _agent: AgentId) {}
|
||||
|
||||
/// Declares the agent's **stall threshold** (its profile's
|
||||
/// [`crate::profile::LivenessStrategy::stall_after_ms`]) so the stall detector knows
|
||||
/// how long without a battement means "frozen" (lot 2). The orchestrator resolves it
|
||||
/// from the target's profile and calls this **before** the enqueue that starts a
|
||||
/// turn; the adapter stashes it and arms a fresh liveness window when the turn
|
||||
/// begins. `None` ⇒ no stall detection for this agent (legacy / no `liveness` block —
|
||||
/// zero regression).
|
||||
///
|
||||
/// Default: no-op (a mediator that does not track liveness).
|
||||
fn set_stall_threshold(&self, _agent: AgentId, _stall_after_ms: Option<u32>) {}
|
||||
|
||||
/// The current [`AgentBusyState`] of `agent`.
|
||||
fn busy_state(&self, agent: AgentId) -> AgentBusyState;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user