1use 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#[derive(Debug, Clone, PartialEq)]
28pub enum SessionEvent {
29 Known(KnownSessionEvent),
31 Other(serde_json::Value),
33}
34
35const KNOWN_INCOMING_TAGS: &[&str] = &[
37 "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.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.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.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 #[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 #[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#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
131#[serde(tag = "type")]
132#[non_exhaustive]
133pub enum KnownSessionEvent {
134 #[serde(rename = "agent.message")]
139 AgentMessage(AgentMessageEvent),
140 #[serde(rename = "agent.thinking")]
142 AgentThinking(AgentThinkingEvent),
143 #[serde(rename = "agent.tool_use")]
145 AgentToolUse(AgentToolUseEvent),
146 #[serde(rename = "agent.tool_result")]
148 AgentToolResult(AgentToolResultEvent),
149 #[serde(rename = "agent.mcp_tool_use")]
151 AgentMcpToolUse(AgentMcpToolUseEvent),
152 #[serde(rename = "agent.mcp_tool_result")]
154 AgentMcpToolResult(AgentMcpToolResultEvent),
155 #[serde(rename = "agent.custom_tool_use")]
158 AgentCustomToolUse(AgentCustomToolUseEvent),
159 #[serde(rename = "agent.thread_context_compacted")]
161 AgentThreadContextCompacted(EventEnvelope),
162 #[serde(rename = "agent.thread_message_sent")]
164 AgentThreadMessageSent(AgentThreadMessageSentEvent),
165 #[serde(rename = "agent.thread_message_received")]
167 AgentThreadMessageReceived(AgentThreadMessageReceivedEvent),
168
169 #[serde(rename = "session.status_running")]
174 SessionStatusRunning(EventEnvelope),
175 #[serde(rename = "session.status_idle")]
177 SessionStatusIdle(SessionStatusIdleEvent),
178 #[serde(rename = "session.status_rescheduled")]
180 SessionStatusRescheduled(EventEnvelope),
181 #[serde(rename = "session.status_terminated")]
183 SessionStatusTerminated(EventEnvelope),
184 #[serde(rename = "session.deleted")]
187 SessionDeleted(EventEnvelope),
188 #[serde(rename = "session.error")]
190 SessionError(SessionErrorEvent),
191 #[serde(rename = "session.outcome_evaluated")]
193 SessionOutcomeEvaluated(EventEnvelope),
194 #[serde(rename = "session.thread_created")]
196 SessionThreadCreated(SessionThreadCreatedEvent),
197 #[serde(rename = "session.thread_idle")]
199 SessionThreadIdle(EventEnvelope),
200
201 #[serde(rename = "span.model_request_start")]
206 SpanModelRequestStart(EventEnvelope),
207 #[serde(rename = "span.model_request_end")]
209 SpanModelRequestEnd(SpanModelRequestEndEvent),
210 #[serde(rename = "span.outcome_evaluation_start")]
212 SpanOutcomeEvaluationStart(EventEnvelope),
213 #[serde(rename = "span.outcome_evaluation_ongoing")]
215 SpanOutcomeEvaluationOngoing(EventEnvelope),
216 #[serde(rename = "span.outcome_evaluation_end")]
218 SpanOutcomeEvaluationEnd(EventEnvelope),
219
220 #[serde(rename = "user.message")]
225 UserMessage(UserMessageEvent),
226 #[serde(rename = "user.interrupt")]
228 UserInterrupt(EventEnvelope),
229 #[serde(rename = "user.custom_tool_result")]
231 UserCustomToolResult(UserCustomToolResultEvent),
232 #[serde(rename = "user.tool_confirmation")]
234 UserToolConfirmation(UserToolConfirmationEvent),
235 #[serde(rename = "user.define_outcome")]
237 UserDefineOutcome(UserDefineOutcomeEvent),
238}
239
240#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
247#[non_exhaustive]
248pub struct EventEnvelope {
249 #[serde(default, skip_serializing_if = "Option::is_none")]
251 pub id: Option<String>,
252 #[serde(default, skip_serializing_if = "Option::is_none")]
254 pub processed_at: Option<String>,
255}
256
257#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
259#[non_exhaustive]
260pub struct AgentMessageEvent {
261 #[serde(default, skip_serializing_if = "Option::is_none")]
263 pub id: Option<String>,
264 #[serde(default, skip_serializing_if = "Option::is_none")]
266 pub processed_at: Option<String>,
267 pub content: Vec<serde_json::Value>,
271}
272
273#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
275#[non_exhaustive]
276pub struct AgentThinkingEvent {
277 #[serde(default, skip_serializing_if = "Option::is_none")]
279 pub id: Option<String>,
280 #[serde(default, skip_serializing_if = "Option::is_none")]
282 pub processed_at: Option<String>,
283 #[serde(default)]
285 pub thinking: String,
286 #[serde(default, skip_serializing_if = "Option::is_none")]
288 pub signature: Option<String>,
289}
290
291#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
295#[serde(rename_all = "lowercase")]
296#[non_exhaustive]
297pub enum AgentEvaluatedPermission {
298 Allow,
300 Ask,
303 Deny,
305}
306
307#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
309#[non_exhaustive]
310pub struct AgentToolUseEvent {
311 #[serde(default, skip_serializing_if = "Option::is_none")]
313 pub id: Option<String>,
314 #[serde(default, skip_serializing_if = "Option::is_none")]
316 pub processed_at: Option<String>,
317 pub name: String,
319 pub input: serde_json::Value,
321 #[serde(default, skip_serializing_if = "Option::is_none")]
326 pub evaluated_permission: Option<AgentEvaluatedPermission>,
327 #[serde(default, skip_serializing_if = "Option::is_none")]
337 pub session_thread_id: Option<String>,
338}
339
340#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
342#[non_exhaustive]
343pub struct AgentToolResultEvent {
344 #[serde(default, skip_serializing_if = "Option::is_none")]
346 pub id: Option<String>,
347 #[serde(default, skip_serializing_if = "Option::is_none")]
349 pub processed_at: Option<String>,
350 #[serde(default, skip_serializing_if = "Option::is_none")]
352 pub tool_use_id: Option<String>,
353 #[serde(default)]
355 pub content: serde_json::Value,
356 #[serde(default, skip_serializing_if = "Option::is_none")]
358 pub is_error: Option<bool>,
359}
360
361pub type AgentMcpToolUseEvent = AgentToolUseEvent;
363
364pub type AgentMcpToolResultEvent = AgentToolResultEvent;
366
367pub type AgentCustomToolUseEvent = AgentToolUseEvent;
371
372#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
375#[non_exhaustive]
376pub struct SessionThreadCreatedEvent {
377 #[serde(default, skip_serializing_if = "Option::is_none")]
379 pub id: Option<String>,
380 #[serde(default, skip_serializing_if = "Option::is_none")]
382 pub processed_at: Option<String>,
383 #[serde(default, skip_serializing_if = "Option::is_none")]
385 pub session_thread_id: Option<String>,
386 #[serde(default, skip_serializing_if = "Option::is_none")]
388 pub model: Option<String>,
389}
390
391#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
394#[non_exhaustive]
395pub struct AgentThreadMessageSentEvent {
396 #[serde(default, skip_serializing_if = "Option::is_none")]
398 pub id: Option<String>,
399 #[serde(default, skip_serializing_if = "Option::is_none")]
401 pub processed_at: Option<String>,
402 #[serde(default, skip_serializing_if = "Option::is_none")]
404 pub to_thread_id: Option<String>,
405 #[serde(default)]
407 pub content: serde_json::Value,
408}
409
410#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
414#[non_exhaustive]
415pub struct AgentThreadMessageReceivedEvent {
416 #[serde(default, skip_serializing_if = "Option::is_none")]
418 pub id: Option<String>,
419 #[serde(default, skip_serializing_if = "Option::is_none")]
421 pub processed_at: Option<String>,
422 #[serde(default, skip_serializing_if = "Option::is_none")]
424 pub from_thread_id: Option<String>,
425 #[serde(default)]
427 pub content: serde_json::Value,
428}
429
430#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
433#[non_exhaustive]
434pub struct SessionStatusIdleEvent {
435 #[serde(default, skip_serializing_if = "Option::is_none")]
437 pub id: Option<String>,
438 #[serde(default, skip_serializing_if = "Option::is_none")]
440 pub processed_at: Option<String>,
441 #[serde(default, skip_serializing_if = "Option::is_none")]
443 pub stop_reason: Option<StopReason>,
444}
445
446#[derive(Debug, Clone, PartialEq)]
449pub enum StopReason {
450 Known(KnownStopReason),
452 Other(serde_json::Value),
454}
455
456#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
458#[serde(tag = "type", rename_all = "snake_case")]
459#[non_exhaustive]
460pub enum KnownStopReason {
461 EndTurn,
463 RequiresAction {
467 event_ids: Vec<String>,
469 },
470 StopSequence,
472 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#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
503#[non_exhaustive]
504pub struct SessionErrorEvent {
505 #[serde(default, skip_serializing_if = "Option::is_none")]
507 pub id: Option<String>,
508 #[serde(default, skip_serializing_if = "Option::is_none")]
510 pub processed_at: Option<String>,
511 #[serde(default)]
515 pub error: serde_json::Value,
516}
517
518#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
520#[non_exhaustive]
521pub struct SpanModelRequestEndEvent {
522 #[serde(default, skip_serializing_if = "Option::is_none")]
524 pub id: Option<String>,
525 #[serde(default, skip_serializing_if = "Option::is_none")]
527 pub processed_at: Option<String>,
528 #[serde(default, skip_serializing_if = "Option::is_none")]
530 pub model_request_start_id: Option<String>,
531 #[serde(default)]
534 pub is_error: bool,
535 #[serde(default, skip_serializing_if = "Option::is_none")]
538 pub model_usage: Option<crate::types::Usage>,
539}
540
541#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
543#[non_exhaustive]
544pub struct UserMessageEvent {
545 #[serde(default, skip_serializing_if = "Option::is_none")]
547 pub id: Option<String>,
548 #[serde(default, skip_serializing_if = "Option::is_none")]
550 pub processed_at: Option<String>,
551 pub content: Vec<UserContentBlock>,
553}
554
555#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
558#[serde(tag = "type", rename_all = "snake_case")]
559#[non_exhaustive]
560pub enum UserContentBlock {
561 Text {
563 text: String,
565 },
566}
567
568impl UserContentBlock {
569 #[must_use]
571 pub fn text(text: impl Into<String>) -> Self {
572 Self::Text { text: text.into() }
573 }
574}
575
576#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
579#[non_exhaustive]
580pub struct UserCustomToolResultEvent {
581 #[serde(default, skip_serializing_if = "Option::is_none")]
583 pub id: Option<String>,
584 #[serde(default, skip_serializing_if = "Option::is_none")]
586 pub processed_at: Option<String>,
587 pub custom_tool_use_id: String,
589 pub content: Vec<UserContentBlock>,
591 #[serde(default, skip_serializing_if = "Option::is_none")]
593 pub is_error: Option<bool>,
594}
595
596#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
598#[non_exhaustive]
599pub struct UserToolConfirmationEvent {
600 #[serde(default, skip_serializing_if = "Option::is_none")]
602 pub id: Option<String>,
603 #[serde(default, skip_serializing_if = "Option::is_none")]
605 pub processed_at: Option<String>,
606 pub tool_use_id: String,
608 pub result: ConfirmationResult,
610 #[serde(default, skip_serializing_if = "Option::is_none")]
612 pub deny_message: Option<String>,
613}
614
615#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
617#[serde(rename_all = "snake_case")]
618#[non_exhaustive]
619pub enum ConfirmationResult {
620 Allow,
622 Deny,
624}
625
626#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
628#[non_exhaustive]
629pub struct UserDefineOutcomeEvent {
630 #[serde(default, skip_serializing_if = "Option::is_none")]
632 pub id: Option<String>,
633 #[serde(default, skip_serializing_if = "Option::is_none")]
635 pub processed_at: Option<String>,
636 #[serde(default, skip_serializing_if = "Option::is_none")]
638 pub outcome_id: Option<String>,
639 pub description: String,
641 pub rubric: OutcomeRubric,
643 #[serde(default, skip_serializing_if = "Option::is_none")]
645 pub max_iterations: Option<u32>,
646}
647
648#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
650#[serde(tag = "type", rename_all = "snake_case")]
651#[non_exhaustive]
652pub enum OutcomeRubric {
653 Text {
655 content: String,
657 },
658 File {
660 file_id: String,
662 },
663}
664
665#[derive(Debug, Clone, PartialEq, Serialize)]
675#[serde(tag = "type")]
676#[non_exhaustive]
677pub enum OutgoingUserEvent {
678 #[serde(rename = "user.message")]
680 Message {
681 content: Vec<UserContentBlock>,
683 },
684 #[serde(rename = "user.interrupt")]
686 Interrupt {},
687 #[serde(rename = "user.custom_tool_result")]
689 CustomToolResult {
690 custom_tool_use_id: String,
692 content: Vec<UserContentBlock>,
694 #[serde(default, skip_serializing_if = "Option::is_none")]
696 is_error: Option<bool>,
697 #[serde(default, skip_serializing_if = "Option::is_none")]
702 session_thread_id: Option<String>,
703 },
704 #[serde(rename = "user.tool_confirmation")]
706 ToolConfirmation {
707 tool_use_id: String,
709 result: ConfirmationResult,
711 #[serde(default, skip_serializing_if = "Option::is_none")]
713 deny_message: Option<String>,
714 #[serde(default, skip_serializing_if = "Option::is_none")]
717 session_thread_id: Option<String>,
718 },
719 #[serde(rename = "user.define_outcome")]
721 DefineOutcome(UserDefineOutcomeEvent),
722}
723
724impl OutgoingUserEvent {
725 #[must_use]
727 pub fn message(text: impl Into<String>) -> Self {
728 Self::Message {
729 content: vec![UserContentBlock::text(text)],
730 }
731 }
732
733 #[must_use]
735 pub fn interrupt() -> Self {
736 Self::Interrupt {}
737 }
738
739 #[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 #[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 #[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 #[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
801pub struct Events<'a> {
810 pub(crate) client: &'a Client,
811 pub(crate) session_id: String,
812}
813
814impl Events<'_> {
815 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 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 #[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#[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 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 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 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 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}