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>
This commit is contained in:
@ -237,6 +237,27 @@ pub struct TerminalSessionDto {
|
||||
/// resumes. `None` for a plain terminal, a resume, or a degraded launch.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub assigned_conversation_id: Option<String>,
|
||||
/// How the frontend should render the cell hosting this session (§17.6):
|
||||
/// [`CellKind::Chat`] for a structured AI session (an `AgentChatView` driven by
|
||||
/// `agent_send`/`reattach_agent_chat`), [`CellKind::Pty`] for a raw terminal
|
||||
/// (xterm). **Derived**, not a layout field: it follows the presence of a
|
||||
/// structured session descriptor on the launch output — a single source of
|
||||
/// truth. Plain terminals and the non-launch construction paths default to
|
||||
/// [`CellKind::Pty`] (the historical, non-breaking shape: a PTY session DTO
|
||||
/// always serialises with `cellKind: "pty"`).
|
||||
pub cell_kind: CellKind,
|
||||
}
|
||||
|
||||
/// Whether a session's hosting cell renders as a structured chat view or a raw
|
||||
/// terminal (ARCHITECTURE §17.6). Serialises as `"chat"` / `"pty"` on the wire;
|
||||
/// the frontend `LayoutGrid` switches `AgentChatView` vs `TerminalView` on it.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum CellKind {
|
||||
/// A raw PTY terminal cell (xterm) — the historical default.
|
||||
Pty,
|
||||
/// A structured AI chat cell (`AgentChatView`), backed by an `AgentSession`.
|
||||
Chat,
|
||||
}
|
||||
|
||||
impl From<OpenTerminalOutput> for TerminalSessionDto {
|
||||
@ -248,6 +269,7 @@ impl From<OpenTerminalOutput> for TerminalSessionDto {
|
||||
rows: s.pty_size.rows,
|
||||
cols: s.pty_size.cols,
|
||||
assigned_conversation_id: None,
|
||||
cell_kind: CellKind::Pty,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1143,6 +1165,15 @@ pub struct LaunchAgentRequestDto {
|
||||
impl From<LaunchAgentOutput> for TerminalSessionDto {
|
||||
fn from(out: LaunchAgentOutput) -> Self {
|
||||
let assigned_conversation_id = out.assigned_conversation_id;
|
||||
// §17.6: the cell kind is *derived* from the launch routing. A structured
|
||||
// descriptor (`structured: Some(..)`) means LaunchAgent routed to an
|
||||
// AgentSession ⇒ chat cell; its absence means the PTY/terminal path ⇒
|
||||
// terminal cell. Single source of truth, no layout migration.
|
||||
let cell_kind = if out.structured.is_some() {
|
||||
CellKind::Chat
|
||||
} else {
|
||||
CellKind::Pty
|
||||
};
|
||||
let s = out.session;
|
||||
Self {
|
||||
session_id: s.id.to_string(),
|
||||
@ -1150,6 +1181,7 @@ impl From<LaunchAgentOutput> for TerminalSessionDto {
|
||||
rows: s.pty_size.rows,
|
||||
cols: s.pty_size.cols,
|
||||
assigned_conversation_id,
|
||||
cell_kind,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1200,10 +1232,63 @@ impl From<TerminalSession> for TerminalSessionDto {
|
||||
rows: s.pty_size.rows,
|
||||
cols: s.pty_size.cols,
|
||||
assigned_conversation_id: None,
|
||||
cell_kind: CellKind::Pty,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Structured chat sessions (§17 — D4)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// One incremental chunk of a structured agent reply, streamed over the chat
|
||||
/// session's [`tauri::ipc::Channel`] (ARCHITECTURE §17.7). The serialised wire
|
||||
/// twin of a [`domain::ports::ReplyEvent`]: the `agent_send` pump maps each turn
|
||||
/// event to one of these and pushes it to the frontend `AgentChatView`.
|
||||
///
|
||||
/// Tagged on `kind` (camelCase: `"textDelta"` | `"toolActivity"` | `"final"`), so
|
||||
/// the front branches without positional parsing. `Final` is the **deterministic
|
||||
/// terminal** chunk of a turn — after it the turn is frozen and the stream ends.
|
||||
/// `Deserialize` is derived too so tests (and a mock gateway) can round-trip it.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(tag = "kind", rename_all = "camelCase")]
|
||||
pub enum ReplyChunk {
|
||||
/// An assistant text fragment (incremental chat rendering).
|
||||
#[serde(rename_all = "camelCase")]
|
||||
TextDelta {
|
||||
/// The text fragment.
|
||||
text: String,
|
||||
},
|
||||
/// A human-readable tool-activity badge (best-effort observability).
|
||||
#[serde(rename_all = "camelCase")]
|
||||
ToolActivity {
|
||||
/// The human-readable activity label.
|
||||
label: String,
|
||||
},
|
||||
/// The deterministic end-of-turn chunk carrying the aggregated final content.
|
||||
#[serde(rename_all = "camelCase")]
|
||||
Final {
|
||||
/// The aggregated final content of the turn.
|
||||
content: String,
|
||||
},
|
||||
}
|
||||
|
||||
/// Response DTO for `reattach_agent_chat`: the retained **conversation
|
||||
/// scrollback** of a still-live structured session, replayed into the
|
||||
/// re-mounting `AgentChatView` before the new reply stream is wired (§17.6). The
|
||||
/// typed twin of [`ReattachResultDto`] (PTY scrollback bytes) — here the
|
||||
/// scrollback is the ordered list of [`ReplyChunk`]s already streamed.
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ReattachChatDto {
|
||||
/// The session that was re-attached (echoed back for the frontend).
|
||||
pub session_id: String,
|
||||
/// The chunks already streamed for this conversation, in order. The frontend
|
||||
/// replays them to rebuild the visible turns, then receives subsequent chunks
|
||||
/// over the freshly-registered channel.
|
||||
pub scrollback: Vec<ReplyChunk>,
|
||||
}
|
||||
|
||||
/// Request DTO for `inspect_conversation` (T7).
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
|
||||
Reference in New Issue
Block a user