perspt_sdk/toolset/
handler.rs1use std::collections::{BTreeMap, BTreeSet};
4use std::sync::Arc;
5
6use async_trait::async_trait;
7
8use super::{ToolCatalog, ToolEntry};
9use crate::capability::{Capability, EffectKind};
10use crate::command::CommandInvocation;
11use crate::error::{Result, SdkError};
12use crate::scheduler::Footprint;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum ConcurrencyClass {
17 ParallelRead,
19 Serial,
22}
23
24#[async_trait]
27pub trait WorkspaceService: Send + Sync {
28 async fn read(&self, path: &str, max_bytes: usize) -> Result<Vec<u8>>;
29 async fn write(&self, path: &str, content: &[u8]) -> Result<()>;
30}
31
32#[async_trait]
34pub trait ProcessService: Send + Sync {
35 async fn execute(&self, invocation: &CommandInvocation, max_bytes: usize) -> Result<String>;
36}
37
38#[async_trait]
40pub trait NetworkService: Send + Sync {
41 async fn fetch(&self, url: &str, max_bytes: usize) -> Result<Vec<u8>>;
42}
43
44pub trait ObservationService: Send + Sync {
46 fn record(&self, kind: &str, observation: &serde_json::Value) -> Result<()>;
47}
48
49#[derive(Clone, Default)]
52pub struct HandlerServices {
53 pub workspace: Option<Arc<dyn WorkspaceService>>,
54 pub process: Option<Arc<dyn ProcessService>>,
55 pub network: Option<Arc<dyn NetworkService>>,
56 pub observations: Option<Arc<dyn ObservationService>>,
57}
58
59impl std::fmt::Debug for HandlerServices {
60 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
61 formatter
62 .debug_struct("HandlerServices")
63 .field("workspace", &self.workspace.is_some())
64 .field("process", &self.process.is_some())
65 .field("network", &self.network.is_some())
66 .field("observations", &self.observations.is_some())
67 .finish()
68 }
69}
70
71#[derive(Debug, Clone)]
73pub struct HandlerContext {
74 session_id: Arc<str>,
75 node_id: Arc<str>,
76 generation: u32,
77 capabilities: Arc<[Capability]>,
78 services: HandlerServices,
79}
80
81impl HandlerContext {
82 pub fn new(
83 session_id: impl Into<Arc<str>>,
84 node_id: impl Into<Arc<str>>,
85 generation: u32,
86 capabilities: Vec<Capability>,
87 services: HandlerServices,
88 ) -> Self {
89 Self {
90 session_id: session_id.into(),
91 node_id: node_id.into(),
92 generation,
93 capabilities: capabilities.into(),
94 services,
95 }
96 }
97
98 pub fn session_id(&self) -> &str {
99 &self.session_id
100 }
101
102 pub fn node_id(&self) -> &str {
103 &self.node_id
104 }
105
106 pub fn generation(&self) -> u32 {
107 self.generation
108 }
109
110 pub fn capabilities(&self) -> &[Capability] {
111 &self.capabilities
112 }
113
114 pub fn services(&self) -> &HandlerServices {
115 &self.services
116 }
117}
118
119#[derive(Debug, Clone)]
122pub struct ToolExecution {
123 pub effect: EffectKind,
124 pub footprint: Footprint,
125 pub output: serde_json::Value,
126 pub artifacts: Vec<String>,
127}
128
129#[async_trait]
130pub trait ToolHandler: Send + Sync {
131 fn catalog_name(&self) -> &str;
132
133 fn concurrency_class(&self) -> ConcurrencyClass {
134 ConcurrencyClass::Serial
135 }
136
137 async fn execute(
138 &self,
139 context: &HandlerContext,
140 arguments: &serde_json::Value,
141 ) -> Result<ToolExecution>;
142}
143
144#[derive(Default)]
146pub struct ToolDispatcher {
147 handlers: BTreeMap<String, Arc<dyn ToolHandler>>,
148}
149
150impl std::fmt::Debug for ToolDispatcher {
151 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
152 formatter
153 .debug_struct("ToolDispatcher")
154 .field("handlers", &self.handlers.keys().collect::<Vec<_>>())
155 .finish()
156 }
157}
158
159impl ToolDispatcher {
160 pub fn new() -> Self {
161 Self::default()
162 }
163
164 pub fn register(&mut self, entry: &ToolEntry, handler: Arc<dyn ToolHandler>) -> Result<()> {
165 if handler.catalog_name() != entry.name {
166 return Err(SdkError::Domain(format!(
167 "handler {:?} does not match catalog entry {:?}",
168 handler.catalog_name(),
169 entry.name
170 )));
171 }
172 if self.handlers.contains_key(&entry.name) {
173 return Err(SdkError::Domain(format!(
174 "duplicate handler for catalog entry {:?}",
175 entry.name
176 )));
177 }
178 self.handlers.insert(entry.name.clone(), handler);
179 Ok(())
180 }
181
182 pub fn validate_complete(&self, catalog: &dyn ToolCatalog) -> Result<()> {
184 let entries: BTreeSet<&str> = catalog
185 .entries()
186 .iter()
187 .map(|entry| entry.name.as_str())
188 .collect();
189 if let Some(missing) = entries
190 .iter()
191 .find(|name| !self.handlers.contains_key(**name))
192 {
193 return Err(SdkError::Domain(format!(
194 "missing handler for catalog entry {missing:?}"
195 )));
196 }
197 if let Some(orphan) = self
198 .handlers
199 .keys()
200 .find(|name| !entries.contains(name.as_str()))
201 {
202 return Err(SdkError::Domain(format!(
203 "handler {orphan:?} has no catalog entry"
204 )));
205 }
206 Ok(())
207 }
208
209 pub fn concurrency_class(&self, name: &str) -> Option<ConcurrencyClass> {
210 self.handlers
211 .get(name)
212 .map(|handler| handler.concurrency_class())
213 }
214
215 pub async fn dispatch(
216 &self,
217 entry: &ToolEntry,
218 context: &HandlerContext,
219 arguments: &serde_json::Value,
220 provider: &str,
221 ) -> Result<ToolExecution> {
222 entry.validate_arguments(arguments)?;
223 let handler = self.handlers.get(&entry.name).ok_or_else(|| {
224 SdkError::Domain(format!(
225 "missing handler for catalog entry {:?}",
226 entry.name
227 ))
228 })?;
229 let result = handler.execute(context, arguments).await?;
230 if result.effect != entry.effect {
231 return Err(SdkError::Domain(format!(
232 "handler {:?} returned undeclared effect {:?}",
233 entry.name, result.effect
234 )));
235 }
236 let declared = entry.footprint.resolve(arguments, provider);
237 if !result.footprint.reads.is_subset(&declared.reads)
238 || !result.footprint.writes.is_subset(&declared.writes)
239 {
240 return Err(SdkError::Domain(format!(
241 "handler {:?} returned an undeclared resource footprint",
242 entry.name
243 )));
244 }
245 Ok(result)
246 }
247}