1#![cfg_attr(docsrs, feature(doc_auto_cfg))]
5#![warn(missing_docs)]
17mod app;
18pub mod capabilities;
19pub(crate) mod componentize;
20mod context;
21mod error;
22pub mod graduation;
23mod ports;
24pub mod provider_errors;
25pub mod provider_registry;
26pub mod providers;
27mod service;
28mod types;
29
30use serde_json::Value;
31use soma_domain::{
32 actions::{SomaAction, action_validation_error, rest_help},
33 errors::{ServiceError, ToolError},
34};
35
36pub use app::SomaApplication;
37pub use context::ExecutionContext;
38pub use error::{ApplicationError, ApplicationErrorDetails};
39pub use ports::{
40 ApplicationPorts, CodeModePort, GatewayPort, OpenApiPort, PortError, PythonEnvironmentPort,
41 PythonEnvironmentUpdateCandidate,
42};
43pub use provider_errors::ProviderError;
44pub use provider_registry::{
45 DynamicResourceTemplate, ProviderAuthMode, ProviderCall, ProviderOutput, ProviderPrincipal,
46 ProviderRegistry, ProviderRequestLimits, ProviderSurface, RegistrySnapshot, ResourceReadOutput,
47};
48pub use providers::filesystem::{
49 FileProviderSource, PythonProviderEnvironmentPreparer, PythonProviderRuntime,
50 PythonRunnerSelection,
51};
52pub use providers::remote::RemoteCatalogProvider;
53pub use providers::static_rust::StaticRustProvider;
54pub use service::{
55 ElicitedNameOutcome, ScaffoldIntent, ScaffoldIntentValidationError, SomaService,
56};
57pub use soma_provider_adapters::python::host::PythonExecutionProfile;
58pub use soma_provider_adapters::python::supervisor::PythonSupervisorConfig;
59pub use soma_provider_adapters::python::{
60 PythonInterpreter,
61 cache::{PythonEnvironmentCache, PythonEnvironmentPrunePlan, PythonEnvironmentPrunePolicy},
62 environment::{ENVIRONMENT_PLAN_VERSION, PythonRuntimeFingerprint},
63 lifecycle::{PythonEnvironmentLifecycle, PythonEnvironmentSpec},
64 materializer::{
65 PreparedPythonEnvironment, PythonEnvironmentRepairReport, PythonEnvironmentUpdateReport,
66 },
67};
68pub use types::CodeModeExecuteRequest;
69pub use types::{
70 CatalogSnapshot, DoctorReport, ElicitedName, ExecuteActionRequest, ExecuteActionResponse,
71 GatewayExecuteRequest, GatewayMcpOutcome, GatewayMcpRoundTrip, GatewayPromptRoute,
72 GatewayReloadRequest, GatewayResourceRoute, GatewayRouteScope, GatewayToolRoute,
73 OpenApiExecuteRequest, OperationResponse, ReadResourceRequest, ResourceContent,
74 ResourceTemplateSpec, ScaffoldIntentRequest,
75};
76
77pub fn configure_provider_state_path(path: std::path::PathBuf) -> Result<(), String> {
79 soma_provider_adapters::configure_provider_state_path(path)
80}
81
82pub use soma_provider_core::{CapabilityGrant, ProviderPrompt, ProviderResource};
83
84pub async fn dispatch_action(
96 service: &SomaService,
97 action: &SomaAction,
98 surface: &str,
99) -> anyhow::Result<Value> {
100 let action_name = action.name();
101 let started = std::time::Instant::now();
102 let result = execute_service_action(service, action).await;
103 let elapsed_ms = started.elapsed().as_millis();
104 let outcome = if result.is_ok() { "ok" } else { "error" };
105
106 tracing::info!(
107 surface,
108 service = "soma",
109 action = action_name,
110 outcome,
111 elapsed_ms = elapsed_ms as u64,
112 "action dispatched"
113 );
114 record_action_metric(surface, action_name, outcome, elapsed_ms as f64);
115
116 result
117}
118
119pub fn static_provider_registry(service: SomaService) -> anyhow::Result<ProviderRegistry> {
121 ProviderRegistry::new(vec![std::sync::Arc::new(StaticRustProvider::new(service))])
122 .map_err(|error| anyhow::anyhow!(error.to_string()))
123}
124
125pub fn dynamic_provider_registry(service: SomaService) -> anyhow::Result<ProviderRegistry> {
128 dynamic_provider_registry_from_dir(service, default_provider_dir())
129}
130
131pub async fn dynamic_provider_registry_with_python(
133 service: SomaService,
134 python_runner: PythonRunnerSelection,
135) -> anyhow::Result<ProviderRegistry> {
136 dynamic_provider_registry_with_python_runtime(
137 service,
138 PythonProviderRuntime::new(python_runner),
139 )
140 .await
141}
142
143pub async fn dynamic_provider_registry_with_python_runtime(
145 service: SomaService,
146 python_runtime: PythonProviderRuntime,
147) -> anyhow::Result<ProviderRegistry> {
148 dynamic_provider_registry_from_dir_with_python_runtime(
149 service,
150 default_provider_dir(),
151 python_runtime,
152 )
153 .await
154}
155
156pub fn dynamic_provider_registry_from_dir(
159 service: SomaService,
160 provider_dir: impl Into<std::path::PathBuf>,
161) -> anyhow::Result<ProviderRegistry> {
162 ProviderRegistry::with_file_source(
163 vec![std::sync::Arc::new(StaticRustProvider::new(service))],
164 crate::capabilities::CapabilityBroker::default_deny(),
165 FileProviderSource::new(provider_dir),
166 )
167 .map_err(|error| anyhow::anyhow!(error.to_string()))
168}
169
170pub async fn dynamic_provider_registry_from_dir_with_python(
172 service: SomaService,
173 provider_dir: impl Into<std::path::PathBuf>,
174 python_runner: PythonRunnerSelection,
175) -> anyhow::Result<ProviderRegistry> {
176 dynamic_provider_registry_from_dir_with_python_runtime(
177 service,
178 provider_dir,
179 PythonProviderRuntime::new(python_runner),
180 )
181 .await
182}
183
184pub async fn dynamic_provider_registry_from_dir_with_python_runtime(
187 service: SomaService,
188 provider_dir: impl Into<std::path::PathBuf>,
189 python_runtime: PythonProviderRuntime,
190) -> anyhow::Result<ProviderRegistry> {
191 let (source, capability_broker) =
192 python_runtime.configure_source(FileProviderSource::new(provider_dir));
193 ProviderRegistry::with_file_source_async(
194 vec![std::sync::Arc::new(StaticRustProvider::new(service))],
195 capability_broker,
196 source,
197 )
198 .await
199 .map_err(|error| anyhow::anyhow!(error.to_string()))
200}
201
202pub async fn remote_provider_registry(service: SomaService) -> anyhow::Result<ProviderRegistry> {
205 let report = service.provider_catalog().await?;
206 let providers = providers::remote::catalogs_from_inspection(&report)?
207 .into_iter()
208 .map(|catalog| {
209 std::sync::Arc::new(RemoteCatalogProvider::new(service.clone(), catalog))
210 as std::sync::Arc<dyn provider_registry::Provider>
211 })
212 .collect::<Vec<_>>();
213 ProviderRegistry::new(providers).map_err(|error| anyhow::anyhow!(error.to_string()))
214}
215
216fn default_provider_dir() -> std::path::PathBuf {
217 std::env::var_os("SOMA_PROVIDER_DIR")
218 .map(std::path::PathBuf::from)
219 .unwrap_or_else(|| std::path::PathBuf::from("providers"))
220}
221
222#[cfg(feature = "observability")]
223fn record_action_metric(surface: &str, action: &str, outcome: &str, elapsed_ms: f64) {
224 metrics::counter!(
225 "soma_actions_total",
226 "surface" => surface.to_owned(),
227 "action" => action.to_owned(),
228 "outcome" => outcome.to_owned(),
229 )
230 .increment(1);
231 metrics::histogram!(
232 "soma_action_duration_ms",
233 "surface" => surface.to_owned(),
234 "action" => action.to_owned(),
235 )
236 .record(elapsed_ms);
237}
238
239#[cfg(not(feature = "observability"))]
240fn record_action_metric(_surface: &str, _action: &str, _outcome: &str, _elapsed_ms: f64) {}
241
242pub async fn execute_service_action(
248 service: &SomaService,
249 action: &SomaAction,
250) -> anyhow::Result<Value> {
251 match action {
252 SomaAction::Greet { name } => service.greet(name.as_deref()).await,
253 SomaAction::Echo { message } => service.echo(message).await,
254 SomaAction::Status => service.status().await,
255 SomaAction::PythonEnvironmentStatus
256 | SomaAction::PythonEnvironmentPrunePlan { .. }
257 | SomaAction::PythonEnvironmentPrune { .. }
258 | SomaAction::PythonEnvironmentRepair { .. }
259 | SomaAction::PythonEnvironmentUpdate { .. }
260 | SomaAction::PythonWorkerStatus
261 | SomaAction::PythonWorkerCancel { .. }
262 | SomaAction::PythonWorkerReset { .. }
263 | SomaAction::PythonGenerationStatus
264 | SomaAction::PythonGenerationRollback { .. }
265 | SomaAction::PythonGraduationStatus { .. }
266 | SomaAction::PythonGraduationApply { .. } => Err(anyhow::anyhow!(
267 "Python operator actions require the shared application control plane"
268 )),
269 SomaAction::Help => Ok(rest_help()),
270 SomaAction::ElicitName => Err(anyhow::anyhow!(
271 "action=elicit_name is only available over MCP because it requires a peer"
272 )),
273 SomaAction::ScaffoldIntent => Err(anyhow::anyhow!(
274 "action=scaffold_intent is only available over MCP because it requires elicitation"
275 )),
276 }
277}
278
279pub fn is_validation_error(error: &anyhow::Error) -> bool {
281 classify_service_error(error).kind == soma_domain::errors::ServiceErrorKind::Validation
282}
283
284pub fn classify_service_error(error: &anyhow::Error) -> ServiceError {
287 if let Some(error) = action_validation_error(error) {
288 return ToolError::from_action_validation(error);
289 }
290 if let Some(error) = error.downcast_ref::<ScaffoldIntentValidationError>() {
291 let mut tool_error =
292 ToolError::validation(error.code(), error.to_string(), error.remediation());
293 if let Some(field) = error.field() {
294 tool_error = tool_error.with_field(field);
295 }
296 if let Some(expected_pattern) = error.expected_pattern() {
297 tool_error = tool_error.with_expected_pattern(expected_pattern);
298 }
299 return tool_error;
300 }
301 ToolError::execution(error)
302}