1use std::{collections::BTreeMap, sync::Arc, time::Duration};
8
9use agent_runtime_config::{AuthConfig, JournalConfig, RuntimeConfig};
10use async_trait::async_trait;
11use axum::Router;
12use runtime_adapter_agent_infra::{
13 AgentInfraConfig as InfraAdapterConfig, AgentInfraSessionFactory, RuntimeBinding,
14 StaticRuntimeBindingResolver,
15};
16use runtime_adapter_execution_journal::MemoryExecutionJournal;
17use runtime_agent_definition_loader::{DefinitionCatalogResolver, load_definition_report};
18use runtime_extension_api::AgentDefinition;
19use runtime_kernel::AgentKernel;
20use runtime_kernel_api::{AgentDefinitionResolver, ContextCompactionLimits, ContextLimits};
21use runtime_link::ExecutionCoordinator;
22use runtime_outer_http::{
23 IngressAuthenticator, ReadinessProbe, RuntimeTransportConfig, StaticTokenAuthenticator,
24 TrustedGatewayAuthenticator,
25};
26use runtime_types::{CallerScope, CredentialHandle, RuntimeInstanceId, WorkspaceId};
27use thiserror::Error;
28
29#[derive(Clone)]
30pub struct RuntimeApplication {
31 pub router: Router,
32 readiness: Arc<RuntimeReadiness>,
33 coordinator: Arc<ExecutionCoordinator>,
34}
35
36impl RuntimeApplication {
37 pub fn begin_shutdown(&self) {
38 self.readiness
39 .accepting
40 .store(false, std::sync::atomic::Ordering::SeqCst);
41 self.coordinator.begin_shutdown();
42 }
43
44 pub async fn shutdown(&self, grace: Duration) -> bool {
45 self.begin_shutdown();
46 self.coordinator.shutdown(grace).await
47 }
48
49 pub fn active_executions(&self) -> usize {
50 self.coordinator.active_executions()
51 }
52}
53
54#[derive(Debug, Error)]
55pub enum BootstrapError {
56 #[error("invalid runtime wiring: {0}")]
57 Invalid(String),
58 #[error("execution journal failed: {0}")]
59 Journal(String),
60}
61
62pub struct RuntimeBootstrap<'a> {
63 config: &'a RuntimeConfig,
64 definition: Option<Arc<AgentDefinition>>,
65 transport: Option<RuntimeTransportConfig>,
66}
67
68impl<'a> RuntimeBootstrap<'a> {
69 pub fn new(config: &'a RuntimeConfig) -> Self {
76 Self {
77 config,
78 definition: None,
79 transport: None,
80 }
81 }
82
83 pub fn with_definition(mut self, definition: Arc<AgentDefinition>) -> Self {
89 self.definition = Some(definition);
90 self
91 }
92
93 pub fn with_transport(mut self, transport: RuntimeTransportConfig) -> Self {
95 self.transport = Some(transport);
96 self
97 }
98
99 pub async fn build(self) -> Result<RuntimeApplication, BootstrapError> {
100 build_application(self.config, self.definition, self.transport).await
101 }
102}
103
104pub async fn build(settings: &RuntimeConfig) -> Result<RuntimeApplication, BootstrapError> {
105 RuntimeBootstrap::new(settings).build().await
106}
107
108async fn build_application(
109 settings: &RuntimeConfig,
110 injected_definition: Option<Arc<AgentDefinition>>,
111 transport_override: Option<RuntimeTransportConfig>,
112) -> Result<RuntimeApplication, BootstrapError> {
113 settings
114 .validate()
115 .map_err(|error| BootstrapError::Invalid(error.to_string()))?;
116 let journal = match &settings.journal {
117 JournalConfig::Memory {
118 max_events_per_execution,
119 } => Arc::new(MemoryExecutionJournal::new(*max_events_per_execution)),
120 };
121 let sessions = Arc::new(
122 AgentInfraSessionFactory::new(InfraAdapterConfig {
123 gateway_url: settings.agent_infra.gateway_url.clone(),
124 request_timeout: Duration::from_millis(settings.agent_infra.request_timeout_ms),
125 max_response_bytes: settings.agent_infra.max_response_bytes,
126 trusted_mesh_http: settings.agent_infra.trusted_mesh_http,
127 workload_subject: settings.agent_infra.workload_subject.clone(),
128 workload_credential: CredentialHandle::new(
129 settings.agent_infra.workload_token.expose(),
130 )
131 .map_err(|error| BootstrapError::Invalid(error.to_string()))?,
132 delegation_ttl: Duration::from_secs(settings.agent_infra.delegation_ttl_seconds),
133 })
134 .map_err(|error| BootstrapError::Invalid(error.to_string()))?,
135 );
136 let runtime_id = RuntimeInstanceId::new(settings.binding.runtime_id.clone())
137 .map_err(|error| BootstrapError::Invalid(error.to_string()))?;
138 let (definition_resolver, definition_digest, runtime_type, definition_id, definition_version): (
139 Arc<dyn AgentDefinitionResolver>,
140 String,
141 String,
142 String,
143 String,
144 ) = match (injected_definition, &settings.binding.definition_path) {
145 (Some(definition), _) => {
146 let catalog = DefinitionCatalogResolver::new([definition.clone()])
147 .map_err(|error| BootstrapError::Invalid(error.to_string()))?;
148 (
149 Arc::new(catalog),
150 definition.manifest.digest.clone(),
151 definition.manifest.runtime_type.clone(),
152 definition.manifest.id.to_string(),
153 definition.manifest.version.clone(),
154 )
155 }
156 (None, Some(path)) => {
157 let report = load_definition_report(std::path::Path::new(path))
158 .map_err(|error| BootstrapError::Invalid(error.to_string()))?;
159 let definition = report.definition;
160 let digest = definition.manifest.digest.clone();
161 let runtime_type = definition.manifest.runtime_type.clone();
162 let definition_id = definition.manifest.id.to_string();
163 let definition_version = definition.manifest.version.clone();
164 let catalog = DefinitionCatalogResolver::new([definition])
165 .map_err(|error| BootstrapError::Invalid(error.to_string()))?;
166 (
167 Arc::new(catalog),
168 digest,
169 runtime_type,
170 definition_id,
171 definition_version,
172 )
173 }
174 (None, None) => return Err(BootstrapError::Invalid(
175 "an Agent Definition must be injected with RuntimeBootstrap::with_definition or selected by binding.definition_path".into(),
176 )),
177 };
178 let binding = RuntimeBinding {
179 agent_id: settings.binding.agent_id.clone(),
180 runtime_type,
181 model: settings.binding.model.clone(),
182 workspace_id: WorkspaceId::new(settings.binding.workspace_id.clone())
183 .map_err(|error| BootstrapError::Invalid(error.to_string()))?,
184 definition_id,
185 definition_version,
186 definition_digest: Some(definition_digest),
187 metadata: BTreeMap::new(),
188 };
189 let resolver = StaticRuntimeBindingResolver::new(BTreeMap::from([(runtime_id, binding)]))
190 .map_err(|error| BootstrapError::Invalid(error.to_string()))?;
191 let link = Arc::new(
192 ExecutionCoordinator::new_with_context_limits(
193 journal,
194 Arc::new(resolver),
195 sessions.clone(),
196 definition_resolver,
197 Arc::new(AgentKernel),
198 settings.scheduler.max_concurrent_executions,
199 ContextLimits {
200 max_messages: settings.context.max_messages,
201 max_input_tokens: settings.context.max_input_tokens,
202 reserved_output_tokens: settings.context.reserved_output_tokens,
203 protocol_overhead_tokens: settings.context.protocol_overhead_tokens,
204 auto_compact: settings.context.auto_compact,
205 compaction: ContextCompactionLimits {
206 trigger_percent: settings.context.compaction.trigger_percent,
207 target_percent: settings.context.compaction.target_percent,
208 emergency_percent: settings.context.compaction.emergency_percent,
209 min_reclaim_percent: settings.context.compaction.min_reclaim_percent,
210 min_turns_between: settings.context.compaction.min_turns_between,
211 checkpoint_max_chars: settings.context.compaction.checkpoint_max_chars,
212 },
213 },
214 )
215 .map_err(|error| BootstrapError::Invalid(error.to_string()))?,
216 );
217 link.recover(1_000)
218 .await
219 .map_err(|error| BootstrapError::Journal(error.to_string()))?;
220 link.start_recovery_loop(Duration::from_secs(5), 1_000)
221 .map_err(|error| BootstrapError::Invalid(error.to_string()))?;
222 let authenticator: Arc<dyn IngressAuthenticator> = match &settings.auth {
223 AuthConfig::LocalStatic {
224 token,
225 subject,
226 tenant_id,
227 project_id,
228 capabilities,
229 } => Arc::new(
230 StaticTokenAuthenticator::new(
231 token.expose(),
232 CallerScope {
233 subject: subject.clone(),
234 tenant_id: tenant_id.clone(),
235 project_id: project_id.clone(),
236 capabilities: capabilities.clone(),
237 },
238 )
239 .map_err(|error| BootstrapError::Invalid(error.message))?,
240 ),
241 AuthConfig::TrustedGateway { shared_secret } => Arc::new(
242 TrustedGatewayAuthenticator::new(shared_secret.expose())
243 .map_err(|error| BootstrapError::Invalid(error.message))?,
244 ),
245 };
246 let readiness = Arc::new(RuntimeReadiness {
247 sessions,
248 accepting: std::sync::atomic::AtomicBool::new(true),
249 });
250 let configured_transport = RuntimeTransportConfig {
251 agent_tasks: settings.server.transport.agent_tasks,
252 openai_responses: settings.server.transport.openai_responses,
253 claude_messages: settings.server.transport.claude_messages,
254 sse: settings.server.transport.sse,
255 websocket: settings.server.transport.websocket,
256 websocket_max_message_bytes: settings.server.transport.websocket_max_message_bytes,
257 };
258 let router = runtime_outer_http::router(
259 link.clone(),
260 authenticator,
261 readiness.clone(),
262 transport_override.unwrap_or(configured_transport),
263 );
264 Ok(RuntimeApplication {
265 router,
266 readiness,
267 coordinator: link,
268 })
269}
270
271struct RuntimeReadiness {
272 sessions: Arc<AgentInfraSessionFactory>,
273 accepting: std::sync::atomic::AtomicBool,
274}
275
276#[async_trait]
277impl ReadinessProbe for RuntimeReadiness {
278 async fn ready(&self) -> Result<(), String> {
279 if !self.accepting.load(std::sync::atomic::Ordering::SeqCst) {
280 return Err("runtime is shutting down".into());
281 }
282 self.sessions
283 .check_ready()
284 .await
285 .map_err(|error| error.to_string())
286 }
287}