Skip to main content

orchestral_runtime/
agent_directory.rs

1//! Runtime registry for provider-neutral Agent session connectors.
2//!
3//! The directory owns discovery routing. Each registration also owns a normal
4//! [`AgentController`](crate::AgentController), so active external turns use
5//! the same durable Agent Protocol path as the built-in Generic Agent.
6
7use std::collections::BTreeMap;
8use std::sync::Arc;
9
10use orchestral_core::agent_connector::{
11    AgentConnector, AgentConnectorDescriptor, AgentConnectorError, AgentConnectorHealth,
12    AgentConnectorId, AgentSessionActionExecution, AgentSessionActionInvocation,
13    AgentSessionActionOutcome, AgentSessionActionStatus, AgentSessionChange, AgentSessionDetail,
14    AgentSessionListQuery, AgentSessionPage, AgentSessionReadQuery, AgentSessionSummary,
15    CreateAgentSessionRequest, InvokeAgentSessionActionRequest, ResolveAgentSessionRequest,
16};
17use orchestral_core::agent_protocol::spi::{
18    AgentJournalStore, AgentProvider, InMemoryAgentJournalStore,
19};
20use orchestral_core::agent_protocol::wire::{AgentSessionId, Extensions, RunId};
21use thiserror::Error;
22use tokio::sync::broadcast;
23use tokio::sync::RwLock;
24
25use crate::api::AgentApi;
26use crate::{AgentController, AgentRunHandle, AgentSdkError};
27
28struct AgentDirectoryEntry {
29    descriptor: AgentConnectorDescriptor,
30    connector: Arc<dyn AgentConnector>,
31    api: AgentApi,
32}
33
34/// Process-level registry for installed Agent connectors.
35///
36/// Connector IDs namespace session IDs. Two different Agents may therefore
37/// expose the same opaque session ID without colliding.
38#[derive(Default)]
39pub struct AgentDirectory {
40    entries: RwLock<BTreeMap<AgentConnectorId, Arc<AgentDirectoryEntry>>>,
41}
42
43impl AgentDirectory {
44    pub fn new() -> Self {
45        Self::default()
46    }
47
48    pub async fn register(
49        &self,
50        connector: Arc<dyn AgentConnector>,
51        provider: Arc<dyn AgentProvider>,
52    ) -> Result<(), AgentDirectoryError> {
53        self.register_with_journal(
54            connector,
55            provider,
56            Arc::new(InMemoryAgentJournalStore::default()),
57        )
58        .await
59    }
60
61    pub async fn register_with_journal(
62        &self,
63        connector: Arc<dyn AgentConnector>,
64        provider: Arc<dyn AgentProvider>,
65        journal: Arc<dyn AgentJournalStore>,
66    ) -> Result<(), AgentDirectoryError> {
67        let descriptor = connector.describe();
68        descriptor.validate()?;
69        let connector_id = descriptor.connector_id.clone();
70        let controller = Arc::new(AgentController::with_journal_store(
71            provider,
72            descriptor.provider_binding.clone(),
73            journal,
74        )?);
75        let entry = Arc::new(AgentDirectoryEntry {
76            descriptor,
77            connector,
78            api: AgentApi::new(controller),
79        });
80
81        let mut entries = self.entries.write().await;
82        if entries.contains_key(&connector_id) {
83            return Err(AgentDirectoryError::RegistrationConflict(connector_id));
84        }
85        entries.insert(connector_id, entry);
86        Ok(())
87    }
88
89    pub async fn connectors(&self) -> Vec<AgentConnectorDescriptor> {
90        self.entries
91            .read()
92            .await
93            .values()
94            .map(|entry| entry.descriptor.clone())
95            .collect()
96    }
97
98    pub async fn health(
99        &self,
100        connector_id: &AgentConnectorId,
101    ) -> Result<AgentConnectorHealth, AgentDirectoryError> {
102        let entry = self.entry(connector_id).await?;
103        self.verify_descriptor(&entry)?;
104        Ok(entry.connector.health().await?)
105    }
106
107    pub async fn list_sessions(
108        &self,
109        connector_id: &AgentConnectorId,
110        query: AgentSessionListQuery,
111    ) -> Result<AgentSessionPage, AgentDirectoryError> {
112        query.validate()?;
113        let entry = self.entry(connector_id).await?;
114        self.verify_descriptor(&entry)?;
115        let requested_limit = query.limit;
116        let page = entry.connector.list_sessions(query).await?;
117        page.validate_for(connector_id, requested_limit)?;
118        Ok(page)
119    }
120
121    pub async fn read_session(
122        &self,
123        connector_id: &AgentConnectorId,
124        session_id: &AgentSessionId,
125    ) -> Result<AgentSessionDetail, AgentDirectoryError> {
126        if session_id.is_empty() {
127            return Err(AgentConnectorError::invalid("session id must not be empty").into());
128        }
129        let entry = self.entry(connector_id).await?;
130        self.verify_descriptor(&entry)?;
131        let detail = entry.connector.read_session(session_id).await?;
132        detail.validate_for(connector_id)?;
133        if detail.summary.session_id != *session_id {
134            return Err(AgentConnectorError::protocol(
135                "connector returned a different session than requested",
136            )
137            .into());
138        }
139        Ok(detail)
140    }
141
142    pub async fn read_session_page(
143        &self,
144        connector_id: &AgentConnectorId,
145        session_id: &AgentSessionId,
146        query: AgentSessionReadQuery,
147    ) -> Result<AgentSessionDetail, AgentDirectoryError> {
148        if session_id.is_empty() {
149            return Err(AgentConnectorError::invalid("session id must not be empty").into());
150        }
151        query.validate()?;
152        let entry = self.entry(connector_id).await?;
153        self.verify_descriptor(&entry)?;
154        let detail = entry.connector.read_session_page(session_id, query).await?;
155        detail.validate_for(connector_id)?;
156        if detail.summary.session_id != *session_id {
157            return Err(AgentConnectorError::protocol(
158                "connector returned a different session than requested",
159            )
160            .into());
161        }
162        Ok(detail)
163    }
164
165    pub async fn subscribe_session_changes(
166        &self,
167        connector_id: &AgentConnectorId,
168        session_id: &AgentSessionId,
169    ) -> Result<broadcast::Receiver<AgentSessionChange>, AgentDirectoryError> {
170        if session_id.is_empty() {
171            return Err(AgentConnectorError::invalid("session id must not be empty").into());
172        }
173        let entry = self.entry(connector_id).await?;
174        self.verify_descriptor(&entry)?;
175        Ok(entry
176            .connector
177            .subscribe_session_changes(session_id)
178            .await?)
179    }
180
181    pub async fn resolve_request(
182        &self,
183        connector_id: &AgentConnectorId,
184        request: ResolveAgentSessionRequest,
185    ) -> Result<(), AgentDirectoryError> {
186        if request.session_id.is_empty() || request.request_id.is_empty() {
187            return Err(AgentConnectorError::invalid(
188                "session request resolution requires session and request identities",
189            )
190            .into());
191        }
192        request.response.validate()?;
193        let entry = self.entry(connector_id).await?;
194        self.verify_descriptor(&entry)?;
195        if !entry.descriptor.capabilities.resolve_requests {
196            return Err(AgentConnectorError::unsupported(
197                "connector does not declare provider-native request resolution",
198            )
199            .into());
200        }
201        entry.connector.resolve_request(request).await?;
202        Ok(())
203    }
204
205    pub async fn create_session(
206        &self,
207        connector_id: &AgentConnectorId,
208        request: CreateAgentSessionRequest,
209    ) -> Result<AgentSessionSummary, AgentDirectoryError> {
210        let entry = self.entry(connector_id).await?;
211        self.verify_descriptor(&entry)?;
212        if !entry.descriptor.capabilities.create {
213            return Err(AgentConnectorError::unsupported(
214                "connector does not declare session creation",
215            )
216            .into());
217        }
218        let summary = entry.connector.create_session(request).await?;
219        summary.validate_for(connector_id)?;
220        entry
221            .api
222            .create_session(Some(summary.session_id.clone()))
223            .await?;
224        Ok(summary)
225    }
226
227    pub async fn invoke_action(
228        &self,
229        connector_id: &AgentConnectorId,
230        request: InvokeAgentSessionActionRequest,
231    ) -> Result<AgentSessionActionOutcome, AgentDirectoryError> {
232        if request.session_id.is_empty() || request.action_id.is_empty() {
233            return Err(AgentConnectorError::invalid(
234                "session action requires session and action identities",
235            )
236            .into());
237        }
238        let entry = self.entry(connector_id).await?;
239        self.verify_descriptor(&entry)?;
240        let action = entry.descriptor.action(&request.action_id).ok_or_else(|| {
241            AgentConnectorError::unsupported(format!(
242                "connector does not declare action {}",
243                request.action_id
244            ))
245        })?;
246        if action.input_schema.is_none() && !request.arguments.is_null() {
247            return Err(AgentConnectorError::invalid(format!(
248                "action {} takes no arguments",
249                request.action_id
250            ))
251            .into());
252        }
253        if action.execution == AgentSessionActionExecution::Run {
254            // Resolve the session before starting so a forged connector/session
255            // pair cannot allocate a Host-only Run.
256            self.read_session_page(
257                connector_id,
258                &request.session_id,
259                AgentSessionReadQuery {
260                    cursor: None,
261                    limit: 1,
262                },
263            )
264            .await?;
265            entry
266                .api
267                .create_session(Some(request.session_id.clone()))
268                .await?;
269            let run_id = request
270                .run_id
271                .unwrap_or_else(|| RunId::new(format!("session-action-{}", uuid::Uuid::new_v4())));
272            entry
273                .api
274                .start_session_action(
275                    &request.session_id,
276                    run_id.clone(),
277                    action.title.clone(),
278                    AgentSessionActionInvocation {
279                        action_id: request.action_id,
280                        arguments: request.arguments,
281                    },
282                )
283                .await?;
284            return Ok(AgentSessionActionOutcome {
285                status: AgentSessionActionStatus::Running { run_id },
286                session: None,
287                content: Vec::new(),
288                details: serde_json::Value::Null,
289            });
290        }
291        if request.run_id.is_some() {
292            return Err(AgentConnectorError::invalid(
293                "run_id is only valid for Run session actions",
294            )
295            .into());
296        }
297        let outcome = entry.connector.invoke_action(request).await?;
298        if !matches!(outcome.status, AgentSessionActionStatus::Completed) {
299            return Err(AgentConnectorError::protocol(
300                "an immediate session action returned a running outcome",
301            )
302            .into());
303        }
304        if let Some(summary) = &outcome.session {
305            summary.validate_for(connector_id)?;
306        }
307        for content in &outcome.content {
308            content
309                .validate_integrity()
310                .map_err(|error| AgentConnectorError::protocol(error.to_string()))?;
311        }
312        Ok(outcome)
313    }
314
315    /// Start one Agent Protocol Run against a connector-owned session.
316    pub async fn start_text(
317        &self,
318        connector_id: &AgentConnectorId,
319        session_id: &AgentSessionId,
320        run_id: Option<RunId>,
321        input: impl Into<String>,
322    ) -> Result<AgentRunHandle, AgentDirectoryError> {
323        // Read first so a stale or forged connector/session pair cannot create
324        // a Host-only session that the external Provider cannot resolve.
325        self.read_session_page(
326            connector_id,
327            session_id,
328            AgentSessionReadQuery {
329                cursor: None,
330                limit: 1,
331            },
332        )
333        .await?;
334        let entry = self.entry(connector_id).await?;
335        entry.api.create_session(Some(session_id.clone())).await?;
336        Ok(entry.api.start_text(session_id, run_id, input).await?)
337    }
338
339    /// Start one Agent Protocol Run with provider-neutral Content blocks.
340    pub async fn start_content(
341        &self,
342        connector_id: &AgentConnectorId,
343        session_id: &AgentSessionId,
344        run_id: Option<RunId>,
345        input: Vec<orchestral_core::agent_protocol::wire::Content>,
346    ) -> Result<AgentRunHandle, AgentDirectoryError> {
347        self.start_content_with_extensions(
348            connector_id,
349            session_id,
350            run_id,
351            input,
352            Extensions::new(),
353        )
354        .await
355    }
356
357    /// Starts a Run with digest-bound, namespaced Host metadata. The directory
358    /// keeps this provider-neutral and forwards the immutable extensions
359    /// through the shared Agent Protocol controller.
360    pub async fn start_content_with_extensions(
361        &self,
362        connector_id: &AgentConnectorId,
363        session_id: &AgentSessionId,
364        run_id: Option<RunId>,
365        input: Vec<orchestral_core::agent_protocol::wire::Content>,
366        extensions: Extensions,
367    ) -> Result<AgentRunHandle, AgentDirectoryError> {
368        self.read_session_page(
369            connector_id,
370            session_id,
371            AgentSessionReadQuery {
372                cursor: None,
373                limit: 1,
374            },
375        )
376        .await?;
377        let entry = self.entry(connector_id).await?;
378        entry.api.create_session(Some(session_id.clone())).await?;
379        Ok(entry
380            .api
381            .start_content_with_extensions(session_id, run_id, input, extensions)
382            .await?)
383    }
384
385    pub async fn agent_api(
386        &self,
387        connector_id: &AgentConnectorId,
388    ) -> Result<AgentApi, AgentDirectoryError> {
389        Ok(self.entry(connector_id).await?.api.clone())
390    }
391
392    async fn entry(
393        &self,
394        connector_id: &AgentConnectorId,
395    ) -> Result<Arc<AgentDirectoryEntry>, AgentDirectoryError> {
396        self.entries
397            .read()
398            .await
399            .get(connector_id)
400            .cloned()
401            .ok_or_else(|| AgentDirectoryError::ConnectorNotFound(connector_id.clone()))
402    }
403
404    fn verify_descriptor(&self, entry: &AgentDirectoryEntry) -> Result<(), AgentDirectoryError> {
405        let observed = entry.connector.describe();
406        observed.validate()?;
407        if observed != entry.descriptor {
408            return Err(AgentDirectoryError::DescriptorChanged(
409                entry.descriptor.connector_id.clone(),
410            ));
411        }
412        Ok(())
413    }
414}
415
416#[derive(Debug, Error)]
417#[non_exhaustive]
418pub enum AgentDirectoryError {
419    #[error("Agent connector is not registered: {0}")]
420    ConnectorNotFound(AgentConnectorId),
421    #[error("Agent connector is already registered: {0}")]
422    RegistrationConflict(AgentConnectorId),
423    #[error("Agent connector descriptor changed after registration: {0}")]
424    DescriptorChanged(AgentConnectorId),
425    #[error(transparent)]
426    Connector(#[from] AgentConnectorError),
427    #[error(transparent)]
428    Protocol(#[from] orchestral_core::agent_protocol::wire::AgentProtocolError),
429    #[error(transparent)]
430    Agent(#[from] AgentSdkError),
431}