1use std::collections::{HashMap, HashSet};
25
26use crate::{Event, MessageId, PatchOperation, SubagentRunId, ToolCallId};
27use serde_json::Value;
28
29pub trait StreamTransformer: Send {
45 fn transform(&mut self, event: Event) -> Vec<Event>;
47}
48
49#[derive(Default)]
54pub struct TransformerChain {
55 transformers: Vec<Box<dyn StreamTransformer>>,
56}
57
58impl std::fmt::Debug for TransformerChain {
59 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
60 f.debug_struct("TransformerChain")
61 .field("len", &self.transformers.len())
62 .finish()
63 }
64}
65
66impl TransformerChain {
67 pub fn new() -> Self {
69 Self::default()
70 }
71
72 #[must_use]
74 pub fn with(mut self, transformer: impl StreamTransformer + 'static) -> Self {
75 self.push(transformer);
76 self
77 }
78
79 pub fn push(&mut self, transformer: impl StreamTransformer + 'static) {
81 self.transformers.push(Box::new(transformer));
82 }
83
84 pub fn len(&self) -> usize {
86 self.transformers.len()
87 }
88
89 pub fn is_empty(&self) -> bool {
91 self.transformers.is_empty()
92 }
93
94 pub fn transform(&mut self, event: Event) -> Vec<Event> {
96 let mut current = vec![event];
97 for transformer in &mut self.transformers {
98 let mut next = Vec::with_capacity(current.len());
99 for event in current.drain(..) {
100 next.extend(transformer.transform(event));
101 }
102 current = next;
103 if current.is_empty() {
104 break;
105 }
106 }
107 current
108 }
109}
110
111impl StreamTransformer for TransformerChain {
112 fn transform(&mut self, event: Event) -> Vec<Event> {
113 Self::transform(self, event)
114 }
115}
116
117#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
119enum FilterMode {
120 Allow,
121 Deny,
122}
123
124#[derive(Clone, Debug)]
140pub struct FilterToolCalls {
141 mode: FilterMode,
142 names: HashSet<String>,
143 dropped: HashSet<ToolCallId>,
144}
145
146impl FilterToolCalls {
147 pub fn allow<I, T>(names: I) -> Self
149 where
150 I: IntoIterator<Item = T>,
151 T: Into<String>,
152 {
153 Self::new(FilterMode::Allow, names)
154 }
155
156 pub fn deny<I, T>(names: I) -> Self
158 where
159 I: IntoIterator<Item = T>,
160 T: Into<String>,
161 {
162 Self::new(FilterMode::Deny, names)
163 }
164
165 fn new<I, T>(mode: FilterMode, names: I) -> Self
166 where
167 I: IntoIterator<Item = T>,
168 T: Into<String>,
169 {
170 Self {
171 mode,
172 names: names.into_iter().map(Into::into).collect(),
173 dropped: HashSet::new(),
174 }
175 }
176
177 fn passes(&self, name: &str) -> bool {
178 match self.mode {
179 FilterMode::Allow => self.names.contains(name),
180 FilterMode::Deny => !self.names.contains(name),
181 }
182 }
183
184 fn judge(&mut self, id: &ToolCallId, name: &str) -> bool {
186 if self.passes(name) {
187 self.dropped.remove(id);
188 false
189 } else {
190 self.dropped.insert(id.clone());
191 true
192 }
193 }
194}
195
196impl StreamTransformer for FilterToolCalls {
197 fn transform(&mut self, event: Event) -> Vec<Event> {
198 let drop = match &event {
199 Event::ToolCallStart(payload) => {
200 self.judge(&payload.tool_call_id, &payload.tool_call_name)
201 }
202 Event::ToolCallChunk(payload) => match (&payload.tool_call_id, &payload.tool_call_name)
203 {
204 (Some(id), Some(name)) => self.judge(id, name),
205 (Some(id), None) => self.dropped.contains(id),
206 _ => false,
207 },
208 Event::ToolCallArgs(payload) => self.dropped.contains(&payload.tool_call_id),
209 Event::ToolCallEnd(payload) => self.dropped.contains(&payload.tool_call_id),
210 Event::ToolCallResult(payload) => self.dropped.contains(&payload.tool_call_id),
211 Event::ReasoningEncryptedValue(payload) => {
212 payload.subtype == crate::ReasoningEncryptedValueSubtype::ToolCall
213 && self.dropped.contains(&ToolCallId::new(&payload.entity_id))
214 }
215 _ => false,
216 };
217
218 if drop { Vec::new() } else { vec![event] }
219 }
220}
221
222#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
224enum StateForm {
225 Snapshot,
227 Delta,
229}
230
231#[derive(Clone, Debug)]
252pub struct ToolResultToState {
253 tool_name: String,
254 form: StateForm,
255 keep_result: bool,
256 names: HashMap<ToolCallId, String>,
257}
258
259impl ToolResultToState {
260 pub fn snapshot(tool_name: impl Into<String>) -> Self {
262 Self::new(tool_name, StateForm::Snapshot)
263 }
264
265 pub fn delta(tool_name: impl Into<String>) -> Self {
268 Self::new(tool_name, StateForm::Delta)
269 }
270
271 fn new(tool_name: impl Into<String>, form: StateForm) -> Self {
272 Self {
273 tool_name: tool_name.into(),
274 form,
275 keep_result: true,
276 names: HashMap::new(),
277 }
278 }
279
280 #[must_use]
283 pub fn replacing(mut self) -> Self {
284 self.keep_result = false;
285 self
286 }
287
288 fn state_event(&self, content: &str) -> Option<Event> {
289 match self.form {
290 StateForm::Snapshot => serde_json::from_str::<Value>(content)
291 .ok()
292 .map(Event::state_snapshot),
293 StateForm::Delta => serde_json::from_str::<Vec<PatchOperation>>(content)
294 .ok()
295 .map(Event::state_delta),
296 }
297 }
298}
299
300impl StreamTransformer for ToolResultToState {
301 fn transform(&mut self, event: Event) -> Vec<Event> {
302 match &event {
303 Event::ToolCallStart(payload) => {
304 if payload.tool_call_name == self.tool_name {
305 self.names
306 .insert(payload.tool_call_id.clone(), payload.tool_call_name.clone());
307 }
308 }
309 Event::ToolCallChunk(payload) => {
310 match (&payload.tool_call_id, &payload.tool_call_name) {
311 (Some(id), Some(name)) if name == &self.tool_name => {
312 self.names.insert(id.clone(), name.clone());
313 }
314 _ => {}
315 }
316 }
317 Event::ToolCallResult(payload) => {
318 let promoted = self
319 .names
320 .remove(&payload.tool_call_id)
321 .and_then(|_| self.state_event(&payload.content))
322 .map(|mut state| {
323 if let Some(id) = &payload.subagent_run_id {
326 state.set_subagent_run_id(id.clone());
327 }
328 state
329 });
330 if let Some(state) = promoted {
331 return if self.keep_result {
332 vec![event, state]
333 } else {
334 vec![state]
335 };
336 }
337 }
338 _ => {}
339 }
340 vec![event]
341 }
342}
343
344#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
373pub enum SubagentVisibility {
374 #[default]
377 Attributed,
378 Inline,
393 Hidden,
407}
408
409impl SubagentVisibility {
410 pub fn filter(self) -> SubagentFilter {
413 SubagentFilter::new(self)
414 }
415
416 pub fn inline() -> SubagentFilter {
418 Self::Inline.filter()
419 }
420
421 pub fn hidden() -> SubagentFilter {
423 Self::Hidden.filter()
424 }
425}
426
427#[derive(Debug)]
447pub struct SubagentFilter {
448 mode: SubagentVisibility,
449 streams: Streams,
451 hidden_text: HashMap<MessageId, SubagentRunId>,
453 hidden_reasoning: HashMap<MessageId, SubagentRunId>,
457 hidden_tool_calls: HashMap<ToolCallId, SubagentRunId>,
459 activities: HashMap<MessageId, bool>,
461 open_visibility: HashMap<(Family, String), bool>,
463 call_visibility: HashMap<ToolCallId, bool>,
466}
467
468#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
470enum Family {
471 Text,
472 Reasoning,
473 Tool,
474}
475
476#[derive(Clone, Debug)]
478struct Stream {
479 family: Family,
480 id: String,
481 tool_name: Option<String>,
483 parent_message_id: Option<MessageId>,
484}
485
486#[derive(Debug, Default)]
489struct Streams {
490 parent: Option<Stream>,
491 subagents: HashMap<SubagentRunId, Stream>,
492}
493
494enum Bare {
496 No,
498 Resolved {
500 owner: Option<SubagentRunId>,
501 family: Family,
502 id: String,
503 },
504 Unresolved,
506}
507
508impl Streams {
509 fn open(&mut self, owner: &Option<SubagentRunId>, stream: Stream) {
512 let current = match owner {
513 None => &mut self.parent,
514 Some(owner) => match self.subagents.get_mut(owner) {
515 Some(current) => {
516 Self::replace(current, stream);
517 return;
518 }
519 None => {
520 self.subagents.insert(owner.clone(), stream);
521 return;
522 }
523 },
524 };
525 match current {
526 Some(current) => Self::replace(current, stream),
527 None => *current = Some(stream),
528 }
529 }
530
531 fn replace(current: &mut Stream, stream: Stream) {
532 if current.family == stream.family && current.id == stream.id {
533 if current.tool_name.is_none() {
534 current.tool_name = stream.tool_name;
535 }
536 if current.parent_message_id.is_none() {
537 current.parent_message_id = stream.parent_message_id;
538 }
539 } else {
540 *current = stream;
541 }
542 }
543
544 fn close_id(&mut self, family: Family, id: &str) {
545 if self
546 .parent
547 .as_ref()
548 .is_some_and(|open| open.family == family && open.id == id)
549 {
550 self.parent = None;
551 }
552 self.subagents
553 .retain(|_, open| !(open.family == family && open.id == id));
554 }
555
556 fn close_owner(&mut self, owner: &Option<SubagentRunId>) {
557 match owner {
558 None => self.parent = None,
559 Some(owner) => {
560 self.subagents.remove(owner);
561 }
562 }
563 }
564
565 fn clear(&mut self) {
566 self.parent = None;
567 self.subagents.clear();
568 }
569
570 fn owner_of_open(&self, family: Family, id: &str) -> Option<SubagentRunId> {
572 self.subagents
573 .iter()
574 .find(|(_, open)| open.family == family && open.id == id)
575 .map(|(owner, _)| owner.clone())
576 }
577
578 fn resolve(
582 &self,
583 family: Family,
584 tag: &Option<SubagentRunId>,
585 ) -> Option<(Option<SubagentRunId>, Stream)> {
586 let of_family = |open: &&Stream| open.family == family;
587 if let Some(owner) = tag {
588 return self
589 .subagents
590 .get(owner)
591 .filter(of_family)
592 .map(|open| (Some(owner.clone()), open.clone()));
593 }
594 if let Some(open) = self.parent.as_ref().filter(of_family) {
595 return Some((None, open.clone()));
596 }
597 let mut candidates = self
598 .subagents
599 .iter()
600 .filter(|(_, open)| open.family == family);
601 match (candidates.next(), candidates.next()) {
602 (Some((owner, open)), None) => Some((Some(owner.clone()), open.clone())),
603 _ => None,
604 }
605 }
606
607 fn observe(
610 &mut self,
611 event: &Event,
612 owner: &Option<SubagentRunId>,
613 tag: &Option<SubagentRunId>,
614 ) {
615 let text = |id: &MessageId| Stream {
616 family: Family::Text,
617 id: id.as_str().to_owned(),
618 tool_name: None,
619 parent_message_id: None,
620 };
621 let reasoning = |id: &MessageId| Stream {
622 family: Family::Reasoning,
623 id: id.as_str().to_owned(),
624 tool_name: None,
625 parent_message_id: None,
626 };
627 let tool = |id: &ToolCallId, name: Option<&String>, parent: Option<&MessageId>| Stream {
628 family: Family::Tool,
629 id: id.as_str().to_owned(),
630 tool_name: name.cloned(),
631 parent_message_id: parent.cloned(),
632 };
633 match event {
634 Event::TextMessageStart(e) => self.open(owner, text(&e.message_id)),
635 Event::TextMessageContent(e) => self.open(owner, text(&e.message_id)),
636 Event::TextMessageEnd(e) => self.close_id(Family::Text, e.message_id.as_str()),
637 Event::TextMessageChunk(e) => {
638 if let Some(id) = &e.message_id {
639 self.open(owner, text(id));
640 }
641 }
642 Event::ReasoningMessageStart(e) => self.open(owner, reasoning(&e.message_id)),
643 Event::ReasoningMessageContent(e) => self.open(owner, reasoning(&e.message_id)),
644 Event::ReasoningMessageEnd(e) => {
645 self.close_id(Family::Reasoning, e.message_id.as_str());
646 }
647 Event::ReasoningEnd(e) => self.close_id(Family::Reasoning, e.message_id.as_str()),
648 Event::ReasoningMessageChunk(e) => {
649 if let Some(id) = &e.message_id {
650 self.open(owner, reasoning(id));
651 }
652 }
653 Event::ToolCallStart(e) => self.open(
654 owner,
655 tool(
656 &e.tool_call_id,
657 Some(&e.tool_call_name),
658 e.parent_message_id.as_ref(),
659 ),
660 ),
661 Event::ToolCallArgs(e) => self.open(owner, tool(&e.tool_call_id, None, None)),
662 Event::ToolCallEnd(e) => self.close_id(Family::Tool, e.tool_call_id.as_str()),
663 Event::ToolCallChunk(e) => {
664 if let Some(id) = &e.tool_call_id {
665 self.open(
666 owner,
667 tool(id, e.tool_call_name.as_ref(), e.parent_message_id.as_ref()),
668 );
669 }
670 }
671 Event::ToolCallResult(e) => {
674 self.close_id(Family::Tool, e.tool_call_id.as_str());
675 self.close_owner(tag);
676 }
677 Event::RunFinished(_) | Event::RunError(_) => self.clear(),
678 _ => {}
679 }
680 }
681}
682
683impl SubagentFilter {
684 pub fn new(mode: SubagentVisibility) -> Self {
686 Self {
687 mode,
688 streams: Streams::default(),
689 hidden_text: HashMap::new(),
690 hidden_reasoning: HashMap::new(),
691 hidden_tool_calls: HashMap::new(),
692 activities: HashMap::new(),
693 open_visibility: HashMap::new(),
694 call_visibility: HashMap::new(),
695 }
696 }
697
698 pub fn mode(&self) -> SubagentVisibility {
700 self.mode
701 }
702
703 fn entity(event: &Event) -> Option<(Family, &str)> {
705 Some(match event {
706 Event::TextMessageStart(e) => (Family::Text, e.message_id.as_str()),
707 Event::TextMessageContent(e) => (Family::Text, e.message_id.as_str()),
708 Event::TextMessageEnd(e) => (Family::Text, e.message_id.as_str()),
709 Event::TextMessageChunk(e) => (Family::Text, e.message_id.as_ref()?.as_str()),
710 Event::ReasoningStart(e) => (Family::Reasoning, e.message_id.as_str()),
711 Event::ReasoningMessageStart(e) => (Family::Reasoning, e.message_id.as_str()),
712 Event::ReasoningMessageContent(e) => (Family::Reasoning, e.message_id.as_str()),
713 Event::ReasoningMessageEnd(e) => (Family::Reasoning, e.message_id.as_str()),
714 Event::ReasoningEnd(e) => (Family::Reasoning, e.message_id.as_str()),
715 Event::ReasoningMessageChunk(e) => (Family::Reasoning, e.message_id.as_ref()?.as_str()),
716 Event::ToolCallStart(e) => (Family::Tool, e.tool_call_id.as_str()),
717 Event::ToolCallArgs(e) => (Family::Tool, e.tool_call_id.as_str()),
718 Event::ToolCallEnd(e) => (Family::Tool, e.tool_call_id.as_str()),
719 Event::ToolCallResult(e) => (Family::Tool, e.tool_call_id.as_str()),
720 Event::ToolCallChunk(e) => (Family::Tool, e.tool_call_id.as_ref()?.as_str()),
721 _ => return None,
722 })
723 }
724
725 fn carrier(event: &Event) -> Option<&MessageId> {
727 match event {
728 Event::ToolCallStart(e) => e.parent_message_id.as_ref(),
729 Event::ToolCallChunk(e) => e.parent_message_id.as_ref(),
730 _ => None,
731 }
732 }
733
734 fn owner_for(&self, event: &Event, tag: &Option<SubagentRunId>) -> Option<SubagentRunId> {
738 if tag.is_some() {
739 return tag.clone();
740 }
741 let (family, id) = Self::entity(event)?;
742 let recorded = match family {
743 Family::Text => self.hidden_text.get(&MessageId::new(id)).cloned(),
744 Family::Reasoning => self.hidden_reasoning.get(&MessageId::new(id)).cloned(),
745 Family::Tool => Self::carrier(event)
746 .and_then(|parent| self.hidden_text.get(parent).cloned())
747 .or_else(|| self.hidden_tool_calls.get(&ToolCallId::new(id)).cloned()),
748 };
749 recorded.or_else(|| self.streams.owner_of_open(family, id))
750 }
751
752 fn fill_bare(&self, event: &mut Event, tag: &Option<SubagentRunId>) -> Bare {
756 let family = match event {
757 Event::TextMessageChunk(e) if e.message_id.is_none() => Family::Text,
758 Event::ReasoningMessageChunk(e) if e.message_id.is_none() => Family::Reasoning,
759 Event::ToolCallChunk(e) if e.tool_call_id.is_none() => Family::Tool,
760 _ => return Bare::No,
761 };
762 let Some((owner, stream)) = self.streams.resolve(family, tag) else {
763 return Bare::Unresolved;
764 };
765 let id = stream.id.clone();
766 match event {
767 Event::TextMessageChunk(e) => e.message_id = Some(MessageId::new(&id)),
768 Event::ReasoningMessageChunk(e) => e.message_id = Some(MessageId::new(&id)),
769 Event::ToolCallChunk(e) => {
770 e.tool_call_id = Some(ToolCallId::new(&id));
771 if e.tool_call_name.is_none() {
772 e.tool_call_name = stream.tool_name;
773 }
774 if e.parent_message_id.is_none() {
775 e.parent_message_id = stream.parent_message_id;
776 }
777 }
778 _ => unreachable!("matched above"),
779 }
780 Bare::Resolved { owner, family, id }
781 }
782
783 fn inline(&mut self, mut event: Event) -> Vec<Event> {
786 let tag = event.subagent_run_id().cloned();
787 if let Bare::No = self.fill_bare(&mut event, &tag) {
788 let owner = self.owner_for(&event, &tag);
789 self.streams.observe(&event, &owner, &tag);
790 }
791 let subagents_step =
792 matches!(event, Event::StepStarted(_) | Event::StepFinished(_)) && tag.is_some();
793 if subagents_step {
794 return Vec::new();
795 }
796 match &mut event {
797 Event::SubagentStarted(_) | Event::SubagentFinished(_) | Event::SubagentError(_) => {
798 return Vec::new();
799 }
800 Event::MessagesSnapshot(snapshot) => {
801 for message in &mut snapshot.messages {
802 message.set_subagent_run_id(None);
803 }
804 }
805 Event::RunStarted(started) => {
806 if let Some(input) = &mut started.input {
807 for message in &mut input.messages {
808 message.set_subagent_run_id(None);
809 }
810 }
811 }
812 Event::RunFinished(finished) => Self::strip_interrupt_tags(finished),
813 _ => {
814 event.clear_subagent_run_id();
815 }
816 }
817 vec![event]
818 }
819
820 fn strip_interrupt_tags(finished: &mut crate::RunFinishedEvent) {
825 if let Some(crate::RunOutcome::Interrupt { interrupts }) = &mut finished.outcome {
826 for interrupt in interrupts {
827 interrupt.subagent_run_id = None;
828 }
829 }
830 }
831
832 fn hidden(&mut self, mut event: Event) -> Vec<Event> {
835 let tag = event.subagent_run_id().cloned();
836 let owned = tag.is_some();
837 let keep = match self.fill_bare(&mut event, &tag) {
838 Bare::Resolved { owner, family, id } => self
840 .open_visibility
841 .get(&(family, id))
842 .copied()
843 .unwrap_or(owner.is_none()),
844 Bare::Unresolved => !owned,
845 Bare::No => {
846 let owner = self.owner_for(&event, &tag);
847 self.streams.observe(&event, &owner, &tag);
848 self.judge(&event, &tag, owner.as_ref())
849 }
850 };
851 if !keep {
852 return Vec::new();
853 }
854 match &mut event {
855 Event::MessagesSnapshot(snapshot) => {
859 self.seed_hidden(&snapshot.messages, true);
860 let hidden_calls = &self.hidden_tool_calls;
861 snapshot
862 .messages
863 .retain_mut(|message| Self::show_message(message, hidden_calls));
864 }
865 Event::RunStarted(started) => {
868 if let Some(input) = &mut started.input {
869 self.seed_hidden(&input.messages, false);
870 let hidden_calls = &self.hidden_tool_calls;
871 input
872 .messages
873 .retain_mut(|message| Self::show_message(message, hidden_calls));
874 }
875 }
876 Event::StateSnapshot(_)
877 | Event::StateDelta(_)
878 | Event::ToolCallResult(_)
879 | Event::ActivitySnapshot(_)
880 | Event::ActivityDelta(_) => {
881 event.clear_subagent_run_id();
882 }
883 Event::RunFinished(finished) => Self::strip_interrupt_tags(finished),
884 _ => {}
885 }
886 vec![event]
887 }
888
889 fn judge(
891 &mut self,
892 event: &Event,
893 tag: &Option<SubagentRunId>,
894 owner: Option<&SubagentRunId>,
895 ) -> bool {
896 let owned = tag.is_some();
897 match event {
898 Event::SubagentStarted(_) | Event::SubagentFinished(_) | Event::SubagentError(_) => {
899 false
900 }
901
902 Event::TextMessageStart(e) => self.opened(Family::Text, &e.message_id, owner),
903 Event::TextMessageChunk(e) => match &e.message_id {
904 Some(id) => self.opened(Family::Text, id, owner),
905 None => unreachable!("a bare chunk was resolved or passed through"),
906 },
907 Event::TextMessageContent(e) => self.continued(Family::Text, &e.message_id, owned),
908 Event::TextMessageEnd(e) => self.closed(Family::Text, &e.message_id, owned),
909
910 Event::ReasoningStart(e) => self.opened(Family::Reasoning, &e.message_id, owner),
911 Event::ReasoningMessageStart(e) => self.opened(Family::Reasoning, &e.message_id, owner),
912 Event::ReasoningMessageChunk(e) => match &e.message_id {
913 Some(id) => self.opened(Family::Reasoning, id, owner),
914 None => unreachable!("a bare chunk was resolved or passed through"),
915 },
916 Event::ReasoningMessageContent(e) => {
917 self.continued(Family::Reasoning, &e.message_id, owned)
918 }
919 Event::ReasoningMessageEnd(e) => self.closed(Family::Reasoning, &e.message_id, owned),
920 Event::ReasoningEnd(e) => self.closed(Family::Reasoning, &e.message_id, owned),
921
922 Event::ToolCallStart(e) => self.call_opened(&e.tool_call_id, owner),
923 Event::ToolCallChunk(e) => match &e.tool_call_id {
924 Some(id) => self.call_opened(id, owner),
925 None => unreachable!("a bare chunk was resolved or passed through"),
926 },
927 Event::ToolCallArgs(e) => self.call_continued(&e.tool_call_id, owned),
928 Event::ToolCallEnd(e) => {
929 let keep = self.call_continued(&e.tool_call_id, owned);
930 self.open_visibility
931 .remove(&(Family::Tool, e.tool_call_id.as_str().to_owned()));
932 keep
933 }
934 Event::ToolCallResult(e) => self
938 .call_visibility
939 .get(&e.tool_call_id)
940 .copied()
941 .unwrap_or_else(|| !self.hidden_tool_calls.contains_key(&e.tool_call_id)),
942
943 Event::ActivitySnapshot(e) => {
949 let existing = self.activities.get(&e.message_id).copied();
950 let hidden = match existing {
951 Some(hidden) if !e.replace => hidden,
952 _ => owned,
953 };
954 self.activities.insert(e.message_id.clone(), hidden);
955 !hidden
956 }
957 Event::ActivityDelta(e) => {
958 !self.activities.get(&e.message_id).copied().unwrap_or(false)
959 }
960
961 Event::ReasoningEncryptedValue(e) => {
964 !owned
965 && match e.subtype {
966 crate::ReasoningEncryptedValueSubtype::ToolCall => !self
967 .hidden_tool_calls
968 .contains_key(&ToolCallId::new(e.entity_id.clone())),
969 crate::ReasoningEncryptedValueSubtype::Message => {
970 let id = MessageId::new(e.entity_id.clone());
971 !self.hidden_text.contains_key(&id)
972 && !self.hidden_reasoning.contains_key(&id)
973 }
974 }
975 }
976
977 Event::StateSnapshot(_) | Event::StateDelta(_) => true,
979
980 Event::RunFinished(_) | Event::RunError(_) => {
981 self.open_visibility.clear();
982 true
983 }
984
985 _ => !owned,
986 }
987 }
988
989 fn owners_mut(&mut self, family: Family) -> &mut HashMap<MessageId, SubagentRunId> {
990 match family {
991 Family::Text => &mut self.hidden_text,
992 Family::Reasoning => &mut self.hidden_reasoning,
993 Family::Tool => unreachable!("tool calls have their own map"),
994 }
995 }
996
997 fn is_hidden(&self, family: Family, id: &str) -> bool {
998 match family {
999 Family::Text => self.hidden_text.contains_key(&MessageId::new(id)),
1000 Family::Reasoning => self.hidden_reasoning.contains_key(&MessageId::new(id)),
1001 Family::Tool => self.hidden_tool_calls.contains_key(&ToolCallId::new(id)),
1002 }
1003 }
1004
1005 fn opened(&mut self, family: Family, id: &MessageId, owner: Option<&SubagentRunId>) -> bool {
1009 if let Some(owner) = owner {
1010 self.owners_mut(family)
1011 .entry(id.clone())
1012 .or_insert_with(|| owner.clone());
1013 }
1014 *self
1015 .open_visibility
1016 .entry((family, id.as_str().to_owned()))
1017 .or_insert(owner.is_none())
1018 }
1019
1020 fn continued(&self, family: Family, id: &MessageId, owned: bool) -> bool {
1021 !owned
1022 && self
1023 .open_visibility
1024 .get(&(family, id.as_str().to_owned()))
1025 .copied()
1026 .unwrap_or_else(|| !self.is_hidden(family, id.as_str()))
1027 }
1028
1029 fn closed(&mut self, family: Family, id: &MessageId, owned: bool) -> bool {
1030 let keep = self.continued(family, id, owned);
1031 self.open_visibility
1032 .remove(&(family, id.as_str().to_owned()));
1033 keep
1034 }
1035
1036 fn call_opened(&mut self, id: &ToolCallId, owner: Option<&SubagentRunId>) -> bool {
1037 if let Some(owner) = owner {
1038 self.hidden_tool_calls
1039 .entry(id.clone())
1040 .or_insert_with(|| owner.clone());
1041 }
1042 let visible = *self
1043 .open_visibility
1044 .entry((Family::Tool, id.as_str().to_owned()))
1045 .or_insert(owner.is_none());
1046 self.call_visibility.insert(id.clone(), visible);
1047 visible
1048 }
1049
1050 fn call_continued(&self, id: &ToolCallId, owned: bool) -> bool {
1051 !owned
1052 && self
1053 .open_visibility
1054 .get(&(Family::Tool, id.as_str().to_owned()))
1055 .copied()
1056 .unwrap_or_else(|| !self.hidden_tool_calls.contains_key(id))
1057 }
1058
1059 fn seed_hidden(&mut self, messages: &[crate::Message], authoritative: bool) {
1065 for message in messages {
1066 let id = message.id().clone();
1067 if let crate::Message::Activity(_) = message {
1068 if authoritative || !self.activities.contains_key(&id) {
1069 self.activities
1070 .insert(id, message.subagent_run_id().is_some());
1071 }
1072 continue;
1073 }
1074 let owner = match message {
1075 crate::Message::Tool(tool) => {
1076 self.hidden_tool_calls.get(&tool.tool_call_id).cloned()
1077 }
1078 _ => message.subagent_run_id().cloned(),
1079 };
1080 let calls: Vec<ToolCallId> = match message {
1081 crate::Message::Assistant(assistant) => assistant
1082 .tool_calls
1083 .iter()
1084 .flatten()
1085 .map(|call| call.id.clone())
1086 .collect(),
1087 _ => Vec::new(),
1088 };
1089 let family = match message {
1090 crate::Message::Reasoning(_) => Family::Reasoning,
1091 _ => Family::Text,
1092 };
1093 match owner {
1094 Some(owner) => {
1095 for call in calls {
1096 self.hidden_tool_calls.insert(call, owner.clone());
1097 }
1098 self.owners_mut(family).insert(id, owner);
1099 }
1100 None if authoritative => {
1101 self.owners_mut(family).remove(&id);
1102 for call in &calls {
1103 self.hidden_tool_calls.remove(call);
1104 }
1105 }
1106 None => {}
1107 }
1108 }
1109 }
1110
1111 fn show_message(
1115 message: &mut crate::Message,
1116 hidden_calls: &HashMap<ToolCallId, SubagentRunId>,
1117 ) -> bool {
1118 match message {
1119 crate::Message::Tool(tool) => {
1120 if hidden_calls.contains_key(&tool.tool_call_id) {
1121 return false;
1122 }
1123 tool.subagent_run_id = None;
1124 true
1125 }
1126 _ => message.subagent_run_id().is_none(),
1127 }
1128 }
1129}
1130
1131impl StreamTransformer for SubagentFilter {
1132 fn transform(&mut self, event: Event) -> Vec<Event> {
1133 match self.mode {
1134 SubagentVisibility::Attributed => vec![event],
1135 SubagentVisibility::Inline => self.inline(event),
1136 SubagentVisibility::Hidden => self.hidden(event),
1137 }
1138 }
1139}
1140
1141#[cfg(test)]
1142mod tests {
1143 use super::*;
1144 use crate::{ReasoningEncryptedValueSubtype, TextMessageRole};
1145 use serde_json::json;
1146
1147 #[test]
1148 fn allow_list_drops_everything_unlisted() {
1149 let mut filter = FilterToolCalls::allow(["search"]);
1150 assert_eq!(
1151 filter
1152 .transform(Event::tool_call_start("c1", "search"))
1153 .len(),
1154 1
1155 );
1156 assert!(
1157 filter
1158 .transform(Event::tool_call_start("c2", "delete_everything"))
1159 .is_empty()
1160 );
1161 assert!(
1162 filter
1163 .transform(Event::tool_call_args("c2", "{}"))
1164 .is_empty()
1165 );
1166 assert!(
1167 filter
1168 .transform(Event::tool_call_result("m1", "c2", "gone"))
1169 .is_empty()
1170 );
1171 assert_eq!(filter.transform(Event::tool_call_args("c1", "{}")).len(), 1);
1172 }
1173
1174 #[test]
1175 fn filter_leaves_unrelated_events_alone() {
1176 let mut filter = FilterToolCalls::deny(["nope"]);
1177 let event = Event::text_message_start("m1", TextMessageRole::Assistant);
1178 assert_eq!(filter.transform(event.clone()), vec![event]);
1179 }
1180
1181 #[test]
1182 fn filter_drops_encrypted_reasoning_for_a_dropped_call() {
1183 let mut filter = FilterToolCalls::deny(["nope"]);
1184 filter.transform(Event::tool_call_start("c1", "nope"));
1185 let blob = Event::reasoning_encrypted_value(
1186 ReasoningEncryptedValueSubtype::ToolCall,
1187 "c1",
1188 "opaque",
1189 );
1190 assert!(filter.transform(blob).is_empty());
1191 }
1192
1193 #[test]
1194 fn promoting_keeps_the_result_by_default() {
1195 let mut promote = ToolResultToState::snapshot("load");
1196 promote.transform(Event::tool_call_start("c1", "load"));
1197 let result = Event::tool_call_result("m1", "c1", r#"{"a":1}"#);
1198 assert_eq!(
1199 promote.transform(result.clone()),
1200 vec![result, Event::state_snapshot(json!({"a": 1}))]
1201 );
1202 }
1203
1204 #[test]
1205 fn promoting_a_patch_emits_a_delta() {
1206 let mut promote = ToolResultToState::delta("patch_state").replacing();
1207 promote.transform(Event::tool_call_start("c1", "patch_state"));
1208 let content = r#"[{"op":"replace","path":"/step","value":2}]"#;
1209 assert_eq!(
1210 promote.transform(Event::tool_call_result("m1", "c1", content)),
1211 vec![Event::state_delta(vec![PatchOperation::replace(
1212 "/step", 2
1213 )])]
1214 );
1215 }
1216
1217 #[test]
1218 fn unparseable_content_passes_through_untouched() {
1219 let mut promote = ToolResultToState::snapshot("load").replacing();
1220 promote.transform(Event::tool_call_start("c1", "load"));
1221 let result = Event::tool_call_result("m1", "c1", "not json");
1222 assert_eq!(promote.transform(result.clone()), vec![result]);
1223 }
1224
1225 #[test]
1226 fn other_tools_are_not_promoted() {
1227 let mut promote = ToolResultToState::snapshot("load");
1228 promote.transform(Event::tool_call_start("c1", "something_else"));
1229 let result = Event::tool_call_result("m1", "c1", r#"{"a":1}"#);
1230 assert_eq!(promote.transform(result.clone()), vec![result]);
1231 }
1232
1233 #[test]
1234 fn chain_runs_transformers_in_order() {
1235 let mut chain = TransformerChain::new()
1236 .with(FilterToolCalls::deny(["load"]))
1237 .with(ToolResultToState::snapshot("load"));
1238 assert!(
1240 chain
1241 .transform(Event::tool_call_start("c1", "load"))
1242 .is_empty()
1243 );
1244 assert!(
1245 chain
1246 .transform(Event::tool_call_result("m1", "c1", r#"{"a":1}"#))
1247 .is_empty()
1248 );
1249 }
1250}