Skip to main content

agent_runtime_bootstrap/
lib.rs

1//! Production composition root and lifecycle management for Agent Runtime.
2//!
3//! This crate wires validated deployment configuration to infrastructure
4//! adapters, execution coordination, the Kernel, and HTTP transport. Agent
5//! implementations remain outside this dependency boundary.
6
7use 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    /// Create the official composition root from typed deployment config.
70    ///
71    /// Without an injected Definition, Bootstrap loads `definition_path`.
72    /// There is intentionally no hidden Code Agent default: Tool/Skill-focused
73    /// distributions call [`Self::with_definition`] so behavior is visible in
74    /// their composition root.
75    pub fn new(config: &'a RuntimeConfig) -> Self {
76        Self {
77            config,
78            definition: None,
79            transport: None,
80        }
81    }
82
83    /// Inject one already-built Definition.
84    ///
85    /// This is the narrow path for Tool + Skill and Skill + Workspace agents:
86    /// Bootstrap still owns Infra SDK sessions, execution, authentication and
87    /// transport, while developer code owns only the Definition.
88    pub fn with_definition(mut self, definition: Arc<AgentDefinition>) -> Self {
89        self.definition = Some(definition);
90        self
91    }
92
93    /// Override the route selection derived from `server.transport`.
94    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}