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::betas;
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 pub(crate) research_preview: bool,
817}
818
819impl Events<'_> {
820 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 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 #[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#[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 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 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 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 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}