1use std::fmt;
67use std::future::Future;
68use std::pin::Pin;
69use std::sync::Arc;
70
71use serde::de::DeserializeOwned;
72use serde_json::Value;
73
74use crate::error::CapabilityError;
75use crate::id::validate_capability_id;
76
77pub use async_trait::async_trait;
78pub use schemars::{self, JsonSchema};
79pub use serde::{self, Deserialize, Serialize};
80pub use serde_json;
81
82pub type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
84
85#[derive(Clone)]
92pub struct Definition {
93 id: String,
94 name: String,
95 description: String,
96 instructions: Option<String>,
97 metadata: Option<Value>,
98 tools: Vec<Tool>,
99}
100
101impl Definition {
102 pub fn new(
109 id: impl Into<String>,
110 name: impl Into<String>,
111 description: impl Into<String>,
112 ) -> Self {
113 Self {
114 id: id.into(),
115 name: name.into(),
116 description: description.into(),
117 instructions: None,
118 metadata: None,
119 tools: Vec::new(),
120 }
121 }
122
123 pub fn instructions(mut self, instructions: impl Into<String>) -> Self {
128 self.instructions = Some(instructions.into());
129 self
130 }
131
132 pub fn metadata(mut self, metadata: impl Into<Value>) -> Self {
137 self.metadata = Some(metadata.into());
138 self
139 }
140
141 pub fn tool<H>(mut self, handler: H) -> Self
143 where
144 H: Handler,
145 {
146 self.tools.push(Tool::new(handler));
147 self
148 }
149
150 pub fn id(&self) -> &str {
152 &self.id
153 }
154
155 pub fn name(&self) -> &str {
157 &self.name
158 }
159
160 pub fn description(&self) -> &str {
162 &self.description
163 }
164
165 pub fn instructions_text(&self) -> Option<&str> {
167 self.instructions.as_deref()
168 }
169
170 pub fn metadata_value(&self) -> Option<&Value> {
172 self.metadata.as_ref()
173 }
174
175 pub fn tools(&self) -> &[Tool] {
177 &self.tools
178 }
179
180 pub fn validate(&self) -> Result<(), CapabilityError> {
182 validate_capability_id(&self.id)?;
183 let invalid = |reason: &str| CapabilityError::InvalidDefinition {
184 id: self.id.clone(),
185 reason: reason.to_string(),
186 };
187 if self.name.trim().is_empty() {
188 return Err(invalid("capability name must not be blank"));
189 }
190 if self.description.trim().is_empty() {
191 return Err(invalid("capability description must not be blank"));
192 }
193 if self
194 .instructions
195 .as_ref()
196 .is_some_and(|text| text.trim().is_empty())
197 {
198 return Err(invalid("capability instructions must not be blank"));
199 }
200 if self.tools.is_empty() {
201 return Err(invalid("capability must define at least one tool"));
202 }
203 if let Some(tool) = self
204 .tools
205 .iter()
206 .find(|tool| tool.spec.description.trim().is_empty())
207 {
208 return Err(CapabilityError::InvalidDefinition {
209 id: self.id.clone(),
210 reason: format!("tool {:?} description must not be blank", tool.spec.name),
211 });
212 }
213 Ok(())
214 }
215}
216
217impl fmt::Debug for Definition {
218 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
219 f.debug_struct("Definition")
220 .field("id", &self.id)
221 .field("name", &self.name)
222 .field("description", &self.description)
223 .field("instructions", &self.instructions)
224 .field("metadata", &self.metadata)
225 .field("tools", &self.tools)
226 .finish()
227 }
228}
229
230#[async_trait]
238pub trait Handler: Send + Sync + 'static {
239 type Input: DeserializeOwned + JsonSchema + Send + 'static;
241 type Output: Serialize + JsonSchema + Send + 'static;
243 type Error: Into<Error> + Send + 'static;
245
246 fn name(&self) -> &str;
248 fn description(&self) -> &str;
250
251 fn display_name(&self) -> Option<&str> {
253 None
254 }
255
256 fn hints(&self) -> Hints {
258 Hints::default()
259 }
260
261 async fn execute(
267 &self,
268 input: Self::Input,
269 context: Context,
270 ) -> Result<Self::Output, Self::Error>;
271}
272
273#[derive(Clone)]
279pub struct Tool {
280 spec: ToolSpec,
281 handler: Arc<dyn ErasedHandler>,
282}
283
284impl Tool {
285 fn new<H: Handler>(handler: H) -> Self {
286 let spec = ToolSpec {
287 name: handler.name().to_string(),
288 display_name: handler.display_name().map(str::to_string),
289 description: handler.description().to_string(),
290 input_schema: schema_for::<H::Input>(),
291 output_schema: schema_for::<H::Output>(),
292 hints: handler.hints(),
293 };
294 Self {
295 spec,
296 handler: Arc::new(HandlerAdapter(handler)),
297 }
298 }
299
300 pub fn spec(&self) -> &ToolSpec {
302 &self.spec
303 }
304
305 pub async fn invoke(&self, arguments: Value, context: Context) -> Result<Value, Error> {
312 self.handler.call(arguments, context).await
313 }
314}
315
316impl fmt::Debug for Tool {
317 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
318 f.debug_struct("Tool").field("spec", &self.spec).finish()
319 }
320}
321
322#[async_trait]
323trait ErasedHandler: Send + Sync {
324 async fn call(&self, input: Value, context: Context) -> Result<Value, Error>;
325}
326
327struct HandlerAdapter<H>(H);
328
329#[async_trait]
330impl<H: Handler> ErasedHandler for HandlerAdapter<H> {
331 async fn call(&self, input: Value, context: Context) -> Result<Value, Error> {
332 let input = serde_json::from_value::<H::Input>(input).map_err(|error| {
333 Error::user(
334 "invalid_arguments",
335 format!("tool arguments did not match the declared schema: {error}"),
336 )
337 })?;
338 let output = self.0.execute(input, context).await.map_err(Into::into)?;
339 serde_json::to_value(output).map_err(|error| {
340 Error::internal(
341 "result_serialization",
342 format!("failed to serialize capability result: {error}"),
343 )
344 })
345 }
346}
347
348fn schema_for<T: JsonSchema>() -> Value {
349 serde_json::to_value(schemars::schema_for!(T)).unwrap_or(Value::Null)
350}
351
352#[derive(Clone, Debug)]
354pub struct ToolSpec {
355 name: String,
356 display_name: Option<String>,
357 description: String,
358 input_schema: Value,
359 output_schema: Value,
360 hints: Hints,
361}
362
363impl ToolSpec {
364 pub fn name(&self) -> &str {
366 &self.name
367 }
368 pub fn display_name(&self) -> Option<&str> {
370 self.display_name.as_deref()
371 }
372 pub fn description(&self) -> &str {
374 &self.description
375 }
376 pub fn input_schema(&self) -> &Value {
378 &self.input_schema
379 }
380 pub fn output_schema(&self) -> &Value {
382 &self.output_schema
383 }
384 pub fn hints(&self) -> &Hints {
386 &self.hints
387 }
388}
389
390#[derive(Clone, Debug, Default, PartialEq)]
395pub struct Hints {
396 pub readonly: Option<bool>,
398 pub destructive: Option<bool>,
400 pub idempotent: Option<bool>,
402 pub open_world: Option<bool>,
404 pub long_running: Option<bool>,
406 pub concurrency_class: Option<String>,
408 pub metadata: Option<Value>,
410}
411
412impl Hints {
413 pub fn readonly(mut self, value: bool) -> Self {
415 self.readonly = Some(value);
416 self
417 }
418 pub fn destructive(mut self, value: bool) -> Self {
420 self.destructive = Some(value);
421 self
422 }
423 pub fn idempotent(mut self, value: bool) -> Self {
425 self.idempotent = Some(value);
426 self
427 }
428 pub fn open_world(mut self, value: bool) -> Self {
430 self.open_world = Some(value);
431 self
432 }
433 pub fn long_running(mut self, value: bool) -> Self {
435 self.long_running = Some(value);
436 self
437 }
438 pub fn concurrency_class(mut self, value: impl Into<String>) -> Self {
440 self.concurrency_class = Some(value.into());
441 self
442 }
443 pub fn metadata(mut self, value: impl Into<Value>) -> Self {
445 self.metadata = Some(value.into());
446 self
447 }
448}
449
450#[async_trait]
455pub trait ProgressSink: Send + Sync {
456 async fn emit(&self, tool_name: &str, message: &str);
458}
459
460pub trait CancellationSignal: Send + Sync {
464 fn is_cancelled(&self) -> bool;
466
467 fn cancelled<'a>(&'a self) -> BoxFuture<'a, ()>;
469}
470
471struct NoopProgressSink;
472
473#[async_trait]
474impl ProgressSink for NoopProgressSink {
475 async fn emit(&self, _tool_name: &str, _message: &str) {}
476}
477
478struct NeverCancelled;
479
480impl CancellationSignal for NeverCancelled {
481 fn is_cancelled(&self) -> bool {
482 false
483 }
484
485 fn cancelled<'a>(&'a self) -> BoxFuture<'a, ()> {
486 Box::pin(std::future::pending())
487 }
488}
489
490#[derive(Clone)]
498pub struct Context {
499 tool_name: String,
500 session_id: String,
501 workspace_id: String,
502 locale: Option<String>,
503 progress: Arc<dyn ProgressSink>,
504 cancellation: CallCancellation,
505}
506
507impl Context {
508 pub fn new(
510 tool_name: impl Into<String>,
511 session_id: impl Into<String>,
512 workspace_id: impl Into<String>,
513 ) -> Self {
514 Self {
515 tool_name: tool_name.into(),
516 session_id: session_id.into(),
517 workspace_id: workspace_id.into(),
518 locale: None,
519 progress: Arc::new(NoopProgressSink),
520 cancellation: CallCancellation {
521 inner: Arc::new(NeverCancelled),
522 },
523 }
524 }
525
526 pub fn with_locale(mut self, locale: Option<String>) -> Self {
528 self.locale = locale;
529 self
530 }
531
532 pub fn with_progress_sink(mut self, sink: Arc<dyn ProgressSink>) -> Self {
534 self.progress = sink;
535 self
536 }
537
538 pub fn with_cancellation_signal(mut self, signal: Arc<dyn CancellationSignal>) -> Self {
540 self.cancellation = CallCancellation { inner: signal };
541 self
542 }
543
544 pub fn tool_name(&self) -> &str {
546 &self.tool_name
547 }
548
549 pub fn session_id(&self) -> &str {
551 &self.session_id
552 }
553
554 pub fn workspace_id(&self) -> &str {
556 &self.workspace_id
557 }
558
559 pub fn locale(&self) -> Option<&str> {
561 self.locale.as_deref()
562 }
563
564 pub fn cancellation(&self) -> &CallCancellation {
566 &self.cancellation
567 }
568
569 pub async fn progress(&self, message: impl AsRef<str>) {
573 self.progress.emit(&self.tool_name, message.as_ref()).await;
574 }
575}
576
577impl fmt::Debug for Context {
578 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
579 f.debug_struct("Context")
580 .field("tool_name", &self.tool_name)
581 .field("session_id", &self.session_id)
582 .field("workspace_id", &self.workspace_id)
583 .field("locale", &self.locale)
584 .field("cancelled", &self.cancellation.is_cancelled())
585 .finish()
586 }
587}
588
589#[derive(Clone)]
596pub struct CallCancellation {
597 inner: Arc<dyn CancellationSignal>,
598}
599
600impl CallCancellation {
601 pub fn is_cancelled(&self) -> bool {
603 self.inner.is_cancelled()
604 }
605
606 pub async fn cancelled(&self) {
608 self.inner.cancelled().await;
609 }
610}
611
612impl fmt::Debug for CallCancellation {
613 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
614 f.debug_struct("CallCancellation")
615 .field("cancelled", &self.is_cancelled())
616 .finish()
617 }
618}
619
620#[derive(Clone, Copy, Debug, PartialEq, Eq)]
622#[non_exhaustive]
623pub enum ErrorVisibility {
624 User,
626 Internal,
628}
629
630#[derive(Debug)]
638pub struct Error {
639 visibility: ErrorVisibility,
640 code: String,
641 message: String,
642 details: Option<Value>,
643}
644
645impl Error {
646 pub fn user(code: impl Into<String>, message: impl Into<String>) -> Self {
648 Self {
649 visibility: ErrorVisibility::User,
650 code: code.into(),
651 message: message.into(),
652 details: None,
653 }
654 }
655
656 pub fn internal(code: impl Into<String>, message: impl Into<String>) -> Self {
658 Self {
659 visibility: ErrorVisibility::Internal,
660 code: code.into(),
661 message: message.into(),
662 details: None,
663 }
664 }
665
666 pub fn details(mut self, details: impl Into<Value>) -> Self {
668 self.details = Some(details.into());
669 self
670 }
671
672 pub fn code(&self) -> &str {
674 &self.code
675 }
676
677 pub fn message(&self) -> &str {
679 &self.message
680 }
681
682 pub fn details_value(&self) -> Option<&Value> {
684 self.details.as_ref()
685 }
686
687 pub fn visibility(&self) -> ErrorVisibility {
689 self.visibility
690 }
691
692 pub fn into_parts(self) -> (ErrorVisibility, String, String, Option<Value>) {
694 (self.visibility, self.code, self.message, self.details)
695 }
696}
697
698impl fmt::Display for Error {
699 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
700 write!(f, "[{}] {}", self.code, self.message)
701 }
702}
703
704impl std::error::Error for Error {}
705
706#[cfg(test)]
707mod tests {
708 use super::*;
709 use serde_json::json;
710
711 #[derive(Deserialize, JsonSchema)]
712 struct LookupInput {
713 city: String,
714 }
715
716 #[derive(Debug, Serialize, JsonSchema)]
717 struct LookupOutput {
718 city: String,
719 temperatures: Vec<i32>,
720 }
721
722 struct Lookup;
723
724 #[async_trait]
725 impl Handler for Lookup {
726 type Input = LookupInput;
727 type Output = LookupOutput;
728 type Error = Error;
729
730 fn name(&self) -> &str {
731 "lookup_weather"
732 }
733
734 fn description(&self) -> &str {
735 "Look up a typed weather forecast."
736 }
737
738 fn hints(&self) -> Hints {
739 Hints::default().readonly(true).idempotent(true)
740 }
741
742 async fn execute(
743 &self,
744 input: Self::Input,
745 context: Context,
746 ) -> Result<Self::Output, Self::Error> {
747 context.progress("forecast ready").await;
748 Ok(LookupOutput {
749 city: input.city,
750 temperatures: vec![18, 21],
751 })
752 }
753 }
754
755 fn lookup_capability() -> Definition {
756 Definition::new("weather", "Weather", "Typed weather tools.")
757 .metadata(json!({ "owner": "example" }))
758 .tool(Lookup)
759 }
760
761 #[test]
762 fn exposes_input_output_schemas_and_hints() {
763 let capability = lookup_capability();
764 let spec = capability.tools()[0].spec();
765
766 assert_eq!(spec.name(), "lookup_weather");
767 assert_eq!(spec.input_schema()["type"], "object");
768 assert_eq!(spec.input_schema()["required"], json!(["city"]));
769 assert_eq!(spec.input_schema()["properties"]["city"]["type"], "string");
770 assert_eq!(spec.output_schema()["type"], "object");
771 assert_eq!(spec.output_schema()["properties"]["city"]["type"], "string");
772 assert_eq!(
773 spec.output_schema()["properties"]["temperatures"]["type"],
774 "array"
775 );
776 assert_eq!(
777 spec.output_schema()["properties"]["temperatures"]["items"]["type"],
778 "integer"
779 );
780 assert_eq!(spec.hints().readonly, Some(true));
781 assert_eq!(spec.hints().idempotent, Some(true));
782 assert_eq!(
783 capability.metadata_value(),
784 Some(&json!({ "owner": "example" }))
785 );
786 capability.validate().unwrap();
787 }
788
789 #[test]
790 fn validate_rejects_structural_problems() {
791 let err = Definition::new("weather", "Weather", "d")
792 .validate()
793 .unwrap_err();
794 assert!(err.reason().contains("at least one tool"));
795
796 let err = Definition::new("2fast", "N", "d")
797 .tool(Lookup)
798 .validate()
799 .unwrap_err();
800 assert!(err.reason().contains("start with a letter"));
801
802 let err = Definition::new("weather", " ", "d")
803 .tool(Lookup)
804 .validate()
805 .unwrap_err();
806 assert!(err.reason().contains("name must not be blank"));
807 for (definition, expected) in [
808 (
809 Definition::new("weather", "Weather", " ").tool(Lookup),
810 "description must not be blank",
811 ),
812 (
813 lookup_capability().instructions(" "),
814 "instructions must not be blank",
815 ),
816 ] {
817 assert!(
818 definition
819 .validate()
820 .unwrap_err()
821 .reason()
822 .contains(expected)
823 );
824 }
825 let mut malformed = lookup_capability();
826 malformed.tools[0].spec.description = " ".into();
827 assert!(
828 malformed
829 .validate()
830 .unwrap_err()
831 .reason()
832 .contains("tool \"lookup_weather\" description must not be blank")
833 );
834 }
835
836 fn ready<F: Future>(future: F) -> F::Output {
838 let mut future = std::pin::pin!(future);
839 let mut context = std::task::Context::from_waker(std::task::Waker::noop());
840 match future.as_mut().poll(&mut context) {
841 std::task::Poll::Ready(value) => value,
842 std::task::Poll::Pending => panic!("test future unexpectedly pending"),
843 }
844 }
845
846 #[derive(Default)]
847 struct RecordingProgress(std::sync::Mutex<Vec<(String, String)>>);
848 #[async_trait]
849 impl ProgressSink for RecordingProgress {
850 async fn emit(&self, tool: &str, message: &str) {
851 self.0.lock().unwrap().push((tool.into(), message.into()));
852 }
853 }
854
855 #[test]
856 fn invoke_serializes_typed_output() {
857 let tool = lookup_capability().tools()[0].clone();
858 let progress = Arc::new(RecordingProgress::default());
859 let context = Context::new("lookup_weather", "session", "workspace")
860 .with_progress_sink(progress.clone());
861 let value = ready(tool.invoke(json!({ "city": "Kyiv" }), context)).unwrap();
862 assert_eq!(value, json!({ "city": "Kyiv", "temperatures": [18, 21] }));
863 assert_eq!(
864 *progress.0.lock().unwrap(),
865 [("lookup_weather".into(), "forecast ready".into())]
866 );
867 }
868
869 #[test]
870 fn invoke_rejects_invalid_arguments_as_user_error() {
871 let tool = lookup_capability().tools()[0].clone();
872 let context = Context::new("lookup_weather", "session", "workspace");
873 let error = ready(tool.invoke(json!({ "city": 42 }), context)).unwrap_err();
874 assert_eq!(error.code(), "invalid_arguments");
875 assert_eq!(error.visibility(), ErrorVisibility::User);
876 }
877
878 #[test]
879 fn context_defaults_are_inert() {
880 let context = Context::new("t", "s", "w");
881 assert!(!context.cancellation().is_cancelled());
882 ready(context.progress("no-op"));
883 let mut cancelled = std::pin::pin!(context.cancellation().cancelled());
884 let mut task = std::task::Context::from_waker(std::task::Waker::noop());
885 assert!(cancelled.as_mut().poll(&mut task).is_pending());
886 }
887}