feat(mcp,orchestrator,frontend): tool idea_restart_self, redemarrage autonome d'un agent — #288 (tool core self-only via requester MCP handshake, retour immediat {restartId, status: scheduled} puis orchestration detachee: snapshot reprise, stop PTY/structured, liberation Busy/mailbox, relance LaunchAgent meme conversation_id fresh_start=false, injection interne invisible; events domain/wire agentRestartRequested/PhaseChanged/Completed/Failed phases stopping/releasingInput/launching/resuming; CLI custom: separateurs exacts redemarrage de l'agent + etat termine/echoue, deduplication (restartId, event), verrouillage composer; TUI native: overlay AgentRestartOverlay sans injection xterm, progress/annonces projetes jusqu'a idle; QA verte: mcp_server restart_self 1/1, restart-self 3/3, backend self_restart_contract + events wire 2/2, frontend 3 suites 92/92, npm build, cargo check --workspace, fmt --check, diff --check; reserves hors perimetre: web-server 103/110 fixture PTY preexistante, 34 echecs npm test globaux listProjectSettings/web hors #288; scripts/ hors perimetre)

This commit is contained in:
2026-09-08 22:04:18 +02:00
parent ee259dc93a
commit 63537572b8
23 changed files with 1207 additions and 37 deletions

View File

@ -113,6 +113,34 @@ pub struct AgentLaunchFailedDto {
#[derive(Debug, Clone, Serialize)]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum DomainEventDto {
/// A requester-scoped agent self-restart was accepted.
#[serde(rename_all = "camelCase")]
AgentRestartRequested {
agent_id: String,
restart_id: String,
reason: Option<String>,
},
/// A self-restart changed phase.
#[serde(rename_all = "camelCase")]
AgentRestartPhaseChanged {
agent_id: String,
restart_id: String,
phase: String,
},
/// A self-restart completed.
#[serde(rename_all = "camelCase")]
AgentRestartCompleted {
agent_id: String,
restart_id: String,
},
/// A self-restart failed after scheduling.
#[serde(rename_all = "camelCase")]
AgentRestartFailed {
agent_id: String,
restart_id: String,
phase: String,
message: String,
},
/// A project was created.
#[serde(rename_all = "camelCase")]
ProjectCreated {
@ -787,6 +815,42 @@ fn rendezvous_conversation_id(rendezvous: &Option<RendezvousContext>) -> Option<
impl From<&DomainEvent> for DomainEventDto {
fn from(e: &DomainEvent) -> Self {
match e {
DomainEvent::AgentRestartRequested {
agent_id,
restart_id,
reason,
} => Self::AgentRestartRequested {
agent_id: agent_id.to_string(),
restart_id: restart_id.clone(),
reason: reason.clone(),
},
DomainEvent::AgentRestartPhaseChanged {
agent_id,
restart_id,
phase,
} => Self::AgentRestartPhaseChanged {
agent_id: agent_id.to_string(),
restart_id: restart_id.clone(),
phase: restart_phase_label(*phase).to_owned(),
},
DomainEvent::AgentRestartCompleted {
agent_id,
restart_id,
} => Self::AgentRestartCompleted {
agent_id: agent_id.to_string(),
restart_id: restart_id.clone(),
},
DomainEvent::AgentRestartFailed {
agent_id,
restart_id,
phase,
message,
} => Self::AgentRestartFailed {
agent_id: agent_id.to_string(),
restart_id: restart_id.clone(),
phase: restart_phase_label(*phase).to_owned(),
message: message.clone(),
},
DomainEvent::ProjectCreated { project_id } => Self::ProjectCreated {
project_id: project_id.to_string(),
},
@ -1312,6 +1376,15 @@ impl From<&DomainEvent> for DomainEventDto {
}
}
fn restart_phase_label(phase: domain::events::AgentRestartPhase) -> &'static str {
match phase {
domain::events::AgentRestartPhase::Stopping => "stopping",
domain::events::AgentRestartPhase::ReleasingInput => "releasingInput",
domain::events::AgentRestartPhase::Launching => "launching",
domain::events::AgentRestartPhase::Resuming => "resuming",
}
}
#[cfg(test)]
mod tests {
use super::*;
@ -1336,6 +1409,32 @@ mod tests {
ConversationId::from_uuid(uuid::Uuid::from_u128(n))
}
#[test]
fn agent_restart_events_expose_stable_camel_case_contract() {
let id = agent(288);
let phase = DomainEventDto::from(&DomainEvent::AgentRestartPhaseChanged {
agent_id: id,
restart_id: "restart-288".to_owned(),
phase: domain::events::AgentRestartPhase::ReleasingInput,
});
let phase = serde_json::to_value(phase).expect("serialisable");
assert_eq!(phase["type"], "agentRestartPhaseChanged");
assert_eq!(phase["agentId"], id.to_string());
assert_eq!(phase["restartId"], "restart-288");
assert_eq!(phase["phase"], "releasingInput");
let failed = DomainEventDto::from(&DomainEvent::AgentRestartFailed {
agent_id: id,
restart_id: "restart-288".to_owned(),
phase: domain::events::AgentRestartPhase::Launching,
message: "launch failed".to_owned(),
});
let failed = serde_json::to_value(failed).expect("serialisable");
assert_eq!(failed["type"], "agentRestartFailed");
assert_eq!(failed["phase"], "launching");
assert_eq!(failed["message"], "launch failed");
}
#[test]
fn model_server_status_changed_relays_ready_to_dto_and_wire() {
let dto = DomainEventDto::from(&DomainEvent::ModelServerStatusChanged {