oxi/internal_urls/
agent_handler.rs1use 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#[derive(Clone, Default)]
13pub struct AgentArtifactStore {
14 dirs: Arc<RwLock<Vec<PathBuf>>>,
15}
16
17impl AgentArtifactStore {
18 pub fn new() -> Self {
20 Self::default()
21 }
22
23 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
53pub struct AgentProtocolHandler {
55 store: AgentArtifactStore,
56}
57
58impl AgentProtocolHandler {
59 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}