🥇 export from upstream (a1fa8e3a)

This commit is contained in:
2026-05-09 21:59:28 +02:00
parent bf5a79d931
commit 5829546671
33 changed files with 323 additions and 3719 deletions
-4
View File
@@ -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");
+2 -2
View File
@@ -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
}
}
+4
View File
@@ -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>>;
}
+5
View File
@@ -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
}
}
}