use std::sync::Arc;
use super::Agent;
use crate::channel::Channel;
pub(crate) fn insert_main_agent_ctx(
env: &mut std::collections::HashMap<String, String>,
conv_id: Option<&str>,
) {
env.insert("ZEPH_AGENT_TYPE".to_owned(), "main".to_owned());
if let Some(id) = conv_id {
env.insert("ZEPH_AGENT_ID".to_owned(), id.to_owned());
}
}
impl<C: Channel> Agent<C> {
pub(super) fn mcp_dispatch(&self) -> Option<McpManagerDispatch> {
self.services
.mcp
.manager
.as_ref()
.map(|m| McpManagerDispatch(Arc::clone(m)))
}
#[tracing::instrument(name = "core.agent.check_cwd_changed", skip_all, level = "debug")]
pub(crate) async fn check_cwd_changed(&mut self) {
let current = match std::env::current_dir() {
Ok(p) => p,
Err(e) => {
tracing::warn!("check_cwd_changed: failed to get cwd: {e}");
return;
}
};
if current == self.runtime.lifecycle.last_known_cwd {
return;
}
let old_cwd =
std::mem::replace(&mut self.runtime.lifecycle.last_known_cwd, current.clone());
self.services.session.env_context.working_dir = current.display().to_string();
tracing::info!(
old = %old_cwd.display(),
new = %current.display(),
"working directory changed"
);
let _ = self
.channel
.send_status("Working directory changed\u{2026}")
.await;
let hooks = self.services.session.hooks_config.cwd_changed.clone();
if hooks.is_empty() {
tracing::debug!("CwdChanged: no hooks configured, skipping");
} else {
tracing::debug!(count = hooks.len(), "CwdChanged: firing hooks");
let mut env = std::collections::HashMap::new();
env.insert("ZEPH_OLD_CWD".to_owned(), old_cwd.display().to_string());
env.insert("ZEPH_NEW_CWD".to_owned(), current.display().to_string());
let conv_id_str = self
.services
.memory
.persistence
.conversation_id
.map(|id| id.0.to_string());
insert_main_agent_ctx(&mut env, conv_id_str.as_deref());
let dispatch = self.mcp_dispatch();
let mcp: Option<&dyn zeph_subagent::McpDispatch> = dispatch
.as_ref()
.map(|d| d as &dyn zeph_subagent::McpDispatch);
if let Err(e) = zeph_subagent::hooks::fire_hooks(&hooks, &env, mcp, None).await {
tracing::warn!(error = %e, "CwdChanged hook failed");
} else {
tracing::info!(count = hooks.len(), "CwdChanged: hooks fired");
}
}
let _ = self.channel.send_status("").await;
}
#[tracing::instrument(name = "core.agent.handle_file_changed", skip_all, level = "debug")]
pub(crate) async fn handle_file_changed(
&mut self,
event: crate::file_watcher::FileChangedEvent,
) {
tracing::info!(path = %event.path.display(), "file changed");
let _ = self
.channel
.send_status("Running file-change hook\u{2026}")
.await;
let hooks = self
.services
.session
.hooks_config
.file_changed_hooks
.clone();
if hooks.is_empty() {
tracing::debug!(path = %event.path.display(), "FileChanged: no hooks configured, skipping");
} else {
tracing::debug!(count = hooks.len(), path = %event.path.display(), "FileChanged: firing hooks");
let mut env = std::collections::HashMap::new();
env.insert(
"ZEPH_CHANGED_PATH".to_owned(),
event.path.display().to_string(),
);
let conv_id_str = self
.services
.memory
.persistence
.conversation_id
.map(|id| id.0.to_string());
insert_main_agent_ctx(&mut env, conv_id_str.as_deref());
let dispatch = self.mcp_dispatch();
let mcp: Option<&dyn zeph_subagent::McpDispatch> = dispatch
.as_ref()
.map(|d| d as &dyn zeph_subagent::McpDispatch);
if let Err(e) = zeph_subagent::hooks::fire_hooks(&hooks, &env, mcp, None).await {
tracing::warn!(error = %e, "FileChanged hook failed");
} else {
tracing::info!(count = hooks.len(), path = %event.path.display(), "FileChanged: hooks fired");
}
}
let _ = self.channel.send_status("").await;
}
}
pub(super) struct McpManagerDispatch(Arc<zeph_mcp::McpManager>);
impl zeph_subagent::McpDispatch for McpManagerDispatch {
fn call_tool<'a>(
&'a self,
server: &'a str,
tool: &'a str,
args: serde_json::Value,
) -> std::pin::Pin<
Box<dyn std::future::Future<Output = Result<serde_json::Value, String>> + Send + 'a>,
> {
Box::pin(async move {
self.0
.call_tool(server, tool, args)
.await
.map(|result| {
let blocks: Vec<serde_json::Value> = result
.content
.iter()
.map(|c| serde_json::Value::String(zeph_mcp::render_content_block(c)))
.collect();
serde_json::Value::Array(blocks)
})
.map_err(|e| e.to_string())
})
}
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use super::insert_main_agent_ctx;
#[test]
fn insert_main_agent_ctx_always_sets_agent_type() {
let mut env = HashMap::new();
insert_main_agent_ctx(&mut env, None);
assert_eq!(env.get("ZEPH_AGENT_TYPE").map(String::as_str), Some("main"));
}
#[test]
fn insert_main_agent_ctx_sets_agent_id_when_some() {
let mut env = HashMap::new();
insert_main_agent_ctx(&mut env, Some("conv-abc"));
assert_eq!(
env.get("ZEPH_AGENT_ID").map(String::as_str),
Some("conv-abc")
);
}
#[test]
fn insert_main_agent_ctx_omits_agent_id_when_none() {
let mut env = HashMap::new();
insert_main_agent_ctx(&mut env, None);
assert!(
!env.contains_key("ZEPH_AGENT_ID"),
"ZEPH_AGENT_ID must be absent when conv_id is None"
);
}
}