Files
IdeA/crates/application/src/error.rs
Blomios f4d5727a69 feat(agent): commandes Tauri + ChatBridge streaming (D4) — §17
Expose l'exécution structurée à l'UI (jumeau du chemin PTY) :
- ChatBridge (chat.rs) : pompe ReplyStream -> Channel<ReplyChunk>,
  generation-tracked (anti-double-pompe) ; scrollback de conversation
  côté transport (le port AgentSession est sans mémoire), purgé à la
  fermeture, préservé à la ré-attache.
- Commandes agent_send / reattach_agent_chat / close_agent_session.
- DTO : ReplyChunk (textDelta/toolActivity/final, camelCase tagué kind),
  ReattachChatDto, CellKind {pty,chat} + champ cellKind dérivé sur
  TerminalSessionDto (chat ssi LaunchAgentOutput.structured = Some).
- Wiring composition root : StructuredSessions + ChatBridge dans AppState,
  via la factory déjà injectée (aucun new d'adapter — règle D §17.8).
- From<AgentSessionError> for AppError (mappe sur PROCESS).

Tests (QA) : 35 unitaires verts — generation supersede validé par mutation
test, séquence deltas*+Final, reattach sans re-spawn, teardown shutdown+
unregister, DTO cellKind + round-trip ReplyChunk, non-régression PTY.
cargo test --workspace : 803 passed, 0 failed.

Reste D5 : AgentChatView (frontend) + routage cellKind dans LayoutGrid.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 23:15:08 +02:00

163 lines
5.1 KiB
Rust

//! [`AppError`] — the single error type returned by every use case.
//!
//! Per-port errors from the domain ([`domain::ports::FsError`],
//! [`domain::ports::StoreError`], …) are mapped into this application-level
//! error so that the presentation layer (Tauri commands) only ever has to deal
//! with one error shape when building its `ErrorDTO`.
use domain::ports::{
AgentSessionError, EmbedderError, FsError, GitError, MemoryError, ProcessError, PtyError,
RemoteError, RuntimeError, StoreError,
};
use domain::{AgentId, NodeId};
/// Errors surfaced by application use cases.
///
/// Each variant carries a stable, machine-readable `code` (see [`AppError::code`])
/// so the presentation layer can map it to an `ErrorDTO` without string matching.
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum AppError {
/// A requested resource was not found.
#[error("not found: {0}")]
NotFound(String),
/// The input failed a domain/application invariant.
#[error("invalid input: {0}")]
Invalid(String),
/// A filesystem operation failed.
#[error("filesystem error: {0}")]
FileSystem(String),
/// A persistence (store) operation failed.
#[error("store error: {0}")]
Store(String),
/// A process/PTY/runtime operation failed.
#[error("process error: {0}")]
Process(String),
/// A git operation failed.
#[error("git error: {0}")]
Git(String),
/// A remote (SSH/WSL) operation failed.
#[error("remote error: {0}")]
Remote(String),
/// An agent is already running in a live cell and cannot be launched again
/// (the "one live session per agent" invariant). Reuse goes through
/// templates (instantiate → distinct agents); IdeA never multi-instances a
/// single agent.
#[error("agent {agent_id} is already running in cell {node_id}")]
AgentAlreadyRunning {
/// The agent that is already live.
agent_id: AgentId,
/// The layout cell (leaf) currently hosting that agent's live session.
node_id: NodeId,
},
/// An unexpected internal error.
#[error("internal error: {0}")]
Internal(String),
}
impl AppError {
/// A stable, machine-readable code for this error, intended for the
/// `ErrorDTO` so the frontend can branch without parsing messages.
#[must_use]
pub fn code(&self) -> &'static str {
match self {
Self::NotFound(_) => "NOT_FOUND",
Self::Invalid(_) => "INVALID",
Self::FileSystem(_) => "FILESYSTEM",
Self::Store(_) => "STORE",
Self::Process(_) => "PROCESS",
Self::Git(_) => "GIT",
Self::Remote(_) => "REMOTE",
Self::AgentAlreadyRunning { .. } => "AGENT_ALREADY_RUNNING",
Self::Internal(_) => "INTERNAL",
}
}
}
impl From<FsError> for AppError {
fn from(e: FsError) -> Self {
match e {
FsError::NotFound(p) => Self::NotFound(p),
other => Self::FileSystem(other.to_string()),
}
}
}
impl From<StoreError> for AppError {
fn from(e: StoreError) -> Self {
match e {
StoreError::NotFound => Self::NotFound("store item".to_owned()),
other => Self::Store(other.to_string()),
}
}
}
impl From<MemoryError> for AppError {
fn from(e: MemoryError) -> Self {
match e {
MemoryError::NotFound => Self::NotFound("memory note".to_owned()),
MemoryError::Frontmatter(m) => Self::Invalid(m),
other => Self::Store(other.to_string()),
}
}
}
impl From<EmbedderError> for AppError {
/// Maps to [`AppError::Store`] — an embedder is a *derived* recall detail; its
/// failure must degrade the recall (fallback to naïve), never fail hard. This
/// mapping exists for completeness; recall adapters degrade *before* an
/// embedder error ever reaches a use case.
fn from(e: EmbedderError) -> Self {
Self::Store(e.to_string())
}
}
impl From<PtyError> for AppError {
fn from(e: PtyError) -> Self {
Self::Process(e.to_string())
}
}
impl From<ProcessError> for AppError {
fn from(e: ProcessError) -> Self {
Self::Process(e.to_string())
}
}
impl From<RuntimeError> for AppError {
fn from(e: RuntimeError) -> Self {
Self::Process(e.to_string())
}
}
impl From<GitError> for AppError {
fn from(e: GitError) -> Self {
Self::Git(e.to_string())
}
}
impl From<RemoteError> for AppError {
fn from(e: RemoteError) -> Self {
Self::Remote(e.to_string())
}
}
impl From<AgentSessionError> for AppError {
/// Maps a structured [`AgentSessionError`] (ARCHITECTURE §17.1) onto the single
/// application error shape. `Start`/`Io`/`Decode`/`Timeout` are all execution
/// failures of a live structured session (a process/SDK conversation), so they
/// fold into [`AppError::Process`] — coherent with [`PtyError`]/[`ProcessError`],
/// the byte-stream twins. The raw CLI JSON never travels through `Decode` (the
/// adapter already redacted it), so this is safe to surface.
fn from(e: AgentSessionError) -> Self {
Self::Process(e.to_string())
}
}