mod session_actor_common;
use session_actor_common::*;
use std::path::Path;
use std::sync::{Arc, Mutex};
use agent_core::memory::author::Author;
use agent_engine::extensions::hooks::events::{HookEvent, HookResult};
use agent_engine::extensions::runtime::process::RegisteredExtensionToolSpec;
use agent_engine::extensions::runtime::ExtensionHandler;
use agent_engine::session::{
ClientKind, ClientMeta, ClientTransport, LocalTransport, SessionCommand, SessionConfig,
SessionEventWire,
};
use agent_engine::{EngineHost, Tool, ToolContext};
use serde_json::{json, Value};
use serial_test::serial;
#[derive(Debug, Clone, PartialEq)]
struct Call {
session_id: Option<String>,
bound: Option<Author>,
caller: Option<Author>,
}
#[derive(Default)]
struct Recorder {
bound: Mutex<Option<Author>>,
calls: Mutex<Vec<Call>>,
}
#[async_trait::async_trait]
impl ExtensionHandler for Recorder {
fn id(&self) -> &str {
"who"
}
async fn handle(&self, _event: &HookEvent) -> HookResult {
HookResult::Continue
}
async fn call_tool_as(
&self,
_name: &str,
_input: Value,
session_id: Option<&str>,
_cwd: Option<&Path>,
caller: Option<&Author>,
) -> Result<Value, String> {
self.calls.lock().unwrap().push(Call {
session_id: session_id.map(str::to_string),
bound: self.bound.lock().unwrap().take(),
caller: caller.cloned(),
});
Ok(json!("here"))
}
async fn shutdown(&self) {}
}
struct Noting(agent_engine::tools::ExtensionTool, Arc<Recorder>);
#[async_trait::async_trait]
impl Tool for Noting {
fn name(&self) -> &str {
self.0.name()
}
fn description(&self) -> &str {
self.0.description()
}
fn parameters(&self) -> Value {
self.0.parameters()
}
async fn execute(&self, params: Value, ctx: ToolContext) -> agent_engine::Result<String> {
*self.1.bound.lock().unwrap() = ctx
.capabilities
.memory_backend
.as_ref()
.map(|binding| binding.author().clone());
self.0.execute(params, ctx).await
}
}
async fn host_with_the_command() -> (Arc<EngineHost>, Arc<Recorder>, tempfile::TempDir) {
let host = host().await;
assert!(
EngineHost::install(Arc::clone(&host)).is_ok(),
"this binary installs one host"
);
let recorder = Arc::new(Recorder::default());
let tool = agent_engine::tools::ExtensionTool::new(
"who",
RegisteredExtensionToolSpec {
name: "ami".into(),
description: "records the caller".into(),
input_schema: json!({"type": "object"}),
},
recorder.clone(),
);
assert_eq!(tool.name(), "who:ami");
host.parts()
.tools
.write()
.await
.register(Arc::new(Noting(tool, recorder.clone())));
let plugin_root = tempfile::tempdir().unwrap();
host.command_registry().rebuild_with_plugins(
vec![],
vec![agent_engine::skills::Plugin {
name: "who".into(),
root: plugin_root.path().to_path_buf(),
marketplace: None,
version: None,
description: None,
extension: None,
manifest: Some(
serde_json::from_value(json!({
"name": "who",
"commands": [{"name": "ami", "tool": "ami", "input": {}}],
}))
.unwrap(),
),
}],
);
(host, recorder, plugin_root)
}
async fn who_am_i(transport: &mut LocalTransport, query: u64) {
transport
.send(SessionCommand::PluginCommand {
id: query,
plugin: "who".into(),
name: "ami".into(),
arg: String::new(),
})
.await
.unwrap();
let seen = until(
transport,
|e| matches!(e, SessionEventWire::QueryResult { id, .. } if *id == query),
)
.await;
let Some(SessionEventWire::QueryResult { value, .. }) = seen.last().map(|e| &e.event) else {
unreachable!()
};
assert_eq!(value["kind"], "plugin_output", "/who:ami: {value}");
assert_eq!(value["status"], 0, "/who:ami: {value}");
assert_eq!(value["stdout"], "here", "/who:ami: {value}");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[serial]
async fn a_chats_plugin_command_names_that_chats_own_author_as_caller() {
let _h = Home::new();
let (url, _) = stub(SSE_HI, false).await;
std::env::set_var("SYNAPS_ANTHROPIC_BASE_URL", &url);
let (host, recorder, _plugin_root) = host_with_the_command().await;
let (dir_a, dir_b) = (tempfile::tempdir().unwrap(), tempfile::tempdir().unwrap());
let mut sessions = Vec::new();
for dir in [dir_a.path(), dir_b.path()] {
let handle = host
.create_session(SessionConfig {
cwd: Some(dir.to_path_buf()),
..cfg()
})
.await
.unwrap();
let (transport, _) =
LocalTransport::attach(handle.clone(), ClientMeta::new(ClientKind::Test))
.await
.unwrap();
sessions.push((handle, transport));
}
for (query, chat) in [0usize, 1, 0].into_iter().enumerate() {
who_am_i(&mut sessions[chat].1, query as u64 + 1).await;
}
let calls = recorder.calls.lock().unwrap().clone();
assert_eq!(calls.len(), 3, "{calls:#?}");
let mut callers = Vec::new();
for (call, chat) in calls.iter().zip([0usize, 1, 0]) {
assert_eq!(
call.session_id.as_deref(),
Some(sessions[chat].0.id.as_str()),
"{call:?}"
);
let caller = call.caller.clone().expect("a chat's command names its caller");
caller.validate().unwrap();
assert!(caller.parent.is_none(), "a chat has no parent: {caller:?}");
assert!(call.bound.is_some(), "the command's tool context has a binding");
assert_eq!(call.caller, call.bound, "the binding's author, not another");
callers.push(caller);
}
assert_eq!(callers[0], callers[2], "one chat, one caller");
assert_ne!(callers[0].actor, callers[1].actor);
assert_ne!(callers[0].group, callers[1].group);
for (_, transport) in sessions.iter_mut() {
end(transport).await;
}
}