Skip to main content

oxi/internal_urls/
agent_handler.rs

1//! `agent://` protocol handler — resolves subagent output artifacts.
2
3use std::path::PathBuf;
4use std::sync::Arc;
5
6use async_trait::async_trait;
7use oxi_sdk::SdkError;
8use oxi_sdk::ports::{ProtocolHandler, ResolveContext, ResolvedUrl};
9use parking_lot::RwLock;
10
11/// Stores per-session agent output artifacts for `agent://` URL resolution.
12#[derive(Clone, Default)]
13pub struct AgentArtifactStore {
14    dirs: Arc<RwLock<Vec<PathBuf>>>,
15}
16
17impl AgentArtifactStore {
18    /// Create an empty store.
19    pub fn new() -> Self {
20        Self::default()
21    }
22
23    /// Register a session's artifacts directory.
24    pub fn register_dir(&self, dir: PathBuf) {
25        self.dirs.write().push(dir);
26    }
27
28    fn find_output(&self, candidate_ids: &[String]) -> Option<(PathBuf, String)> {
29        let dirs = self.dirs.read();
30        for dir in dirs.iter() {
31            let Ok(entries) = std::fs::read_dir(dir) else {
32                continue;
33            };
34            for entry in entries.flatten() {
35                let file_name = entry.file_name();
36                let name = file_name.to_string_lossy();
37                let Some(id) = name.strip_suffix(".md") else {
38                    continue;
39                };
40                for candidate in candidate_ids {
41                    if id == *candidate
42                        && let Ok(content) = std::fs::read_to_string(entry.path())
43                    {
44                        return Some((entry.path(), content));
45                    }
46                }
47            }
48        }
49        None
50    }
51}
52
53/// Protocol handler for `agent://` URLs.
54pub struct AgentProtocolHandler {
55    store: AgentArtifactStore,
56}
57
58impl AgentProtocolHandler {
59    /// Create a new handler backed by the given artifact store.
60    pub fn new(store: AgentArtifactStore) -> Self {
61        Self { store }
62    }
63}
64
65#[async_trait]
66impl ProtocolHandler for AgentProtocolHandler {
67    fn scheme(&self) -> &str {
68        "agent"
69    }
70    fn immutable(&self) -> bool {
71        true
72    }
73
74    async fn resolve(
75        &self,
76        url: &str,
77        _selector: Option<&str>,
78        _ctx: &ResolveContext,
79    ) -> Result<ResolvedUrl, SdkError> {
80        let suffix = url.strip_prefix("agent://").unwrap_or(url);
81        if suffix.is_empty() {
82            return Err(SdkError::ExecutionFailed {
83                reason: "agent:// URL requires an agent output ID".into(),
84            });
85        }
86
87        let parts: Vec<&str> = suffix.split('/').collect();
88        let dotted = parts.join(".");
89        let candidates = vec![suffix.to_string(), dotted];
90
91        match self.store.find_output(&candidates) {
92            Some((path, content)) => Ok(ResolvedUrl {
93                url: format!("agent://{suffix}"),
94                content,
95                content_type: "text/markdown".into(),
96                size: None,
97                source_path: Some(path.to_string_lossy().into_owned()),
98                notes: vec![],
99                immutable: true,
100            }),
101            None => Err(SdkError::ExecutionFailed {
102                reason: format!(
103                    "Agent output '{suffix}' not found in any registered artifacts directory"
104                ),
105            }),
106        }
107    }
108}