1#![cfg_attr(docsrs, feature(doc_auto_cfg))]
5mod app;
12pub mod capabilities;
13mod context;
14mod error;
15mod ports;
16pub mod provider_errors;
17pub mod provider_registry;
18pub mod providers;
19mod service;
20mod types;
21
22use serde_json::Value;
23use soma_domain::{
24 actions::{action_validation_error, rest_help, SomaAction},
25 errors::{ServiceError, ToolError},
26};
27
28pub use app::SomaApplication;
29pub use context::ExecutionContext;
30pub use error::{ApplicationError, ApplicationErrorDetails};
31pub use ports::{ApplicationPorts, CodeModePort, GatewayPort, OpenApiPort, PortError};
32pub use provider_errors::ProviderError;
33pub use provider_registry::{
34 DynamicResourceTemplate, ProviderAuthMode, ProviderCall, ProviderOutput, ProviderPrincipal,
35 ProviderRegistry, ProviderRequestLimits, ProviderSurface, RegistrySnapshot, ResourceReadOutput,
36};
37pub use providers::filesystem::FileProviderSource;
38pub use providers::remote::RemoteCatalogProvider;
39pub use providers::static_rust::StaticRustProvider;
40pub use service::{
41 ElicitedNameOutcome, ScaffoldIntent, ScaffoldIntentValidationError, SomaService,
42};
43pub use types::CodeModeExecuteRequest;
44pub use types::{
45 CatalogSnapshot, DoctorReport, ElicitedName, ExecuteActionRequest, ExecuteActionResponse,
46 GatewayExecuteRequest, GatewayPromptRoute, GatewayReloadRequest, GatewayResourceRoute,
47 GatewayRouteScope, GatewayToolRoute, OpenApiExecuteRequest, OperationResponse,
48 ReadResourceRequest, ResourceContent, ResourceTemplateSpec, ScaffoldIntentRequest,
49};
50
51pub use soma_provider_core::{ProviderPrompt, ProviderResource};
52
53pub async fn dispatch_action(
65 service: &SomaService,
66 action: &SomaAction,
67 surface: &str,
68) -> anyhow::Result<Value> {
69 let action_name = action.name();
70 let started = std::time::Instant::now();
71 let result = execute_service_action(service, action).await;
72 let elapsed_ms = started.elapsed().as_millis();
73 let outcome = if result.is_ok() { "ok" } else { "error" };
74
75 tracing::info!(
76 surface,
77 service = "soma",
78 action = action_name,
79 outcome,
80 elapsed_ms = elapsed_ms as u64,
81 "action dispatched"
82 );
83 record_action_metric(surface, action_name, outcome, elapsed_ms as f64);
84
85 result
86}
87
88pub fn static_provider_registry(service: SomaService) -> anyhow::Result<ProviderRegistry> {
90 ProviderRegistry::new(vec![std::sync::Arc::new(StaticRustProvider::new(service))])
91 .map_err(|error| anyhow::anyhow!(error.to_string()))
92}
93
94pub fn dynamic_provider_registry(service: SomaService) -> anyhow::Result<ProviderRegistry> {
97 dynamic_provider_registry_from_dir(service, default_provider_dir())
98}
99
100pub fn dynamic_provider_registry_from_dir(
103 service: SomaService,
104 provider_dir: impl Into<std::path::PathBuf>,
105) -> anyhow::Result<ProviderRegistry> {
106 ProviderRegistry::with_file_source(
107 vec![std::sync::Arc::new(StaticRustProvider::new(service))],
108 crate::capabilities::CapabilityBroker::default_deny(),
109 FileProviderSource::new(provider_dir),
110 )
111 .map_err(|error| anyhow::anyhow!(error.to_string()))
112}
113
114pub async fn remote_provider_registry(service: SomaService) -> anyhow::Result<ProviderRegistry> {
117 let report = service.provider_catalog().await?;
118 let providers = providers::remote::catalogs_from_inspection(&report)?
119 .into_iter()
120 .map(|catalog| {
121 std::sync::Arc::new(RemoteCatalogProvider::new(service.clone(), catalog))
122 as std::sync::Arc<dyn provider_registry::Provider>
123 })
124 .collect::<Vec<_>>();
125 ProviderRegistry::new(providers).map_err(|error| anyhow::anyhow!(error.to_string()))
126}
127
128fn default_provider_dir() -> std::path::PathBuf {
129 std::env::var_os("SOMA_PROVIDER_DIR")
130 .map(std::path::PathBuf::from)
131 .unwrap_or_else(|| std::path::PathBuf::from("providers"))
132}
133
134#[cfg(feature = "observability")]
135fn record_action_metric(surface: &str, action: &str, outcome: &str, elapsed_ms: f64) {
136 metrics::counter!(
137 "soma_actions_total",
138 "surface" => surface.to_owned(),
139 "action" => action.to_owned(),
140 "outcome" => outcome.to_owned(),
141 )
142 .increment(1);
143 metrics::histogram!(
144 "soma_action_duration_ms",
145 "surface" => surface.to_owned(),
146 "action" => action.to_owned(),
147 )
148 .record(elapsed_ms);
149}
150
151#[cfg(not(feature = "observability"))]
152fn record_action_metric(_surface: &str, _action: &str, _outcome: &str, _elapsed_ms: f64) {}
153
154pub async fn execute_service_action(
160 service: &SomaService,
161 action: &SomaAction,
162) -> anyhow::Result<Value> {
163 match action {
164 SomaAction::Greet { name } => service.greet(name.as_deref()).await,
165 SomaAction::Echo { message } => service.echo(message).await,
166 SomaAction::Status => service.status().await,
167 SomaAction::Help => Ok(rest_help()),
168 SomaAction::ElicitName => Err(anyhow::anyhow!(
169 "action=elicit_name is only available over MCP because it requires a peer"
170 )),
171 SomaAction::ScaffoldIntent => Err(anyhow::anyhow!(
172 "action=scaffold_intent is only available over MCP because it requires elicitation"
173 )),
174 }
175}
176
177pub fn is_validation_error(error: &anyhow::Error) -> bool {
179 classify_service_error(error).kind == soma_domain::errors::ServiceErrorKind::Validation
180}
181
182pub fn classify_service_error(error: &anyhow::Error) -> ServiceError {
185 if let Some(error) = action_validation_error(error) {
186 return ToolError::from_action_validation(error);
187 }
188 if let Some(error) = error.downcast_ref::<ScaffoldIntentValidationError>() {
189 let mut tool_error =
190 ToolError::validation(error.code(), error.to_string(), error.remediation());
191 if let Some(field) = error.field() {
192 tool_error = tool_error.with_field(field);
193 }
194 if let Some(expected_pattern) = error.expected_pattern() {
195 tool_error = tool_error.with_expected_pattern(expected_pattern);
196 }
197 return tool_error;
198 }
199 ToolError::execution(error)
200}