Skip to main content

agent_sdk_core/application/
runtime.rs

1//! Runtime coordination for starting, observing, and completing runs. Use this module
2//! when a host wires providers, package resolution, policy, journals, and event
3//! streams into the core loop. Runtime methods may call configured adapters, mutate
4//! run state, append journals, and publish events.
5//!
6use std::{
7    collections::BTreeMap,
8    sync::{
9        Arc, Mutex,
10        atomic::{AtomicBool, AtomicU64, Ordering},
11    },
12};
13
14use crate::{
15    approval_ports::ApprovalDispatcher,
16    capability::ProjectionMode,
17    content_ports::ContentResolver,
18    domain::{
19        AgentError, AgentErrorKind, AgentId, RetryClassification, RunId, RuntimePackageId,
20        SessionId, SourceRef, TurnId,
21    },
22    error::CausalIds,
23    event::{CompiledEventFilter, EventCursor, EventKind},
24    event_bus::{AgentEventBus, AgentEventStream},
25    hook_ports::{HookExecutorRegistry, InMemoryHookExecutorRegistry},
26    journal::{JournalCursor, JournalRecord},
27    journal_ports::RunJournal,
28    package::{RuntimePackage, RuntimePackageFingerprint},
29    policy::PolicyDecision,
30    ports::{
31        InMemoryRuntimePackageResolver, OutputSinkPort, OutputSinkRegistry, ProviderAdapter,
32        ProviderRegistry, RuntimePackageResolver, RuntimePolicyPort,
33    },
34    provider::ProviderToolSpec,
35    run::{RunRequest, RunResult, RunStatus},
36    run_handle::{InMemoryRunControlStore, RunControlStore, RunHandle},
37    subscription::RunSubscriptionSource,
38    tool_execution::ToolExecutionCoordinator,
39    tool_ports::{
40        ToolExecutor, ToolExecutorRegistry, ToolPolicyPort, ToolRegistrySnapshot, ToolRoute,
41        ToolRouter,
42    },
43};
44
45#[derive(Clone)]
46/// Holds agent runtime application-layer state or configuration.
47/// Use it with the documented coordinator methods; run, journal, event, provider, or port effects are called out on those methods rather than on construction.
48pub struct AgentRuntime {
49    inner: Arc<RuntimeInner>,
50}
51
52impl AgentRuntime {
53    /// Starts a builder for this application::runtime value. Building
54    /// is data-only; runtime side effects occur only when a later
55    /// coordinator or host port executes the built configuration.
56    pub fn builder() -> AgentRuntimeBuilder {
57        AgentRuntimeBuilder::default()
58    }
59
60    /// Registers a run with the runtime and returns a handle for control and
61    /// subscription.
62    /// This resolves package, provider, journal, event, content, and policy
63    /// ports, evaluates start policy, mutates the run registry, and registers
64    /// run control; it does not call the provider model.
65    pub fn start_run(&self, request: RunRequest) -> Result<RunHandle, AgentError> {
66        let _journal = self.journal_port(&request.run_id)?;
67        let _events = self.event_bus_port(&request.run_id)?;
68        let _content = self.content_port(&request.run_id)?;
69        let policy = self.policy_port(&request.run_id)?;
70
71        let effective = self.resolve_effective_package(&request)?;
72        self.provider_for(&effective.package, &request.run_id)?;
73        crate::hooks::validate_package_hooks(
74            &effective.package.hooks,
75            self.inner.hook_registry.as_ref(),
76        )
77        .map_err(|error| {
78            error.with_causal_ids(CausalIds {
79                run_id: Some(request.run_id.clone()),
80                ..CausalIds::default()
81            })
82        })?;
83        self.evaluate_run_start_policy(policy.as_ref(), &request, &effective.package)?;
84
85        let cancellation = CancellationHandle::new();
86        let entry = RegisteredRun {
87            run_id: request.run_id.clone(),
88            session_id: request.session_id.clone(),
89            turn_id: request.turn_id.clone(),
90            agent_id: request.agent_id.clone(),
91            source: request.source.clone(),
92            status: RunRegistryStatus::Registered,
93            runtime_package_id: effective.package.package_id.clone(),
94            runtime_package_fingerprint: effective.fingerprint,
95            provider_route_id: effective.package.provider_route.route_id,
96            provider_model_id: effective.package.provider_route.model_id,
97            cancellation: cancellation.clone(),
98        };
99
100        self.insert_run(entry, &request.run_id)?;
101        self.inner
102            .run_control
103            .register_run(request.run_id.clone(), request.agent_id.clone())?;
104        Ok(RunHandle::new(
105            request.run_id,
106            Arc::new(RuntimeRunControlStore {
107                runtime: self.clone(),
108            }),
109            Arc::new(RuntimeRunSubscriptionSource {
110                runtime: self.clone(),
111            }),
112        ))
113    }
114
115    /// Runs a P0 text request to completion through the configured runtime.
116    /// This registers the run, calls the P0 loop driver, and may use provider,
117    /// journal, content, event, validation, and policy ports selected by the
118    /// resolved package.
119    pub fn run_text(&self, request: RunRequest) -> Result<RunResult, AgentError> {
120        let handle = self.start_run(request.clone())?;
121        crate::loop_driver::run_p0_text(self, request, handle)
122    }
123
124    /// Runs a typed request by attaching the model's output contract and using
125    /// the same runtime path as `run_text`.
126    /// Validation and repair side effects remain on the canonical P1 output
127    /// pipeline; this helper does not create a parallel typed-output path.
128    pub fn run_typed<T: crate::typed_output_ports::TypedOutputModel>(
129        &self,
130        request: RunRequest,
131    ) -> Result<RunResult, AgentError> {
132        self.run_text(request.with_output_contract(crate::output::OutputContract::for_type::<T>()))
133    }
134
135    /// Resolve effective package.
136    /// This reads configured runtime package state, applies request-level tightening, and
137    /// computes the package fingerprint; it does not call provider or tool executors.
138    pub fn resolve_effective_package(
139        &self,
140        request: &RunRequest,
141    ) -> Result<EffectiveRuntimePackage, AgentError> {
142        let package_id = self
143            .inner
144            .default_package_id
145            .as_ref()
146            .ok_or_else(|| missing_port_error("default runtime package", &request.run_id))?;
147        let resolver = self.package_resolver_port(&request.run_id)?;
148        let mut package = resolver.resolve(package_id).map_err(|error| {
149            error.with_causal_ids(CausalIds {
150                run_id: Some(request.run_id.clone()),
151                ..CausalIds::default()
152            })
153        })?;
154        if let Some(output_contract) = &request.output_contract {
155            package = package
156                .with_output_contract(output_contract)
157                .map_err(|error| {
158                    error.with_causal_ids(CausalIds {
159                        run_id: Some(request.run_id.clone()),
160                        ..CausalIds::default()
161                    })
162                })?;
163        }
164
165        package.validate().map_err(|error| {
166            error.with_causal_ids(CausalIds {
167                run_id: Some(request.run_id.clone()),
168                ..CausalIds::default()
169            })
170        })?;
171        if package.agent.agent_id != request.agent_id {
172            return Err(AgentError::new(
173                AgentErrorKind::InvalidPackage,
174                RetryClassification::HostConfigurationNeeded,
175                "runtime package agent snapshot must match the run request agent_id",
176            )
177            .with_causal_ids(CausalIds {
178                run_id: Some(request.run_id.clone()),
179                ..CausalIds::default()
180            }));
181        }
182
183        let fingerprint = package.fingerprint().map_err(|error| {
184            error.with_causal_ids(CausalIds {
185                run_id: Some(request.run_id.clone()),
186                ..CausalIds::default()
187            })
188        })?;
189        Ok(EffectiveRuntimePackage {
190            package,
191            fingerprint,
192        })
193    }
194
195    /// Cancel run.
196    /// This marks the registered run as cancellation requested and forwards the request to run
197    /// control; actual adapter cleanup happens in the owning control path.
198    pub fn cancel_run(&self, run_id: &RunId) -> Result<(), AgentError> {
199        let mut runs = self
200            .inner
201            .runs
202            .lock()
203            .map_err(|_| AgentError::contract_violation("run registry lock poisoned"))?;
204        let entry = runs.get_mut(run_id).ok_or_else(|| {
205            AgentError::new(
206                AgentErrorKind::InvalidStateTransition,
207                RetryClassification::RepairNeeded,
208                "run is not registered with this runtime",
209            )
210            .with_causal_ids(CausalIds {
211                run_id: Some(run_id.clone()),
212                ..CausalIds::default()
213            })
214        })?;
215        entry.cancellation.cancel();
216        entry.status = RunRegistryStatus::CancellationRequested;
217        self.inner.run_control.request_cancel(run_id)?;
218        Ok(())
219    }
220
221    /// Returns run snapshot for callers that need to inspect the contract state.
222    /// This reads the in-memory run registry and returns a snapshot without mutating run state.
223    pub fn run_snapshot(&self, run_id: &RunId) -> Result<RunSnapshot, AgentError> {
224        let runs = self
225            .inner
226            .runs
227            .lock()
228            .map_err(|_| AgentError::contract_violation("run registry lock poisoned"))?;
229        runs.get(run_id)
230            .map(RegisteredRun::snapshot)
231            .ok_or_else(|| {
232                AgentError::new(
233                    AgentErrorKind::InvalidStateTransition,
234                    RetryClassification::RepairNeeded,
235                    "run is not registered with this runtime",
236                )
237                .with_causal_ids(CausalIds {
238                    run_id: Some(run_id.clone()),
239                    ..CausalIds::default()
240                })
241            })
242    }
243
244    /// Returns registered run count for callers that need to inspect the contract state.
245    /// This reads the in-memory run registry length without starting, cancelling, or replaying
246    /// runs.
247    pub fn registered_run_count(&self) -> Result<usize, AgentError> {
248        Ok(self
249            .inner
250            .runs
251            .lock()
252            .map_err(|_| AgentError::contract_violation("run registry lock poisoned"))?
253            .len())
254    }
255
256    /// Returns the configured event bus as a subscription source.
257    /// This retrieves the port so callers can subscribe; it does not publish events or drive a
258    /// run.
259    pub fn events(&self) -> Result<Arc<dyn AgentEventBus>, AgentError> {
260        self.event_bus_subscription_port()
261    }
262
263    /// Subscribe all.
264    /// This delegates to the configured event bus to create a read-only stream for all visible
265    /// events.
266    pub fn subscribe_all(
267        &self,
268        cursor: Option<EventCursor>,
269    ) -> Result<AgentEventStream, AgentError> {
270        self.event_bus_subscription_port()?.subscribe_all(cursor)
271    }
272
273    /// Subscribe run.
274    /// This delegates to the configured event bus to create a read-only stream scoped to one
275    /// run.
276    pub fn subscribe_run(
277        &self,
278        run_id: RunId,
279        cursor: Option<EventCursor>,
280    ) -> Result<AgentEventStream, AgentError> {
281        self.event_bus_subscription_port()?
282            .subscribe_run(run_id, cursor)
283    }
284
285    /// Subscribe agent.
286    /// This delegates to the event-bus subscription port to create a read-only stream for
287    /// matching agent events.
288    pub fn subscribe_agent(
289        &self,
290        agent_id: AgentId,
291        cursor: Option<EventCursor>,
292    ) -> Result<AgentEventStream, AgentError> {
293        self.event_bus_subscription_port()?
294            .subscribe_agent(agent_id, cursor)
295    }
296
297    /// Subscribe events.
298    /// This delegates to the event-bus subscription port to create a read-only filtered stream.
299    pub fn subscribe_events(
300        &self,
301        filter: CompiledEventFilter,
302        cursor: Option<EventCursor>,
303    ) -> Result<AgentEventStream, AgentError> {
304        self.event_bus_subscription_port()?
305            .subscribe_filtered(filter, cursor)
306    }
307
308    /// Returns provider registry for callers that need to inspect the contract state.
309    /// This returns the configured provider registry reference; callers must still use
310    /// policy-checked runtime paths to execute providers.
311    pub fn provider_registry(&self) -> &ProviderRegistry {
312        &self.inner.providers
313    }
314
315    /// Returns the output sinks currently held by this value.
316    /// This returns the configured sink registry; sending remains owned by output-delivery
317    /// paths.
318    pub fn output_sinks(&self) -> &OutputSinkRegistry {
319        &self.inner.output_sinks
320    }
321
322    /// Builds the tool execution coordinator for the effective package.
323    /// This validates configured tool routes against the runtime package and
324    /// returns a coordinator over the shared tool router, policy, journal,
325    /// and effect spine. It does not execute a tool.
326    pub(crate) fn tool_execution_coordinator(
327        &self,
328        package: &RuntimePackage,
329        run_id: &RunId,
330    ) -> Result<ToolExecutionCoordinator, AgentError> {
331        if self.inner.tool_routes.is_empty() {
332            return Err(missing_port_error("tool routes", run_id));
333        }
334        let snapshot =
335            ToolRegistrySnapshot::from_runtime_package(package, self.inner.tool_routes.clone())
336                .map_err(|error| {
337                    error.with_causal_ids(CausalIds {
338                        run_id: Some(run_id.clone()),
339                        ..CausalIds::default()
340                    })
341                })?;
342        let coordinator = ToolExecutionCoordinator::new(
343            ToolRouter::new(snapshot),
344            self.inner.tool_executors.clone(),
345        );
346        let coordinator = match self.inner.tool_policy.clone() {
347            Some(policy) => coordinator.with_policy(policy),
348            None => coordinator,
349        };
350        Ok(match self.inner.approval_dispatcher.clone() {
351            Some(dispatcher) => coordinator.with_approval_dispatcher(dispatcher),
352            None => coordinator,
353        })
354    }
355
356    /// Builds provider-visible tool declarations for the effective package.
357    /// This projects package/tool-route metadata only; execution remains owned
358    /// by `ToolExecutionCoordinator` after the provider requests a tool call.
359    pub(crate) fn provider_tool_specs(
360        &self,
361        package: &RuntimePackage,
362        run_id: &RunId,
363    ) -> Result<Vec<ProviderToolSpec>, AgentError> {
364        let projected_tools = package.provider_tool_specs()?;
365        if projected_tools.is_empty() {
366            return Ok(Vec::new());
367        }
368        if self.inner.tool_routes.is_empty() {
369            return Err(missing_port_error("tool routes", run_id));
370        }
371        let snapshot =
372            ToolRegistrySnapshot::from_runtime_package(package, self.inner.tool_routes.clone())
373                .map_err(|error| {
374                    error.with_causal_ids(CausalIds {
375                        run_id: Some(run_id.clone()),
376                        ..CausalIds::default()
377                    })
378                })?;
379        let projected_schema_refs = projected_tools
380            .into_iter()
381            .filter_map(|projection| match projection.projection {
382                ProjectionMode::ProviderToolSchema { schema_ref } => {
383                    Some((projection.capability_id, schema_ref))
384                }
385                _ => None,
386            })
387            .collect::<BTreeMap<_, _>>();
388        let mut specs = Vec::new();
389        for route in snapshot.routes {
390            let Some(schema_ref) = projected_schema_refs.get(&route.capability_id).cloned() else {
391                return Err(
392                    AgentError::missing_required_field("provider_tool_spec.schema_ref")
393                        .with_causal_ids(CausalIds {
394                            run_id: Some(run_id.clone()),
395                            ..CausalIds::default()
396                        }),
397                );
398            };
399            let mut spec = ProviderToolSpec::new(
400                route.canonical_tool_name.as_str(),
401                route.capability_id,
402                route.namespace,
403                schema_ref.clone(),
404                route.policy_refs,
405            );
406            if let Some(description) = route.description {
407                spec = spec.with_description(description);
408            }
409            if let Some(schema) = provider_schema_from_sidecars(package, &schema_ref) {
410                spec = spec.with_redacted_schema(schema);
411            }
412            specs.push(spec);
413        }
414        Ok(specs)
415    }
416
417    /// Returns hook executor registry for application coordinators.
418    /// This retrieves the configured port without invoking hook executors.
419    pub(crate) fn hook_registry_port(&self) -> Arc<dyn HookExecutorRegistry> {
420        self.inner.hook_registry.clone()
421    }
422
423    fn insert_run(&self, entry: RegisteredRun, run_id: &RunId) -> Result<(), AgentError> {
424        let mut runs = self
425            .inner
426            .runs
427            .lock()
428            .map_err(|_| AgentError::contract_violation("run registry lock poisoned"))?;
429        if runs.contains_key(run_id) {
430            return Err(AgentError::new(
431                AgentErrorKind::InvalidStateTransition,
432                RetryClassification::NotRetryable,
433                "run_id is already registered with this runtime",
434            )
435            .with_causal_ids(CausalIds {
436                run_id: Some(run_id.clone()),
437                ..CausalIds::default()
438            }));
439        }
440        runs.insert(run_id.clone(), entry);
441        Ok(())
442    }
443
444    /// Returns the provider adapter configured for the runtime package route.
445    /// This is a registry lookup only; the provider call happens later in the loop driver.
446    pub(crate) fn provider_for(
447        &self,
448        package: &RuntimePackage,
449        run_id: &RunId,
450    ) -> Result<Arc<dyn ProviderAdapter>, AgentError> {
451        self.inner
452            .providers
453            .get(&package.provider_route.route_id)
454            .ok_or_else(|| {
455                AgentError::new(
456                    AgentErrorKind::ProviderFailure,
457                    RetryClassification::HostConfigurationNeeded,
458                    format!(
459                        "missing provider adapter for package route {}",
460                        package.provider_route.route_id
461                    ),
462                )
463                .with_causal_ids(CausalIds {
464                    run_id: Some(run_id.clone()),
465                    ..CausalIds::default()
466                })
467            })
468    }
469
470    /// Returns the provider adapter configured for a route id.
471    /// This is a registry lookup only; it does not send a model request.
472    pub(crate) fn provider_for_route(
473        &self,
474        route_id: &str,
475        run_id: &RunId,
476    ) -> Result<Arc<dyn ProviderAdapter>, AgentError> {
477        self.inner.providers.get(route_id).ok_or_else(|| {
478            AgentError::new(
479                AgentErrorKind::ProviderFailure,
480                RetryClassification::HostConfigurationNeeded,
481                format!("missing provider adapter for package route {route_id}"),
482            )
483            .with_causal_ids(CausalIds {
484                run_id: Some(run_id.clone()),
485                ..CausalIds::default()
486            })
487        })
488    }
489
490    fn evaluate_run_start_policy(
491        &self,
492        policy: &dyn RuntimePolicyPort,
493        request: &RunRequest,
494        package: &RuntimePackage,
495    ) -> Result<(), AgentError> {
496        let outcome = policy.evaluate_run_start(request, package)?;
497        if outcome.is_allowed() {
498            return Ok(());
499        }
500
501        let mut error = AgentError::new(
502            AgentErrorKind::PolicyDenial,
503            RetryClassification::UserActionNeeded,
504            policy_denial_message(&outcome.decision),
505        )
506        .with_causal_ids(CausalIds {
507            run_id: Some(request.run_id.clone()),
508            ..CausalIds::default()
509        })
510        .with_source(request.source.clone());
511        for policy_ref in outcome.policy_refs {
512            error = error.with_policy_ref(policy_ref);
513        }
514        Err(error)
515    }
516
517    fn package_resolver_port(
518        &self,
519        run_id: &RunId,
520    ) -> Result<Arc<dyn RuntimePackageResolver>, AgentError> {
521        self.inner
522            .package_resolver
523            .clone()
524            .ok_or_else(|| missing_port_error("runtime package resolver", run_id))
525    }
526
527    /// Returns the journal port currently held by this value.
528    /// This returns the journal port selected for the run and does not append a record.
529    pub(crate) fn journal_port(&self, run_id: &RunId) -> Result<Arc<dyn RunJournal>, AgentError> {
530        self.inner
531            .journal
532            .clone()
533            .ok_or_else(|| missing_port_error("run journal", run_id))
534    }
535
536    /// Returns the event bus port selected for the run.
537    /// This retrieves the configured port without publishing an event.
538    pub(crate) fn event_bus_port(
539        &self,
540        run_id: &RunId,
541    ) -> Result<Arc<dyn AgentEventBus>, AgentError> {
542        self.inner
543            .events
544            .clone()
545            .ok_or_else(|| missing_port_error("agent event bus", run_id))
546    }
547
548    fn event_bus_subscription_port(&self) -> Result<Arc<dyn AgentEventBus>, AgentError> {
549        self.inner
550            .events
551            .clone()
552            .ok_or_else(|| missing_runtime_port_error("agent event bus"))
553    }
554
555    /// Returns the content port currently held by this value.
556    /// This returns the content resolver selected for the run and does not resolve raw content.
557    pub(crate) fn content_port(
558        &self,
559        run_id: &RunId,
560    ) -> Result<Arc<dyn ContentResolver + Send + Sync>, AgentError> {
561        self.inner
562            .content
563            .clone()
564            .ok_or_else(|| missing_port_error("content resolver", run_id))
565    }
566
567    fn policy_port(&self, run_id: &RunId) -> Result<Arc<dyn RuntimePolicyPort>, AgentError> {
568        self.inner
569            .policy
570            .clone()
571            .ok_or_else(|| missing_port_error("runtime policy port", run_id))
572    }
573
574    /// Seals run-control terminal state from a journal terminal record.
575    /// This delegates to the run-control store and may mutate handle state; it does not append a
576    /// new journal record or publish an event.
577    pub(crate) fn seal_terminal_result_from_journal(
578        &self,
579        record: &JournalRecord,
580        output: impl Into<String>,
581    ) -> Result<RunResult, AgentError> {
582        self.inner
583            .run_control
584            .seal_terminal_result_from_journal(record, output)
585    }
586
587    /// Allocates the next in-memory journal sequence number for this runtime.
588    /// This advances an atomic counter; the caller is responsible for appending the record.
589    pub(crate) fn next_journal_seq(&self) -> u64 {
590        let _guard = self
591            .inner
592            .journal_sequence_lock
593            .lock()
594            .unwrap_or_else(|poisoned| poisoned.into_inner());
595        self.inner.next_journal_seq.fetch_add(1, Ordering::SeqCst) + 1
596    }
597
598    /// Reserves a contiguous journal sequence block for records appended by a coordinator.
599    /// This does not append records or call host-controlled code.
600    pub(crate) fn reserve_journal_seq_block(&self, width: u64) -> u64 {
601        debug_assert!(width > 0);
602        let _guard = self
603            .inner
604            .journal_sequence_lock
605            .lock()
606            .unwrap_or_else(|poisoned| poisoned.into_inner());
607        self.inner
608            .next_journal_seq
609            .fetch_add(width, Ordering::SeqCst)
610            + 1
611    }
612
613    /// Returns the next journal sequence number without reserving it.
614    /// This is used by coordinators that may or may not append records.
615    pub(crate) fn next_journal_seq_hint(&self) -> u64 {
616        self.inner.next_journal_seq.load(Ordering::SeqCst) + 1
617    }
618}
619
620impl Default for AgentRuntime {
621    fn default() -> Self {
622        AgentRuntimeBuilder::default()
623            .build()
624            .expect("empty runtime builder is infallible")
625    }
626}
627
628#[derive(Default)]
629/// Holds agent runtime builder application-layer state or configuration.
630/// Use it with the documented coordinator methods; run, journal, event, provider, or port effects are called out on those methods rather than on construction.
631pub struct AgentRuntimeBuilder {
632    providers: ProviderRegistry,
633    package_resolver: Option<Arc<dyn RuntimePackageResolver>>,
634    local_packages: Vec<RuntimePackage>,
635    default_package_id: Option<RuntimePackageId>,
636    journal: Option<Arc<dyn RunJournal>>,
637    events: Option<Arc<dyn AgentEventBus>>,
638    content: Option<Arc<dyn ContentResolver + Send + Sync>>,
639    policy: Option<Arc<dyn RuntimePolicyPort>>,
640    output_sinks: OutputSinkRegistry,
641    hook_registry: Option<Arc<dyn HookExecutorRegistry>>,
642    tool_routes: Vec<ToolRoute>,
643    tool_executors: ToolExecutorRegistry,
644    tool_policy: Option<Arc<dyn ToolPolicyPort>>,
645    approval_dispatcher: Option<Arc<dyn ApprovalDispatcher>>,
646}
647
648impl AgentRuntimeBuilder {
649    /// Returns an updated value with providers configured.
650    /// This stores a provider registry in the builder and performs no provider calls.
651    pub fn providers(mut self, providers: ProviderRegistry) -> Self {
652        self.providers = providers;
653        self
654    }
655
656    /// Returns an updated value with provider configured.
657    /// This adds one provider adapter to the builder registry and performs no provider calls.
658    pub fn provider<P>(
659        mut self,
660        route_id: impl Into<String>,
661        provider: P,
662    ) -> Result<Self, AgentError>
663    where
664        P: ProviderAdapter + 'static,
665    {
666        self.providers.register(route_id, Arc::new(provider))?;
667        Ok(self)
668    }
669
670    /// Returns an updated value with package resolver configured.
671    /// This is builder configuration only; it stores the resolver for future run starts and
672    /// performs no I/O.
673    pub fn package_resolver<R>(mut self, resolver: R) -> Self
674    where
675        R: RuntimePackageResolver + 'static,
676    {
677        self.package_resolver = Some(Arc::new(resolver));
678        self
679    }
680
681    /// Returns an updated value with default package id configured.
682    /// This is data-only and does not perform I/O, call host ports, append journals, publish
683    /// events, or start processes.
684    pub fn default_package_id(mut self, package_id: RuntimePackageId) -> Self {
685        self.default_package_id = Some(package_id);
686        self
687    }
688
689    /// Returns an updated value with package configured.
690    /// This reads or configures runtime state without executing a provider or tool.
691    pub fn package(mut self, package: RuntimePackage) -> Self {
692        self.local_packages.push(package);
693        self
694    }
695
696    /// Returns an updated value with default package configured.
697    /// This is data-only and does not perform I/O, call host ports, append journals, publish
698    /// events, or start processes.
699    pub fn default_package(mut self, package: RuntimePackage) -> Self {
700        self.default_package_id = Some(package.package_id.clone());
701        self.local_packages.push(package);
702        self
703    }
704
705    /// Returns an updated value with journal configured.
706    /// This reads or configures runtime state without executing a provider or tool.
707    pub fn journal<J>(mut self, journal: J) -> Self
708    where
709        J: RunJournal + 'static,
710    {
711        self.journal = Some(Arc::new(journal));
712        self
713    }
714
715    /// Returns an updated value with event bus configured.
716    /// This stores the event-bus port in the builder and does not publish events.
717    pub fn event_bus<E>(mut self, event_bus: E) -> Self
718    where
719        E: AgentEventBus + 'static,
720    {
721        self.events = Some(Arc::new(event_bus));
722        self
723    }
724
725    /// Returns an updated value with content configured.
726    /// This reads or configures runtime state without executing a provider or tool.
727    pub fn content<C>(mut self, content: C) -> Self
728    where
729        C: ContentResolver + Send + Sync + 'static,
730    {
731        self.content = Some(Arc::new(content));
732        self
733    }
734
735    /// Returns policy for the current value.
736    /// This is a read-only or data-construction helper unless the method body explicitly calls
737    /// a port or store.
738    pub fn policy<P>(mut self, policy: P) -> Self
739    where
740        P: RuntimePolicyPort + 'static,
741    {
742        self.policy = Some(Arc::new(policy));
743        self
744    }
745
746    /// Returns output sink for the current value.
747    /// This is a read-only or data-construction helper unless the method body explicitly calls
748    /// a port or store.
749    pub fn output_sink<S>(mut self, sink: S) -> Result<Self, AgentError>
750    where
751        S: OutputSinkPort + 'static,
752    {
753        self.output_sinks.register(Arc::new(sink))?;
754        Ok(self)
755    }
756
757    /// Adds one tool route to the runtime's app-facing tool execution
758    /// configuration. The route is validated against the effective runtime
759    /// package only when a model-requested tool call is lowered.
760    pub fn tool_route(mut self, route: ToolRoute) -> Self {
761        self.tool_routes.push(route);
762        self
763    }
764
765    /// Replaces the runtime's configured tool routes. This is builder
766    /// configuration only and does not execute or resolve tools.
767    pub fn tool_routes(mut self, routes: impl IntoIterator<Item = ToolRoute>) -> Self {
768        self.tool_routes = routes.into_iter().collect();
769        self
770    }
771
772    /// Replaces the runtime's configured tool executor registry. This is
773    /// builder configuration only and does not execute tools.
774    pub fn tool_executors(mut self, executors: ToolExecutorRegistry) -> Self {
775        self.tool_executors = executors;
776        self
777    }
778
779    /// Adds one tool executor to the runtime's configured executor
780    /// registry. This stores the executor behind the public port and does
781    /// not execute it.
782    pub fn tool_executor(mut self, executor: Arc<dyn ToolExecutor>) -> Result<Self, AgentError> {
783        self.tool_executors.register(executor)?;
784        Ok(self)
785    }
786
787    /// Configures the runtime's tool policy port. This is builder
788    /// configuration only and does not evaluate policy.
789    pub fn tool_policy<P>(mut self, policy: P) -> Self
790    where
791        P: ToolPolicyPort + 'static,
792    {
793        self.tool_policy = Some(Arc::new(policy));
794        self
795    }
796
797    /// Configures the host-owned approval dispatcher used by approval-gated
798    /// tool execution.
799    pub fn approval_dispatcher<D>(mut self, dispatcher: D) -> Self
800    where
801        D: ApprovalDispatcher + 'static,
802    {
803        self.approval_dispatcher = Some(Arc::new(dispatcher));
804        self
805    }
806
807    /// Configures a shared host-owned approval dispatcher.
808    pub fn shared_approval_dispatcher(mut self, dispatcher: Arc<dyn ApprovalDispatcher>) -> Self {
809        self.approval_dispatcher = Some(dispatcher);
810        self
811    }
812
813    /// Returns hook executor registry for the current value.
814    /// This stores the registry for future run starts and hook invocation; it does not invoke
815    /// hook executors.
816    pub fn hook_executor_registry<R>(mut self, registry: R) -> Self
817    where
818        R: HookExecutorRegistry + 'static,
819    {
820        self.hook_registry = Some(Arc::new(registry));
821        self
822    }
823
824    /// Finishes builder validation and returns the configured value.
825    /// This is data-only unless the surrounding builder explicitly
826    /// documents adapter or store access.
827    pub fn build(self) -> Result<AgentRuntime, AgentError> {
828        let package_resolver = match (self.package_resolver, self.local_packages.is_empty()) {
829            (Some(resolver), _) => Some(resolver),
830            (None, true) => None,
831            (None, false) => Some(Arc::new(InMemoryRuntimePackageResolver::from_packages(
832                self.local_packages,
833            )?) as Arc<dyn RuntimePackageResolver>),
834        };
835
836        Ok(AgentRuntime {
837            inner: Arc::new(RuntimeInner {
838                providers: self.providers,
839                package_resolver,
840                default_package_id: self.default_package_id,
841                journal: self.journal,
842                events: self.events,
843                content: self.content,
844                policy: self.policy,
845                output_sinks: self.output_sinks,
846                hook_registry: self
847                    .hook_registry
848                    .unwrap_or_else(|| Arc::new(InMemoryHookExecutorRegistry::default())),
849                tool_routes: self.tool_routes,
850                tool_executors: self.tool_executors,
851                tool_policy: self.tool_policy,
852                approval_dispatcher: self.approval_dispatcher,
853                run_control: InMemoryRunControlStore::default(),
854                next_journal_seq: AtomicU64::new(0),
855                journal_sequence_lock: Mutex::new(()),
856                runs: Mutex::new(BTreeMap::new()),
857            }),
858        })
859    }
860}
861
862struct RuntimeInner {
863    providers: ProviderRegistry,
864    package_resolver: Option<Arc<dyn RuntimePackageResolver>>,
865    default_package_id: Option<RuntimePackageId>,
866    journal: Option<Arc<dyn RunJournal>>,
867    events: Option<Arc<dyn AgentEventBus>>,
868    content: Option<Arc<dyn ContentResolver + Send + Sync>>,
869    policy: Option<Arc<dyn RuntimePolicyPort>>,
870    output_sinks: OutputSinkRegistry,
871    hook_registry: Arc<dyn HookExecutorRegistry>,
872    tool_routes: Vec<ToolRoute>,
873    tool_executors: ToolExecutorRegistry,
874    tool_policy: Option<Arc<dyn ToolPolicyPort>>,
875    approval_dispatcher: Option<Arc<dyn ApprovalDispatcher>>,
876    run_control: InMemoryRunControlStore,
877    next_journal_seq: AtomicU64,
878    journal_sequence_lock: Mutex<()>,
879    runs: Mutex<BTreeMap<RunId, RegisteredRun>>,
880}
881
882fn provider_schema_from_sidecars(
883    package: &RuntimePackage,
884    schema_ref: &crate::capability::PackageSidecarRef,
885) -> Option<serde_json::Value> {
886    package
887        .sidecars
888        .iter()
889        .filter_map(|sidecar| sidecar.redacted_payload.as_ref())
890        .find_map(|payload| {
891            payload.get("tools")?.as_array()?.iter().find_map(|tool| {
892                let tool_schema_ref = tool.get("schema_ref")?;
893                if tool_schema_ref.get("sidecar_id")?.as_str()? == schema_ref.sidecar_id {
894                    tool.get("redacted_schema").cloned()
895                } else {
896                    None
897                }
898            })
899        })
900}
901
902#[derive(Clone, Debug, Eq, PartialEq)]
903/// Holds effective runtime package application-layer state or configuration.
904/// Use it with the documented coordinator methods; run, journal, event, provider, or port effects are called out on those methods rather than on construction.
905pub struct EffectiveRuntimePackage {
906    /// Package used by this record or request.
907    pub package: RuntimePackage,
908    /// Deterministic fingerprint for package, event, telemetry, or validation
909    /// evidence.
910    pub fingerprint: RuntimePackageFingerprint,
911}
912
913#[derive(Clone, Debug, Eq, PartialEq)]
914/// Holds run snapshot application-layer state or configuration.
915/// Use it with the documented coordinator methods; run, journal, event, provider, or port effects are called out on those methods rather than on construction.
916pub struct RunSnapshot {
917    /// Run identifier used for lineage, filtering, replay, and dedupe.
918    pub run_id: RunId,
919    /// Optional host-provided session identifier for grouping related turns.
920    pub session_id: Option<SessionId>,
921    /// Optional host-provided turn identifier for this run.
922    pub turn_id: Option<TurnId>,
923    /// Agent identifier used for lineage, filtering, and ownership checks.
924    pub agent_id: AgentId,
925    /// Source label or ref for this item; it is metadata and does not fetch
926    /// content by itself.
927    pub source: SourceRef,
928    /// Finite status for this record or lifecycle stage.
929    pub status: RunRegistryStatus,
930    /// Stable runtime package id used for typed lineage, lookup, or dedupe.
931    pub runtime_package_id: RuntimePackageId,
932    /// Fingerprint of the runtime package snapshot in force when this value was produced.
933    /// Use it for replay, dedupe, and package-lineage checks; the field is evidence and does
934    /// not execute package behavior.
935    pub runtime_package_fingerprint: RuntimePackageFingerprint,
936    /// Stable provider route id used for typed lineage, lookup, or dedupe.
937    pub provider_route_id: String,
938    /// Stable provider model id used for typed lineage, lookup, or dedupe.
939    pub provider_model_id: String,
940    /// Whether cancellation requested is enabled.
941    /// Policy, validation, or routing code uses this flag to choose the explicit behavior.
942    pub cancellation_requested: bool,
943}
944
945#[derive(Clone, Debug, Eq, PartialEq)]
946/// Enumerates the finite run registry status cases.
947/// Serialized names are part of the SDK contract; update fixtures when variants change.
948pub enum RunRegistryStatus {
949    /// Use this variant when the contract needs to represent registered; selecting it has no side effect by itself.
950    Registered,
951    /// Use this variant when the contract needs to represent cancellation requested; selecting it has no side effect by itself.
952    CancellationRequested,
953}
954
955#[derive(Clone, Default)]
956/// Holds cancellation handle application-layer state or configuration.
957/// Use it with the documented coordinator methods; run, journal, event, provider, or port effects are called out on those methods rather than on construction.
958pub struct CancellationHandle {
959    cancelled: Arc<AtomicBool>,
960}
961
962impl CancellationHandle {
963    /// Creates a new application::runtime value with explicit
964    /// caller-provided inputs. This constructor is data-only and
965    /// performs no I/O or external side effects.
966    pub fn new() -> Self {
967        Self::default()
968    }
969
970    /// Cancel.
971    /// This flips the cancellation token in memory; callers still need the owning run path to
972    /// observe and apply cancellation.
973    pub fn cancel(&self) {
974        self.cancelled.store(true, Ordering::SeqCst);
975    }
976
977    /// Reports whether this value is cancelled. The check is pure and
978    /// does not mutate SDK or host state.
979    pub fn is_cancelled(&self) -> bool {
980        self.cancelled.load(Ordering::SeqCst)
981    }
982}
983
984impl std::fmt::Debug for CancellationHandle {
985    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
986        formatter
987            .debug_struct("CancellationHandle")
988            .field("cancelled", &self.is_cancelled())
989            .finish()
990    }
991}
992
993#[derive(Clone, Debug)]
994struct RegisteredRun {
995    run_id: RunId,
996    session_id: Option<SessionId>,
997    turn_id: Option<TurnId>,
998    agent_id: AgentId,
999    source: SourceRef,
1000    status: RunRegistryStatus,
1001    runtime_package_id: RuntimePackageId,
1002    runtime_package_fingerprint: RuntimePackageFingerprint,
1003    provider_route_id: String,
1004    provider_model_id: String,
1005    cancellation: CancellationHandle,
1006}
1007
1008impl RegisteredRun {
1009    fn snapshot(&self) -> RunSnapshot {
1010        RunSnapshot {
1011            run_id: self.run_id.clone(),
1012            session_id: self.session_id.clone(),
1013            turn_id: self.turn_id.clone(),
1014            agent_id: self.agent_id.clone(),
1015            source: self.source.clone(),
1016            status: self.status.clone(),
1017            runtime_package_id: self.runtime_package_id.clone(),
1018            runtime_package_fingerprint: self.runtime_package_fingerprint.clone(),
1019            provider_route_id: self.provider_route_id.clone(),
1020            provider_model_id: self.provider_model_id.clone(),
1021            cancellation_requested: self.cancellation.is_cancelled(),
1022        }
1023    }
1024}
1025
1026fn missing_port_error(port_name: &str, run_id: &RunId) -> AgentError {
1027    AgentError::new(
1028        AgentErrorKind::HostConfigurationNeeded,
1029        RetryClassification::HostConfigurationNeeded,
1030        format!("missing required runtime port: {port_name}"),
1031    )
1032    .with_causal_ids(CausalIds {
1033        run_id: Some(run_id.clone()),
1034        ..CausalIds::default()
1035    })
1036}
1037
1038fn missing_runtime_port_error(port_name: &str) -> AgentError {
1039    AgentError::new(
1040        AgentErrorKind::HostConfigurationNeeded,
1041        RetryClassification::HostConfigurationNeeded,
1042        format!("missing required runtime port: {port_name}"),
1043    )
1044}
1045
1046fn policy_denial_message(decision: &PolicyDecision) -> String {
1047    match decision {
1048        PolicyDecision::Deny { reason }
1049        | PolicyDecision::Interrupt { reason }
1050        | PolicyDecision::Defer {
1051            resume_policy: crate::policy::ResumePolicy { reason, .. },
1052        } => reason.code.clone(),
1053        PolicyDecision::Ask { .. } => "policy requested host approval before run start".to_string(),
1054        PolicyDecision::Modify { .. } => {
1055            "policy modification is not valid for runtime start".to_string()
1056        }
1057        PolicyDecision::Allow { .. } => "policy allowed run start".to_string(),
1058    }
1059}
1060
1061#[derive(Clone)]
1062struct RuntimeRunControlStore {
1063    runtime: AgentRuntime,
1064}
1065
1066impl RunControlStore for RuntimeRunControlStore {
1067    fn status(&self, run_id: &RunId) -> Result<RunStatus, AgentError> {
1068        self.runtime.inner.run_control.status(run_id)
1069    }
1070
1071    fn terminal_result(&self, run_id: &RunId) -> Result<Option<RunResult>, AgentError> {
1072        self.runtime.inner.run_control.terminal_result(run_id)
1073    }
1074
1075    fn request_cancel(&self, run_id: &RunId) -> Result<(), AgentError> {
1076        self.runtime.cancel_run(run_id)
1077    }
1078}
1079
1080#[derive(Clone)]
1081struct RuntimeRunSubscriptionSource {
1082    runtime: AgentRuntime,
1083}
1084
1085impl RunSubscriptionSource for RuntimeRunSubscriptionSource {
1086    fn subscribe_all(&self, cursor: Option<EventCursor>) -> Result<AgentEventStream, AgentError> {
1087        self.runtime.subscribe_all(cursor)
1088    }
1089
1090    fn subscribe_run(
1091        &self,
1092        run_id: RunId,
1093        cursor: Option<EventCursor>,
1094    ) -> Result<AgentEventStream, AgentError> {
1095        self.runtime.subscribe_run(run_id, cursor)
1096    }
1097
1098    fn subscribe_agent(
1099        &self,
1100        agent_id: AgentId,
1101        cursor: Option<EventCursor>,
1102    ) -> Result<AgentEventStream, AgentError> {
1103        self.runtime.subscribe_agent(agent_id, cursor)
1104    }
1105
1106    fn subscribe_events(
1107        &self,
1108        filter: CompiledEventFilter,
1109        cursor: Option<EventCursor>,
1110    ) -> Result<AgentEventStream, AgentError> {
1111        self.runtime.subscribe_events(filter, cursor)
1112    }
1113
1114    fn replay_run_from_cursor(
1115        &self,
1116        _run_id: RunId,
1117        _cursor: JournalCursor,
1118    ) -> Result<AgentEventStream, AgentError> {
1119        Err(AgentError::host_configuration_needed(
1120            "run journal replay subscription requires an archive-backed subscription source",
1121        ))
1122    }
1123
1124    fn latest_terminal_event(
1125        &self,
1126        run_id: &RunId,
1127    ) -> Result<Option<crate::event::EventFrame>, AgentError> {
1128        Ok(self
1129            .runtime
1130            .subscribe_run(run_id.clone(), None)?
1131            .filter(|frame| {
1132                matches!(
1133                    frame.event.envelope.event_kind,
1134                    EventKind::RunCompleted | EventKind::RunFailed | EventKind::RunCancelled
1135                )
1136            })
1137            .last())
1138    }
1139}