1use 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#[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 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 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 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 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 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}