1use std::sync::Arc;
9
10use tokio::sync::mpsc;
11
12use serde_json::Value;
13
14use crate::tools::{BashCompletionSink, ToolCall, ToolSchema};
15use crate::{AgentEvent, Session};
16use bamboo_domain::{
17 PermissionMode, SessionAuthorityIdentity, SessionKind, SupervisorReference,
18 DEFAULT_SUPERVISOR_SESSION_ID,
19};
20use uuid::Uuid;
21
22tokio::task_local! {
23 static ROOT_ACTOR_TOOL_WRITER: Option<bamboo_domain::RootActorRuntimeWrite>;
24}
25
26pub async fn with_root_actor_tool_writer<F: std::future::Future>(
29 owner: Option<bamboo_domain::RootActorRuntimeWrite>,
30 dispatch: F,
31) -> F::Output {
32 ROOT_ACTOR_TOOL_WRITER.scope(owner, dispatch).await
33}
34
35pub fn root_actor_tool_writer() -> Option<Option<bamboo_domain::RootActorRuntimeWrite>> {
38 ROOT_ACTOR_TOOL_WRITER.try_with(Clone::clone).ok()
39}
40
41#[derive(Clone, Copy, Debug, PartialEq, Eq)]
47pub struct ExecutingSupervisorObservation {
48 incarnation_id: Uuid,
49}
50
51const SUPERVISOR_PERMISSION_REPLAY_METADATA_KEY: &str = "permission.executing_supervisor.v1";
53
54#[derive(serde::Serialize, serde::Deserialize)]
55#[serde(deny_unknown_fields)]
56struct SupervisorPermissionReplayRecord {
57 version: u8,
58 incarnation_id: Uuid,
59 session_id: String,
60 result_message_id: String,
61 request_generation: String,
62 tool_call: ToolCall,
63 execution_name: String,
64}
65
66impl ExecutingSupervisorObservation {
67 pub const PERMISSION_REPLAY_METADATA_KEY: &'static str =
68 SUPERVISOR_PERMISSION_REPLAY_METADATA_KEY;
69
70 pub fn capture_from_executing_session(session: &Session) -> Option<Self> {
74 session
75 .supervisor_incarnation_id()
76 .map(|incarnation_id| Self { incarnation_id })
77 }
78
79 pub fn supervisor_reference(self) -> SupervisorReference {
82 SupervisorReference {
83 session_id: DEFAULT_SUPERVISOR_SESSION_ID.to_string(),
84 incarnation_id: self.incarnation_id,
85 }
86 }
87
88 pub fn permission_replay_record(
92 self,
93 session_id: &str,
94 result_message_id: &str,
95 tool_call: &ToolCall,
96 execution_name: &str,
97 request_generation: &str,
98 ) -> Value {
99 serde_json::to_value(SupervisorPermissionReplayRecord {
100 version: 1,
101 incarnation_id: self.incarnation_id,
102 session_id: session_id.to_string(),
103 result_message_id: result_message_id.to_string(),
104 request_generation: request_generation.to_string(),
105 tool_call: tool_call.clone(),
106 execution_name: execution_name.to_string(),
107 })
108 .expect("Supervisor permission binding serializes")
109 }
110
111 pub fn restore_permission_replay_record(
116 session: &Session,
117 result_index: usize,
118 tool_call: &ToolCall,
119 execution_name: &str,
120 request_generation: Option<&str>,
121 ) -> Result<Option<Self>, &'static str> {
122 let message = session
123 .messages
124 .get(result_index)
125 .ok_or("result occurrence missing")?;
126 if serde_json::from_str::<Value>(&message.content)
127 .ok()
128 .is_some_and(|payload| {
129 payload
130 .get(SUPERVISOR_PERMISSION_REPLAY_METADATA_KEY)
131 .is_some()
132 })
133 {
134 return Err("Supervisor authority is not accepted from a result payload");
135 }
136 let Some(value) = message
137 .metadata
138 .as_ref()
139 .and_then(|metadata| metadata.get(SUPERVISOR_PERMISSION_REPLAY_METADATA_KEY))
140 else {
141 return Ok(None);
142 };
143 let record: SupervisorPermissionReplayRecord = serde_json::from_value(value.clone())
144 .map_err(|_| "Supervisor permission binding is malformed")?;
145 let latest_result = session
146 .messages
147 .iter()
148 .rposition(|message| message.tool_call_id.as_deref() == Some(tool_call.id.as_str()));
149 let newer_call = session.messages[result_index + 1..].iter().any(|message| {
150 message
151 .tool_calls
152 .as_ref()
153 .is_some_and(|calls| calls.iter().any(|call| call.id == tool_call.id))
154 });
155 let preceding_call = session.messages[..result_index]
156 .iter()
157 .rev()
158 .find(|message| {
159 message
160 .tool_calls
161 .as_ref()
162 .is_some_and(|calls| calls.iter().any(|call| call.id == tool_call.id))
163 });
164 let exact_call = preceding_call.is_some_and(|message| {
165 message.role == bamboo_domain::Role::Assistant
166 && message.tool_calls.as_ref().is_some_and(|calls| {
167 let matching: Vec<_> = calls
168 .iter()
169 .filter(|call| call.id == tool_call.id)
170 .collect();
171 matching.len() == 1 && matching[0] == tool_call
172 })
173 });
174 if record.version != 1
175 || record.incarnation_id.is_nil()
176 || session.id != DEFAULT_SUPERVISOR_SESSION_ID
177 || session.kind != SessionKind::Root
178 || session.root_session_id != session.id
179 || session.parent_session_id.is_some()
180 || session.spawn_depth != 0
181 || session.authority_identity
182 != (SessionAuthorityIdentity::Supervisor {
183 incarnation_id: record.incarnation_id,
184 })
185 || message.role != bamboo_domain::Role::Tool
186 || latest_result != Some(result_index)
187 || newer_call
188 || !exact_call
189 || record.session_id != session.id
190 || record.result_message_id != message.id
191 || record.tool_call != *tool_call
192 || record.execution_name != execution_name
193 || execution_name.trim().is_empty()
194 || record.request_generation.trim().is_empty()
195 || Some(record.request_generation.as_str()) != request_generation
196 {
197 return Err("Supervisor permission binding does not match the current operation");
198 }
199 Ok(Some(Self {
200 incarnation_id: record.incarnation_id,
201 }))
202 }
203
204 pub(super) fn for_caller(self, session_id: Option<&str>) -> Option<Self> {
205 (session_id == Some(DEFAULT_SUPERVISOR_SESSION_ID)).then_some(self)
206 }
207}
208
209#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
219pub struct ToolExecutionSessionFlags {
220 pub bypass_permissions: bool,
223 pub auto_approve_permissions: bool,
226 pub plan_read_only: bool,
229}
230
231impl ToolExecutionSessionFlags {
232 pub fn from_session(session: &Session) -> Self {
235 Self::from_session_and_configured_mode(session, PermissionMode::Default)
236 }
237
238 pub fn from_session_and_configured_mode(
243 session: &Session,
244 configured_mode: PermissionMode,
245 ) -> Self {
246 let requested = session
247 .agent_runtime_state
248 .as_ref()
249 .map(|state| state.effective_permission_mode())
250 .unwrap_or_default();
251 let configured_mode = if session
252 .agent_runtime_state
253 .as_ref()
254 .is_some_and(|state| state.plan_mode.is_some())
255 {
256 PermissionMode::Plan
257 } else {
258 configured_mode
259 };
260 let resolution = bamboo_domain::resolve_permission_mode(requested, configured_mode);
261 Self {
262 bypass_permissions: resolution.bypass_permissions(),
263 auto_approve_permissions: resolution.auto_approve_permissions(),
264 plan_read_only: resolution.effective == PermissionMode::Plan,
265 }
266 }
267}
268
269#[derive(Clone, Copy)]
280pub struct ToolExecutionContext<'a> {
281 pub session_id: Option<&'a str>,
283 pub executing_supervisor: Option<ExecutingSupervisorObservation>,
285 pub root_session_id: Option<&'a str>,
289 pub tool_call_id: &'a str,
291 pub event_tx: Option<&'a mpsc::Sender<AgentEvent>>,
293 pub available_tool_schemas: Option<&'a [ToolSchema]>,
295 pub bypass_permissions: bool,
299 pub auto_approve_permissions: bool,
302 pub plan_read_only: bool,
304 pub can_async_resume: bool,
314 pub bash_completion_sink: Option<&'a Arc<dyn BashCompletionSink>>,
325 pub pre_parsed_args: Option<&'a Value>,
335}
336
337impl std::fmt::Debug for ToolExecutionContext<'_> {
340 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
341 f.debug_struct("ToolExecutionContext")
342 .field("session_id", &self.session_id)
343 .field("executing_supervisor", &self.executing_supervisor)
344 .field("root_session_id", &self.root_session_id)
345 .field("tool_call_id", &self.tool_call_id)
346 .field("event_tx", &self.event_tx)
347 .field("available_tool_schemas", &self.available_tool_schemas)
348 .field("bypass_permissions", &self.bypass_permissions)
349 .field("auto_approve_permissions", &self.auto_approve_permissions)
350 .field("plan_read_only", &self.plan_read_only)
351 .field("can_async_resume", &self.can_async_resume)
352 .field("bash_completion_sink", &self.bash_completion_sink.is_some())
353 .field("pre_parsed_args", &self.pre_parsed_args)
354 .finish()
355 }
356}
357
358impl<'a> ToolExecutionContext<'a> {
359 pub fn none(tool_call_id: &'a str) -> Self {
360 Self {
361 session_id: None,
362 executing_supervisor: None,
363 root_session_id: None,
364 tool_call_id,
365 event_tx: None,
366 available_tool_schemas: None,
367 bypass_permissions: false,
368 auto_approve_permissions: false,
369 plan_read_only: false,
370 can_async_resume: false,
371 bash_completion_sink: None,
372 pre_parsed_args: None,
373 }
374 }
375
376 #[allow(clippy::too_many_arguments)]
384 pub fn for_dispatch(
385 session_id: &'a str,
386 root_session_id: &'a str,
387 tool_call_id: &'a str,
388 event_tx: &'a mpsc::Sender<AgentEvent>,
389 available_tool_schemas: &'a [ToolSchema],
390 flags: ToolExecutionSessionFlags,
391 can_async_resume: bool,
396 bash_completion_sink: Option<&'a Arc<dyn BashCompletionSink>>,
401 pre_parsed_args: Option<&'a Value>,
409 ) -> Self {
410 Self {
411 session_id: Some(session_id),
412 executing_supervisor: None,
413 root_session_id: Some(if root_session_id.trim().is_empty() {
414 session_id
415 } else {
416 root_session_id
417 }),
418 tool_call_id,
419 event_tx: Some(event_tx),
420 available_tool_schemas: Some(available_tool_schemas),
421 bypass_permissions: flags.bypass_permissions,
422 auto_approve_permissions: flags.auto_approve_permissions,
423 plan_read_only: flags.plan_read_only,
424 can_async_resume,
425 bash_completion_sink,
426 pre_parsed_args,
427 }
428 }
429
430 pub fn with_executing_supervisor(
433 mut self,
434 observation: Option<ExecutingSupervisorObservation>,
435 ) -> Self {
436 self.executing_supervisor =
437 observation.and_then(|observation| observation.for_caller(self.session_id));
438 self
439 }
440
441 pub fn cloned_sender(&self) -> Option<mpsc::Sender<AgentEvent>> {
443 self.event_tx.cloned()
444 }
445
446 pub fn cloned_bash_completion_sink(&self) -> Option<Arc<dyn BashCompletionSink>> {
451 self.bash_completion_sink.map(Arc::clone)
452 }
453
454 pub fn root_session_id(&self) -> Option<&'a str> {
456 self.root_session_id
457 }
458
459 pub fn to_tool_ctx(&self) -> crate::tools::ToolCtx {
466 crate::tools::ToolCtx {
467 session_id: self.session_id.map(Arc::from),
468 executing_supervisor: self
469 .executing_supervisor
470 .and_then(|observation| observation.for_caller(self.session_id)),
471 tool_call_id: Arc::from(self.tool_call_id),
472 event_tx: self.event_tx.cloned(),
473 available_tool_schemas: self
474 .available_tool_schemas
475 .map(Arc::from)
476 .unwrap_or_else(|| Arc::from(Vec::new())),
477 bypass_permissions: self.bypass_permissions,
478 auto_approve_permissions: self.auto_approve_permissions,
479 plan_read_only: self.plan_read_only,
480 can_async_resume: self.can_async_resume,
481 async_completion_sink: None,
482 bash_completion_sink: self.bash_completion_sink.map(Arc::clone),
483 }
484 }
485
486 pub async fn emit(&self, event: AgentEvent) {
488 if let Some(tx) = self.event_tx {
489 let event = match event {
493 AgentEvent::Token { content } => AgentEvent::ToolToken {
494 tool_call_id: self.tool_call_id.to_string(),
495 content,
496 },
497 other => other,
498 };
499 let _ = tx.try_send(event);
500 }
501 }
502
503 pub async fn emit_tool_token(&self, content: impl Into<String>) {
505 self.emit(AgentEvent::ToolToken {
506 tool_call_id: self.tool_call_id.to_string(),
507 content: content.into(),
508 })
509 .await;
510 }
511}
512
513#[cfg(test)]
514mod supervisor_observation_tests {
515 use super::*;
516
517 fn supervisor() -> Session {
518 let mut session = Session::new(DEFAULT_SUPERVISOR_SESSION_ID, "model");
519 session.authority_identity = SessionAuthorityIdentity::Supervisor {
520 incarnation_id: Uuid::new_v4(),
521 };
522 session
523 }
524
525 #[test]
526 fn supervisor_observation_requires_typed_canonical_root_lineage() {
527 let valid = supervisor();
528 let observed = ExecutingSupervisorObservation::capture_from_executing_session(&valid)
529 .expect("canonical executing Supervisor");
530 assert_eq!(observed.supervisor_reference().session_id, valid.id);
531 for mutation in [
532 |s: &mut Session| s.authority_identity = SessionAuthorityIdentity::Ordinary,
533 |s: &mut Session| s.id = "other".into(),
534 |s: &mut Session| s.root_session_id = "other".into(),
535 |s: &mut Session| s.parent_session_id = Some("parent".into()),
536 |s: &mut Session| s.spawn_depth = 1,
537 |s: &mut Session| s.kind = SessionKind::Child,
538 |s: &mut Session| {
539 s.authority_identity = SessionAuthorityIdentity::Supervisor {
540 incarnation_id: Uuid::nil(),
541 }
542 },
543 ] {
544 let mut malformed = valid.clone();
545 mutation(&mut malformed);
546 assert_eq!(
547 ExecutingSupervisorObservation::capture_from_executing_session(&malformed),
548 None,
549 "malformed Session: {malformed:?}"
550 );
551 }
552 let mut ordinary = Session::new(DEFAULT_SUPERVISOR_SESSION_ID, "model");
553 ordinary.metadata.insert(
554 "authority_identity".into(),
555 serde_json::to_string(&valid.authority_identity).unwrap(),
556 );
557 ordinary.metadata.insert("supervisor".into(), "true".into());
558 assert_eq!(
559 ExecutingSupervisorObservation::capture_from_executing_session(&ordinary),
560 None,
561 "reserved ID and forged metadata cannot supply a typed identity"
562 );
563 }
564
565 #[test]
566 fn supervisor_observation_is_not_minted_by_flags_arguments_or_generic_contexts() {
567 let session = supervisor();
568 let (tx, _rx) = mpsc::channel(1);
569 let forged_args = serde_json::json!({
570 "executing_supervisor": session.authority_identity,
571 "session_id": DEFAULT_SUPERVISOR_SESSION_ID,
572 });
573 let ctx = ToolExecutionContext::for_dispatch(
574 &session.id,
575 &session.root_session_id,
576 "call",
577 &tx,
578 &[],
579 ToolExecutionSessionFlags {
580 bypass_permissions: true,
581 auto_approve_permissions: true,
582 plan_read_only: true,
583 },
584 false,
585 None,
586 Some(&forged_args),
587 );
588 assert_eq!(ctx.executing_supervisor, None);
589 assert_eq!(ctx.to_tool_ctx().executing_supervisor, None);
590 assert_eq!(
591 ToolExecutionContext::none("none").executing_supervisor,
592 None
593 );
594 assert_eq!(
595 crate::tools::ToolCtx::none("owned").executing_supervisor,
596 None
597 );
598
599 let observation = ExecutingSupervisorObservation::capture_from_executing_session(&session);
600 let captured = ctx.with_executing_supervisor(observation);
601 let owned = captured.to_tool_ctx().clone();
602 assert_eq!(owned.executing_supervisor_for(&session.id), observation);
603 assert_eq!(owned.executing_supervisor_for("child"), None);
604 assert_eq!(
605 ToolExecutionContext::none("missing")
606 .with_executing_supervisor(observation)
607 .executing_supervisor,
608 None
609 );
610 let mismatched = ToolExecutionContext {
612 session_id: Some("child"),
613 ..captured
614 };
615 assert_eq!(mismatched.to_tool_ctx().executing_supervisor, None);
616 }
617}
618
619#[cfg(test)]
620mod session_flags_tests {
621 use super::*;
622 use bamboo_domain::{AgentRuntimeState, SessionPermissionMode};
623
624 #[test]
625 fn dispatch_context_falls_back_from_empty_legacy_root_to_session_id() {
626 let (event_tx, _event_rx) = mpsc::channel(1);
627 let ctx = ToolExecutionContext::for_dispatch(
628 "session-id",
629 " ",
630 "call-id",
631 &event_tx,
632 &[],
633 ToolExecutionSessionFlags::default(),
634 false,
635 None,
636 None,
637 );
638
639 assert_eq!(ctx.root_session_id(), Some("session-id"));
640 }
641
642 #[test]
643 fn from_session_defaults_false_without_runtime_state() {
644 let session = Session::new("s-none", "test-model");
645 assert_eq!(
646 ToolExecutionSessionFlags::from_session(&session),
647 ToolExecutionSessionFlags {
648 bypass_permissions: false,
649 auto_approve_permissions: false,
650 plan_read_only: false,
651 }
652 );
653 }
654
655 #[test]
656 fn from_session_reads_bypass_from_runtime_state() {
657 let mut session = Session::new("s-bypass", "test-model");
658 let mut runtime = AgentRuntimeState::new("run-1");
659 runtime.bypass_permissions = true;
660 session.agent_runtime_state = Some(runtime);
661 assert!(ToolExecutionSessionFlags::from_session(&session).bypass_permissions);
662 assert!(!ToolExecutionSessionFlags::from_session(&session).auto_approve_permissions);
663 }
664
665 #[test]
666 fn from_session_distinguishes_auto_from_legacy_bypass() {
667 let mut session = Session::new("s-auto", "test-model");
668 let mut runtime = AgentRuntimeState::new("run-1");
669 runtime.set_permission_mode(SessionPermissionMode::Auto);
670 session.agent_runtime_state = Some(runtime);
671
672 let flags = ToolExecutionSessionFlags::from_session(&session);
673 assert!(!flags.bypass_permissions);
674 assert!(flags.auto_approve_permissions);
675 }
676
677 #[test]
678 fn configured_auto_applies_to_default_but_not_explicit_bypass() {
679 let default_session = Session::new("s-global-auto", "test-model");
680 let flags = ToolExecutionSessionFlags::from_session_and_configured_mode(
681 &default_session,
682 PermissionMode::Auto,
683 );
684 assert_eq!(
685 flags,
686 ToolExecutionSessionFlags {
687 bypass_permissions: false,
688 auto_approve_permissions: true,
689 plan_read_only: false,
690 }
691 );
692
693 let mut bypass_session = default_session;
694 bypass_session
695 .agent_runtime_state
696 .get_or_insert_default()
697 .set_permission_mode(SessionPermissionMode::Bypass);
698 let flags = ToolExecutionSessionFlags::from_session_and_configured_mode(
699 &bypass_session,
700 PermissionMode::Auto,
701 );
702 assert!(flags.bypass_permissions);
703 assert!(!flags.auto_approve_permissions);
704 }
705
706 #[test]
707 fn configured_plan_preserves_no_prompt_but_sets_read_only_gate() {
708 let mut session = Session::new("s-plan-auto", "test-model");
709 session
710 .agent_runtime_state
711 .get_or_insert_default()
712 .set_permission_mode(SessionPermissionMode::Auto);
713 let flags = ToolExecutionSessionFlags::from_session_and_configured_mode(
714 &session,
715 PermissionMode::Plan,
716 );
717 assert!(!flags.bypass_permissions);
718 assert!(flags.auto_approve_permissions);
719 assert!(flags.plan_read_only);
720 }
721
722 #[test]
723 fn persisted_plan_overlay_is_honored_without_config_reconstruction() {
724 let mut session = Session::new("s-persisted-plan-auto", "test-model");
725 let runtime = session.agent_runtime_state.get_or_insert_default();
726 runtime.set_permission_mode(SessionPermissionMode::Auto);
727 runtime.plan_mode = Some(bamboo_domain::PlanModeState {
728 entered_at: chrono::Utc::now(),
729 pre_permission_mode: "auto".to_string(),
730 plan_file_path: None,
731 status: bamboo_domain::PlanModeStatus::Designing,
732 });
733
734 let flags = ToolExecutionSessionFlags::from_session(&session);
735 assert!(flags.plan_read_only);
736 assert!(flags.auto_approve_permissions);
737 assert!(!flags.bypass_permissions);
738 }
739
740 #[test]
741 fn typed_read_only_child_does_not_reactivate_the_legacy_plan_name_gate() {
742 for requested in [SessionPermissionMode::Auto, SessionPermissionMode::Bypass] {
743 let mut session = Session::new("s-read-only-child", "test-model");
744 let runtime = session.agent_runtime_state.get_or_insert_default();
745 runtime.set_permission_mode(requested);
746 runtime.read_only = true;
747
748 let flags = ToolExecutionSessionFlags::from_session_and_configured_mode(
753 &session,
754 PermissionMode::Auto,
755 );
756
757 assert!(!flags.plan_read_only);
758 assert_eq!(
759 flags.bypass_permissions,
760 requested == SessionPermissionMode::Bypass
761 );
762 assert_eq!(
763 flags.auto_approve_permissions,
764 requested == SessionPermissionMode::Auto
765 );
766 }
767 }
768
769 #[test]
770 fn for_dispatch_maps_flags_onto_context() {
771 let (tx, _rx) = mpsc::channel(1);
772 let ctx = ToolExecutionContext::for_dispatch(
773 "s1",
774 "root-s1",
775 "call-1",
776 &tx,
777 &[],
778 ToolExecutionSessionFlags {
779 bypass_permissions: true,
780 auto_approve_permissions: false,
781 plan_read_only: false,
782 },
783 true,
784 None,
785 None,
786 );
787 assert_eq!(ctx.session_id, Some("s1"));
788 assert_eq!(ctx.root_session_id(), Some("root-s1"));
789 assert!(ctx.bypass_permissions);
790 assert!(!ctx.auto_approve_permissions);
791 assert!(ctx.can_async_resume);
792 assert!(ctx.pre_parsed_args.is_none());
793
794 let owned = ctx.to_tool_ctx();
795 assert!(owned.bypass_permissions);
796 assert!(!owned.auto_approve_permissions);
797 assert!(!owned.plan_read_only);
798 }
799
800 #[test]
801 fn for_dispatch_threads_pre_parsed_args() {
802 let (tx, _rx) = mpsc::channel(1);
803 let parsed = serde_json::json!({"v": "x"});
804 let ctx = ToolExecutionContext::for_dispatch(
805 "s1",
806 "root-s1",
807 "call-1",
808 &tx,
809 &[],
810 ToolExecutionSessionFlags::default(),
811 false,
812 None,
813 Some(&parsed),
814 );
815 assert_eq!(ctx.pre_parsed_args, Some(&parsed));
816 }
817}
818
819#[cfg(test)]
820mod tests {
821 use super::*;
822
823 #[tokio::test]
824 async fn emit_does_not_block_when_channel_is_full() {
825 let (tx, mut rx) = mpsc::channel(1);
826 tx.send(AgentEvent::Token {
827 content: "full".to_string(),
828 })
829 .await
830 .unwrap();
831 let ctx = ToolExecutionContext {
832 executing_supervisor: None,
833 session_id: Some("session_1"),
834 root_session_id: None,
835 tool_call_id: "call_1",
836 event_tx: Some(&tx),
837 available_tool_schemas: None,
838 bypass_permissions: false,
839 auto_approve_permissions: false,
840 plan_read_only: false,
841 can_async_resume: false,
842 bash_completion_sink: None,
843 pre_parsed_args: None,
844 };
845
846 tokio::time::timeout(
847 std::time::Duration::from_millis(100),
848 ctx.emit(AgentEvent::Token {
849 content: "next".to_string(),
850 }),
851 )
852 .await
853 .expect("emit should not block on full channel");
854
855 let first = rx.recv().await.unwrap();
856 match first {
857 AgentEvent::Token { content } => assert_eq!(content, "full"),
858 other => panic!("unexpected event: {other:?}"),
859 }
860 }
861
862 #[tokio::test]
863 async fn emit_converts_token_to_tool_token() {
864 let (tx, mut rx) = mpsc::channel(10);
865 let ctx = ToolExecutionContext {
866 executing_supervisor: None,
867 session_id: Some("session_1"),
868 root_session_id: None,
869 tool_call_id: "call_123",
870 event_tx: Some(&tx),
871 available_tool_schemas: None,
872 bypass_permissions: false,
873 auto_approve_permissions: false,
874 plan_read_only: false,
875 can_async_resume: false,
876 bash_completion_sink: None,
877 pre_parsed_args: None,
878 };
879
880 ctx.emit(AgentEvent::Token {
881 content: "test content".to_string(),
882 })
883 .await;
884
885 let event = rx.recv().await.unwrap();
886 match event {
887 AgentEvent::ToolToken {
888 tool_call_id,
889 content,
890 } => {
891 assert_eq!(tool_call_id, "call_123");
892 assert_eq!(content, "test content");
893 }
894 other => panic!("Expected ToolToken, got: {other:?}"),
895 }
896 }
897
898 #[tokio::test]
899 async fn emit_passes_through_non_token_events() {
900 let (tx, mut rx) = mpsc::channel(10);
901 let ctx = ToolExecutionContext {
902 executing_supervisor: None,
903 session_id: Some("session_1"),
904 root_session_id: None,
905 tool_call_id: "call_456",
906 event_tx: Some(&tx),
907 available_tool_schemas: None,
908 bypass_permissions: false,
909 auto_approve_permissions: false,
910 plan_read_only: false,
911 can_async_resume: false,
912 bash_completion_sink: None,
913 pre_parsed_args: None,
914 };
915
916 ctx.emit(AgentEvent::ToolToken {
918 tool_call_id: "other".to_string(),
919 content: "direct tool token".to_string(),
920 })
921 .await;
922
923 let event = rx.recv().await.unwrap();
924 match event {
925 AgentEvent::ToolToken { content, .. } => {
926 assert_eq!(content, "direct tool token");
927 }
928 other => panic!("Expected ToolToken, got: {other:?}"),
929 }
930 }
931
932 #[tokio::test]
933 async fn emit_does_nothing_when_no_sender() {
934 let ctx = ToolExecutionContext::none("call_789");
935
936 ctx.emit(AgentEvent::Token {
938 content: "test".to_string(),
939 })
940 .await;
941
942 }
944
945 #[tokio::test]
946 async fn emit_tool_token_convenience_method() {
947 let (tx, mut rx) = mpsc::channel(10);
948 let ctx = ToolExecutionContext {
949 executing_supervisor: None,
950 session_id: None,
951 root_session_id: None,
952 tool_call_id: "call_abc",
953 event_tx: Some(&tx),
954 available_tool_schemas: None,
955 bypass_permissions: false,
956 auto_approve_permissions: false,
957 plan_read_only: false,
958 can_async_resume: false,
959 bash_completion_sink: None,
960 pre_parsed_args: None,
961 };
962
963 ctx.emit_tool_token("convenient output").await;
964
965 let event = rx.recv().await.unwrap();
966 match event {
967 AgentEvent::ToolToken {
968 tool_call_id,
969 content,
970 } => {
971 assert_eq!(tool_call_id, "call_abc");
972 assert_eq!(content, "convenient output");
973 }
974 other => panic!("Expected ToolToken, got: {other:?}"),
975 }
976 }
977
978 #[tokio::test]
979 async fn emit_tool_token_with_no_sender_does_nothing() {
980 let ctx = ToolExecutionContext::none("call_def");
981
982 ctx.emit_tool_token("test").await;
984
985 }
987
988 #[test]
989 fn none_creates_context_with_no_optional_fields() {
990 let ctx = ToolExecutionContext::none("call_xyz");
991
992 assert_eq!(ctx.session_id, None);
993 assert_eq!(ctx.tool_call_id, "call_xyz");
994 assert!(ctx.event_tx.is_none());
995 }
996
997 #[test]
998 fn cloned_sender_returns_none_when_no_sender() {
999 let ctx = ToolExecutionContext::none("call_test");
1000 assert!(ctx.cloned_sender().is_none());
1001 }
1002
1003 #[tokio::test]
1004 async fn cloned_sender_returns_clone_when_sender_present() {
1005 let (tx, _rx) = mpsc::channel(10);
1006 let ctx = ToolExecutionContext {
1007 executing_supervisor: None,
1008 session_id: None,
1009 root_session_id: None,
1010 tool_call_id: "call_clone",
1011 event_tx: Some(&tx),
1012 available_tool_schemas: None,
1013 bypass_permissions: false,
1014 auto_approve_permissions: false,
1015 plan_read_only: false,
1016 can_async_resume: false,
1017 bash_completion_sink: None,
1018 pre_parsed_args: None,
1019 };
1020
1021 let cloned = ctx.cloned_sender();
1022 assert!(cloned.is_some());
1023
1024 cloned
1026 .unwrap()
1027 .send(AgentEvent::Token {
1028 content: "test".to_string(),
1029 })
1030 .await
1031 .unwrap();
1032 }
1033
1034 #[tokio::test]
1035 async fn emit_handles_multiple_sequential_calls() {
1036 let (tx, mut rx) = mpsc::channel(10);
1037 let ctx = ToolExecutionContext {
1038 executing_supervisor: None,
1039 session_id: Some("session_multi"),
1040 root_session_id: None,
1041 tool_call_id: "call_multi",
1042 event_tx: Some(&tx),
1043 available_tool_schemas: None,
1044 bypass_permissions: false,
1045 auto_approve_permissions: false,
1046 plan_read_only: false,
1047 can_async_resume: false,
1048 bash_completion_sink: None,
1049 pre_parsed_args: None,
1050 };
1051
1052 for i in 0..5 {
1053 ctx.emit(AgentEvent::Token {
1054 content: format!("message {}", i),
1055 })
1056 .await;
1057 }
1058
1059 for i in 0..5 {
1060 let event = rx.recv().await.unwrap();
1061 match event {
1062 AgentEvent::ToolToken { content, .. } => {
1063 assert_eq!(content, format!("message {}", i));
1064 }
1065 other => panic!("Expected ToolToken, got: {other:?}"),
1066 }
1067 }
1068 }
1069
1070 #[test]
1071 fn context_is_clone_and_copy() {
1072 let (tx, _rx) = mpsc::channel(10);
1073 let ctx = ToolExecutionContext {
1074 executing_supervisor: None,
1075 session_id: Some("session_copy"),
1076 root_session_id: None,
1077 tool_call_id: "call_copy",
1078 event_tx: Some(&tx),
1079 available_tool_schemas: None,
1080 bypass_permissions: false,
1081 auto_approve_permissions: false,
1082 plan_read_only: false,
1083 can_async_resume: false,
1084 bash_completion_sink: None,
1085 pre_parsed_args: None,
1086 };
1087
1088 let _cloned = ctx;
1090
1091 let copied = ctx;
1093
1094 assert_eq!(copied.tool_call_id, "call_copy");
1096 }
1097
1098 #[test]
1099 fn context_is_debug() {
1100 let ctx = ToolExecutionContext::none("call_debug");
1101 let debug_str = format!("{:?}", ctx);
1102 assert!(debug_str.contains("call_debug"));
1103 }
1104
1105 #[tokio::test]
1106 async fn emit_with_empty_tool_call_id() {
1107 let (tx, mut rx) = mpsc::channel(10);
1108 let ctx = ToolExecutionContext {
1109 executing_supervisor: None,
1110 session_id: None,
1111 root_session_id: None,
1112 tool_call_id: "",
1113 event_tx: Some(&tx),
1114 available_tool_schemas: None,
1115 bypass_permissions: false,
1116 auto_approve_permissions: false,
1117 plan_read_only: false,
1118 can_async_resume: false,
1119 bash_completion_sink: None,
1120 pre_parsed_args: None,
1121 };
1122
1123 ctx.emit(AgentEvent::Token {
1124 content: "test".to_string(),
1125 })
1126 .await;
1127
1128 let event = rx.recv().await.unwrap();
1129 match event {
1130 AgentEvent::ToolToken { tool_call_id, .. } => {
1131 assert_eq!(tool_call_id, "");
1132 }
1133 other => panic!("Expected ToolToken, got: {other:?}"),
1134 }
1135 }
1136
1137 #[tokio::test]
1138 async fn emit_with_unicode_content() {
1139 let (tx, mut rx) = mpsc::channel(10);
1140 let ctx = ToolExecutionContext {
1141 executing_supervisor: None,
1142 session_id: Some("会话"),
1143 root_session_id: None,
1144 tool_call_id: "调用_123",
1145 event_tx: Some(&tx),
1146 available_tool_schemas: None,
1147 bypass_permissions: false,
1148 auto_approve_permissions: false,
1149 plan_read_only: false,
1150 can_async_resume: false,
1151 bash_completion_sink: None,
1152 pre_parsed_args: None,
1153 };
1154
1155 ctx.emit(AgentEvent::Token {
1156 content: "测试内容 🎯".to_string(),
1157 })
1158 .await;
1159
1160 let event = rx.recv().await.unwrap();
1161 match event {
1162 AgentEvent::ToolToken {
1163 tool_call_id,
1164 content,
1165 } => {
1166 assert_eq!(tool_call_id, "调用_123");
1167 assert_eq!(content, "测试内容 🎯");
1168 }
1169 other => panic!("Expected ToolToken, got: {other:?}"),
1170 }
1171 }
1172
1173 #[tokio::test]
1174 async fn emit_with_special_characters_in_tool_call_id() {
1175 let (tx, mut rx) = mpsc::channel(10);
1176 let ctx = ToolExecutionContext {
1177 executing_supervisor: None,
1178 session_id: None,
1179 root_session_id: None,
1180 tool_call_id: "call-with_special.chars:123",
1181 event_tx: Some(&tx),
1182 available_tool_schemas: None,
1183 bypass_permissions: false,
1184 auto_approve_permissions: false,
1185 plan_read_only: false,
1186 can_async_resume: false,
1187 bash_completion_sink: None,
1188 pre_parsed_args: None,
1189 };
1190
1191 ctx.emit(AgentEvent::Token {
1192 content: "test".to_string(),
1193 })
1194 .await;
1195
1196 let event = rx.recv().await.unwrap();
1197 match event {
1198 AgentEvent::ToolToken { tool_call_id, .. } => {
1199 assert_eq!(tool_call_id, "call-with_special.chars:123");
1200 }
1201 other => panic!("Expected ToolToken, got: {other:?}"),
1202 }
1203 }
1204
1205 #[tokio::test]
1206 async fn emit_tool_token_with_string_content() {
1207 let (tx, mut rx) = mpsc::channel(10);
1208 let ctx = ToolExecutionContext {
1209 executing_supervisor: None,
1210 session_id: None,
1211 root_session_id: None,
1212 tool_call_id: "call_string",
1213 event_tx: Some(&tx),
1214 available_tool_schemas: None,
1215 bypass_permissions: false,
1216 auto_approve_permissions: false,
1217 plan_read_only: false,
1218 can_async_resume: false,
1219 bash_completion_sink: None,
1220 pre_parsed_args: None,
1221 };
1222
1223 let content = String::from("owned string");
1224 ctx.emit_tool_token(content).await;
1225
1226 let event = rx.recv().await.unwrap();
1227 match event {
1228 AgentEvent::ToolToken { content, .. } => {
1229 assert_eq!(content, "owned string");
1230 }
1231 other => panic!("Expected ToolToken, got: {other:?}"),
1232 }
1233 }
1234
1235 #[tokio::test]
1236 async fn emit_tool_token_with_str_content() {
1237 let (tx, mut rx) = mpsc::channel(10);
1238 let ctx = ToolExecutionContext {
1239 executing_supervisor: None,
1240 session_id: None,
1241 root_session_id: None,
1242 tool_call_id: "call_str",
1243 event_tx: Some(&tx),
1244 available_tool_schemas: None,
1245 bypass_permissions: false,
1246 auto_approve_permissions: false,
1247 plan_read_only: false,
1248 can_async_resume: false,
1249 bash_completion_sink: None,
1250 pre_parsed_args: None,
1251 };
1252
1253 ctx.emit_tool_token("string slice").await;
1254
1255 let event = rx.recv().await.unwrap();
1256 match event {
1257 AgentEvent::ToolToken { content, .. } => {
1258 assert_eq!(content, "string slice");
1259 }
1260 other => panic!("Expected ToolToken, got: {other:?}"),
1261 }
1262 }
1263}