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::betas;
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    /// Inherited from
813    /// [`Sessions::with_research_preview`](super::sessions::Sessions::with_research_preview).
814    /// When `true`, the research-preview beta header is sent in
815    /// addition to the base managed-agents header.
816    pub(crate) research_preview: bool,
817}
818
819impl Events<'_> {
820    /// `POST /v1/sessions/{id}/events`. The server returns 204 on
821    /// success; this method returns `()`.
822    pub async fn send(&self, events: &[OutgoingUserEvent]) -> Result<()> {
823        let path = format!("/v1/sessions/{}/events", self.session_id);
824        let body = SendEventsRequest { events };
825        let _: serde_json::Value = self
826            .client
827            .execute_with_retry(
828                || {
829                    self.client
830                        .request_builder(reqwest::Method::POST, &path)
831                        .json(&body)
832                },
833                betas(self.research_preview),
834            )
835            .await?;
836        Ok(())
837    }
838
839    /// `GET /v1/sessions/{id}/events`. Returns the full event history
840    /// for the session as a [`Paginated<SessionEvent>`].
841    pub async fn list(&self) -> Result<Paginated<SessionEvent>> {
842        let path = format!("/v1/sessions/{}/events", self.session_id);
843        self.client
844            .execute_with_retry(
845                || self.client.request_builder(reqwest::Method::GET, &path),
846                betas(self.research_preview),
847            )
848            .await
849    }
850
851    /// `GET /v1/sessions/{id}/stream`. Returns an [`EventStream`]
852    /// yielding [`SessionEvent`]s as they're emitted server-side.
853    ///
854    /// **Open the stream before sending events** to avoid a race: only
855    /// events emitted *after* the stream is opened are delivered. To
856    /// reconnect to an existing session without missing events, open
857    /// the stream first, then [`list`](Self::list) the history to seed
858    /// a set of seen event IDs and skip duplicates from the live tail.
859    ///
860    /// Streaming requests are *not* retried -- a mid-stream retry
861    /// would silently drop events.
862    #[cfg(feature = "streaming")]
863    #[cfg_attr(docsrs, doc(cfg(feature = "streaming")))]
864    pub async fn stream(&self) -> Result<EventStream> {
865        let path = format!("/v1/sessions/{}/stream", self.session_id);
866        let response = self
867            .client
868            .execute_streaming(
869                self.client
870                    .request_builder(reqwest::Method::GET, &path)
871                    .header("accept", "text/event-stream"),
872                betas(self.research_preview),
873            )
874            .await?;
875        Ok(EventStream::from_response(response))
876    }
877}
878
879// =====================================================================
880// Streaming event stream
881// =====================================================================
882
883/// SSE-backed stream of [`SessionEvent`]s for a Managed Agents session.
884///
885/// Obtain via [`Events::stream`]. Iterate as a `futures_util::Stream`:
886///
887/// ```ignore
888/// use futures_util::StreamExt;
889/// let mut stream = client
890///     .managed_agents()
891///     .sessions()
892///     .events("sesn_x")
893///     .stream()
894///     .await?;
895/// while let Some(event) = stream.next().await {
896///     match event? {
897///         SessionEvent::Known(KnownSessionEvent::AgentMessage(m)) => {
898///             // handle text deltas
899///         }
900///         _ => {}
901///     }
902/// }
903/// ```
904#[cfg(feature = "streaming")]
905#[cfg_attr(docsrs, doc(cfg(feature = "streaming")))]
906pub struct EventStream {
907    inner: futures_util::stream::BoxStream<'static, Result<SessionEvent>>,
908}
909
910#[cfg(feature = "streaming")]
911impl EventStream {
912    /// Wrap a streaming HTTP response into a typed event stream.
913    pub(crate) fn from_response(response: reqwest::Response) -> Self {
914        use futures_util::StreamExt;
915        Self {
916            inner: crate::sse::into_typed_stream::<SessionEvent>(response).boxed(),
917        }
918    }
919}
920
921#[cfg(feature = "streaming")]
922impl futures_util::Stream for EventStream {
923    type Item = Result<SessionEvent>;
924
925    fn poll_next(
926        mut self: std::pin::Pin<&mut Self>,
927        cx: &mut std::task::Context<'_>,
928    ) -> std::task::Poll<Option<Self::Item>> {
929        self.inner.as_mut().poll_next(cx)
930    }
931}
932
933#[cfg(feature = "streaming")]
934impl std::fmt::Debug for EventStream {
935    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
936        f.debug_struct("EventStream").finish_non_exhaustive()
937    }
938}
939
940#[cfg(test)]
941mod tests {
942    use super::*;
943    use pretty_assertions::assert_eq;
944    use serde_json::json;
945    use wiremock::matchers::{body_partial_json, header, method, path};
946    use wiremock::{Mock, MockServer, ResponseTemplate};
947
948    fn client_for(mock: &MockServer) -> Client {
949        Client::builder()
950            .api_key("sk-ant-test")
951            .base_url(mock.uri())
952            .build()
953            .unwrap()
954    }
955
956    #[test]
957    fn known_agent_message_round_trips() {
958        let raw = json!({
959            "type": "agent.message",
960            "id": "sevt_01",
961            "processed_at": "2026-04-30T12:00:00Z",
962            "content": [{"type": "text", "text": "hello"}]
963        });
964        let ev: SessionEvent = serde_json::from_value(raw.clone()).unwrap();
965        match &ev {
966            SessionEvent::Known(KnownSessionEvent::AgentMessage(m)) => {
967                assert_eq!(m.id.as_deref(), Some("sevt_01"));
968                assert_eq!(m.content.len(), 1);
969            }
970            other => panic!("expected AgentMessage, got {other:?}"),
971        }
972        // Round-trip preserves shape (allowing field reordering).
973        let back = serde_json::to_value(&ev).unwrap();
974        assert_eq!(back, raw);
975    }
976
977    #[test]
978    fn unknown_event_type_falls_through_to_other() {
979        let raw = json!({
980            "type": "agent.future_event",
981            "id": "sevt_99",
982            "extra": [1, 2, 3]
983        });
984        let ev: SessionEvent = serde_json::from_value(raw.clone()).unwrap();
985        match &ev {
986            SessionEvent::Other(v) => assert_eq!(v, &raw),
987            SessionEvent::Known(_) => panic!("expected Other, got Known: {ev:?}"),
988        }
989        // Round-trip.
990        assert_eq!(serde_json::to_value(&ev).unwrap(), raw);
991        assert_eq!(ev.type_tag().as_deref(), Some("agent.future_event"));
992    }
993
994    #[test]
995    fn malformed_known_event_errors() {
996        // type matches "agent.tool_use" but `input` is missing.
997        let raw = json!({"type": "agent.tool_use", "name": "bash"});
998        let parsed: std::result::Result<SessionEvent, _> = serde_json::from_value(raw);
999        assert!(parsed.is_err(), "must not silently fall through to Other");
1000    }
1001
1002    #[test]
1003    fn session_status_idle_with_requires_action_decodes_event_ids() {
1004        let raw = json!({
1005            "type": "session.status_idle",
1006            "id": "sevt_77",
1007            "stop_reason": {
1008                "type": "requires_action",
1009                "event_ids": ["sevt_a", "sevt_b"]
1010            }
1011        });
1012        let ev: SessionEvent = serde_json::from_value(raw).unwrap();
1013        let SessionEvent::Known(KnownSessionEvent::SessionStatusIdle(idle)) = &ev else {
1014            panic!("expected SessionStatusIdle, got {ev:?}");
1015        };
1016        let StopReason::Known(KnownStopReason::RequiresAction { event_ids }) =
1017            idle.stop_reason.as_ref().unwrap()
1018        else {
1019            panic!("expected RequiresAction stop reason");
1020        };
1021        assert_eq!(event_ids, &["sevt_a", "sevt_b"]);
1022    }
1023
1024    #[test]
1025    fn session_status_idle_with_unknown_stop_reason_lands_in_other() {
1026        let raw = json!({
1027            "type": "session.status_idle",
1028            "stop_reason": {"type": "future_reason", "x": 1}
1029        });
1030        let ev: SessionEvent = serde_json::from_value(raw).unwrap();
1031        let SessionEvent::Known(KnownSessionEvent::SessionStatusIdle(idle)) = &ev else {
1032            panic!("expected SessionStatusIdle");
1033        };
1034        match idle.stop_reason.as_ref().unwrap() {
1035            StopReason::Other(v) => assert_eq!(v["type"], "future_reason"),
1036            StopReason::Known(_) => panic!("expected Other stop reason, got Known"),
1037        }
1038    }
1039
1040    #[test]
1041    fn outgoing_user_message_serializes_with_text_block() {
1042        let ev = OutgoingUserEvent::message("hi");
1043        let v = serde_json::to_value(&ev).unwrap();
1044        assert_eq!(
1045            v,
1046            json!({
1047                "type": "user.message",
1048                "content": [{"type": "text", "text": "hi"}]
1049            })
1050        );
1051    }
1052
1053    #[test]
1054    fn outgoing_user_interrupt_serializes_minimal_object() {
1055        let ev = OutgoingUserEvent::interrupt();
1056        let v = serde_json::to_value(&ev).unwrap();
1057        assert_eq!(v, json!({"type": "user.interrupt"}));
1058    }
1059
1060    #[test]
1061    fn outgoing_tool_confirmation_serializes_allow_and_deny() {
1062        let allow = OutgoingUserEvent::allow_tool("sevt_1");
1063        assert_eq!(
1064            serde_json::to_value(&allow).unwrap(),
1065            json!({
1066                "type": "user.tool_confirmation",
1067                "tool_use_id": "sevt_1",
1068                "result": "allow"
1069            })
1070        );
1071
1072        let deny = OutgoingUserEvent::deny_tool("sevt_2", "policy violation");
1073        assert_eq!(
1074            serde_json::to_value(&deny).unwrap(),
1075            json!({
1076                "type": "user.tool_confirmation",
1077                "tool_use_id": "sevt_2",
1078                "result": "deny",
1079                "deny_message": "policy violation"
1080            })
1081        );
1082    }
1083
1084    #[test]
1085    fn session_thread_created_event_decodes_thread_id_and_model() {
1086        let raw = json!({
1087            "type": "session.thread_created",
1088            "id": "sevt_1",
1089            "session_thread_id": "sthr_a",
1090            "model": "claude-opus-4-7"
1091        });
1092        let ev: SessionEvent = serde_json::from_value(raw).unwrap();
1093        let SessionEvent::Known(KnownSessionEvent::SessionThreadCreated(t)) = ev else {
1094            panic!("expected SessionThreadCreated");
1095        };
1096        assert_eq!(t.session_thread_id.as_deref(), Some("sthr_a"));
1097        assert_eq!(t.model.as_deref(), Some("claude-opus-4-7"));
1098    }
1099
1100    #[test]
1101    fn agent_thread_message_sent_event_decodes_to_thread_id() {
1102        let raw = json!({
1103            "type": "agent.thread_message_sent",
1104            "id": "sevt_2",
1105            "to_thread_id": "sthr_b",
1106            "content": [{"type": "text", "text": "delegate"}]
1107        });
1108        let ev: SessionEvent = serde_json::from_value(raw).unwrap();
1109        let SessionEvent::Known(KnownSessionEvent::AgentThreadMessageSent(m)) = ev else {
1110            panic!("expected AgentThreadMessageSent");
1111        };
1112        assert_eq!(m.to_thread_id.as_deref(), Some("sthr_b"));
1113    }
1114
1115    #[test]
1116    fn agent_thread_message_received_event_decodes_from_thread_id() {
1117        let raw = json!({
1118            "type": "agent.thread_message_received",
1119            "id": "sevt_3",
1120            "from_thread_id": "sthr_b",
1121            "content": [{"type": "text", "text": "done"}]
1122        });
1123        let ev: SessionEvent = serde_json::from_value(raw).unwrap();
1124        let SessionEvent::Known(KnownSessionEvent::AgentThreadMessageReceived(m)) = ev else {
1125            panic!("expected AgentThreadMessageReceived");
1126        };
1127        assert_eq!(m.from_thread_id.as_deref(), Some("sthr_b"));
1128    }
1129
1130    #[test]
1131    fn agent_tool_use_event_carries_session_thread_id_when_in_subagent_thread() {
1132        let raw = json!({
1133            "type": "agent.tool_use",
1134            "id": "sevt_4",
1135            "name": "bash",
1136            "input": {"cmd": "ls"},
1137            "session_thread_id": "sthr_b"
1138        });
1139        let ev: SessionEvent = serde_json::from_value(raw).unwrap();
1140        let SessionEvent::Known(KnownSessionEvent::AgentToolUse(t)) = ev else {
1141            panic!("expected AgentToolUse");
1142        };
1143        assert_eq!(t.session_thread_id.as_deref(), Some("sthr_b"));
1144    }
1145
1146    #[test]
1147    fn outgoing_tool_confirmation_with_thread_id_routes_reply() {
1148        let ev = OutgoingUserEvent::allow_tool("sevt_4").with_session_thread_id("sthr_b");
1149        let v = serde_json::to_value(&ev).unwrap();
1150        assert_eq!(v["session_thread_id"], "sthr_b");
1151        assert_eq!(v["type"], "user.tool_confirmation");
1152    }
1153
1154    #[test]
1155    fn outgoing_custom_tool_result_with_thread_id_routes_reply() {
1156        let ev = OutgoingUserEvent::custom_tool_result_text("sevt_5", "ok")
1157            .with_session_thread_id("sthr_c");
1158        let v = serde_json::to_value(&ev).unwrap();
1159        assert_eq!(v["session_thread_id"], "sthr_c");
1160        assert_eq!(v["custom_tool_use_id"], "sevt_5");
1161    }
1162
1163    #[test]
1164    fn outgoing_tool_confirmation_without_thread_id_omits_field() {
1165        let ev = OutgoingUserEvent::allow_tool("sevt_4");
1166        let v = serde_json::to_value(&ev).unwrap();
1167        assert!(v.get("session_thread_id").is_none(), "{v}");
1168    }
1169
1170    #[test]
1171    fn agent_evaluated_permission_round_trips_lowercase() {
1172        for (perm, wire) in [
1173            (AgentEvaluatedPermission::Allow, "allow"),
1174            (AgentEvaluatedPermission::Ask, "ask"),
1175            (AgentEvaluatedPermission::Deny, "deny"),
1176        ] {
1177            let v = serde_json::to_value(perm).unwrap();
1178            assert_eq!(v, json!(wire));
1179            let parsed: AgentEvaluatedPermission = serde_json::from_value(v).unwrap();
1180            assert_eq!(parsed, perm);
1181        }
1182    }
1183
1184    #[tokio::test]
1185    async fn events_send_posts_to_events_subpath() {
1186        let mock = MockServer::start().await;
1187        Mock::given(method("POST"))
1188            .and(path("/v1/sessions/sesn_x/events"))
1189            .and(header("anthropic-beta", "managed-agents-2026-04-01"))
1190            .and(body_partial_json(json!({
1191                "events": [
1192                    {"type": "user.message", "content": [{"type": "text", "text": "go"}]}
1193                ]
1194            })))
1195            .respond_with(ResponseTemplate::new(200).set_body_json(json!({})))
1196            .mount(&mock)
1197            .await;
1198
1199        let client = client_for(&mock);
1200        client
1201            .managed_agents()
1202            .sessions()
1203            .events("sesn_x")
1204            .send(&[OutgoingUserEvent::message("go")])
1205            .await
1206            .unwrap();
1207    }
1208
1209    #[cfg(feature = "streaming")]
1210    #[tokio::test]
1211    async fn events_stream_yields_typed_session_events() {
1212        use futures_util::StreamExt;
1213        let sse_body = concat!(
1214            "event: message\n",
1215            "data: {\"type\":\"agent.message\",\"id\":\"sevt_1\",\"content\":[{\"type\":\"text\",\"text\":\"hi\"}]}\n",
1216            "\n",
1217            "event: message\n",
1218            "data: {\"type\":\"session.status_idle\",\"id\":\"sevt_2\",\"stop_reason\":{\"type\":\"end_turn\"}}\n",
1219            "\n",
1220        );
1221
1222        let mock = MockServer::start().await;
1223        Mock::given(method("GET"))
1224            .and(path("/v1/sessions/sesn_x/stream"))
1225            .and(header("anthropic-beta", "managed-agents-2026-04-01"))
1226            .respond_with(
1227                ResponseTemplate::new(200)
1228                    .insert_header("content-type", "text/event-stream")
1229                    .set_body_string(sse_body),
1230            )
1231            .mount(&mock)
1232            .await;
1233
1234        let client = client_for(&mock);
1235        let mut stream = client
1236            .managed_agents()
1237            .sessions()
1238            .events("sesn_x")
1239            .stream()
1240            .await
1241            .unwrap();
1242
1243        let first = stream.next().await.unwrap().unwrap();
1244        assert!(matches!(
1245            first,
1246            SessionEvent::Known(KnownSessionEvent::AgentMessage(_))
1247        ));
1248
1249        let second = stream.next().await.unwrap().unwrap();
1250        let SessionEvent::Known(KnownSessionEvent::SessionStatusIdle(idle)) = second else {
1251            panic!("expected SessionStatusIdle");
1252        };
1253        assert!(matches!(
1254            idle.stop_reason,
1255            Some(StopReason::Known(KnownStopReason::EndTurn))
1256        ));
1257    }
1258
1259    #[cfg(feature = "streaming")]
1260    #[tokio::test]
1261    async fn events_stream_propagates_unauthorized_response() {
1262        let mock = MockServer::start().await;
1263        Mock::given(method("GET"))
1264            .and(path("/v1/sessions/sesn_x/stream"))
1265            .respond_with(
1266                ResponseTemplate::new(401)
1267                    .insert_header("request-id", "req_unauth")
1268                    .set_body_json(json!({
1269                        "type": "error",
1270                        "error": {"type": "authentication_error", "message": "bad key"}
1271                    })),
1272            )
1273            .mount(&mock)
1274            .await;
1275
1276        let client = client_for(&mock);
1277        let err = client
1278            .managed_agents()
1279            .sessions()
1280            .events("sesn_x")
1281            .stream()
1282            .await
1283            .unwrap_err();
1284        assert_eq!(err.status(), Some(http::StatusCode::UNAUTHORIZED));
1285        assert_eq!(err.request_id(), Some("req_unauth"));
1286    }
1287
1288    #[tokio::test]
1289    async fn events_list_returns_paginated_event_stream() {
1290        let mock = MockServer::start().await;
1291        Mock::given(method("GET"))
1292            .and(path("/v1/sessions/sesn_x/events"))
1293            .respond_with(ResponseTemplate::new(200).set_body_json(json!({
1294                "data": [
1295                    {"type": "user.message", "content": [{"type": "text", "text": "hi"}]},
1296                    {"type": "agent.message", "content": [{"type": "text", "text": "hello"}]}
1297                ],
1298                "has_more": false
1299            })))
1300            .mount(&mock)
1301            .await;
1302
1303        let client = client_for(&mock);
1304        let page = client
1305            .managed_agents()
1306            .sessions()
1307            .events("sesn_x")
1308            .list()
1309            .await
1310            .unwrap();
1311        assert_eq!(page.data.len(), 2);
1312        assert!(matches!(
1313            page.data[0],
1314            SessionEvent::Known(KnownSessionEvent::UserMessage(_))
1315        ));
1316        assert!(matches!(
1317            page.data[1],
1318            SessionEvent::Known(KnownSessionEvent::AgentMessage(_))
1319        ));
1320    }
1321}