feat(session-limits): LS6 — câblage des événements de limite vers le front
Relaie les 5 nouvelles variantes DomainEvent (AgentRateLimited, ResumeScheduled, ResumeCancelled, Resumed, RateLimitSuspected) via leurs DTO miroirs et bras From dans events.rs, et propage ReplyEvent::RateLimited en chunk côté chat.rs pour que le front soit informé des suspensions/reprises de session. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -178,8 +178,10 @@ impl ChatBridge {
|
|||||||
///
|
///
|
||||||
/// Returns `None` for [`ReplyEvent::Heartbeat`]: a heartbeat is a non-terminal
|
/// Returns `None` for [`ReplyEvent::Heartbeat`]: a heartbeat is a non-terminal
|
||||||
/// liveness proof (readiness/heartbeat lot 1) with **no chat content**, so it maps
|
/// liveness proof (readiness/heartbeat lot 1) with **no chat content**, so it maps
|
||||||
/// to no wire chunk — the pump simply skips it. Every content-bearing event still
|
/// to no wire chunk — the pump simply skips it. Likewise [`ReplyEvent::RateLimited`]
|
||||||
/// maps to exactly one chunk.
|
/// is non-terminal and content-free (ports §21.2-T4): the UI badge comes from the
|
||||||
|
/// `DomainEvent::AgentRateLimited` bus, not the chat stream, so it maps to `None`
|
||||||
|
/// too. Every content-bearing event still maps to exactly one chunk.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn chunk_from_event(event: ReplyEvent) -> Option<ReplyChunk> {
|
pub fn chunk_from_event(event: ReplyEvent) -> Option<ReplyChunk> {
|
||||||
match event {
|
match event {
|
||||||
@ -187,5 +189,6 @@ pub fn chunk_from_event(event: ReplyEvent) -> Option<ReplyChunk> {
|
|||||||
ReplyEvent::ToolActivity { label } => Some(ReplyChunk::ToolActivity { label }),
|
ReplyEvent::ToolActivity { label } => Some(ReplyChunk::ToolActivity { label }),
|
||||||
ReplyEvent::Final { content } => Some(ReplyChunk::Final { content }),
|
ReplyEvent::Final { content } => Some(ReplyChunk::Final { content }),
|
||||||
ReplyEvent::Heartbeat => None,
|
ReplyEvent::Heartbeat => None,
|
||||||
|
ReplyEvent::RateLimited { .. } => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -205,6 +205,52 @@ pub enum DomainEventDto {
|
|||||||
/// Whether the in-process ONNX capability is compiled in.
|
/// Whether the in-process ONNX capability is compiled in.
|
||||||
vector_onnx_enabled: bool,
|
vector_onnx_enabled: bool,
|
||||||
},
|
},
|
||||||
|
/// An agent entered a **session/rate limit** (ARCHITECTURE §21). Low-frequency,
|
||||||
|
/// model-agnostic badge: carries only the neutral "limited, maybe resets at T"
|
||||||
|
/// fact. The frontend badges "limité jusqu'à HH:MM".
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
AgentRateLimited {
|
||||||
|
/// Agent id (UUID string).
|
||||||
|
agent_id: String,
|
||||||
|
/// Reset instant in **epoch-milliseconds**; `null` ⇒ unknown (no auto-resume).
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
resets_at_ms: Option<i64>,
|
||||||
|
},
|
||||||
|
/// An **auto-resume** was armed for a rate-limited agent (ARCHITECTURE §21). The
|
||||||
|
/// frontend shows the countdown + the "Annuler la reprise" button (cancellable
|
||||||
|
/// window).
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
AgentResumeScheduled {
|
||||||
|
/// Agent id (UUID string).
|
||||||
|
agent_id: String,
|
||||||
|
/// Wake-up deadline in **epoch-milliseconds**.
|
||||||
|
fire_at_ms: i64,
|
||||||
|
},
|
||||||
|
/// An agent's **auto-resume** was **cancelled** (ARCHITECTURE §21): the user
|
||||||
|
/// clicked "Annuler la reprise". The frontend removes the countdown.
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
AgentResumeCancelled {
|
||||||
|
/// Agent id (UUID string).
|
||||||
|
agent_id: String,
|
||||||
|
},
|
||||||
|
/// An agent was effectively **resumed** after a limit (ARCHITECTURE §21): the
|
||||||
|
/// wake-up fired (or immediate resume). The frontend clears the "limited" state.
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
AgentResumed {
|
||||||
|
/// Agent id (UUID string).
|
||||||
|
agent_id: String,
|
||||||
|
},
|
||||||
|
/// **Human net (level 3)**: a session limit is **suspected** without any reliable
|
||||||
|
/// reset time (ARCHITECTURE §21.1). IdeA never resumes blind: this asks the front
|
||||||
|
/// to **prompt the user** ("limit detected but time unknown — resume at?").
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
AgentRateLimitSuspected {
|
||||||
|
/// Agent id (UUID string).
|
||||||
|
agent_id: String,
|
||||||
|
/// Reset instant in **epoch-milliseconds** if an estimate exists, else `null`.
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
resets_at_ms: Option<i64>,
|
||||||
|
},
|
||||||
/// Raw PTY output (normally routed to a per-session channel, not here).
|
/// Raw PTY output (normally routed to a per-session channel, not here).
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
PtyOutput {
|
PtyOutput {
|
||||||
@ -377,6 +423,33 @@ impl From<&DomainEvent> for DomainEventDto {
|
|||||||
vector_http_enabled: *vector_http_enabled,
|
vector_http_enabled: *vector_http_enabled,
|
||||||
vector_onnx_enabled: *vector_onnx_enabled,
|
vector_onnx_enabled: *vector_onnx_enabled,
|
||||||
},
|
},
|
||||||
|
DomainEvent::AgentRateLimited {
|
||||||
|
agent_id,
|
||||||
|
resets_at_ms,
|
||||||
|
} => Self::AgentRateLimited {
|
||||||
|
agent_id: agent_id.to_string(),
|
||||||
|
resets_at_ms: *resets_at_ms,
|
||||||
|
},
|
||||||
|
DomainEvent::AgentResumeScheduled {
|
||||||
|
agent_id,
|
||||||
|
fire_at_ms,
|
||||||
|
} => Self::AgentResumeScheduled {
|
||||||
|
agent_id: agent_id.to_string(),
|
||||||
|
fire_at_ms: *fire_at_ms,
|
||||||
|
},
|
||||||
|
DomainEvent::AgentResumeCancelled { agent_id } => Self::AgentResumeCancelled {
|
||||||
|
agent_id: agent_id.to_string(),
|
||||||
|
},
|
||||||
|
DomainEvent::AgentResumed { agent_id } => Self::AgentResumed {
|
||||||
|
agent_id: agent_id.to_string(),
|
||||||
|
},
|
||||||
|
DomainEvent::AgentRateLimitSuspected {
|
||||||
|
agent_id,
|
||||||
|
resets_at_ms,
|
||||||
|
} => Self::AgentRateLimitSuspected {
|
||||||
|
agent_id: agent_id.to_string(),
|
||||||
|
resets_at_ms: *resets_at_ms,
|
||||||
|
},
|
||||||
DomainEvent::PtyOutput { session_id, bytes } => Self::PtyOutput {
|
DomainEvent::PtyOutput { session_id, bytes } => Self::PtyOutput {
|
||||||
session_id: session_id.to_string(),
|
session_id: session_id.to_string(),
|
||||||
bytes: bytes.clone(),
|
bytes: bytes.clone(),
|
||||||
@ -450,4 +523,19 @@ mod tests {
|
|||||||
assert_eq!(json["type"], "agentLivenessChanged");
|
assert_eq!(json["type"], "agentLivenessChanged");
|
||||||
assert_eq!(json["liveness"], "alive");
|
assert_eq!(json["liveness"], "alive");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// LS6 : un `AgentRateLimited` du domaine se relaie en DTO portant le même agent
|
||||||
|
/// et l'heure de reset (époche-ms), et se sérialise en `"agentRateLimited"` avec
|
||||||
|
/// `resetsAtMs` — le fait neutre que le front badge « limité jusqu'à HH:MM ».
|
||||||
|
#[test]
|
||||||
|
fn rate_limited_relays_to_dto_and_wire() {
|
||||||
|
let dto = DomainEventDto::from(&DomainEvent::AgentRateLimited {
|
||||||
|
agent_id: agent(11),
|
||||||
|
resets_at_ms: Some(1_700_000_000_000),
|
||||||
|
});
|
||||||
|
let json = serde_json::to_value(&dto).expect("serialisable");
|
||||||
|
assert_eq!(json["type"], "agentRateLimited");
|
||||||
|
assert_eq!(json["agentId"], agent(11).to_string());
|
||||||
|
assert_eq!(json["resetsAtMs"], 1_700_000_000_000_i64);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user