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, SessionCallerReference, SessionKind,
18 SupervisorReference, DEFAULT_SUPERVISOR_SESSION_ID,
19};
20use uuid::Uuid;
21
22tokio::task_local! {
23 static ROOT_ACTOR_TOOL_WRITER: Option<bamboo_domain::RootActorRuntimeWrite>;
24 static EXECUTING_SESSION_CALLER: Option<Arc<SessionCallerReference>>;
25}
26
27pub async fn with_executing_session_caller<F: std::future::Future>(
31 caller: Option<Arc<SessionCallerReference>>,
32 dispatch: F,
33) -> F::Output {
34 EXECUTING_SESSION_CALLER.scope(caller, dispatch).await
35}
36
37fn executing_session_caller(session_id: Option<&str>) -> Option<Arc<SessionCallerReference>> {
38 EXECUTING_SESSION_CALLER
39 .try_with(Clone::clone)
40 .ok()
41 .flatten()
42 .filter(|caller| Some(caller.session_id.as_str()) == session_id)
43}
44
45pub async fn with_root_actor_tool_writer<F: std::future::Future>(
48 owner: Option<bamboo_domain::RootActorRuntimeWrite>,
49 dispatch: F,
50) -> F::Output {
51 ROOT_ACTOR_TOOL_WRITER.scope(owner, dispatch).await
52}
53
54pub fn root_actor_tool_writer() -> Option<Option<bamboo_domain::RootActorRuntimeWrite>> {
57 ROOT_ACTOR_TOOL_WRITER.try_with(Clone::clone).ok()
58}
59
60#[derive(Clone, Copy, Debug, PartialEq, Eq)]
66pub struct ExecutingSupervisorObservation {
67 incarnation_id: Uuid,
68}
69
70const SUPERVISOR_PERMISSION_REPLAY_METADATA_KEY: &str = "permission.executing_supervisor.v1";
72
73#[derive(serde::Serialize, serde::Deserialize)]
74#[serde(deny_unknown_fields)]
75struct SupervisorPermissionReplayRecord {
76 version: u8,
77 incarnation_id: Uuid,
78 session_id: String,
79 result_message_id: String,
80 request_generation: String,
81 tool_call: ToolCall,
82 execution_name: String,
83}
84
85impl ExecutingSupervisorObservation {
86 pub const PERMISSION_REPLAY_METADATA_KEY: &'static str =
87 SUPERVISOR_PERMISSION_REPLAY_METADATA_KEY;
88
89 pub fn capture_from_executing_session(session: &Session) -> Option<Self> {
93 session
94 .supervisor_incarnation_id()
95 .map(|incarnation_id| Self { incarnation_id })
96 }
97
98 pub fn supervisor_reference(self) -> SupervisorReference {
101 SupervisorReference {
102 session_id: DEFAULT_SUPERVISOR_SESSION_ID.to_string(),
103 incarnation_id: self.incarnation_id,
104 }
105 }
106
107 pub fn permission_replay_record(
111 self,
112 session_id: &str,
113 result_message_id: &str,
114 tool_call: &ToolCall,
115 execution_name: &str,
116 request_generation: &str,
117 ) -> Value {
118 serde_json::to_value(SupervisorPermissionReplayRecord {
119 version: 1,
120 incarnation_id: self.incarnation_id,
121 session_id: session_id.to_string(),
122 result_message_id: result_message_id.to_string(),
123 request_generation: request_generation.to_string(),
124 tool_call: tool_call.clone(),
125 execution_name: execution_name.to_string(),
126 })
127 .expect("Supervisor permission binding serializes")
128 }
129
130 pub fn restore_permission_replay_record(
135 session: &Session,
136 result_index: usize,
137 tool_call: &ToolCall,
138 execution_name: &str,
139 request_generation: Option<&str>,
140 ) -> Result<Option<Self>, &'static str> {
141 let message = session
142 .messages
143 .get(result_index)
144 .ok_or("result occurrence missing")?;
145 if serde_json::from_str::<Value>(&message.content)
146 .ok()
147 .is_some_and(|payload| {
148 payload
149 .get(SUPERVISOR_PERMISSION_REPLAY_METADATA_KEY)
150 .is_some()
151 })
152 {
153 return Err("Supervisor authority is not accepted from a result payload");
154 }
155 let Some(value) = message
156 .metadata
157 .as_ref()
158 .and_then(|metadata| metadata.get(SUPERVISOR_PERMISSION_REPLAY_METADATA_KEY))
159 else {
160 return Ok(None);
161 };
162 let record: SupervisorPermissionReplayRecord = serde_json::from_value(value.clone())
163 .map_err(|_| "Supervisor permission binding is malformed")?;
164 let latest_result = session
165 .messages
166 .iter()
167 .rposition(|message| message.tool_call_id.as_deref() == Some(tool_call.id.as_str()));
168 let newer_call = session.messages[result_index + 1..].iter().any(|message| {
169 message
170 .tool_calls
171 .as_ref()
172 .is_some_and(|calls| calls.iter().any(|call| call.id == tool_call.id))
173 });
174 let preceding_call = session.messages[..result_index]
175 .iter()
176 .rev()
177 .find(|message| {
178 message
179 .tool_calls
180 .as_ref()
181 .is_some_and(|calls| calls.iter().any(|call| call.id == tool_call.id))
182 });
183 let exact_call = preceding_call.is_some_and(|message| {
184 message.role == bamboo_domain::Role::Assistant
185 && message.tool_calls.as_ref().is_some_and(|calls| {
186 let matching: Vec<_> = calls
187 .iter()
188 .filter(|call| call.id == tool_call.id)
189 .collect();
190 matching.len() == 1 && matching[0] == tool_call
191 })
192 });
193 if record.version != 1
194 || record.incarnation_id.is_nil()
195 || session.id != DEFAULT_SUPERVISOR_SESSION_ID
196 || session.kind != SessionKind::Root
197 || session.root_session_id != session.id
198 || session.parent_session_id.is_some()
199 || session.spawn_depth != 0
200 || session.authority_identity
201 != (SessionAuthorityIdentity::Supervisor {
202 incarnation_id: record.incarnation_id,
203 })
204 || message.role != bamboo_domain::Role::Tool
205 || latest_result != Some(result_index)
206 || newer_call
207 || !exact_call
208 || record.session_id != session.id
209 || record.result_message_id != message.id
210 || record.tool_call != *tool_call
211 || record.execution_name != execution_name
212 || execution_name.trim().is_empty()
213 || record.request_generation.trim().is_empty()
214 || Some(record.request_generation.as_str()) != request_generation
215 {
216 return Err("Supervisor permission binding does not match the current operation");
217 }
218 Ok(Some(Self {
219 incarnation_id: record.incarnation_id,
220 }))
221 }
222
223 pub(super) fn for_caller(self, session_id: Option<&str>) -> Option<Self> {
224 (session_id == Some(DEFAULT_SUPERVISOR_SESSION_ID)).then_some(self)
225 }
226}
227
228#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
238pub struct ToolExecutionSessionFlags {
239 pub bypass_permissions: bool,
242 pub auto_approve_permissions: bool,
245 pub plan_read_only: bool,
248}
249
250impl ToolExecutionSessionFlags {
251 pub fn from_session(session: &Session) -> Self {
254 Self::from_session_and_configured_mode(session, PermissionMode::Default)
255 }
256
257 pub fn from_session_and_configured_mode(
262 session: &Session,
263 configured_mode: PermissionMode,
264 ) -> Self {
265 let requested = session
266 .agent_runtime_state
267 .as_ref()
268 .map(|state| state.effective_permission_mode())
269 .unwrap_or_default();
270 let configured_mode = if session
271 .agent_runtime_state
272 .as_ref()
273 .is_some_and(|state| state.plan_mode.is_some())
274 {
275 PermissionMode::Plan
276 } else {
277 configured_mode
278 };
279 let resolution = bamboo_domain::resolve_permission_mode(requested, configured_mode);
280 Self {
281 bypass_permissions: resolution.bypass_permissions(),
282 auto_approve_permissions: resolution.auto_approve_permissions(),
283 plan_read_only: resolution.effective == PermissionMode::Plan,
284 }
285 }
286}
287
288#[derive(Clone, Copy)]
299pub struct ToolExecutionContext<'a> {
300 pub session_id: Option<&'a str>,
302 pub executing_supervisor: Option<ExecutingSupervisorObservation>,
304 pub root_session_id: Option<&'a str>,
308 pub tool_call_id: &'a str,
310 pub event_tx: Option<&'a mpsc::Sender<AgentEvent>>,
312 pub available_tool_schemas: Option<&'a [ToolSchema]>,
314 pub bypass_permissions: bool,
318 pub auto_approve_permissions: bool,
321 pub plan_read_only: bool,
323 pub can_async_resume: bool,
333 pub bash_completion_sink: Option<&'a Arc<dyn BashCompletionSink>>,
344 pub pre_parsed_args: Option<&'a Value>,
354}
355
356impl std::fmt::Debug for ToolExecutionContext<'_> {
359 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
360 f.debug_struct("ToolExecutionContext")
361 .field("session_id", &self.session_id)
362 .field("executing_supervisor", &self.executing_supervisor)
363 .field("root_session_id", &self.root_session_id)
364 .field("tool_call_id", &self.tool_call_id)
365 .field("event_tx", &self.event_tx)
366 .field("available_tool_schemas", &self.available_tool_schemas)
367 .field("bypass_permissions", &self.bypass_permissions)
368 .field("auto_approve_permissions", &self.auto_approve_permissions)
369 .field("plan_read_only", &self.plan_read_only)
370 .field("can_async_resume", &self.can_async_resume)
371 .field("bash_completion_sink", &self.bash_completion_sink.is_some())
372 .field("pre_parsed_args", &self.pre_parsed_args)
373 .finish()
374 }
375}
376
377impl<'a> ToolExecutionContext<'a> {
378 pub fn none(tool_call_id: &'a str) -> Self {
379 Self {
380 session_id: None,
381 executing_supervisor: None,
382 root_session_id: None,
383 tool_call_id,
384 event_tx: None,
385 available_tool_schemas: None,
386 bypass_permissions: false,
387 auto_approve_permissions: false,
388 plan_read_only: false,
389 can_async_resume: false,
390 bash_completion_sink: None,
391 pre_parsed_args: None,
392 }
393 }
394
395 #[allow(clippy::too_many_arguments)]
403 pub fn for_dispatch(
404 session_id: &'a str,
405 root_session_id: &'a str,
406 tool_call_id: &'a str,
407 event_tx: &'a mpsc::Sender<AgentEvent>,
408 available_tool_schemas: &'a [ToolSchema],
409 flags: ToolExecutionSessionFlags,
410 can_async_resume: bool,
415 bash_completion_sink: Option<&'a Arc<dyn BashCompletionSink>>,
420 pre_parsed_args: Option<&'a Value>,
428 ) -> Self {
429 Self {
430 session_id: Some(session_id),
431 executing_supervisor: None,
432 root_session_id: Some(if root_session_id.trim().is_empty() {
433 session_id
434 } else {
435 root_session_id
436 }),
437 tool_call_id,
438 event_tx: Some(event_tx),
439 available_tool_schemas: Some(available_tool_schemas),
440 bypass_permissions: flags.bypass_permissions,
441 auto_approve_permissions: flags.auto_approve_permissions,
442 plan_read_only: flags.plan_read_only,
443 can_async_resume,
444 bash_completion_sink,
445 pre_parsed_args,
446 }
447 }
448
449 pub fn with_executing_supervisor(
452 mut self,
453 observation: Option<ExecutingSupervisorObservation>,
454 ) -> Self {
455 self.executing_supervisor =
456 observation.and_then(|observation| observation.for_caller(self.session_id));
457 self
458 }
459
460 pub fn cloned_sender(&self) -> Option<mpsc::Sender<AgentEvent>> {
462 self.event_tx.cloned()
463 }
464
465 pub fn cloned_bash_completion_sink(&self) -> Option<Arc<dyn BashCompletionSink>> {
470 self.bash_completion_sink.map(Arc::clone)
471 }
472
473 pub fn root_session_id(&self) -> Option<&'a str> {
475 self.root_session_id
476 }
477
478 pub fn to_tool_ctx(&self) -> crate::tools::ToolCtx {
485 crate::tools::ToolCtx {
486 session_id: self.session_id.map(Arc::from),
487 executing_caller: executing_session_caller(self.session_id),
488 executing_supervisor: self
489 .executing_supervisor
490 .and_then(|observation| observation.for_caller(self.session_id)),
491 tool_call_id: Arc::from(self.tool_call_id),
492 event_tx: self.event_tx.cloned(),
493 available_tool_schemas: self
494 .available_tool_schemas
495 .map(Arc::from)
496 .unwrap_or_else(|| Arc::from(Vec::new())),
497 bypass_permissions: self.bypass_permissions,
498 auto_approve_permissions: self.auto_approve_permissions,
499 plan_read_only: self.plan_read_only,
500 can_async_resume: self.can_async_resume,
501 async_completion_sink: None,
502 bash_completion_sink: self.bash_completion_sink.map(Arc::clone),
503 }
504 }
505
506 pub async fn emit(&self, event: AgentEvent) {
508 if let Some(tx) = self.event_tx {
509 let event = match event {
513 AgentEvent::Token { content } => AgentEvent::ToolToken {
514 tool_call_id: self.tool_call_id.to_string(),
515 content,
516 },
517 other => other,
518 };
519 let _ = tx.try_send(event);
520 }
521 }
522
523 pub async fn emit_tool_token(&self, content: impl Into<String>) {
525 self.emit(AgentEvent::ToolToken {
526 tool_call_id: self.tool_call_id.to_string(),
527 content: content.into(),
528 })
529 .await;
530 }
531}
532
533#[cfg(test)]
534mod supervisor_observation_tests {
535 use super::*;
536
537 fn supervisor() -> Session {
538 let mut session = Session::new(DEFAULT_SUPERVISOR_SESSION_ID, "model");
539 session.authority_identity = SessionAuthorityIdentity::Supervisor {
540 incarnation_id: Uuid::new_v4(),
541 };
542 session
543 }
544
545 #[test]
546 fn supervisor_observation_requires_typed_canonical_root_lineage() {
547 let valid = supervisor();
548 let observed = ExecutingSupervisorObservation::capture_from_executing_session(&valid)
549 .expect("canonical executing Supervisor");
550 assert_eq!(observed.supervisor_reference().session_id, valid.id);
551 for mutation in [
552 |s: &mut Session| s.authority_identity = SessionAuthorityIdentity::Ordinary,
553 |s: &mut Session| s.id = "other".into(),
554 |s: &mut Session| s.root_session_id = "other".into(),
555 |s: &mut Session| s.parent_session_id = Some("parent".into()),
556 |s: &mut Session| s.spawn_depth = 1,
557 |s: &mut Session| s.kind = SessionKind::Child,
558 |s: &mut Session| {
559 s.authority_identity = SessionAuthorityIdentity::Supervisor {
560 incarnation_id: Uuid::nil(),
561 }
562 },
563 ] {
564 let mut malformed = valid.clone();
565 mutation(&mut malformed);
566 assert_eq!(
567 ExecutingSupervisorObservation::capture_from_executing_session(&malformed),
568 None,
569 "malformed Session: {malformed:?}"
570 );
571 }
572 let mut ordinary = Session::new(DEFAULT_SUPERVISOR_SESSION_ID, "model");
573 ordinary.metadata.insert(
574 "authority_identity".into(),
575 serde_json::to_string(&valid.authority_identity).unwrap(),
576 );
577 ordinary.metadata.insert("supervisor".into(), "true".into());
578 assert_eq!(
579 ExecutingSupervisorObservation::capture_from_executing_session(&ordinary),
580 None,
581 "reserved ID and forged metadata cannot supply a typed identity"
582 );
583 }
584
585 #[test]
586 fn supervisor_observation_is_not_minted_by_flags_arguments_or_generic_contexts() {
587 let session = supervisor();
588 let (tx, _rx) = mpsc::channel(1);
589 let forged_args = serde_json::json!({
590 "executing_supervisor": session.authority_identity,
591 "session_id": DEFAULT_SUPERVISOR_SESSION_ID,
592 });
593 let ctx = ToolExecutionContext::for_dispatch(
594 &session.id,
595 &session.root_session_id,
596 "call",
597 &tx,
598 &[],
599 ToolExecutionSessionFlags {
600 bypass_permissions: true,
601 auto_approve_permissions: true,
602 plan_read_only: true,
603 },
604 false,
605 None,
606 Some(&forged_args),
607 );
608 assert_eq!(ctx.executing_supervisor, None);
609 assert_eq!(ctx.to_tool_ctx().executing_supervisor, None);
610 assert_eq!(
611 ToolExecutionContext::none("none").executing_supervisor,
612 None
613 );
614 assert_eq!(
615 crate::tools::ToolCtx::none("owned").executing_supervisor,
616 None
617 );
618
619 let observation = ExecutingSupervisorObservation::capture_from_executing_session(&session);
620 let captured = ctx.with_executing_supervisor(observation);
621 let owned = captured.to_tool_ctx().clone();
622 assert_eq!(owned.executing_supervisor_for(&session.id), observation);
623 assert_eq!(owned.executing_supervisor_for("child"), None);
624 assert_eq!(
625 ToolExecutionContext::none("missing")
626 .with_executing_supervisor(observation)
627 .executing_supervisor,
628 None
629 );
630 let mismatched = ToolExecutionContext {
632 session_id: Some("child"),
633 ..captured
634 };
635 assert_eq!(mismatched.to_tool_ctx().executing_supervisor, None);
636 }
637}
638
639#[cfg(test)]
640mod session_flags_tests {
641 use super::*;
642 use bamboo_domain::{AgentRuntimeState, SessionPermissionMode};
643
644 #[test]
645 fn dispatch_context_falls_back_from_empty_legacy_root_to_session_id() {
646 let (event_tx, _event_rx) = mpsc::channel(1);
647 let ctx = ToolExecutionContext::for_dispatch(
648 "session-id",
649 " ",
650 "call-id",
651 &event_tx,
652 &[],
653 ToolExecutionSessionFlags::default(),
654 false,
655 None,
656 None,
657 );
658
659 assert_eq!(ctx.root_session_id(), Some("session-id"));
660 }
661
662 #[test]
663 fn from_session_defaults_false_without_runtime_state() {
664 let session = Session::new("s-none", "test-model");
665 assert_eq!(
666 ToolExecutionSessionFlags::from_session(&session),
667 ToolExecutionSessionFlags {
668 bypass_permissions: false,
669 auto_approve_permissions: false,
670 plan_read_only: false,
671 }
672 );
673 }
674
675 #[test]
676 fn from_session_reads_bypass_from_runtime_state() {
677 let mut session = Session::new("s-bypass", "test-model");
678 let mut runtime = AgentRuntimeState::new("run-1");
679 runtime.bypass_permissions = true;
680 session.agent_runtime_state = Some(runtime);
681 assert!(ToolExecutionSessionFlags::from_session(&session).bypass_permissions);
682 assert!(!ToolExecutionSessionFlags::from_session(&session).auto_approve_permissions);
683 }
684
685 #[test]
686 fn from_session_distinguishes_auto_from_legacy_bypass() {
687 let mut session = Session::new("s-auto", "test-model");
688 let mut runtime = AgentRuntimeState::new("run-1");
689 runtime.set_permission_mode(SessionPermissionMode::Auto);
690 session.agent_runtime_state = Some(runtime);
691
692 let flags = ToolExecutionSessionFlags::from_session(&session);
693 assert!(!flags.bypass_permissions);
694 assert!(flags.auto_approve_permissions);
695 }
696
697 #[test]
698 fn configured_auto_applies_to_default_but_not_explicit_bypass() {
699 let default_session = Session::new("s-global-auto", "test-model");
700 let flags = ToolExecutionSessionFlags::from_session_and_configured_mode(
701 &default_session,
702 PermissionMode::Auto,
703 );
704 assert_eq!(
705 flags,
706 ToolExecutionSessionFlags {
707 bypass_permissions: false,
708 auto_approve_permissions: true,
709 plan_read_only: false,
710 }
711 );
712
713 let mut bypass_session = default_session;
714 bypass_session
715 .agent_runtime_state
716 .get_or_insert_default()
717 .set_permission_mode(SessionPermissionMode::Bypass);
718 let flags = ToolExecutionSessionFlags::from_session_and_configured_mode(
719 &bypass_session,
720 PermissionMode::Auto,
721 );
722 assert!(flags.bypass_permissions);
723 assert!(!flags.auto_approve_permissions);
724 }
725
726 #[test]
727 fn configured_plan_preserves_no_prompt_but_sets_read_only_gate() {
728 let mut session = Session::new("s-plan-auto", "test-model");
729 session
730 .agent_runtime_state
731 .get_or_insert_default()
732 .set_permission_mode(SessionPermissionMode::Auto);
733 let flags = ToolExecutionSessionFlags::from_session_and_configured_mode(
734 &session,
735 PermissionMode::Plan,
736 );
737 assert!(!flags.bypass_permissions);
738 assert!(flags.auto_approve_permissions);
739 assert!(flags.plan_read_only);
740 }
741
742 #[test]
743 fn persisted_plan_overlay_is_honored_without_config_reconstruction() {
744 let mut session = Session::new("s-persisted-plan-auto", "test-model");
745 let runtime = session.agent_runtime_state.get_or_insert_default();
746 runtime.set_permission_mode(SessionPermissionMode::Auto);
747 runtime.plan_mode = Some(bamboo_domain::PlanModeState {
748 entered_at: chrono::Utc::now(),
749 pre_permission_mode: "auto".to_string(),
750 plan_file_path: None,
751 status: bamboo_domain::PlanModeStatus::Designing,
752 });
753
754 let flags = ToolExecutionSessionFlags::from_session(&session);
755 assert!(flags.plan_read_only);
756 assert!(flags.auto_approve_permissions);
757 assert!(!flags.bypass_permissions);
758 }
759
760 #[test]
761 fn typed_read_only_child_does_not_reactivate_the_legacy_plan_name_gate() {
762 for requested in [SessionPermissionMode::Auto, SessionPermissionMode::Bypass] {
763 let mut session = Session::new("s-read-only-child", "test-model");
764 let runtime = session.agent_runtime_state.get_or_insert_default();
765 runtime.set_permission_mode(requested);
766 runtime.read_only = true;
767
768 let flags = ToolExecutionSessionFlags::from_session_and_configured_mode(
773 &session,
774 PermissionMode::Auto,
775 );
776
777 assert!(!flags.plan_read_only);
778 assert_eq!(
779 flags.bypass_permissions,
780 requested == SessionPermissionMode::Bypass
781 );
782 assert_eq!(
783 flags.auto_approve_permissions,
784 requested == SessionPermissionMode::Auto
785 );
786 }
787 }
788
789 #[test]
790 fn for_dispatch_maps_flags_onto_context() {
791 let (tx, _rx) = mpsc::channel(1);
792 let ctx = ToolExecutionContext::for_dispatch(
793 "s1",
794 "root-s1",
795 "call-1",
796 &tx,
797 &[],
798 ToolExecutionSessionFlags {
799 bypass_permissions: true,
800 auto_approve_permissions: false,
801 plan_read_only: false,
802 },
803 true,
804 None,
805 None,
806 );
807 assert_eq!(ctx.session_id, Some("s1"));
808 assert_eq!(ctx.root_session_id(), Some("root-s1"));
809 assert!(ctx.bypass_permissions);
810 assert!(!ctx.auto_approve_permissions);
811 assert!(ctx.can_async_resume);
812 assert!(ctx.pre_parsed_args.is_none());
813
814 let owned = ctx.to_tool_ctx();
815 assert!(owned.bypass_permissions);
816 assert!(!owned.auto_approve_permissions);
817 assert!(!owned.plan_read_only);
818 }
819
820 #[test]
821 fn for_dispatch_threads_pre_parsed_args() {
822 let (tx, _rx) = mpsc::channel(1);
823 let parsed = serde_json::json!({"v": "x"});
824 let ctx = ToolExecutionContext::for_dispatch(
825 "s1",
826 "root-s1",
827 "call-1",
828 &tx,
829 &[],
830 ToolExecutionSessionFlags::default(),
831 false,
832 None,
833 Some(&parsed),
834 );
835 assert_eq!(ctx.pre_parsed_args, Some(&parsed));
836 }
837}
838
839#[cfg(test)]
840mod tests {
841 use super::*;
842
843 #[tokio::test]
844 async fn emit_does_not_block_when_channel_is_full() {
845 let (tx, mut rx) = mpsc::channel(1);
846 tx.send(AgentEvent::Token {
847 content: "full".to_string(),
848 })
849 .await
850 .unwrap();
851 let ctx = ToolExecutionContext {
852 executing_supervisor: None,
853 session_id: Some("session_1"),
854 root_session_id: None,
855 tool_call_id: "call_1",
856 event_tx: Some(&tx),
857 available_tool_schemas: None,
858 bypass_permissions: false,
859 auto_approve_permissions: false,
860 plan_read_only: false,
861 can_async_resume: false,
862 bash_completion_sink: None,
863 pre_parsed_args: None,
864 };
865
866 tokio::time::timeout(
867 std::time::Duration::from_millis(100),
868 ctx.emit(AgentEvent::Token {
869 content: "next".to_string(),
870 }),
871 )
872 .await
873 .expect("emit should not block on full channel");
874
875 let first = rx.recv().await.unwrap();
876 match first {
877 AgentEvent::Token { content } => assert_eq!(content, "full"),
878 other => panic!("unexpected event: {other:?}"),
879 }
880 }
881
882 #[tokio::test]
883 async fn emit_converts_token_to_tool_token() {
884 let (tx, mut rx) = mpsc::channel(10);
885 let ctx = ToolExecutionContext {
886 executing_supervisor: None,
887 session_id: Some("session_1"),
888 root_session_id: None,
889 tool_call_id: "call_123",
890 event_tx: Some(&tx),
891 available_tool_schemas: None,
892 bypass_permissions: false,
893 auto_approve_permissions: false,
894 plan_read_only: false,
895 can_async_resume: false,
896 bash_completion_sink: None,
897 pre_parsed_args: None,
898 };
899
900 ctx.emit(AgentEvent::Token {
901 content: "test content".to_string(),
902 })
903 .await;
904
905 let event = rx.recv().await.unwrap();
906 match event {
907 AgentEvent::ToolToken {
908 tool_call_id,
909 content,
910 } => {
911 assert_eq!(tool_call_id, "call_123");
912 assert_eq!(content, "test content");
913 }
914 other => panic!("Expected ToolToken, got: {other:?}"),
915 }
916 }
917
918 #[tokio::test]
919 async fn emit_passes_through_non_token_events() {
920 let (tx, mut rx) = mpsc::channel(10);
921 let ctx = ToolExecutionContext {
922 executing_supervisor: None,
923 session_id: Some("session_1"),
924 root_session_id: None,
925 tool_call_id: "call_456",
926 event_tx: Some(&tx),
927 available_tool_schemas: None,
928 bypass_permissions: false,
929 auto_approve_permissions: false,
930 plan_read_only: false,
931 can_async_resume: false,
932 bash_completion_sink: None,
933 pre_parsed_args: None,
934 };
935
936 ctx.emit(AgentEvent::ToolToken {
938 tool_call_id: "other".to_string(),
939 content: "direct tool token".to_string(),
940 })
941 .await;
942
943 let event = rx.recv().await.unwrap();
944 match event {
945 AgentEvent::ToolToken { content, .. } => {
946 assert_eq!(content, "direct tool token");
947 }
948 other => panic!("Expected ToolToken, got: {other:?}"),
949 }
950 }
951
952 #[tokio::test]
953 async fn emit_does_nothing_when_no_sender() {
954 let ctx = ToolExecutionContext::none("call_789");
955
956 ctx.emit(AgentEvent::Token {
958 content: "test".to_string(),
959 })
960 .await;
961
962 }
964
965 #[tokio::test]
966 async fn emit_tool_token_convenience_method() {
967 let (tx, mut rx) = mpsc::channel(10);
968 let ctx = ToolExecutionContext {
969 executing_supervisor: None,
970 session_id: None,
971 root_session_id: None,
972 tool_call_id: "call_abc",
973 event_tx: Some(&tx),
974 available_tool_schemas: None,
975 bypass_permissions: false,
976 auto_approve_permissions: false,
977 plan_read_only: false,
978 can_async_resume: false,
979 bash_completion_sink: None,
980 pre_parsed_args: None,
981 };
982
983 ctx.emit_tool_token("convenient output").await;
984
985 let event = rx.recv().await.unwrap();
986 match event {
987 AgentEvent::ToolToken {
988 tool_call_id,
989 content,
990 } => {
991 assert_eq!(tool_call_id, "call_abc");
992 assert_eq!(content, "convenient output");
993 }
994 other => panic!("Expected ToolToken, got: {other:?}"),
995 }
996 }
997
998 #[tokio::test]
999 async fn emit_tool_token_with_no_sender_does_nothing() {
1000 let ctx = ToolExecutionContext::none("call_def");
1001
1002 ctx.emit_tool_token("test").await;
1004
1005 }
1007
1008 #[test]
1009 fn none_creates_context_with_no_optional_fields() {
1010 let ctx = ToolExecutionContext::none("call_xyz");
1011
1012 assert_eq!(ctx.session_id, None);
1013 assert_eq!(ctx.tool_call_id, "call_xyz");
1014 assert!(ctx.event_tx.is_none());
1015 }
1016
1017 #[test]
1018 fn cloned_sender_returns_none_when_no_sender() {
1019 let ctx = ToolExecutionContext::none("call_test");
1020 assert!(ctx.cloned_sender().is_none());
1021 }
1022
1023 #[tokio::test]
1024 async fn cloned_sender_returns_clone_when_sender_present() {
1025 let (tx, _rx) = mpsc::channel(10);
1026 let ctx = ToolExecutionContext {
1027 executing_supervisor: None,
1028 session_id: None,
1029 root_session_id: None,
1030 tool_call_id: "call_clone",
1031 event_tx: Some(&tx),
1032 available_tool_schemas: None,
1033 bypass_permissions: false,
1034 auto_approve_permissions: false,
1035 plan_read_only: false,
1036 can_async_resume: false,
1037 bash_completion_sink: None,
1038 pre_parsed_args: None,
1039 };
1040
1041 let cloned = ctx.cloned_sender();
1042 assert!(cloned.is_some());
1043
1044 cloned
1046 .unwrap()
1047 .send(AgentEvent::Token {
1048 content: "test".to_string(),
1049 })
1050 .await
1051 .unwrap();
1052 }
1053
1054 #[tokio::test]
1055 async fn emit_handles_multiple_sequential_calls() {
1056 let (tx, mut rx) = mpsc::channel(10);
1057 let ctx = ToolExecutionContext {
1058 executing_supervisor: None,
1059 session_id: Some("session_multi"),
1060 root_session_id: None,
1061 tool_call_id: "call_multi",
1062 event_tx: Some(&tx),
1063 available_tool_schemas: None,
1064 bypass_permissions: false,
1065 auto_approve_permissions: false,
1066 plan_read_only: false,
1067 can_async_resume: false,
1068 bash_completion_sink: None,
1069 pre_parsed_args: None,
1070 };
1071
1072 for i in 0..5 {
1073 ctx.emit(AgentEvent::Token {
1074 content: format!("message {}", i),
1075 })
1076 .await;
1077 }
1078
1079 for i in 0..5 {
1080 let event = rx.recv().await.unwrap();
1081 match event {
1082 AgentEvent::ToolToken { content, .. } => {
1083 assert_eq!(content, format!("message {}", i));
1084 }
1085 other => panic!("Expected ToolToken, got: {other:?}"),
1086 }
1087 }
1088 }
1089
1090 #[test]
1091 fn context_is_clone_and_copy() {
1092 let (tx, _rx) = mpsc::channel(10);
1093 let ctx = ToolExecutionContext {
1094 executing_supervisor: None,
1095 session_id: Some("session_copy"),
1096 root_session_id: None,
1097 tool_call_id: "call_copy",
1098 event_tx: Some(&tx),
1099 available_tool_schemas: None,
1100 bypass_permissions: false,
1101 auto_approve_permissions: false,
1102 plan_read_only: false,
1103 can_async_resume: false,
1104 bash_completion_sink: None,
1105 pre_parsed_args: None,
1106 };
1107
1108 let _cloned = ctx;
1110
1111 let copied = ctx;
1113
1114 assert_eq!(copied.tool_call_id, "call_copy");
1116 }
1117
1118 #[test]
1119 fn context_is_debug() {
1120 let ctx = ToolExecutionContext::none("call_debug");
1121 let debug_str = format!("{:?}", ctx);
1122 assert!(debug_str.contains("call_debug"));
1123 }
1124
1125 #[tokio::test]
1126 async fn emit_with_empty_tool_call_id() {
1127 let (tx, mut rx) = mpsc::channel(10);
1128 let ctx = ToolExecutionContext {
1129 executing_supervisor: None,
1130 session_id: None,
1131 root_session_id: None,
1132 tool_call_id: "",
1133 event_tx: Some(&tx),
1134 available_tool_schemas: None,
1135 bypass_permissions: false,
1136 auto_approve_permissions: false,
1137 plan_read_only: false,
1138 can_async_resume: false,
1139 bash_completion_sink: None,
1140 pre_parsed_args: None,
1141 };
1142
1143 ctx.emit(AgentEvent::Token {
1144 content: "test".to_string(),
1145 })
1146 .await;
1147
1148 let event = rx.recv().await.unwrap();
1149 match event {
1150 AgentEvent::ToolToken { tool_call_id, .. } => {
1151 assert_eq!(tool_call_id, "");
1152 }
1153 other => panic!("Expected ToolToken, got: {other:?}"),
1154 }
1155 }
1156
1157 #[tokio::test]
1158 async fn emit_with_unicode_content() {
1159 let (tx, mut rx) = mpsc::channel(10);
1160 let ctx = ToolExecutionContext {
1161 executing_supervisor: None,
1162 session_id: Some("会话"),
1163 root_session_id: None,
1164 tool_call_id: "调用_123",
1165 event_tx: Some(&tx),
1166 available_tool_schemas: None,
1167 bypass_permissions: false,
1168 auto_approve_permissions: false,
1169 plan_read_only: false,
1170 can_async_resume: false,
1171 bash_completion_sink: None,
1172 pre_parsed_args: None,
1173 };
1174
1175 ctx.emit(AgentEvent::Token {
1176 content: "测试内容 🎯".to_string(),
1177 })
1178 .await;
1179
1180 let event = rx.recv().await.unwrap();
1181 match event {
1182 AgentEvent::ToolToken {
1183 tool_call_id,
1184 content,
1185 } => {
1186 assert_eq!(tool_call_id, "调用_123");
1187 assert_eq!(content, "测试内容 🎯");
1188 }
1189 other => panic!("Expected ToolToken, got: {other:?}"),
1190 }
1191 }
1192
1193 #[tokio::test]
1194 async fn emit_with_special_characters_in_tool_call_id() {
1195 let (tx, mut rx) = mpsc::channel(10);
1196 let ctx = ToolExecutionContext {
1197 executing_supervisor: None,
1198 session_id: None,
1199 root_session_id: None,
1200 tool_call_id: "call-with_special.chars:123",
1201 event_tx: Some(&tx),
1202 available_tool_schemas: None,
1203 bypass_permissions: false,
1204 auto_approve_permissions: false,
1205 plan_read_only: false,
1206 can_async_resume: false,
1207 bash_completion_sink: None,
1208 pre_parsed_args: None,
1209 };
1210
1211 ctx.emit(AgentEvent::Token {
1212 content: "test".to_string(),
1213 })
1214 .await;
1215
1216 let event = rx.recv().await.unwrap();
1217 match event {
1218 AgentEvent::ToolToken { tool_call_id, .. } => {
1219 assert_eq!(tool_call_id, "call-with_special.chars:123");
1220 }
1221 other => panic!("Expected ToolToken, got: {other:?}"),
1222 }
1223 }
1224
1225 #[tokio::test]
1226 async fn emit_tool_token_with_string_content() {
1227 let (tx, mut rx) = mpsc::channel(10);
1228 let ctx = ToolExecutionContext {
1229 executing_supervisor: None,
1230 session_id: None,
1231 root_session_id: None,
1232 tool_call_id: "call_string",
1233 event_tx: Some(&tx),
1234 available_tool_schemas: None,
1235 bypass_permissions: false,
1236 auto_approve_permissions: false,
1237 plan_read_only: false,
1238 can_async_resume: false,
1239 bash_completion_sink: None,
1240 pre_parsed_args: None,
1241 };
1242
1243 let content = String::from("owned string");
1244 ctx.emit_tool_token(content).await;
1245
1246 let event = rx.recv().await.unwrap();
1247 match event {
1248 AgentEvent::ToolToken { content, .. } => {
1249 assert_eq!(content, "owned string");
1250 }
1251 other => panic!("Expected ToolToken, got: {other:?}"),
1252 }
1253 }
1254
1255 #[tokio::test]
1256 async fn emit_tool_token_with_str_content() {
1257 let (tx, mut rx) = mpsc::channel(10);
1258 let ctx = ToolExecutionContext {
1259 executing_supervisor: None,
1260 session_id: None,
1261 root_session_id: None,
1262 tool_call_id: "call_str",
1263 event_tx: Some(&tx),
1264 available_tool_schemas: None,
1265 bypass_permissions: false,
1266 auto_approve_permissions: false,
1267 plan_read_only: false,
1268 can_async_resume: false,
1269 bash_completion_sink: None,
1270 pre_parsed_args: None,
1271 };
1272
1273 ctx.emit_tool_token("string slice").await;
1274
1275 let event = rx.recv().await.unwrap();
1276 match event {
1277 AgentEvent::ToolToken { content, .. } => {
1278 assert_eq!(content, "string slice");
1279 }
1280 other => panic!("Expected ToolToken, got: {other:?}"),
1281 }
1282 }
1283}
1284
1285#[cfg(test)]
1286mod caller_observation_tests {
1287 use super::*;
1288
1289 #[tokio::test]
1290 async fn original_caller_survives_owned_handoff_for_each_role_and_version() {
1291 for version in [None, Some(1), Some(2)] {
1292 let mut root = Session::new("root", "model");
1293 root.session_version = version;
1294 let mut child = Session::new_child_of("child", &root, "model", "short task");
1295 child.session_version = version;
1296 let mut supervisor = Session::new(DEFAULT_SUPERVISOR_SESSION_ID, "model");
1297 supervisor.session_version = version;
1298 supervisor.authority_identity = SessionAuthorityIdentity::Supervisor {
1299 incarnation_id: Uuid::new_v4(),
1300 };
1301 for mut session in [root, child, supervisor] {
1302 let caller = Arc::new(SessionCallerReference::from(&session));
1303 let borrowed = ToolExecutionContext {
1304 session_id: Some(&session.id),
1305 ..ToolExecutionContext::none("original")
1306 };
1307 let owned = with_executing_session_caller(Some(Arc::clone(&caller)), async {
1308 borrowed.to_tool_ctx()
1309 })
1310 .await;
1311 session.created_at += chrono::Duration::seconds(1);
1314 assert!(!caller.matches_session(&session));
1315 let id = session.id.clone();
1316 let observed = tokio::spawn(async move {
1317 let same = owned.executing_caller_for(&id).unwrap().clone();
1318 assert!(ToolExecutionContext {
1319 session_id: Some(&id),
1320 ..ToolExecutionContext::none("unbound-host")
1321 }
1322 .to_tool_ctx()
1323 .executing_caller
1324 .is_none());
1325 same
1326 })
1327 .await
1328 .unwrap();
1329 assert_eq!(observed, *caller);
1330 }
1331 }
1332 }
1333
1334 #[tokio::test]
1335 async fn caller_scope_filters_mismatch_and_explicit_host_work_then_restores() {
1336 let session = Session::new("original", "model");
1337 let caller = Arc::new(SessionCallerReference::from(&session));
1338 with_executing_session_caller(Some(Arc::clone(&caller)), async {
1339 let original = ToolExecutionContext {
1340 session_id: Some(&session.id),
1341 ..ToolExecutionContext::none("same")
1342 };
1343 assert!(original
1344 .to_tool_ctx()
1345 .executing_caller_for(&session.id)
1346 .is_some());
1347 let mismatched = ToolExecutionContext {
1348 session_id: Some("other"),
1349 ..original
1350 };
1351 assert!(mismatched.to_tool_ctx().executing_caller.is_none());
1352 assert!(ToolExecutionContext::none("synthetic")
1353 .to_tool_ctx()
1354 .executing_caller
1355 .is_none());
1356 with_executing_session_caller(None, async {
1357 assert!(original.to_tool_ctx().executing_caller.is_none());
1358 })
1359 .await;
1360 let mut owned = original.to_tool_ctx();
1361 owned.session_id = Some(Arc::from("other"));
1362 assert!(owned.executing_caller_for(&session.id).is_none());
1363 assert!(owned.executing_caller_for("other").is_none());
1364 assert_eq!(
1365 original.to_tool_ctx().executing_caller.as_deref(),
1366 Some(caller.as_ref())
1367 );
1368 })
1369 .await;
1370 assert!(crate::tools::ToolCtx::none("new-host")
1371 .executing_caller
1372 .is_none());
1373 }
1374}