Skip to main content

claude_api/managed_agents/
events.rs

1//! Session events: typed user / agent / session / span events.
2//!
3//! Communication with a session is event-based. You send `user.*`
4//! events and receive `agent.*`, `session.*`, and `span.*` events back.
5//! Every event in this module is forward-compatible: an unknown wire
6//! `type` tag falls through to [`SessionEvent::Other`] preserving the
7//! raw JSON, so brand-new server variants don't break the build.
8
9use serde::{Deserialize, Serialize};
10
11use crate::client::Client;
12use crate::error::Result;
13use crate::forward_compat::dispatch_known_or_other;
14use crate::pagination::Paginated;
15
16use super::MANAGED_AGENTS_BETA;
17
18// =====================================================================
19// Wire envelope
20// =====================================================================
21
22/// One event on a Managed Agents session.
23///
24/// Forward-compatible: known types deserialize into [`Self::Known`];
25/// unrecognized wire `type` tags land in [`Self::Other`] preserving the
26/// raw JSON.
27#[derive(Debug, Clone, PartialEq)]
28pub enum SessionEvent {
29    /// Recognized event.
30    Known(KnownSessionEvent),
31    /// Unknown event type; the raw JSON is preserved.
32    Other(serde_json::Value),
33}
34
35/// `type` tags this SDK version recognizes for incoming session events.
36const KNOWN_INCOMING_TAGS: &[&str] = &[
37    // Agent
38    "agent.message",
39    "agent.thinking",
40    "agent.tool_use",
41    "agent.tool_result",
42    "agent.mcp_tool_use",
43    "agent.mcp_tool_result",
44    "agent.custom_tool_use",
45    "agent.thread_context_compacted",
46    "agent.thread_message_sent",
47    "agent.thread_message_received",
48    // Session
49    "session.status_running",
50    "session.status_idle",
51    "session.status_rescheduled",
52    "session.status_terminated",
53    "session.deleted",
54    "session.error",
55    "session.outcome_evaluated",
56    "session.thread_created",
57    "session.thread_idle",
58    // Span
59    "span.model_request_start",
60    "span.model_request_end",
61    "span.outcome_evaluation_start",
62    "span.outcome_evaluation_ongoing",
63    "span.outcome_evaluation_end",
64    // User events are also visible on history reads, so include them.
65    "user.message",
66    "user.interrupt",
67    "user.custom_tool_result",
68    "user.tool_confirmation",
69    "user.define_outcome",
70];
71
72impl Serialize for SessionEvent {
73    fn serialize<S: serde::Serializer>(&self, s: S) -> std::result::Result<S::Ok, S::Error> {
74        match self {
75            Self::Known(k) => k.serialize(s),
76            Self::Other(v) => v.serialize(s),
77        }
78    }
79}
80
81impl<'de> Deserialize<'de> for SessionEvent {
82    fn deserialize<D: serde::Deserializer<'de>>(d: D) -> std::result::Result<Self, D::Error> {
83        let raw = serde_json::Value::deserialize(d)?;
84        dispatch_known_or_other(
85            raw,
86            KNOWN_INCOMING_TAGS,
87            SessionEvent::Known,
88            SessionEvent::Other,
89        )
90        .map_err(serde::de::Error::custom)
91    }
92}
93
94impl SessionEvent {
95    /// If this is a known event, return the inner [`KnownSessionEvent`].
96    #[must_use]
97    pub fn known(&self) -> Option<&KnownSessionEvent> {
98        match self {
99            Self::Known(k) => Some(k),
100            Self::Other(_) => None,
101        }
102    }
103
104    /// Wire-level `type` tag for this event regardless of variant.
105    #[must_use]
106    pub fn type_tag(&self) -> Option<String> {
107        match self {
108            Self::Known(k) => serde_json::to_value(k).ok().and_then(|v| {
109                v.get("type")
110                    .and_then(serde_json::Value::as_str)
111                    .map(String::from)
112            }),
113            Self::Other(v) => v
114                .get("type")
115                .and_then(serde_json::Value::as_str)
116                .map(String::from),
117        }
118    }
119}
120
121// =====================================================================
122// Known event union
123// =====================================================================
124
125/// All event variants this SDK version recognizes.
126///
127/// Common envelope fields (`id`, `processed_at`) are present on most
128/// events; we capture them as optional fields when the server includes
129/// them and let new fields land in `Other` via the parent enum.
130#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
131#[serde(tag = "type")]
132#[non_exhaustive]
133pub enum KnownSessionEvent {
134    // -----------------------------------------------------------------
135    // Agent events
136    // -----------------------------------------------------------------
137    /// Agent response containing text content blocks.
138    #[serde(rename = "agent.message")]
139    AgentMessage(AgentMessageEvent),
140    /// Agent thinking content, emitted separately from messages.
141    #[serde(rename = "agent.thinking")]
142    AgentThinking(AgentThinkingEvent),
143    /// Agent invokes a pre-built agent tool (bash, file ops, etc.).
144    #[serde(rename = "agent.tool_use")]
145    AgentToolUse(AgentToolUseEvent),
146    /// Result of a pre-built agent tool execution.
147    #[serde(rename = "agent.tool_result")]
148    AgentToolResult(AgentToolResultEvent),
149    /// Agent invokes an MCP server tool.
150    #[serde(rename = "agent.mcp_tool_use")]
151    AgentMcpToolUse(AgentMcpToolUseEvent),
152    /// Result of an MCP tool execution.
153    #[serde(rename = "agent.mcp_tool_result")]
154    AgentMcpToolResult(AgentMcpToolResultEvent),
155    /// Agent invokes one of your custom tools. Respond with an
156    /// [`OutgoingUserEvent::CustomToolResult`].
157    #[serde(rename = "agent.custom_tool_use")]
158    AgentCustomToolUse(AgentCustomToolUseEvent),
159    /// Conversation history was compacted.
160    #[serde(rename = "agent.thread_context_compacted")]
161    AgentThreadContextCompacted(EventEnvelope),
162    /// Agent sent a message to another multi-agent thread.
163    #[serde(rename = "agent.thread_message_sent")]
164    AgentThreadMessageSent(AgentThreadMessageSentEvent),
165    /// Agent received a message from another multi-agent thread.
166    #[serde(rename = "agent.thread_message_received")]
167    AgentThreadMessageReceived(AgentThreadMessageReceivedEvent),
168
169    // -----------------------------------------------------------------
170    // Session events
171    // -----------------------------------------------------------------
172    /// Session is now actively processing.
173    #[serde(rename = "session.status_running")]
174    SessionStatusRunning(EventEnvelope),
175    /// Session finished its current task and is waiting for input.
176    #[serde(rename = "session.status_idle")]
177    SessionStatusIdle(SessionStatusIdleEvent),
178    /// Transient error; session is auto-retrying.
179    #[serde(rename = "session.status_rescheduled")]
180    SessionStatusRescheduled(EventEnvelope),
181    /// Session ended due to an unrecoverable error.
182    #[serde(rename = "session.status_terminated")]
183    SessionStatusTerminated(EventEnvelope),
184    /// Session was deleted; emitted as the final event before the
185    /// session disappears from listings.
186    #[serde(rename = "session.deleted")]
187    SessionDeleted(EventEnvelope),
188    /// An error occurred during processing.
189    #[serde(rename = "session.error")]
190    SessionError(SessionErrorEvent),
191    /// An outcome evaluation reached a terminal status.
192    #[serde(rename = "session.outcome_evaluated")]
193    SessionOutcomeEvaluated(EventEnvelope),
194    /// Coordinator spawned a new multi-agent thread.
195    #[serde(rename = "session.thread_created")]
196    SessionThreadCreated(SessionThreadCreatedEvent),
197    /// A multi-agent thread finished its current work.
198    #[serde(rename = "session.thread_idle")]
199    SessionThreadIdle(EventEnvelope),
200
201    // -----------------------------------------------------------------
202    // Span events
203    // -----------------------------------------------------------------
204    /// A model inference call has started.
205    #[serde(rename = "span.model_request_start")]
206    SpanModelRequestStart(EventEnvelope),
207    /// A model inference call has completed.
208    #[serde(rename = "span.model_request_end")]
209    SpanModelRequestEnd(SpanModelRequestEndEvent),
210    /// Outcome evaluation has started.
211    #[serde(rename = "span.outcome_evaluation_start")]
212    SpanOutcomeEvaluationStart(EventEnvelope),
213    /// Heartbeat during an ongoing outcome evaluation.
214    #[serde(rename = "span.outcome_evaluation_ongoing")]
215    SpanOutcomeEvaluationOngoing(EventEnvelope),
216    /// Outcome evaluation has completed.
217    #[serde(rename = "span.outcome_evaluation_end")]
218    SpanOutcomeEvaluationEnd(EventEnvelope),
219
220    // -----------------------------------------------------------------
221    // User events (echoed on history reads)
222    // -----------------------------------------------------------------
223    /// User-authored text message.
224    #[serde(rename = "user.message")]
225    UserMessage(UserMessageEvent),
226    /// User-issued interrupt (no body beyond envelope).
227    #[serde(rename = "user.interrupt")]
228    UserInterrupt(EventEnvelope),
229    /// Result of a custom tool the user executed locally.
230    #[serde(rename = "user.custom_tool_result")]
231    UserCustomToolResult(UserCustomToolResultEvent),
232    /// Allow / deny a pending agent or MCP tool call.
233    #[serde(rename = "user.tool_confirmation")]
234    UserToolConfirmation(UserToolConfirmationEvent),
235    /// Define an outcome for the agent to work toward.
236    #[serde(rename = "user.define_outcome")]
237    UserDefineOutcome(UserDefineOutcomeEvent),
238}
239
240// =====================================================================
241// Per-variant payload structs
242// =====================================================================
243
244/// Common envelope fields present on most events. Used as the body of
245/// any event that has no additional payload beyond the envelope.
246#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
247#[non_exhaustive]
248pub struct EventEnvelope {
249    /// Server-assigned event ID (`sevt_...`).
250    #[serde(default, skip_serializing_if = "Option::is_none")]
251    pub id: Option<String>,
252    /// Server-side recording timestamp. `None` if the event is queued.
253    #[serde(default, skip_serializing_if = "Option::is_none")]
254    pub processed_at: Option<String>,
255}
256
257/// `agent.message`: text content blocks the agent emitted.
258#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
259#[non_exhaustive]
260pub struct AgentMessageEvent {
261    /// Envelope.
262    #[serde(default, skip_serializing_if = "Option::is_none")]
263    pub id: Option<String>,
264    /// Server-side recording timestamp.
265    #[serde(default, skip_serializing_if = "Option::is_none")]
266    pub processed_at: Option<String>,
267    /// Content blocks. Captured as raw JSON; the same `ContentBlock`
268    /// shape from the messages API applies, but we don't pin to it
269    /// here so a divergent server-side schema doesn't break parsing.
270    pub content: Vec<serde_json::Value>,
271}
272
273/// `agent.thinking`: extended-thinking content.
274#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
275#[non_exhaustive]
276pub struct AgentThinkingEvent {
277    /// Envelope.
278    #[serde(default, skip_serializing_if = "Option::is_none")]
279    pub id: Option<String>,
280    /// Server-side recording timestamp.
281    #[serde(default, skip_serializing_if = "Option::is_none")]
282    pub processed_at: Option<String>,
283    /// Thinking text.
284    #[serde(default)]
285    pub thinking: String,
286    /// Optional signature.
287    #[serde(default, skip_serializing_if = "Option::is_none")]
288    pub signature: Option<String>,
289}
290
291/// Permission verdict the platform applied to a pending tool call.
292/// Returned on [`AgentToolUseEvent`] when the agent's permission
293/// policy includes `ask`.
294#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
295#[serde(rename_all = "lowercase")]
296#[non_exhaustive]
297pub enum AgentEvaluatedPermission {
298    /// Tool call may proceed.
299    Allow,
300    /// Tool call requires user confirmation; client must reply with a
301    /// `user.tool_confirmation` event.
302    Ask,
303    /// Tool call is denied.
304    Deny,
305}
306
307/// `agent.tool_use`: agent invokes a pre-built tool.
308#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
309#[non_exhaustive]
310pub struct AgentToolUseEvent {
311    /// Envelope.
312    #[serde(default, skip_serializing_if = "Option::is_none")]
313    pub id: Option<String>,
314    /// Server-side recording timestamp.
315    #[serde(default, skip_serializing_if = "Option::is_none")]
316    pub processed_at: Option<String>,
317    /// Tool name.
318    pub name: String,
319    /// Tool input.
320    pub input: serde_json::Value,
321    /// Permission verdict the platform applied. `Ask` means the client
322    /// must reply with a `user.tool_confirmation` event before the tool
323    /// runs. `None` when the tool's permission policy is `allow` or the
324    /// field isn't reported.
325    #[serde(default, skip_serializing_if = "Option::is_none")]
326    pub evaluated_permission: Option<AgentEvaluatedPermission>,
327    /// Set on multi-agent sessions when the request originated in a
328    /// sub-agent thread. Echo this on the corresponding
329    /// [`OutgoingUserEvent::ToolConfirmation`] or
330    /// [`OutgoingUserEvent::CustomToolResult`] reply so the platform
331    /// routes it back to the waiting thread. Absent for primary-thread
332    /// events.
333    ///
334    /// **Research-preview**: only populated when multi-agent threads
335    /// are in use.
336    #[serde(default, skip_serializing_if = "Option::is_none")]
337    pub session_thread_id: Option<String>,
338}
339
340/// `agent.tool_result`.
341#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
342#[non_exhaustive]
343pub struct AgentToolResultEvent {
344    /// Envelope.
345    #[serde(default, skip_serializing_if = "Option::is_none")]
346    pub id: Option<String>,
347    /// Server-side recording timestamp.
348    #[serde(default, skip_serializing_if = "Option::is_none")]
349    pub processed_at: Option<String>,
350    /// ID of the matching `agent.tool_use` event.
351    #[serde(default, skip_serializing_if = "Option::is_none")]
352    pub tool_use_id: Option<String>,
353    /// Tool result content.
354    #[serde(default)]
355    pub content: serde_json::Value,
356    /// `true` if the tool reported an error.
357    #[serde(default, skip_serializing_if = "Option::is_none")]
358    pub is_error: Option<bool>,
359}
360
361/// `agent.mcp_tool_use`: agent invokes an MCP server tool.
362pub type AgentMcpToolUseEvent = AgentToolUseEvent;
363
364/// `agent.mcp_tool_result`.
365pub type AgentMcpToolResultEvent = AgentToolResultEvent;
366
367/// `agent.custom_tool_use`: agent invokes one of the caller's custom
368/// tools. The session pauses; respond with an
369/// [`OutgoingUserEvent::CustomToolResult`].
370pub type AgentCustomToolUseEvent = AgentToolUseEvent;
371
372/// `session.thread_created`. Carries the new thread's ID and the
373/// model the spawned agent runs against.
374#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
375#[non_exhaustive]
376pub struct SessionThreadCreatedEvent {
377    /// Envelope.
378    #[serde(default, skip_serializing_if = "Option::is_none")]
379    pub id: Option<String>,
380    /// Server-side recording timestamp.
381    #[serde(default, skip_serializing_if = "Option::is_none")]
382    pub processed_at: Option<String>,
383    /// Newly-spawned thread ID (`sthr_...`).
384    #[serde(default, skip_serializing_if = "Option::is_none")]
385    pub session_thread_id: Option<String>,
386    /// Model the spawned agent runs.
387    #[serde(default, skip_serializing_if = "Option::is_none")]
388    pub model: Option<String>,
389}
390
391/// `agent.thread_message_sent`. The agent sent a message to another
392/// thread (typically the coordinator delegating to a sub-agent).
393#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
394#[non_exhaustive]
395pub struct AgentThreadMessageSentEvent {
396    /// Envelope.
397    #[serde(default, skip_serializing_if = "Option::is_none")]
398    pub id: Option<String>,
399    /// Server-side recording timestamp.
400    #[serde(default, skip_serializing_if = "Option::is_none")]
401    pub processed_at: Option<String>,
402    /// Destination thread ID.
403    #[serde(default, skip_serializing_if = "Option::is_none")]
404    pub to_thread_id: Option<String>,
405    /// Message content as raw JSON (typed shape may evolve).
406    #[serde(default)]
407    pub content: serde_json::Value,
408}
409
410/// `agent.thread_message_received`. The agent received a message from
411/// another thread (typically a sub-agent responding to the
412/// coordinator).
413#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
414#[non_exhaustive]
415pub struct AgentThreadMessageReceivedEvent {
416    /// Envelope.
417    #[serde(default, skip_serializing_if = "Option::is_none")]
418    pub id: Option<String>,
419    /// Server-side recording timestamp.
420    #[serde(default, skip_serializing_if = "Option::is_none")]
421    pub processed_at: Option<String>,
422    /// Source thread ID.
423    #[serde(default, skip_serializing_if = "Option::is_none")]
424    pub from_thread_id: Option<String>,
425    /// Message content as raw JSON.
426    #[serde(default)]
427    pub content: serde_json::Value,
428}
429
430/// `session.status_idle`. Carries an optional `stop_reason` describing
431/// why the agent paused.
432#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
433#[non_exhaustive]
434pub struct SessionStatusIdleEvent {
435    /// Envelope.
436    #[serde(default, skip_serializing_if = "Option::is_none")]
437    pub id: Option<String>,
438    /// Server-side recording timestamp.
439    #[serde(default, skip_serializing_if = "Option::is_none")]
440    pub processed_at: Option<String>,
441    /// Why the session went idle. `None` if the server didn't send one.
442    #[serde(default, skip_serializing_if = "Option::is_none")]
443    pub stop_reason: Option<StopReason>,
444}
445
446/// Reason the session went idle. Forward-compatible: unknown `type`
447/// tags fall through to [`Self::Other`].
448#[derive(Debug, Clone, PartialEq)]
449pub enum StopReason {
450    /// Recognized stop reason.
451    Known(KnownStopReason),
452    /// Unknown stop reason; raw JSON preserved.
453    Other(serde_json::Value),
454}
455
456/// Stop-reason variants this SDK version knows about.
457#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
458#[serde(tag = "type", rename_all = "snake_case")]
459#[non_exhaustive]
460pub enum KnownStopReason {
461    /// Agent finished its turn naturally.
462    EndTurn,
463    /// One or more tool/confirmation events are blocking; the
464    /// session waits for `user.tool_confirmation` or
465    /// `user.custom_tool_result` keyed off the listed event IDs.
466    RequiresAction {
467        /// Event IDs the session is waiting on.
468        event_ids: Vec<String>,
469    },
470    /// Hit a configured stop sequence.
471    StopSequence,
472    /// Reached the max-tokens cap.
473    MaxTokens,
474}
475
476const KNOWN_STOP_REASON_TAGS: &[&str] =
477    &["end_turn", "requires_action", "stop_sequence", "max_tokens"];
478
479impl Serialize for StopReason {
480    fn serialize<S: serde::Serializer>(&self, s: S) -> std::result::Result<S::Ok, S::Error> {
481        match self {
482            Self::Known(k) => k.serialize(s),
483            Self::Other(v) => v.serialize(s),
484        }
485    }
486}
487
488impl<'de> Deserialize<'de> for StopReason {
489    fn deserialize<D: serde::Deserializer<'de>>(d: D) -> std::result::Result<Self, D::Error> {
490        let raw = serde_json::Value::deserialize(d)?;
491        dispatch_known_or_other(
492            raw,
493            KNOWN_STOP_REASON_TAGS,
494            StopReason::Known,
495            StopReason::Other,
496        )
497        .map_err(serde::de::Error::custom)
498    }
499}
500
501/// `session.error`. Includes a typed error payload with retry status.
502#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
503#[non_exhaustive]
504pub struct SessionErrorEvent {
505    /// Envelope.
506    #[serde(default, skip_serializing_if = "Option::is_none")]
507    pub id: Option<String>,
508    /// Server-side recording timestamp.
509    #[serde(default, skip_serializing_if = "Option::is_none")]
510    pub processed_at: Option<String>,
511    /// The error payload. Captured as raw JSON until the schema
512    /// stabilizes (the docs show a `retry_status` field but don't
513    /// enumerate its values).
514    #[serde(default)]
515    pub error: serde_json::Value,
516}
517
518/// `span.model_request_end`. Includes `model_usage` with token counts.
519#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
520#[non_exhaustive]
521pub struct SpanModelRequestEndEvent {
522    /// Envelope.
523    #[serde(default, skip_serializing_if = "Option::is_none")]
524    pub id: Option<String>,
525    /// Server-side recording timestamp.
526    #[serde(default, skip_serializing_if = "Option::is_none")]
527    pub processed_at: Option<String>,
528    /// ID of the matching `span.model_request_start` event.
529    #[serde(default, skip_serializing_if = "Option::is_none")]
530    pub model_request_start_id: Option<String>,
531    /// `true` if the model request errored. Inspect surrounding
532    /// `session.error` events for details.
533    #[serde(default)]
534    pub is_error: bool,
535    /// Model usage counts. Matches the [`SessionUsage`](super::sessions::SessionUsage)
536    /// shape but at finer per-call granularity.
537    #[serde(default, skip_serializing_if = "Option::is_none")]
538    pub model_usage: Option<crate::types::Usage>,
539}
540
541/// `user.message`: a text user message.
542#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
543#[non_exhaustive]
544pub struct UserMessageEvent {
545    /// Envelope.
546    #[serde(default, skip_serializing_if = "Option::is_none")]
547    pub id: Option<String>,
548    /// Server-side recording timestamp.
549    #[serde(default, skip_serializing_if = "Option::is_none")]
550    pub processed_at: Option<String>,
551    /// Content blocks. The simplest form is `[{"type":"text","text":"..."}]`.
552    pub content: Vec<UserContentBlock>,
553}
554
555/// One block of content inside a [`UserMessageEvent`] or
556/// [`UserCustomToolResultEvent`].
557#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
558#[serde(tag = "type", rename_all = "snake_case")]
559#[non_exhaustive]
560pub enum UserContentBlock {
561    /// Plain text.
562    Text {
563        /// Text body.
564        text: String,
565    },
566}
567
568impl UserContentBlock {
569    /// Convenience: build a `text` block.
570    #[must_use]
571    pub fn text(text: impl Into<String>) -> Self {
572        Self::Text { text: text.into() }
573    }
574}
575
576/// `user.custom_tool_result`: caller-side response to an
577/// `agent.custom_tool_use`.
578#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
579#[non_exhaustive]
580pub struct UserCustomToolResultEvent {
581    /// Envelope.
582    #[serde(default, skip_serializing_if = "Option::is_none")]
583    pub id: Option<String>,
584    /// Server-side recording timestamp.
585    #[serde(default, skip_serializing_if = "Option::is_none")]
586    pub processed_at: Option<String>,
587    /// ID of the matching `agent.custom_tool_use` event.
588    pub custom_tool_use_id: String,
589    /// Result content.
590    pub content: Vec<UserContentBlock>,
591    /// Optional error flag.
592    #[serde(default, skip_serializing_if = "Option::is_none")]
593    pub is_error: Option<bool>,
594}
595
596/// `user.tool_confirmation`: allow / deny a pending tool call.
597#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
598#[non_exhaustive]
599pub struct UserToolConfirmationEvent {
600    /// Envelope.
601    #[serde(default, skip_serializing_if = "Option::is_none")]
602    pub id: Option<String>,
603    /// Server-side recording timestamp.
604    #[serde(default, skip_serializing_if = "Option::is_none")]
605    pub processed_at: Option<String>,
606    /// ID of the matching `agent.tool_use` or `agent.mcp_tool_use` event.
607    pub tool_use_id: String,
608    /// Verdict.
609    pub result: ConfirmationResult,
610    /// Optional message to surface to the agent on a `Deny`.
611    #[serde(default, skip_serializing_if = "Option::is_none")]
612    pub deny_message: Option<String>,
613}
614
615/// `allow` / `deny` verdict for [`UserToolConfirmationEvent`].
616#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
617#[serde(rename_all = "snake_case")]
618#[non_exhaustive]
619pub enum ConfirmationResult {
620    /// Run the pending tool call.
621    Allow,
622    /// Skip the pending tool call. Use `deny_message` to explain.
623    Deny,
624}
625
626/// `user.define_outcome`: define an outcome the agent works toward.
627#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
628#[non_exhaustive]
629pub struct UserDefineOutcomeEvent {
630    /// Envelope.
631    #[serde(default, skip_serializing_if = "Option::is_none")]
632    pub id: Option<String>,
633    /// Server-side recording timestamp.
634    #[serde(default, skip_serializing_if = "Option::is_none")]
635    pub processed_at: Option<String>,
636    /// Server-assigned outcome ID, present on echoed events.
637    #[serde(default, skip_serializing_if = "Option::is_none")]
638    pub outcome_id: Option<String>,
639    /// Human description of the desired outcome.
640    pub description: String,
641    /// Rubric: inline text or a Files API reference.
642    pub rubric: OutcomeRubric,
643    /// Maximum revision iterations. Defaults to 3, capped at 20.
644    #[serde(default, skip_serializing_if = "Option::is_none")]
645    pub max_iterations: Option<u32>,
646}
647
648/// Rubric source for an outcome.
649#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
650#[serde(tag = "type", rename_all = "snake_case")]
651#[non_exhaustive]
652pub enum OutcomeRubric {
653    /// Inline rubric text.
654    Text {
655        /// Rubric body.
656        content: String,
657    },
658    /// Rubric stored as a [`File`](crate::files::FileMetadata).
659    File {
660        /// Files API ID.
661        file_id: String,
662    },
663}
664
665// =====================================================================
666// Send-events request shape
667// =====================================================================
668
669/// One event included in a [`Events::send`] call.
670///
671/// This is the *outgoing* form -- the user-event variants only. For the
672/// echoed / received form (which can also carry agent / session / span
673/// events) use [`SessionEvent`].
674#[derive(Debug, Clone, PartialEq, Serialize)]
675#[serde(tag = "type")]
676#[non_exhaustive]
677pub enum OutgoingUserEvent {
678    /// Send a user message.
679    #[serde(rename = "user.message")]
680    Message {
681        /// Content blocks.
682        content: Vec<UserContentBlock>,
683    },
684    /// Interrupt the agent mid-execution.
685    #[serde(rename = "user.interrupt")]
686    Interrupt {},
687    /// Respond to an `agent.custom_tool_use`.
688    #[serde(rename = "user.custom_tool_result")]
689    CustomToolResult {
690        /// ID of the matching `agent.custom_tool_use` event.
691        custom_tool_use_id: String,
692        /// Result content.
693        content: Vec<UserContentBlock>,
694        /// Optional error flag.
695        #[serde(default, skip_serializing_if = "Option::is_none")]
696        is_error: Option<bool>,
697        /// Multi-agent routing: set to the value from the originating
698        /// `agent.custom_tool_use` event's `session_thread_id` field
699        /// when responding to a sub-agent thread. Leave `None` for
700        /// primary-thread events.
701        #[serde(default, skip_serializing_if = "Option::is_none")]
702        session_thread_id: Option<String>,
703    },
704    /// Allow or deny a pending tool call.
705    #[serde(rename = "user.tool_confirmation")]
706    ToolConfirmation {
707        /// ID of the matching `agent.tool_use` event.
708        tool_use_id: String,
709        /// Allow or deny.
710        result: ConfirmationResult,
711        /// Optional explanation for a deny.
712        #[serde(default, skip_serializing_if = "Option::is_none")]
713        deny_message: Option<String>,
714        /// Multi-agent routing: set to the originating event's
715        /// `session_thread_id`. Leave `None` for primary-thread events.
716        #[serde(default, skip_serializing_if = "Option::is_none")]
717        session_thread_id: Option<String>,
718    },
719    /// Define an outcome.
720    #[serde(rename = "user.define_outcome")]
721    DefineOutcome(UserDefineOutcomeEvent),
722}
723
724impl OutgoingUserEvent {
725    /// Build a simple `user.message` from a single text string.
726    #[must_use]
727    pub fn message(text: impl Into<String>) -> Self {
728        Self::Message {
729            content: vec![UserContentBlock::text(text)],
730        }
731    }
732
733    /// Build a `user.interrupt`.
734    #[must_use]
735    pub fn interrupt() -> Self {
736        Self::Interrupt {}
737    }
738
739    /// Build a `user.tool_confirmation` (allow).
740    #[must_use]
741    pub fn allow_tool(tool_use_id: impl Into<String>) -> Self {
742        Self::ToolConfirmation {
743            tool_use_id: tool_use_id.into(),
744            result: ConfirmationResult::Allow,
745            deny_message: None,
746            session_thread_id: None,
747        }
748    }
749
750    /// Build a `user.tool_confirmation` (deny with message).
751    #[must_use]
752    pub fn deny_tool(tool_use_id: impl Into<String>, deny_message: impl Into<String>) -> Self {
753        Self::ToolConfirmation {
754            tool_use_id: tool_use_id.into(),
755            result: ConfirmationResult::Deny,
756            deny_message: Some(deny_message.into()),
757            session_thread_id: None,
758        }
759    }
760
761    /// Build a `user.custom_tool_result` with a single text block.
762    #[must_use]
763    pub fn custom_tool_result_text(
764        custom_tool_use_id: impl Into<String>,
765        text: impl Into<String>,
766    ) -> Self {
767        Self::CustomToolResult {
768            custom_tool_use_id: custom_tool_use_id.into(),
769            content: vec![UserContentBlock::text(text)],
770            is_error: None,
771            session_thread_id: None,
772        }
773    }
774
775    /// Attach a `session_thread_id` to a `ToolConfirmation` or
776    /// `CustomToolResult` event for multi-agent thread routing. No-op
777    /// on other variants.
778    #[must_use]
779    pub fn with_session_thread_id(mut self, thread_id: impl Into<String>) -> Self {
780        let id = thread_id.into();
781        match &mut self {
782            Self::ToolConfirmation {
783                session_thread_id, ..
784            }
785            | Self::CustomToolResult {
786                session_thread_id, ..
787            } => {
788                *session_thread_id = Some(id);
789            }
790            Self::Message { .. } | Self::Interrupt {} | Self::DefineOutcome(_) => {}
791        }
792        self
793    }
794}
795
796#[derive(Debug, Clone, Serialize)]
797struct SendEventsRequest<'a> {
798    events: &'a [OutgoingUserEvent],
799}
800
801// =====================================================================
802// Namespace handle (events.send / events.list)
803// =====================================================================
804
805/// Namespace handle for session-events operations.
806///
807/// Obtained via
808/// [`Sessions::events`](super::sessions::Sessions::events).
809pub struct Events<'a> {
810    pub(crate) client: &'a Client,
811    pub(crate) session_id: String,
812}
813
814impl Events<'_> {
815    /// `POST /v1/sessions/{id}/events`. The server returns 204 on
816    /// success; this method returns `()`.
817    pub async fn send(&self, events: &[OutgoingUserEvent]) -> Result<()> {
818        let path = format!("/v1/sessions/{}/events", self.session_id);
819        let body = SendEventsRequest { events };
820        let _: serde_json::Value = self
821            .client
822            .execute_with_retry(
823                || {
824                    self.client
825                        .request_builder(reqwest::Method::POST, &path)
826                        .json(&body)
827                },
828                &[MANAGED_AGENTS_BETA],
829            )
830            .await?;
831        Ok(())
832    }
833
834    /// `GET /v1/sessions/{id}/events`. Returns the full event history
835    /// for the session as a [`Paginated<SessionEvent>`].
836    pub async fn list(&self) -> Result<Paginated<SessionEvent>> {
837        let path = format!("/v1/sessions/{}/events", self.session_id);
838        self.client
839            .execute_with_retry(
840                || self.client.request_builder(reqwest::Method::GET, &path),
841                &[MANAGED_AGENTS_BETA],
842            )
843            .await
844    }
845
846    /// `GET /v1/sessions/{id}/stream`. Returns an [`EventStream`]
847    /// yielding [`SessionEvent`]s as they're emitted server-side.
848    ///
849    /// **Open the stream before sending events** to avoid a race: only
850    /// events emitted *after* the stream is opened are delivered. To
851    /// reconnect to an existing session without missing events, open
852    /// the stream first, then [`list`](Self::list) the history to seed
853    /// a set of seen event IDs and skip duplicates from the live tail.
854    ///
855    /// Streaming requests are *not* retried -- a mid-stream retry
856    /// would silently drop events.
857    #[cfg(feature = "streaming")]
858    #[cfg_attr(docsrs, doc(cfg(feature = "streaming")))]
859    pub async fn stream(&self) -> Result<EventStream> {
860        let path = format!("/v1/sessions/{}/stream", self.session_id);
861        let response = self
862            .client
863            .execute_streaming(
864                self.client
865                    .request_builder(reqwest::Method::GET, &path)
866                    .header("accept", "text/event-stream"),
867                &[MANAGED_AGENTS_BETA],
868            )
869            .await?;
870        Ok(EventStream::from_response(response))
871    }
872}
873
874// =====================================================================
875// Streaming event stream
876// =====================================================================
877
878/// SSE-backed stream of [`SessionEvent`]s for a Managed Agents session.
879///
880/// Obtain via [`Events::stream`]. Iterate as a `futures_util::Stream`:
881///
882/// ```ignore
883/// use futures_util::StreamExt;
884/// let mut stream = client
885///     .managed_agents()
886///     .sessions()
887///     .events("sesn_x")
888///     .stream()
889///     .await?;
890/// while let Some(event) = stream.next().await {
891///     match event? {
892///         SessionEvent::Known(KnownSessionEvent::AgentMessage(m)) => {
893///             // handle text deltas
894///         }
895///         _ => {}
896///     }
897/// }
898/// ```
899#[cfg(feature = "streaming")]
900#[cfg_attr(docsrs, doc(cfg(feature = "streaming")))]
901pub struct EventStream {
902    inner: futures_util::stream::BoxStream<'static, Result<SessionEvent>>,
903}
904
905#[cfg(feature = "streaming")]
906impl EventStream {
907    /// Wrap a streaming HTTP response into a typed event stream.
908    pub(crate) fn from_response(response: reqwest::Response) -> Self {
909        use futures_util::StreamExt;
910        Self {
911            inner: crate::sse::into_typed_stream::<SessionEvent>(response).boxed(),
912        }
913    }
914}
915
916#[cfg(feature = "streaming")]
917impl futures_util::Stream for EventStream {
918    type Item = Result<SessionEvent>;
919
920    fn poll_next(
921        mut self: std::pin::Pin<&mut Self>,
922        cx: &mut std::task::Context<'_>,
923    ) -> std::task::Poll<Option<Self::Item>> {
924        self.inner.as_mut().poll_next(cx)
925    }
926}
927
928#[cfg(feature = "streaming")]
929impl std::fmt::Debug for EventStream {
930    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
931        f.debug_struct("EventStream").finish_non_exhaustive()
932    }
933}
934
935#[cfg(test)]
936mod tests {
937    use super::*;
938    use pretty_assertions::assert_eq;
939    use serde_json::json;
940    use wiremock::matchers::{body_partial_json, header, method, path};
941    use wiremock::{Mock, MockServer, ResponseTemplate};
942
943    fn client_for(mock: &MockServer) -> Client {
944        Client::builder()
945            .api_key("sk-ant-test")
946            .base_url(mock.uri())
947            .build()
948            .unwrap()
949    }
950
951    #[test]
952    fn known_agent_message_round_trips() {
953        let raw = json!({
954            "type": "agent.message",
955            "id": "sevt_01",
956            "processed_at": "2026-04-30T12:00:00Z",
957            "content": [{"type": "text", "text": "hello"}]
958        });
959        let ev: SessionEvent = serde_json::from_value(raw.clone()).unwrap();
960        match &ev {
961            SessionEvent::Known(KnownSessionEvent::AgentMessage(m)) => {
962                assert_eq!(m.id.as_deref(), Some("sevt_01"));
963                assert_eq!(m.content.len(), 1);
964            }
965            other => panic!("expected AgentMessage, got {other:?}"),
966        }
967        // Round-trip preserves shape (allowing field reordering).
968        let back = serde_json::to_value(&ev).unwrap();
969        assert_eq!(back, raw);
970    }
971
972    #[test]
973    fn unknown_event_type_falls_through_to_other() {
974        let raw = json!({
975            "type": "agent.future_event",
976            "id": "sevt_99",
977            "extra": [1, 2, 3]
978        });
979        let ev: SessionEvent = serde_json::from_value(raw.clone()).unwrap();
980        match &ev {
981            SessionEvent::Other(v) => assert_eq!(v, &raw),
982            SessionEvent::Known(_) => panic!("expected Other, got Known: {ev:?}"),
983        }
984        // Round-trip.
985        assert_eq!(serde_json::to_value(&ev).unwrap(), raw);
986        assert_eq!(ev.type_tag().as_deref(), Some("agent.future_event"));
987    }
988
989    #[test]
990    fn malformed_known_event_errors() {
991        // type matches "agent.tool_use" but `input` is missing.
992        let raw = json!({"type": "agent.tool_use", "name": "bash"});
993        let parsed: std::result::Result<SessionEvent, _> = serde_json::from_value(raw);
994        assert!(parsed.is_err(), "must not silently fall through to Other");
995    }
996
997    #[test]
998    fn session_status_idle_with_requires_action_decodes_event_ids() {
999        let raw = json!({
1000            "type": "session.status_idle",
1001            "id": "sevt_77",
1002            "stop_reason": {
1003                "type": "requires_action",
1004                "event_ids": ["sevt_a", "sevt_b"]
1005            }
1006        });
1007        let ev: SessionEvent = serde_json::from_value(raw).unwrap();
1008        let SessionEvent::Known(KnownSessionEvent::SessionStatusIdle(idle)) = &ev else {
1009            panic!("expected SessionStatusIdle, got {ev:?}");
1010        };
1011        let StopReason::Known(KnownStopReason::RequiresAction { event_ids }) =
1012            idle.stop_reason.as_ref().unwrap()
1013        else {
1014            panic!("expected RequiresAction stop reason");
1015        };
1016        assert_eq!(event_ids, &["sevt_a", "sevt_b"]);
1017    }
1018
1019    #[test]
1020    fn session_status_idle_with_unknown_stop_reason_lands_in_other() {
1021        let raw = json!({
1022            "type": "session.status_idle",
1023            "stop_reason": {"type": "future_reason", "x": 1}
1024        });
1025        let ev: SessionEvent = serde_json::from_value(raw).unwrap();
1026        let SessionEvent::Known(KnownSessionEvent::SessionStatusIdle(idle)) = &ev else {
1027            panic!("expected SessionStatusIdle");
1028        };
1029        match idle.stop_reason.as_ref().unwrap() {
1030            StopReason::Other(v) => assert_eq!(v["type"], "future_reason"),
1031            StopReason::Known(_) => panic!("expected Other stop reason, got Known"),
1032        }
1033    }
1034
1035    #[test]
1036    fn outgoing_user_message_serializes_with_text_block() {
1037        let ev = OutgoingUserEvent::message("hi");
1038        let v = serde_json::to_value(&ev).unwrap();
1039        assert_eq!(
1040            v,
1041            json!({
1042                "type": "user.message",
1043                "content": [{"type": "text", "text": "hi"}]
1044            })
1045        );
1046    }
1047
1048    #[test]
1049    fn outgoing_user_interrupt_serializes_minimal_object() {
1050        let ev = OutgoingUserEvent::interrupt();
1051        let v = serde_json::to_value(&ev).unwrap();
1052        assert_eq!(v, json!({"type": "user.interrupt"}));
1053    }
1054
1055    #[test]
1056    fn outgoing_tool_confirmation_serializes_allow_and_deny() {
1057        let allow = OutgoingUserEvent::allow_tool("sevt_1");
1058        assert_eq!(
1059            serde_json::to_value(&allow).unwrap(),
1060            json!({
1061                "type": "user.tool_confirmation",
1062                "tool_use_id": "sevt_1",
1063                "result": "allow"
1064            })
1065        );
1066
1067        let deny = OutgoingUserEvent::deny_tool("sevt_2", "policy violation");
1068        assert_eq!(
1069            serde_json::to_value(&deny).unwrap(),
1070            json!({
1071                "type": "user.tool_confirmation",
1072                "tool_use_id": "sevt_2",
1073                "result": "deny",
1074                "deny_message": "policy violation"
1075            })
1076        );
1077    }
1078
1079    #[test]
1080    fn session_thread_created_event_decodes_thread_id_and_model() {
1081        let raw = json!({
1082            "type": "session.thread_created",
1083            "id": "sevt_1",
1084            "session_thread_id": "sthr_a",
1085            "model": "claude-opus-4-7"
1086        });
1087        let ev: SessionEvent = serde_json::from_value(raw).unwrap();
1088        let SessionEvent::Known(KnownSessionEvent::SessionThreadCreated(t)) = ev else {
1089            panic!("expected SessionThreadCreated");
1090        };
1091        assert_eq!(t.session_thread_id.as_deref(), Some("sthr_a"));
1092        assert_eq!(t.model.as_deref(), Some("claude-opus-4-7"));
1093    }
1094
1095    #[test]
1096    fn agent_thread_message_sent_event_decodes_to_thread_id() {
1097        let raw = json!({
1098            "type": "agent.thread_message_sent",
1099            "id": "sevt_2",
1100            "to_thread_id": "sthr_b",
1101            "content": [{"type": "text", "text": "delegate"}]
1102        });
1103        let ev: SessionEvent = serde_json::from_value(raw).unwrap();
1104        let SessionEvent::Known(KnownSessionEvent::AgentThreadMessageSent(m)) = ev else {
1105            panic!("expected AgentThreadMessageSent");
1106        };
1107        assert_eq!(m.to_thread_id.as_deref(), Some("sthr_b"));
1108    }
1109
1110    #[test]
1111    fn agent_thread_message_received_event_decodes_from_thread_id() {
1112        let raw = json!({
1113            "type": "agent.thread_message_received",
1114            "id": "sevt_3",
1115            "from_thread_id": "sthr_b",
1116            "content": [{"type": "text", "text": "done"}]
1117        });
1118        let ev: SessionEvent = serde_json::from_value(raw).unwrap();
1119        let SessionEvent::Known(KnownSessionEvent::AgentThreadMessageReceived(m)) = ev else {
1120            panic!("expected AgentThreadMessageReceived");
1121        };
1122        assert_eq!(m.from_thread_id.as_deref(), Some("sthr_b"));
1123    }
1124
1125    #[test]
1126    fn agent_tool_use_event_carries_session_thread_id_when_in_subagent_thread() {
1127        let raw = json!({
1128            "type": "agent.tool_use",
1129            "id": "sevt_4",
1130            "name": "bash",
1131            "input": {"cmd": "ls"},
1132            "session_thread_id": "sthr_b"
1133        });
1134        let ev: SessionEvent = serde_json::from_value(raw).unwrap();
1135        let SessionEvent::Known(KnownSessionEvent::AgentToolUse(t)) = ev else {
1136            panic!("expected AgentToolUse");
1137        };
1138        assert_eq!(t.session_thread_id.as_deref(), Some("sthr_b"));
1139    }
1140
1141    #[test]
1142    fn outgoing_tool_confirmation_with_thread_id_routes_reply() {
1143        let ev = OutgoingUserEvent::allow_tool("sevt_4").with_session_thread_id("sthr_b");
1144        let v = serde_json::to_value(&ev).unwrap();
1145        assert_eq!(v["session_thread_id"], "sthr_b");
1146        assert_eq!(v["type"], "user.tool_confirmation");
1147    }
1148
1149    #[test]
1150    fn outgoing_custom_tool_result_with_thread_id_routes_reply() {
1151        let ev = OutgoingUserEvent::custom_tool_result_text("sevt_5", "ok")
1152            .with_session_thread_id("sthr_c");
1153        let v = serde_json::to_value(&ev).unwrap();
1154        assert_eq!(v["session_thread_id"], "sthr_c");
1155        assert_eq!(v["custom_tool_use_id"], "sevt_5");
1156    }
1157
1158    #[test]
1159    fn outgoing_tool_confirmation_without_thread_id_omits_field() {
1160        let ev = OutgoingUserEvent::allow_tool("sevt_4");
1161        let v = serde_json::to_value(&ev).unwrap();
1162        assert!(v.get("session_thread_id").is_none(), "{v}");
1163    }
1164
1165    #[test]
1166    fn agent_evaluated_permission_round_trips_lowercase() {
1167        for (perm, wire) in [
1168            (AgentEvaluatedPermission::Allow, "allow"),
1169            (AgentEvaluatedPermission::Ask, "ask"),
1170            (AgentEvaluatedPermission::Deny, "deny"),
1171        ] {
1172            let v = serde_json::to_value(perm).unwrap();
1173            assert_eq!(v, json!(wire));
1174            let parsed: AgentEvaluatedPermission = serde_json::from_value(v).unwrap();
1175            assert_eq!(parsed, perm);
1176        }
1177    }
1178
1179    #[tokio::test]
1180    async fn events_send_posts_to_events_subpath() {
1181        let mock = MockServer::start().await;
1182        Mock::given(method("POST"))
1183            .and(path("/v1/sessions/sesn_x/events"))
1184            .and(header("anthropic-beta", "managed-agents-2026-04-01"))
1185            .and(body_partial_json(json!({
1186                "events": [
1187                    {"type": "user.message", "content": [{"type": "text", "text": "go"}]}
1188                ]
1189            })))
1190            .respond_with(ResponseTemplate::new(200).set_body_json(json!({})))
1191            .mount(&mock)
1192            .await;
1193
1194        let client = client_for(&mock);
1195        client
1196            .managed_agents()
1197            .sessions()
1198            .events("sesn_x")
1199            .send(&[OutgoingUserEvent::message("go")])
1200            .await
1201            .unwrap();
1202    }
1203
1204    #[cfg(feature = "streaming")]
1205    #[tokio::test]
1206    async fn events_stream_yields_typed_session_events() {
1207        use futures_util::StreamExt;
1208        let sse_body = concat!(
1209            "event: message\n",
1210            "data: {\"type\":\"agent.message\",\"id\":\"sevt_1\",\"content\":[{\"type\":\"text\",\"text\":\"hi\"}]}\n",
1211            "\n",
1212            "event: message\n",
1213            "data: {\"type\":\"session.status_idle\",\"id\":\"sevt_2\",\"stop_reason\":{\"type\":\"end_turn\"}}\n",
1214            "\n",
1215        );
1216
1217        let mock = MockServer::start().await;
1218        Mock::given(method("GET"))
1219            .and(path("/v1/sessions/sesn_x/stream"))
1220            .and(header("anthropic-beta", "managed-agents-2026-04-01"))
1221            .respond_with(
1222                ResponseTemplate::new(200)
1223                    .insert_header("content-type", "text/event-stream")
1224                    .set_body_string(sse_body),
1225            )
1226            .mount(&mock)
1227            .await;
1228
1229        let client = client_for(&mock);
1230        let mut stream = client
1231            .managed_agents()
1232            .sessions()
1233            .events("sesn_x")
1234            .stream()
1235            .await
1236            .unwrap();
1237
1238        let first = stream.next().await.unwrap().unwrap();
1239        assert!(matches!(
1240            first,
1241            SessionEvent::Known(KnownSessionEvent::AgentMessage(_))
1242        ));
1243
1244        let second = stream.next().await.unwrap().unwrap();
1245        let SessionEvent::Known(KnownSessionEvent::SessionStatusIdle(idle)) = second else {
1246            panic!("expected SessionStatusIdle");
1247        };
1248        assert!(matches!(
1249            idle.stop_reason,
1250            Some(StopReason::Known(KnownStopReason::EndTurn))
1251        ));
1252    }
1253
1254    #[cfg(feature = "streaming")]
1255    #[tokio::test]
1256    async fn events_stream_propagates_unauthorized_response() {
1257        let mock = MockServer::start().await;
1258        Mock::given(method("GET"))
1259            .and(path("/v1/sessions/sesn_x/stream"))
1260            .respond_with(
1261                ResponseTemplate::new(401)
1262                    .insert_header("request-id", "req_unauth")
1263                    .set_body_json(json!({
1264                        "type": "error",
1265                        "error": {"type": "authentication_error", "message": "bad key"}
1266                    })),
1267            )
1268            .mount(&mock)
1269            .await;
1270
1271        let client = client_for(&mock);
1272        let err = client
1273            .managed_agents()
1274            .sessions()
1275            .events("sesn_x")
1276            .stream()
1277            .await
1278            .unwrap_err();
1279        assert_eq!(err.status(), Some(http::StatusCode::UNAUTHORIZED));
1280        assert_eq!(err.request_id(), Some("req_unauth"));
1281    }
1282
1283    #[tokio::test]
1284    async fn events_list_returns_paginated_event_stream() {
1285        let mock = MockServer::start().await;
1286        Mock::given(method("GET"))
1287            .and(path("/v1/sessions/sesn_x/events"))
1288            .respond_with(ResponseTemplate::new(200).set_body_json(json!({
1289                "data": [
1290                    {"type": "user.message", "content": [{"type": "text", "text": "hi"}]},
1291                    {"type": "agent.message", "content": [{"type": "text", "text": "hello"}]}
1292                ],
1293                "has_more": false
1294            })))
1295            .mount(&mock)
1296            .await;
1297
1298        let client = client_for(&mock);
1299        let page = client
1300            .managed_agents()
1301            .sessions()
1302            .events("sesn_x")
1303            .list()
1304            .await
1305            .unwrap();
1306        assert_eq!(page.data.len(), 2);
1307        assert!(matches!(
1308            page.data[0],
1309            SessionEvent::Known(KnownSessionEvent::UserMessage(_))
1310        ));
1311        assert!(matches!(
1312            page.data[1],
1313            SessionEvent::Known(KnownSessionEvent::AgentMessage(_))
1314        ));
1315    }
1316}