🥇 export from upstream (a1fa8e3a)
This commit is contained in:
@@ -31,8 +31,6 @@ dirigent_acp_api = { path = "../dirigent_acp_api", optional = true }
|
||||
# Workspace dependencies
|
||||
dirigent_config = { path = "../dirigent_config", optional = true }
|
||||
dirigent_auth = { path = "../dirigent_auth" }
|
||||
dirigent_process = { path = "../dirigent_process", features = ["tokio"], optional = true }
|
||||
dirigent_inspector = { path = "../dirigent_inspector", optional = true }
|
||||
dirigent_protocol = { path = "../dirigent_protocol", features = ["adapters"], optional = true }
|
||||
dirigent_tools = { path = "../dirigent_tools", optional = true }
|
||||
# SSE client for ACP transport
|
||||
@@ -84,7 +82,6 @@ server = [
|
||||
"dep:blake3",
|
||||
"dep:dirigent_acp_api",
|
||||
"dep:dirigent_config",
|
||||
"dep:dirigent_inspector",
|
||||
"dep:dirigent_protocol",
|
||||
"dep:dirigent_tools",
|
||||
"dep:eventsource-client",
|
||||
@@ -99,5 +96,4 @@ server = [
|
||||
"dep:tower-http",
|
||||
"dep:tracing",
|
||||
"dep:tracing-subscriber",
|
||||
"dep:dirigent_process",
|
||||
]
|
||||
|
||||
@@ -83,29 +83,29 @@ macro_rules! debug_log {
|
||||
/// Called after `upsert_session` and on session metadata changes.
|
||||
#[cfg(feature = "server")]
|
||||
async fn inspector_upsert_session(
|
||||
inspector: &Arc<dirigent_inspector::InspectorRegistry>,
|
||||
inspector: &Arc<dyn crate::traits::ConnectorInspector>,
|
||||
connector_id: &str,
|
||||
session: &SessionInfo,
|
||||
) {
|
||||
let sess_node_id = dirigent_inspector::NodeId::new(format!(
|
||||
let sess_node_id = dirigent_protocol::inspector::NodeId::new(format!(
|
||||
"dirigent/connectors/{}/sessions/{}",
|
||||
connector_id, session.id
|
||||
));
|
||||
let parent_id =
|
||||
dirigent_inspector::NodeId::new(format!("dirigent/connectors/{}", connector_id));
|
||||
dirigent_protocol::inspector::NodeId::new(format!("dirigent/connectors/{}", connector_id));
|
||||
|
||||
let node_state = match session.status {
|
||||
SessionStatus::Active => dirigent_inspector::NodeState::Running,
|
||||
SessionStatus::Processing => dirigent_inspector::NodeState::Busy("Generating".to_string()),
|
||||
SessionStatus::Idle => dirigent_inspector::NodeState::Idle,
|
||||
SessionStatus::Ended => dirigent_inspector::NodeState::Stopped,
|
||||
SessionStatus::Active => dirigent_protocol::inspector::NodeState::Running,
|
||||
SessionStatus::Processing => dirigent_protocol::inspector::NodeState::Busy("Generating".to_string()),
|
||||
SessionStatus::Idle => dirigent_protocol::inspector::NodeState::Idle,
|
||||
SessionStatus::Ended => dirigent_protocol::inspector::NodeState::Stopped,
|
||||
};
|
||||
|
||||
let label = session.title.as_deref().unwrap_or(&session.id);
|
||||
|
||||
// Try to register; if already exists, update instead
|
||||
let meta =
|
||||
dirigent_inspector::NodeMetadata::new(dirigent_inspector::NodeKind::AsyncTask, label)
|
||||
dirigent_protocol::inspector::NodeMetadata::new(dirigent_protocol::inspector::NodeKind::AsyncTask, label)
|
||||
.with_state(node_state.clone())
|
||||
.with_property("session_id", serde_json::json!(&session.id))
|
||||
.with_property("status", serde_json::json!(format!("{:?}", session.status)));
|
||||
@@ -129,11 +129,10 @@ async fn inspector_upsert_session(
|
||||
};
|
||||
|
||||
match inspector
|
||||
.register(sess_node_id.clone(), &parent_id, meta, None)
|
||||
.register_node(sess_node_id.clone(), &parent_id, meta)
|
||||
.await
|
||||
{
|
||||
Ok(mut handle) => {
|
||||
handle.detach();
|
||||
Ok(()) => {
|
||||
trace!(connector_id = %connector_id, session_id = %session.id, "Registered session with inspector");
|
||||
}
|
||||
Err(_) => {
|
||||
@@ -165,12 +164,12 @@ async fn inspector_upsert_session(
|
||||
/// Update only the inspector state for a session (lightweight, no property changes).
|
||||
#[cfg(feature = "server")]
|
||||
async fn inspector_update_session_state(
|
||||
inspector: &Arc<dirigent_inspector::InspectorRegistry>,
|
||||
inspector: &Arc<dyn crate::traits::ConnectorInspector>,
|
||||
connector_id: &str,
|
||||
session_id: &str,
|
||||
state: dirigent_inspector::NodeState,
|
||||
state: dirigent_protocol::inspector::NodeState,
|
||||
) {
|
||||
let sess_node_id = dirigent_inspector::NodeId::new(format!(
|
||||
let sess_node_id = dirigent_protocol::inspector::NodeId::new(format!(
|
||||
"dirigent/connectors/{}/sessions/{}",
|
||||
connector_id, session_id
|
||||
));
|
||||
@@ -180,13 +179,13 @@ async fn inspector_update_session_state(
|
||||
/// Deregister all session nodes for a connector from the inspector.
|
||||
#[cfg(feature = "server")]
|
||||
async fn inspector_deregister_all_sessions(
|
||||
inspector: &Arc<dirigent_inspector::InspectorRegistry>,
|
||||
inspector: &Arc<dyn crate::traits::ConnectorInspector>,
|
||||
connector_id: &str,
|
||||
internal_state: &InternalState,
|
||||
) {
|
||||
let sessions = internal_state.list_sessions().await;
|
||||
for session in sessions {
|
||||
let sess_node_id = dirigent_inspector::NodeId::new(format!(
|
||||
let sess_node_id = dirigent_protocol::inspector::NodeId::new(format!(
|
||||
"dirigent/connectors/{}/sessions/{}",
|
||||
connector_id, session.id
|
||||
));
|
||||
@@ -277,7 +276,7 @@ pub struct AcpConnector {
|
||||
|
||||
/// Optional inspector registry for PID tracking of stdio processes
|
||||
#[cfg(feature = "server")]
|
||||
inspector: Option<Arc<dirigent_inspector::InspectorRegistry>>,
|
||||
inspector: Option<Arc<dyn crate::traits::ConnectorInspector>>,
|
||||
|
||||
/// Optional process group manager for lifecycle management of stdio processes.
|
||||
///
|
||||
@@ -286,7 +285,7 @@ pub struct AcpConnector {
|
||||
/// tracked in the platform job object / process group, and are shut down
|
||||
/// gracefully on close.
|
||||
#[cfg(feature = "server")]
|
||||
process_manager: Option<Arc<dyn dirigent_process::ProcessGroupManager>>,
|
||||
process_manager: Option<Arc<dyn crate::traits::ProcessGroupManager>>,
|
||||
}
|
||||
|
||||
impl AcpConnector {
|
||||
@@ -388,7 +387,7 @@ impl AcpConnector {
|
||||
#[cfg(feature = "server")]
|
||||
pub fn with_inspector(
|
||||
mut self,
|
||||
inspector: Option<Arc<dirigent_inspector::InspectorRegistry>>,
|
||||
inspector: Option<Arc<dyn crate::traits::ConnectorInspector>>,
|
||||
) -> Self {
|
||||
self.inspector = inspector;
|
||||
self
|
||||
@@ -402,7 +401,7 @@ impl AcpConnector {
|
||||
#[cfg(feature = "server")]
|
||||
pub fn with_process_manager(
|
||||
mut self,
|
||||
process_manager: Option<Arc<dyn dirigent_process::ProcessGroupManager>>,
|
||||
process_manager: Option<Arc<dyn crate::traits::ProcessGroupManager>>,
|
||||
) -> Self {
|
||||
self.process_manager = process_manager;
|
||||
self
|
||||
@@ -504,8 +503,8 @@ impl AcpConnector {
|
||||
pending_agent_requests: Arc<Mutex<HashSet<String>>>,
|
||||
session_states: Arc<Mutex<HashMap<String, SessionState>>>,
|
||||
mut cmd_rx: mpsc::Receiver<ConnectorCommand>,
|
||||
#[cfg(feature = "server")] inspector: Option<Arc<dirigent_inspector::InspectorRegistry>>,
|
||||
#[cfg(feature = "server")] process_manager: Option<Arc<dyn dirigent_process::ProcessGroupManager>>,
|
||||
#[cfg(feature = "server")] inspector: Option<Arc<dyn crate::traits::ConnectorInspector>>,
|
||||
#[cfg(feature = "server")] process_manager: Option<Arc<dyn crate::traits::ProcessGroupManager>>,
|
||||
) {
|
||||
debug_log!("🚀 ACP connector {} task started", id);
|
||||
info!(connector_id = %id, "ACP connector task started");
|
||||
@@ -690,26 +689,25 @@ impl AcpConnector {
|
||||
#[cfg(feature = "server")]
|
||||
if let Some(ref inspector) = inspector {
|
||||
if let Some(pid) = transport.pid().await {
|
||||
let process_node_id = dirigent_inspector::NodeId::new(format!(
|
||||
let process_node_id = dirigent_protocol::inspector::NodeId::new(format!(
|
||||
"dirigent/connectors/{}/process",
|
||||
id
|
||||
));
|
||||
let parent_node_id = dirigent_inspector::NodeId::new(format!(
|
||||
let parent_node_id = dirigent_protocol::inspector::NodeId::new(format!(
|
||||
"dirigent/connectors/{}",
|
||||
id
|
||||
));
|
||||
let meta = dirigent_inspector::NodeMetadata::new(
|
||||
dirigent_inspector::NodeKind::Process,
|
||||
let meta = dirigent_protocol::inspector::NodeMetadata::new(
|
||||
dirigent_protocol::inspector::NodeKind::Process,
|
||||
"stdio-process",
|
||||
)
|
||||
.with_state(dirigent_inspector::NodeState::Running)
|
||||
.with_state(dirigent_protocol::inspector::NodeState::Running)
|
||||
.with_property("pid", serde_json::json!(pid))
|
||||
.with_property("transport", serde_json::json!("stdio"));
|
||||
if let Ok(mut handle) = inspector
|
||||
.register(process_node_id, &parent_node_id, meta, None)
|
||||
if let Ok(()) = inspector
|
||||
.register_node(process_node_id, &parent_node_id, meta)
|
||||
.await
|
||||
{
|
||||
handle.detach();
|
||||
info!(connector_id = %id, pid = pid, "Registered stdio process with inspector");
|
||||
}
|
||||
}
|
||||
@@ -1560,7 +1558,7 @@ impl AcpConnector {
|
||||
inspector,
|
||||
&id,
|
||||
&session_id,
|
||||
dirigent_inspector::NodeState::Busy("Generating".to_string()),
|
||||
dirigent_protocol::inspector::NodeState::Busy("Generating".to_string()),
|
||||
).await;
|
||||
}
|
||||
|
||||
@@ -2433,7 +2431,7 @@ impl AcpConnector {
|
||||
inspector,
|
||||
&id,
|
||||
session_id,
|
||||
dirigent_inspector::NodeState::Idle,
|
||||
dirigent_protocol::inspector::NodeState::Idle,
|
||||
).await;
|
||||
}
|
||||
}
|
||||
@@ -2453,7 +2451,7 @@ impl AcpConnector {
|
||||
/// Create transport based on configuration
|
||||
async fn create_transport(
|
||||
config: &AcpConfig,
|
||||
#[cfg(feature = "server")] process_manager: Option<&Arc<dyn dirigent_process::ProcessGroupManager>>,
|
||||
#[cfg(feature = "server")] process_manager: Option<&Arc<dyn crate::traits::ProcessGroupManager>>,
|
||||
) -> AcpResult<Box<dyn AcpTransport>> {
|
||||
match &config.transport {
|
||||
TransportKind::Stdio {
|
||||
|
||||
@@ -179,7 +179,7 @@ pub struct StdioTransport {
|
||||
/// force-killing the process. Without it, the original hard-kill behavior
|
||||
/// is preserved.
|
||||
#[cfg(feature = "server")]
|
||||
process_lifecycle: Option<Box<dyn dirigent_process::ProcessLifecycle>>,
|
||||
process_lifecycle: Option<Box<dyn crate::traits::ProcessLifecycle>>,
|
||||
}
|
||||
|
||||
impl StdioTransport {
|
||||
@@ -307,7 +307,7 @@ impl StdioTransport {
|
||||
///
|
||||
/// Must be called before `connect()`.
|
||||
#[cfg(feature = "server")]
|
||||
pub fn set_process_lifecycle(&mut self, lifecycle: Box<dyn dirigent_process::ProcessLifecycle>) {
|
||||
pub fn set_process_lifecycle(&mut self, lifecycle: Box<dyn crate::traits::ProcessLifecycle>) {
|
||||
self.process_lifecycle = Some(lifecycle);
|
||||
}
|
||||
|
||||
@@ -837,7 +837,7 @@ impl AcpTransport for StdioTransport {
|
||||
if let Some(ref lifecycle) = self.process_lifecycle {
|
||||
if child.id().is_some() {
|
||||
// Graceful shutdown: SIGTERM/CTRL_BREAK → wait → force kill
|
||||
dirigent_process::graceful_shutdown_async(
|
||||
crate::traits::graceful_shutdown_async(
|
||||
lifecycle.as_ref(),
|
||||
&mut child,
|
||||
std::time::Duration::from_secs(5),
|
||||
|
||||
@@ -292,32 +292,32 @@ impl Default for GatewayConfig {
|
||||
/// Register a Gateway session node in the inspector registry.
|
||||
#[cfg(feature = "server")]
|
||||
async fn inspector_register_gateway_session(
|
||||
inspector: &Arc<dirigent_inspector::InspectorRegistry>,
|
||||
inspector: &Arc<dyn crate::traits::ConnectorInspector>,
|
||||
connector_id: &str,
|
||||
session: &GatewaySession,
|
||||
) {
|
||||
let sess_node_id = dirigent_inspector::NodeId::new(format!(
|
||||
let sess_node_id = dirigent_protocol::inspector::NodeId::new(format!(
|
||||
"dirigent/connectors/{}/sessions/{}",
|
||||
connector_id, session.id
|
||||
));
|
||||
let parent_id =
|
||||
dirigent_inspector::NodeId::new(format!("dirigent/connectors/{}", connector_id));
|
||||
dirigent_protocol::inspector::NodeId::new(format!("dirigent/connectors/{}", connector_id));
|
||||
|
||||
let meta = dirigent_inspector::NodeMetadata::new(
|
||||
dirigent_inspector::NodeKind::AsyncTask,
|
||||
let meta = dirigent_protocol::inspector::NodeMetadata::new(
|
||||
dirigent_protocol::inspector::NodeKind::AsyncTask,
|
||||
&session.title,
|
||||
)
|
||||
.with_state(dirigent_inspector::NodeState::Running)
|
||||
.with_state(dirigent_protocol::inspector::NodeState::Running)
|
||||
.with_property("session_id", serde_json::json!(&session.id))
|
||||
.with_property("status", serde_json::json!("Active"))
|
||||
.with_property("message_count", serde_json::json!(session.messages.len()));
|
||||
|
||||
match inspector
|
||||
.register(sess_node_id, &parent_id, meta, None)
|
||||
.register_node(sess_node_id, &parent_id, meta)
|
||||
.await
|
||||
{
|
||||
Ok(mut handle) => {
|
||||
handle.detach();
|
||||
Ok(()) => {
|
||||
// Registered successfully
|
||||
}
|
||||
Err(_) => {
|
||||
// Already registered — that's fine
|
||||
@@ -328,12 +328,12 @@ async fn inspector_register_gateway_session(
|
||||
/// Deregister all Gateway session nodes from the inspector.
|
||||
#[cfg(feature = "server")]
|
||||
async fn inspector_deregister_gateway_sessions(
|
||||
inspector: &Arc<dirigent_inspector::InspectorRegistry>,
|
||||
inspector: &Arc<dyn crate::traits::ConnectorInspector>,
|
||||
connector_id: &str,
|
||||
sessions: &HashMap<String, GatewaySession>,
|
||||
) {
|
||||
for session_id in sessions.keys() {
|
||||
let sess_node_id = dirigent_inspector::NodeId::new(format!(
|
||||
let sess_node_id = dirigent_protocol::inspector::NodeId::new(format!(
|
||||
"dirigent/connectors/{}/sessions/{}",
|
||||
connector_id, session_id
|
||||
));
|
||||
@@ -387,7 +387,7 @@ pub struct GatewayConnector {
|
||||
|
||||
/// Optional inspector registry for session tracking
|
||||
#[cfg(feature = "server")]
|
||||
inspector: Option<Arc<dirigent_inspector::InspectorRegistry>>,
|
||||
inspector: Option<Arc<dyn crate::traits::ConnectorInspector>>,
|
||||
}
|
||||
|
||||
/// Helper that publishes an event to both the per-connector broadcast
|
||||
@@ -471,7 +471,7 @@ impl GatewayConnector {
|
||||
|
||||
/// Set the inspector registry for session tracking.
|
||||
#[cfg(feature = "server")]
|
||||
pub fn set_inspector(&mut self, inspector: Option<Arc<dirigent_inspector::InspectorRegistry>>) {
|
||||
pub fn set_inspector(&mut self, inspector: Option<Arc<dyn crate::traits::ConnectorInspector>>) {
|
||||
self.inspector = inspector;
|
||||
}
|
||||
|
||||
@@ -553,7 +553,7 @@ impl GatewayConnector {
|
||||
mut cmd_rx: mpsc::Receiver<ConnectorCommand>,
|
||||
connector_list_callback: Option<ConnectorListCallback>,
|
||||
session_transfer_callback: Option<SessionTransferCallback>,
|
||||
#[cfg(feature = "server")] inspector: Option<Arc<dirigent_inspector::InspectorRegistry>>,
|
||||
#[cfg(feature = "server")] inspector: Option<Arc<dyn crate::traits::ConnectorInspector>>,
|
||||
) {
|
||||
info!(connector_id = %id, "Gateway connector task started");
|
||||
|
||||
|
||||
@@ -26,14 +26,14 @@ pub trait ConnectorLifecycleHooks: Send + Sync {
|
||||
async fn on_connector_removed(&self, _connector_id: &str) {}
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
fn inspector(&self) -> Option<std::sync::Arc<dirigent_inspector::InspectorRegistry>> {
|
||||
fn inspector(&self) -> Option<std::sync::Arc<dyn crate::traits::ConnectorInspector>> {
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
fn process_manager(
|
||||
&self,
|
||||
) -> Option<std::sync::Arc<dyn dirigent_process::ProcessGroupManager>> {
|
||||
) -> Option<std::sync::Arc<dyn crate::traits::ProcessGroupManager>> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,6 +74,10 @@ pub mod connectors;
|
||||
#[cfg(feature = "server")]
|
||||
pub mod vendors;
|
||||
|
||||
// Abstract traits for connector-injected services (server-only)
|
||||
#[cfg(feature = "server")]
|
||||
pub mod traits;
|
||||
|
||||
// ACP module - Agent-Client Protocol implementation (server-only)
|
||||
#[cfg(feature = "server")]
|
||||
pub mod acp;
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
use dirigent_protocol::inspector::{NodeId, NodeMetadata, NodeState};
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Abstract interface for registering and updating nodes in the inspector tree.
|
||||
///
|
||||
/// Connectors use this trait to expose their internal process hierarchy
|
||||
/// (child processes, services, async tasks) without depending on the
|
||||
/// concrete `dirigent_inspector` crate.
|
||||
#[async_trait::async_trait]
|
||||
pub trait ConnectorInspector: Send + Sync {
|
||||
/// Register a new node under `parent` with the given metadata.
|
||||
async fn register_node(
|
||||
&self,
|
||||
id: NodeId,
|
||||
parent: &NodeId,
|
||||
metadata: NodeMetadata,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>>;
|
||||
|
||||
/// Remove `id` and every node below it from the tree.
|
||||
async fn deregister_subtree(
|
||||
&self,
|
||||
id: &NodeId,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>>;
|
||||
|
||||
/// Update the runtime state of an existing node.
|
||||
async fn update_state(
|
||||
&self,
|
||||
id: &NodeId,
|
||||
state: NodeState,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>>;
|
||||
|
||||
/// Merge additional properties into an existing node's metadata.
|
||||
async fn update_properties(
|
||||
&self,
|
||||
id: &NodeId,
|
||||
props: HashMap<String, serde_json::Value>,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>>;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
mod inspector;
|
||||
mod process;
|
||||
|
||||
pub use inspector::ConnectorInspector;
|
||||
pub use process::{graceful_shutdown_async, ProcessGroupManager, ProcessLifecycle};
|
||||
@@ -0,0 +1,62 @@
|
||||
use std::io;
|
||||
|
||||
/// Factory for creating platform-specific process lifecycle handlers.
|
||||
///
|
||||
/// Each connector receives a `ProcessGroupManager` at construction time and
|
||||
/// calls `create_lifecycle()` to obtain a handler scoped to a single child
|
||||
/// process (or process group).
|
||||
pub trait ProcessGroupManager: Send + Sync {
|
||||
/// Create a fresh lifecycle handler for a new child process.
|
||||
fn create_lifecycle(&self) -> Box<dyn ProcessLifecycle>;
|
||||
}
|
||||
|
||||
/// Platform-specific process lifecycle operations.
|
||||
///
|
||||
/// Implementations handle the OS-level details of job objects (Windows),
|
||||
/// process groups (Unix), and signal delivery so that connector code
|
||||
/// remains platform-agnostic.
|
||||
pub trait ProcessLifecycle: Send + Sync {
|
||||
/// Configure a `tokio::process::Command` before spawning
|
||||
/// (e.g., assign to a job object or set `setsid`).
|
||||
fn configure_async_command(&self, cmd: &mut tokio::process::Command);
|
||||
|
||||
/// Register a spawned child by PID for later signal delivery.
|
||||
fn register_child(&self, pid: u32) -> Result<(), io::Error>;
|
||||
|
||||
/// Send a graceful shutdown signal (SIGTERM on Unix, CTRL_BREAK on Windows).
|
||||
fn send_shutdown_signal(&self, pid: u32) -> Result<(), io::Error>;
|
||||
|
||||
/// Force-kill the process (SIGKILL on Unix, TerminateProcess on Windows).
|
||||
fn send_kill_signal(&self, pid: u32) -> Result<(), io::Error>;
|
||||
}
|
||||
|
||||
/// Attempt a graceful shutdown of `child`, falling back to a forced kill
|
||||
/// after `timeout` elapses.
|
||||
///
|
||||
/// Returns `true` if the process exited within the timeout (or was already
|
||||
/// gone), `false` if a forced kill was required.
|
||||
pub async fn graceful_shutdown_async(
|
||||
lifecycle: &dyn ProcessLifecycle,
|
||||
child: &mut tokio::process::Child,
|
||||
timeout: std::time::Duration,
|
||||
) -> bool {
|
||||
let pid = match child.id() {
|
||||
Some(0) | None => return true,
|
||||
Some(pid) => pid,
|
||||
};
|
||||
|
||||
if lifecycle.send_shutdown_signal(pid).is_err() {
|
||||
return true;
|
||||
}
|
||||
|
||||
match tokio::time::timeout(timeout, child.wait()).await {
|
||||
Ok(Ok(_)) => true,
|
||||
Ok(Err(_)) => true,
|
||||
Err(_) => {
|
||||
tracing::debug!(pid, "Graceful shutdown timed out, force killing");
|
||||
let _ = lifecycle.send_kill_signal(pid);
|
||||
let _ = child.wait().await;
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user