Skip to main content

codex_runtime/domain/artifact/
mod.rs

1use std::sync::Arc;
2
3use crate::plugin::PluginContractVersion;
4use crate::runtime::core::Runtime;
5
6mod adapter;
7mod execution;
8mod lock_policy;
9mod models;
10mod store;
11
12#[cfg(test)]
13pub(crate) use adapter::{ArtifactAdapterFuture, ArtifactTurnOutput};
14pub use adapter::{ArtifactPluginAdapter, RuntimeArtifactAdapter};
15
16#[cfg(test)]
17pub(crate) use models::DocEdit;
18pub use models::{
19    apply_doc_patch, compute_revision, validate_doc_patch, ArtifactMeta, ArtifactSession,
20    ArtifactStore, ArtifactTaskKind, ArtifactTaskResult, ArtifactTaskSpec, DocPatch, DomainError,
21    FsArtifactStore, PatchConflict, SaveMeta, StoreErr, ValidatedPatch,
22};
23
24#[cfg(test)]
25pub(crate) use execution::collect_turn_output_from_live_with_limits;
26#[cfg(test)]
27pub(crate) use execution::debug_with_forced_turn_start_params_serialization_failure;
28#[cfg(test)]
29pub(crate) use execution::{build_turn_prompt, build_turn_start_params};
30#[cfg(test)]
31pub(crate) use store::artifact_key;
32
33#[derive(Clone)]
34pub struct ArtifactSessionManager {
35    adapter: Arc<dyn ArtifactPluginAdapter>,
36    store: Arc<dyn ArtifactStore>,
37    contract_mismatch: Option<ContractMismatch>,
38}
39
40#[derive(Clone, Copy, Debug, PartialEq, Eq)]
41struct ContractMismatch {
42    expected: PluginContractVersion,
43    actual: PluginContractVersion,
44}
45
46impl ArtifactSessionManager {
47    pub fn new(runtime: Runtime, store: Arc<dyn ArtifactStore>) -> Self {
48        let adapter: Arc<dyn ArtifactPluginAdapter> =
49            Arc::new(RuntimeArtifactAdapter::new(runtime));
50        Self::new_with_adapter(adapter, store)
51    }
52
53    pub fn new_with_adapter(
54        adapter: Arc<dyn ArtifactPluginAdapter>,
55        store: Arc<dyn ArtifactStore>,
56    ) -> Self {
57        let contract_mismatch = detect_contract_mismatch(adapter.as_ref());
58        Self {
59            adapter,
60            store,
61            contract_mismatch,
62        }
63    }
64
65    // ArtifactStore implementations may perform synchronous filesystem I/O.
66    // spawn_blocking moves that work off the async executor thread so it
67    // cannot block other tasks while the store operation runs.
68    async fn store_io<T: Send + 'static>(
69        &self,
70        op: impl FnOnce(&dyn ArtifactStore) -> Result<T, StoreErr> + Send + 'static,
71    ) -> Result<T, DomainError> {
72        let store = Arc::clone(&self.store);
73        let joined = tokio::task::spawn_blocking(move || op(store.as_ref()))
74            .await
75            .map_err(|err| {
76                DomainError::Store(StoreErr::Io(format!("store worker join failed: {err}")))
77            })?;
78        joined.map_err(DomainError::from)
79    }
80
81    pub async fn open(&self, artifact_id: &str) -> Result<ArtifactSession, DomainError> {
82        self.ensure_contract_compatible()?;
83
84        let artifact_id_owned = artifact_id.to_owned();
85        let mut meta = self
86            .store_io({
87                let artifact_id = artifact_id_owned.clone();
88                move |store| execution::load_or_default_meta(store, &artifact_id)
89            })
90            .await?;
91
92        let thread_id = match meta.runtime_thread_id.as_deref() {
93            Some(existing) => self.adapter.resume_thread(existing).await?,
94            None => self.adapter.start_thread().await?,
95        };
96        meta.runtime_thread_id = Some(thread_id.clone());
97        self.store_io({
98            let artifact_id = artifact_id_owned.clone();
99            let meta_to_store = meta.clone();
100            move |store| store.set_meta(&artifact_id, meta_to_store)
101        })
102        .await?;
103
104        Ok(ArtifactSession {
105            artifact_id: artifact_id_owned,
106            thread_id,
107            format: meta.format,
108            revision: meta.revision,
109        })
110    }
111
112    /// Domain task runner with explicit side-effect boundary:
113    /// runtime RPC call + store read/write.
114    /// Allocation: prompt string + output JSON parse structures.
115    /// Complexity: O(L + e) for DocEdit (L=text size, e=edit count).
116    pub async fn run_task(
117        &self,
118        spec: ArtifactTaskSpec,
119    ) -> Result<ArtifactTaskResult, DomainError> {
120        execution::run_task(self, spec).await
121    }
122
123    fn ensure_contract_compatible(&self) -> Result<(), DomainError> {
124        if let Some(mismatch) = self.contract_mismatch {
125            return Err(DomainError::IncompatibleContract {
126                expected_major: mismatch.expected.major,
127                expected_minor: mismatch.expected.minor,
128                actual_major: mismatch.actual.major,
129                actual_minor: mismatch.actual.minor,
130            });
131        }
132        Ok(())
133    }
134}
135
136fn detect_contract_mismatch(adapter: &dyn ArtifactPluginAdapter) -> Option<ContractMismatch> {
137    let expected = PluginContractVersion::CURRENT;
138    let actual = adapter.plugin_contract_version();
139    if expected.is_compatible_with(actual) {
140        None
141    } else {
142        Some(ContractMismatch { expected, actual })
143    }
144}
145#[cfg(test)]
146mod tests;