use std::sync::Arc;
use tokio::sync::mpsc;
use serde_json::Value;
use crate::tools::{BashCompletionSink, ToolCall, ToolSchema};
use crate::{AgentEvent, Session};
use bamboo_domain::{
PermissionMode, SessionAuthorityIdentity, SessionKind, SupervisorReference,
DEFAULT_SUPERVISOR_SESSION_ID,
};
use uuid::Uuid;
tokio::task_local! {
static ROOT_ACTOR_TOOL_WRITER: Option<bamboo_domain::RootActorRuntimeWrite>;
}
pub async fn with_root_actor_tool_writer<F: std::future::Future>(
owner: Option<bamboo_domain::RootActorRuntimeWrite>,
dispatch: F,
) -> F::Output {
ROOT_ACTOR_TOOL_WRITER.scope(owner, dispatch).await
}
pub fn root_actor_tool_writer() -> Option<Option<bamboo_domain::RootActorRuntimeWrite>> {
ROOT_ACTOR_TOOL_WRITER.try_with(Clone::clone).ok()
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ExecutingSupervisorObservation {
incarnation_id: Uuid,
}
const SUPERVISOR_PERMISSION_REPLAY_METADATA_KEY: &str = "permission.executing_supervisor.v1";
#[derive(serde::Serialize, serde::Deserialize)]
#[serde(deny_unknown_fields)]
struct SupervisorPermissionReplayRecord {
version: u8,
incarnation_id: Uuid,
session_id: String,
result_message_id: String,
request_generation: String,
tool_call: ToolCall,
execution_name: String,
}
impl ExecutingSupervisorObservation {
pub const PERMISSION_REPLAY_METADATA_KEY: &'static str =
SUPERVISOR_PERMISSION_REPLAY_METADATA_KEY;
pub fn capture_from_executing_session(session: &Session) -> Option<Self> {
session
.supervisor_incarnation_id()
.map(|incarnation_id| Self { incarnation_id })
}
pub fn supervisor_reference(self) -> SupervisorReference {
SupervisorReference {
session_id: DEFAULT_SUPERVISOR_SESSION_ID.to_string(),
incarnation_id: self.incarnation_id,
}
}
pub fn permission_replay_record(
self,
session_id: &str,
result_message_id: &str,
tool_call: &ToolCall,
execution_name: &str,
request_generation: &str,
) -> Value {
serde_json::to_value(SupervisorPermissionReplayRecord {
version: 1,
incarnation_id: self.incarnation_id,
session_id: session_id.to_string(),
result_message_id: result_message_id.to_string(),
request_generation: request_generation.to_string(),
tool_call: tool_call.clone(),
execution_name: execution_name.to_string(),
})
.expect("Supervisor permission binding serializes")
}
pub fn restore_permission_replay_record(
session: &Session,
result_index: usize,
tool_call: &ToolCall,
execution_name: &str,
request_generation: Option<&str>,
) -> Result<Option<Self>, &'static str> {
let message = session
.messages
.get(result_index)
.ok_or("result occurrence missing")?;
if serde_json::from_str::<Value>(&message.content)
.ok()
.is_some_and(|payload| {
payload
.get(SUPERVISOR_PERMISSION_REPLAY_METADATA_KEY)
.is_some()
})
{
return Err("Supervisor authority is not accepted from a result payload");
}
let Some(value) = message
.metadata
.as_ref()
.and_then(|metadata| metadata.get(SUPERVISOR_PERMISSION_REPLAY_METADATA_KEY))
else {
return Ok(None);
};
let record: SupervisorPermissionReplayRecord = serde_json::from_value(value.clone())
.map_err(|_| "Supervisor permission binding is malformed")?;
let latest_result = session
.messages
.iter()
.rposition(|message| message.tool_call_id.as_deref() == Some(tool_call.id.as_str()));
let newer_call = session.messages[result_index + 1..].iter().any(|message| {
message
.tool_calls
.as_ref()
.is_some_and(|calls| calls.iter().any(|call| call.id == tool_call.id))
});
let preceding_call = session.messages[..result_index]
.iter()
.rev()
.find(|message| {
message
.tool_calls
.as_ref()
.is_some_and(|calls| calls.iter().any(|call| call.id == tool_call.id))
});
let exact_call = preceding_call.is_some_and(|message| {
message.role == bamboo_domain::Role::Assistant
&& message.tool_calls.as_ref().is_some_and(|calls| {
let matching: Vec<_> = calls
.iter()
.filter(|call| call.id == tool_call.id)
.collect();
matching.len() == 1 && matching[0] == tool_call
})
});
if record.version != 1
|| record.incarnation_id.is_nil()
|| session.id != DEFAULT_SUPERVISOR_SESSION_ID
|| session.kind != SessionKind::Root
|| session.root_session_id != session.id
|| session.parent_session_id.is_some()
|| session.spawn_depth != 0
|| session.authority_identity
!= (SessionAuthorityIdentity::Supervisor {
incarnation_id: record.incarnation_id,
})
|| message.role != bamboo_domain::Role::Tool
|| latest_result != Some(result_index)
|| newer_call
|| !exact_call
|| record.session_id != session.id
|| record.result_message_id != message.id
|| record.tool_call != *tool_call
|| record.execution_name != execution_name
|| execution_name.trim().is_empty()
|| record.request_generation.trim().is_empty()
|| Some(record.request_generation.as_str()) != request_generation
{
return Err("Supervisor permission binding does not match the current operation");
}
Ok(Some(Self {
incarnation_id: record.incarnation_id,
}))
}
pub(super) fn for_caller(self, session_id: Option<&str>) -> Option<Self> {
(session_id == Some(DEFAULT_SUPERVISOR_SESSION_ID)).then_some(self)
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct ToolExecutionSessionFlags {
pub bypass_permissions: bool,
pub auto_approve_permissions: bool,
pub plan_read_only: bool,
}
impl ToolExecutionSessionFlags {
pub fn from_session(session: &Session) -> Self {
Self::from_session_and_configured_mode(session, PermissionMode::Default)
}
pub fn from_session_and_configured_mode(
session: &Session,
configured_mode: PermissionMode,
) -> Self {
let requested = session
.agent_runtime_state
.as_ref()
.map(|state| state.effective_permission_mode())
.unwrap_or_default();
let configured_mode = if session
.agent_runtime_state
.as_ref()
.is_some_and(|state| state.plan_mode.is_some())
{
PermissionMode::Plan
} else {
configured_mode
};
let resolution = bamboo_domain::resolve_permission_mode(requested, configured_mode);
Self {
bypass_permissions: resolution.bypass_permissions(),
auto_approve_permissions: resolution.auto_approve_permissions(),
plan_read_only: resolution.effective == PermissionMode::Plan,
}
}
}
#[derive(Clone, Copy)]
pub struct ToolExecutionContext<'a> {
pub session_id: Option<&'a str>,
pub executing_supervisor: Option<ExecutingSupervisorObservation>,
pub root_session_id: Option<&'a str>,
pub tool_call_id: &'a str,
pub event_tx: Option<&'a mpsc::Sender<AgentEvent>>,
pub available_tool_schemas: Option<&'a [ToolSchema]>,
pub bypass_permissions: bool,
pub auto_approve_permissions: bool,
pub plan_read_only: bool,
pub can_async_resume: bool,
pub bash_completion_sink: Option<&'a Arc<dyn BashCompletionSink>>,
pub pre_parsed_args: Option<&'a Value>,
}
impl std::fmt::Debug for ToolExecutionContext<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ToolExecutionContext")
.field("session_id", &self.session_id)
.field("executing_supervisor", &self.executing_supervisor)
.field("root_session_id", &self.root_session_id)
.field("tool_call_id", &self.tool_call_id)
.field("event_tx", &self.event_tx)
.field("available_tool_schemas", &self.available_tool_schemas)
.field("bypass_permissions", &self.bypass_permissions)
.field("auto_approve_permissions", &self.auto_approve_permissions)
.field("plan_read_only", &self.plan_read_only)
.field("can_async_resume", &self.can_async_resume)
.field("bash_completion_sink", &self.bash_completion_sink.is_some())
.field("pre_parsed_args", &self.pre_parsed_args)
.finish()
}
}
impl<'a> ToolExecutionContext<'a> {
pub fn none(tool_call_id: &'a str) -> Self {
Self {
session_id: None,
executing_supervisor: None,
root_session_id: None,
tool_call_id,
event_tx: None,
available_tool_schemas: None,
bypass_permissions: false,
auto_approve_permissions: false,
plan_read_only: false,
can_async_resume: false,
bash_completion_sink: None,
pre_parsed_args: None,
}
}
#[allow(clippy::too_many_arguments)]
pub fn for_dispatch(
session_id: &'a str,
root_session_id: &'a str,
tool_call_id: &'a str,
event_tx: &'a mpsc::Sender<AgentEvent>,
available_tool_schemas: &'a [ToolSchema],
flags: ToolExecutionSessionFlags,
can_async_resume: bool,
bash_completion_sink: Option<&'a Arc<dyn BashCompletionSink>>,
pre_parsed_args: Option<&'a Value>,
) -> Self {
Self {
session_id: Some(session_id),
executing_supervisor: None,
root_session_id: Some(if root_session_id.trim().is_empty() {
session_id
} else {
root_session_id
}),
tool_call_id,
event_tx: Some(event_tx),
available_tool_schemas: Some(available_tool_schemas),
bypass_permissions: flags.bypass_permissions,
auto_approve_permissions: flags.auto_approve_permissions,
plan_read_only: flags.plan_read_only,
can_async_resume,
bash_completion_sink,
pre_parsed_args,
}
}
pub fn with_executing_supervisor(
mut self,
observation: Option<ExecutingSupervisorObservation>,
) -> Self {
self.executing_supervisor =
observation.and_then(|observation| observation.for_caller(self.session_id));
self
}
pub fn cloned_sender(&self) -> Option<mpsc::Sender<AgentEvent>> {
self.event_tx.cloned()
}
pub fn cloned_bash_completion_sink(&self) -> Option<Arc<dyn BashCompletionSink>> {
self.bash_completion_sink.map(Arc::clone)
}
pub fn root_session_id(&self) -> Option<&'a str> {
self.root_session_id
}
pub fn to_tool_ctx(&self) -> crate::tools::ToolCtx {
crate::tools::ToolCtx {
session_id: self.session_id.map(Arc::from),
executing_supervisor: self
.executing_supervisor
.and_then(|observation| observation.for_caller(self.session_id)),
tool_call_id: Arc::from(self.tool_call_id),
event_tx: self.event_tx.cloned(),
available_tool_schemas: self
.available_tool_schemas
.map(Arc::from)
.unwrap_or_else(|| Arc::from(Vec::new())),
bypass_permissions: self.bypass_permissions,
auto_approve_permissions: self.auto_approve_permissions,
plan_read_only: self.plan_read_only,
can_async_resume: self.can_async_resume,
async_completion_sink: None,
bash_completion_sink: self.bash_completion_sink.map(Arc::clone),
}
}
pub async fn emit(&self, event: AgentEvent) {
if let Some(tx) = self.event_tx {
let event = match event {
AgentEvent::Token { content } => AgentEvent::ToolToken {
tool_call_id: self.tool_call_id.to_string(),
content,
},
other => other,
};
let _ = tx.try_send(event);
}
}
pub async fn emit_tool_token(&self, content: impl Into<String>) {
self.emit(AgentEvent::ToolToken {
tool_call_id: self.tool_call_id.to_string(),
content: content.into(),
})
.await;
}
}
#[cfg(test)]
mod supervisor_observation_tests {
use super::*;
fn supervisor() -> Session {
let mut session = Session::new(DEFAULT_SUPERVISOR_SESSION_ID, "model");
session.authority_identity = SessionAuthorityIdentity::Supervisor {
incarnation_id: Uuid::new_v4(),
};
session
}
#[test]
fn supervisor_observation_requires_typed_canonical_root_lineage() {
let valid = supervisor();
let observed = ExecutingSupervisorObservation::capture_from_executing_session(&valid)
.expect("canonical executing Supervisor");
assert_eq!(observed.supervisor_reference().session_id, valid.id);
for mutation in [
|s: &mut Session| s.authority_identity = SessionAuthorityIdentity::Ordinary,
|s: &mut Session| s.id = "other".into(),
|s: &mut Session| s.root_session_id = "other".into(),
|s: &mut Session| s.parent_session_id = Some("parent".into()),
|s: &mut Session| s.spawn_depth = 1,
|s: &mut Session| s.kind = SessionKind::Child,
|s: &mut Session| {
s.authority_identity = SessionAuthorityIdentity::Supervisor {
incarnation_id: Uuid::nil(),
}
},
] {
let mut malformed = valid.clone();
mutation(&mut malformed);
assert_eq!(
ExecutingSupervisorObservation::capture_from_executing_session(&malformed),
None,
"malformed Session: {malformed:?}"
);
}
let mut ordinary = Session::new(DEFAULT_SUPERVISOR_SESSION_ID, "model");
ordinary.metadata.insert(
"authority_identity".into(),
serde_json::to_string(&valid.authority_identity).unwrap(),
);
ordinary.metadata.insert("supervisor".into(), "true".into());
assert_eq!(
ExecutingSupervisorObservation::capture_from_executing_session(&ordinary),
None,
"reserved ID and forged metadata cannot supply a typed identity"
);
}
#[test]
fn supervisor_observation_is_not_minted_by_flags_arguments_or_generic_contexts() {
let session = supervisor();
let (tx, _rx) = mpsc::channel(1);
let forged_args = serde_json::json!({
"executing_supervisor": session.authority_identity,
"session_id": DEFAULT_SUPERVISOR_SESSION_ID,
});
let ctx = ToolExecutionContext::for_dispatch(
&session.id,
&session.root_session_id,
"call",
&tx,
&[],
ToolExecutionSessionFlags {
bypass_permissions: true,
auto_approve_permissions: true,
plan_read_only: true,
},
false,
None,
Some(&forged_args),
);
assert_eq!(ctx.executing_supervisor, None);
assert_eq!(ctx.to_tool_ctx().executing_supervisor, None);
assert_eq!(
ToolExecutionContext::none("none").executing_supervisor,
None
);
assert_eq!(
crate::tools::ToolCtx::none("owned").executing_supervisor,
None
);
let observation = ExecutingSupervisorObservation::capture_from_executing_session(&session);
let captured = ctx.with_executing_supervisor(observation);
let owned = captured.to_tool_ctx().clone();
assert_eq!(owned.executing_supervisor_for(&session.id), observation);
assert_eq!(owned.executing_supervisor_for("child"), None);
assert_eq!(
ToolExecutionContext::none("missing")
.with_executing_supervisor(observation)
.executing_supervisor,
None
);
let mismatched = ToolExecutionContext {
session_id: Some("child"),
..captured
};
assert_eq!(mismatched.to_tool_ctx().executing_supervisor, None);
}
}
#[cfg(test)]
mod session_flags_tests {
use super::*;
use bamboo_domain::{AgentRuntimeState, SessionPermissionMode};
#[test]
fn dispatch_context_falls_back_from_empty_legacy_root_to_session_id() {
let (event_tx, _event_rx) = mpsc::channel(1);
let ctx = ToolExecutionContext::for_dispatch(
"session-id",
" ",
"call-id",
&event_tx,
&[],
ToolExecutionSessionFlags::default(),
false,
None,
None,
);
assert_eq!(ctx.root_session_id(), Some("session-id"));
}
#[test]
fn from_session_defaults_false_without_runtime_state() {
let session = Session::new("s-none", "test-model");
assert_eq!(
ToolExecutionSessionFlags::from_session(&session),
ToolExecutionSessionFlags {
bypass_permissions: false,
auto_approve_permissions: false,
plan_read_only: false,
}
);
}
#[test]
fn from_session_reads_bypass_from_runtime_state() {
let mut session = Session::new("s-bypass", "test-model");
let mut runtime = AgentRuntimeState::new("run-1");
runtime.bypass_permissions = true;
session.agent_runtime_state = Some(runtime);
assert!(ToolExecutionSessionFlags::from_session(&session).bypass_permissions);
assert!(!ToolExecutionSessionFlags::from_session(&session).auto_approve_permissions);
}
#[test]
fn from_session_distinguishes_auto_from_legacy_bypass() {
let mut session = Session::new("s-auto", "test-model");
let mut runtime = AgentRuntimeState::new("run-1");
runtime.set_permission_mode(SessionPermissionMode::Auto);
session.agent_runtime_state = Some(runtime);
let flags = ToolExecutionSessionFlags::from_session(&session);
assert!(!flags.bypass_permissions);
assert!(flags.auto_approve_permissions);
}
#[test]
fn configured_auto_applies_to_default_but_not_explicit_bypass() {
let default_session = Session::new("s-global-auto", "test-model");
let flags = ToolExecutionSessionFlags::from_session_and_configured_mode(
&default_session,
PermissionMode::Auto,
);
assert_eq!(
flags,
ToolExecutionSessionFlags {
bypass_permissions: false,
auto_approve_permissions: true,
plan_read_only: false,
}
);
let mut bypass_session = default_session;
bypass_session
.agent_runtime_state
.get_or_insert_default()
.set_permission_mode(SessionPermissionMode::Bypass);
let flags = ToolExecutionSessionFlags::from_session_and_configured_mode(
&bypass_session,
PermissionMode::Auto,
);
assert!(flags.bypass_permissions);
assert!(!flags.auto_approve_permissions);
}
#[test]
fn configured_plan_preserves_no_prompt_but_sets_read_only_gate() {
let mut session = Session::new("s-plan-auto", "test-model");
session
.agent_runtime_state
.get_or_insert_default()
.set_permission_mode(SessionPermissionMode::Auto);
let flags = ToolExecutionSessionFlags::from_session_and_configured_mode(
&session,
PermissionMode::Plan,
);
assert!(!flags.bypass_permissions);
assert!(flags.auto_approve_permissions);
assert!(flags.plan_read_only);
}
#[test]
fn persisted_plan_overlay_is_honored_without_config_reconstruction() {
let mut session = Session::new("s-persisted-plan-auto", "test-model");
let runtime = session.agent_runtime_state.get_or_insert_default();
runtime.set_permission_mode(SessionPermissionMode::Auto);
runtime.plan_mode = Some(bamboo_domain::PlanModeState {
entered_at: chrono::Utc::now(),
pre_permission_mode: "auto".to_string(),
plan_file_path: None,
status: bamboo_domain::PlanModeStatus::Designing,
});
let flags = ToolExecutionSessionFlags::from_session(&session);
assert!(flags.plan_read_only);
assert!(flags.auto_approve_permissions);
assert!(!flags.bypass_permissions);
}
#[test]
fn typed_read_only_child_does_not_reactivate_the_legacy_plan_name_gate() {
for requested in [SessionPermissionMode::Auto, SessionPermissionMode::Bypass] {
let mut session = Session::new("s-read-only-child", "test-model");
let runtime = session.agent_runtime_state.get_or_insert_default();
runtime.set_permission_mode(requested);
runtime.read_only = true;
let flags = ToolExecutionSessionFlags::from_session_and_configured_mode(
&session,
PermissionMode::Auto,
);
assert!(!flags.plan_read_only);
assert_eq!(
flags.bypass_permissions,
requested == SessionPermissionMode::Bypass
);
assert_eq!(
flags.auto_approve_permissions,
requested == SessionPermissionMode::Auto
);
}
}
#[test]
fn for_dispatch_maps_flags_onto_context() {
let (tx, _rx) = mpsc::channel(1);
let ctx = ToolExecutionContext::for_dispatch(
"s1",
"root-s1",
"call-1",
&tx,
&[],
ToolExecutionSessionFlags {
bypass_permissions: true,
auto_approve_permissions: false,
plan_read_only: false,
},
true,
None,
None,
);
assert_eq!(ctx.session_id, Some("s1"));
assert_eq!(ctx.root_session_id(), Some("root-s1"));
assert!(ctx.bypass_permissions);
assert!(!ctx.auto_approve_permissions);
assert!(ctx.can_async_resume);
assert!(ctx.pre_parsed_args.is_none());
let owned = ctx.to_tool_ctx();
assert!(owned.bypass_permissions);
assert!(!owned.auto_approve_permissions);
assert!(!owned.plan_read_only);
}
#[test]
fn for_dispatch_threads_pre_parsed_args() {
let (tx, _rx) = mpsc::channel(1);
let parsed = serde_json::json!({"v": "x"});
let ctx = ToolExecutionContext::for_dispatch(
"s1",
"root-s1",
"call-1",
&tx,
&[],
ToolExecutionSessionFlags::default(),
false,
None,
Some(&parsed),
);
assert_eq!(ctx.pre_parsed_args, Some(&parsed));
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn emit_does_not_block_when_channel_is_full() {
let (tx, mut rx) = mpsc::channel(1);
tx.send(AgentEvent::Token {
content: "full".to_string(),
})
.await
.unwrap();
let ctx = ToolExecutionContext {
executing_supervisor: None,
session_id: Some("session_1"),
root_session_id: None,
tool_call_id: "call_1",
event_tx: Some(&tx),
available_tool_schemas: None,
bypass_permissions: false,
auto_approve_permissions: false,
plan_read_only: false,
can_async_resume: false,
bash_completion_sink: None,
pre_parsed_args: None,
};
tokio::time::timeout(
std::time::Duration::from_millis(100),
ctx.emit(AgentEvent::Token {
content: "next".to_string(),
}),
)
.await
.expect("emit should not block on full channel");
let first = rx.recv().await.unwrap();
match first {
AgentEvent::Token { content } => assert_eq!(content, "full"),
other => panic!("unexpected event: {other:?}"),
}
}
#[tokio::test]
async fn emit_converts_token_to_tool_token() {
let (tx, mut rx) = mpsc::channel(10);
let ctx = ToolExecutionContext {
executing_supervisor: None,
session_id: Some("session_1"),
root_session_id: None,
tool_call_id: "call_123",
event_tx: Some(&tx),
available_tool_schemas: None,
bypass_permissions: false,
auto_approve_permissions: false,
plan_read_only: false,
can_async_resume: false,
bash_completion_sink: None,
pre_parsed_args: None,
};
ctx.emit(AgentEvent::Token {
content: "test content".to_string(),
})
.await;
let event = rx.recv().await.unwrap();
match event {
AgentEvent::ToolToken {
tool_call_id,
content,
} => {
assert_eq!(tool_call_id, "call_123");
assert_eq!(content, "test content");
}
other => panic!("Expected ToolToken, got: {other:?}"),
}
}
#[tokio::test]
async fn emit_passes_through_non_token_events() {
let (tx, mut rx) = mpsc::channel(10);
let ctx = ToolExecutionContext {
executing_supervisor: None,
session_id: Some("session_1"),
root_session_id: None,
tool_call_id: "call_456",
event_tx: Some(&tx),
available_tool_schemas: None,
bypass_permissions: false,
auto_approve_permissions: false,
plan_read_only: false,
can_async_resume: false,
bash_completion_sink: None,
pre_parsed_args: None,
};
ctx.emit(AgentEvent::ToolToken {
tool_call_id: "other".to_string(),
content: "direct tool token".to_string(),
})
.await;
let event = rx.recv().await.unwrap();
match event {
AgentEvent::ToolToken { content, .. } => {
assert_eq!(content, "direct tool token");
}
other => panic!("Expected ToolToken, got: {other:?}"),
}
}
#[tokio::test]
async fn emit_does_nothing_when_no_sender() {
let ctx = ToolExecutionContext::none("call_789");
ctx.emit(AgentEvent::Token {
content: "test".to_string(),
})
.await;
}
#[tokio::test]
async fn emit_tool_token_convenience_method() {
let (tx, mut rx) = mpsc::channel(10);
let ctx = ToolExecutionContext {
executing_supervisor: None,
session_id: None,
root_session_id: None,
tool_call_id: "call_abc",
event_tx: Some(&tx),
available_tool_schemas: None,
bypass_permissions: false,
auto_approve_permissions: false,
plan_read_only: false,
can_async_resume: false,
bash_completion_sink: None,
pre_parsed_args: None,
};
ctx.emit_tool_token("convenient output").await;
let event = rx.recv().await.unwrap();
match event {
AgentEvent::ToolToken {
tool_call_id,
content,
} => {
assert_eq!(tool_call_id, "call_abc");
assert_eq!(content, "convenient output");
}
other => panic!("Expected ToolToken, got: {other:?}"),
}
}
#[tokio::test]
async fn emit_tool_token_with_no_sender_does_nothing() {
let ctx = ToolExecutionContext::none("call_def");
ctx.emit_tool_token("test").await;
}
#[test]
fn none_creates_context_with_no_optional_fields() {
let ctx = ToolExecutionContext::none("call_xyz");
assert_eq!(ctx.session_id, None);
assert_eq!(ctx.tool_call_id, "call_xyz");
assert!(ctx.event_tx.is_none());
}
#[test]
fn cloned_sender_returns_none_when_no_sender() {
let ctx = ToolExecutionContext::none("call_test");
assert!(ctx.cloned_sender().is_none());
}
#[tokio::test]
async fn cloned_sender_returns_clone_when_sender_present() {
let (tx, _rx) = mpsc::channel(10);
let ctx = ToolExecutionContext {
executing_supervisor: None,
session_id: None,
root_session_id: None,
tool_call_id: "call_clone",
event_tx: Some(&tx),
available_tool_schemas: None,
bypass_permissions: false,
auto_approve_permissions: false,
plan_read_only: false,
can_async_resume: false,
bash_completion_sink: None,
pre_parsed_args: None,
};
let cloned = ctx.cloned_sender();
assert!(cloned.is_some());
cloned
.unwrap()
.send(AgentEvent::Token {
content: "test".to_string(),
})
.await
.unwrap();
}
#[tokio::test]
async fn emit_handles_multiple_sequential_calls() {
let (tx, mut rx) = mpsc::channel(10);
let ctx = ToolExecutionContext {
executing_supervisor: None,
session_id: Some("session_multi"),
root_session_id: None,
tool_call_id: "call_multi",
event_tx: Some(&tx),
available_tool_schemas: None,
bypass_permissions: false,
auto_approve_permissions: false,
plan_read_only: false,
can_async_resume: false,
bash_completion_sink: None,
pre_parsed_args: None,
};
for i in 0..5 {
ctx.emit(AgentEvent::Token {
content: format!("message {}", i),
})
.await;
}
for i in 0..5 {
let event = rx.recv().await.unwrap();
match event {
AgentEvent::ToolToken { content, .. } => {
assert_eq!(content, format!("message {}", i));
}
other => panic!("Expected ToolToken, got: {other:?}"),
}
}
}
#[test]
fn context_is_clone_and_copy() {
let (tx, _rx) = mpsc::channel(10);
let ctx = ToolExecutionContext {
executing_supervisor: None,
session_id: Some("session_copy"),
root_session_id: None,
tool_call_id: "call_copy",
event_tx: Some(&tx),
available_tool_schemas: None,
bypass_permissions: false,
auto_approve_permissions: false,
plan_read_only: false,
can_async_resume: false,
bash_completion_sink: None,
pre_parsed_args: None,
};
let _cloned = ctx;
let copied = ctx;
assert_eq!(copied.tool_call_id, "call_copy");
}
#[test]
fn context_is_debug() {
let ctx = ToolExecutionContext::none("call_debug");
let debug_str = format!("{:?}", ctx);
assert!(debug_str.contains("call_debug"));
}
#[tokio::test]
async fn emit_with_empty_tool_call_id() {
let (tx, mut rx) = mpsc::channel(10);
let ctx = ToolExecutionContext {
executing_supervisor: None,
session_id: None,
root_session_id: None,
tool_call_id: "",
event_tx: Some(&tx),
available_tool_schemas: None,
bypass_permissions: false,
auto_approve_permissions: false,
plan_read_only: false,
can_async_resume: false,
bash_completion_sink: None,
pre_parsed_args: None,
};
ctx.emit(AgentEvent::Token {
content: "test".to_string(),
})
.await;
let event = rx.recv().await.unwrap();
match event {
AgentEvent::ToolToken { tool_call_id, .. } => {
assert_eq!(tool_call_id, "");
}
other => panic!("Expected ToolToken, got: {other:?}"),
}
}
#[tokio::test]
async fn emit_with_unicode_content() {
let (tx, mut rx) = mpsc::channel(10);
let ctx = ToolExecutionContext {
executing_supervisor: None,
session_id: Some("会话"),
root_session_id: None,
tool_call_id: "调用_123",
event_tx: Some(&tx),
available_tool_schemas: None,
bypass_permissions: false,
auto_approve_permissions: false,
plan_read_only: false,
can_async_resume: false,
bash_completion_sink: None,
pre_parsed_args: None,
};
ctx.emit(AgentEvent::Token {
content: "测试内容 🎯".to_string(),
})
.await;
let event = rx.recv().await.unwrap();
match event {
AgentEvent::ToolToken {
tool_call_id,
content,
} => {
assert_eq!(tool_call_id, "调用_123");
assert_eq!(content, "测试内容 🎯");
}
other => panic!("Expected ToolToken, got: {other:?}"),
}
}
#[tokio::test]
async fn emit_with_special_characters_in_tool_call_id() {
let (tx, mut rx) = mpsc::channel(10);
let ctx = ToolExecutionContext {
executing_supervisor: None,
session_id: None,
root_session_id: None,
tool_call_id: "call-with_special.chars:123",
event_tx: Some(&tx),
available_tool_schemas: None,
bypass_permissions: false,
auto_approve_permissions: false,
plan_read_only: false,
can_async_resume: false,
bash_completion_sink: None,
pre_parsed_args: None,
};
ctx.emit(AgentEvent::Token {
content: "test".to_string(),
})
.await;
let event = rx.recv().await.unwrap();
match event {
AgentEvent::ToolToken { tool_call_id, .. } => {
assert_eq!(tool_call_id, "call-with_special.chars:123");
}
other => panic!("Expected ToolToken, got: {other:?}"),
}
}
#[tokio::test]
async fn emit_tool_token_with_string_content() {
let (tx, mut rx) = mpsc::channel(10);
let ctx = ToolExecutionContext {
executing_supervisor: None,
session_id: None,
root_session_id: None,
tool_call_id: "call_string",
event_tx: Some(&tx),
available_tool_schemas: None,
bypass_permissions: false,
auto_approve_permissions: false,
plan_read_only: false,
can_async_resume: false,
bash_completion_sink: None,
pre_parsed_args: None,
};
let content = String::from("owned string");
ctx.emit_tool_token(content).await;
let event = rx.recv().await.unwrap();
match event {
AgentEvent::ToolToken { content, .. } => {
assert_eq!(content, "owned string");
}
other => panic!("Expected ToolToken, got: {other:?}"),
}
}
#[tokio::test]
async fn emit_tool_token_with_str_content() {
let (tx, mut rx) = mpsc::channel(10);
let ctx = ToolExecutionContext {
executing_supervisor: None,
session_id: None,
root_session_id: None,
tool_call_id: "call_str",
event_tx: Some(&tx),
available_tool_schemas: None,
bypass_permissions: false,
auto_approve_permissions: false,
plan_read_only: false,
can_async_resume: false,
bash_completion_sink: None,
pre_parsed_args: None,
};
ctx.emit_tool_token("string slice").await;
let event = rx.recv().await.unwrap();
match event {
AgentEvent::ToolToken { content, .. } => {
assert_eq!(content, "string slice");
}
other => panic!("Expected ToolToken, got: {other:?}"),
}
}
}