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    async fn store_io<T: Send + 'static>(
66        &self,
67        op: impl FnOnce(&dyn ArtifactStore) -> Result<T, StoreErr> + Send + 'static,
68    ) -> Result<T, DomainError> {
69        let store = Arc::clone(&self.store);
70        let joined = tokio::task::spawn_blocking(move || op(store.as_ref()))
71            .await
72            .map_err(|err| {
73                DomainError::Store(StoreErr::Io(format!("store worker join failed: {err}")))
74            })?;
75        joined.map_err(DomainError::from)
76    }
77
78    pub async fn open(&self, artifact_id: &str) -> Result<ArtifactSession, DomainError> {
79        self.ensure_contract_compatible()?;
80
81        let artifact_id_owned = artifact_id.to_owned();
82        let mut meta = self
83            .store_io({
84                let artifact_id = artifact_id_owned.clone();
85                move |store| execution::load_or_default_meta(store, &artifact_id)
86            })
87            .await?;
88
89        let thread_id = match meta.runtime_thread_id.as_deref() {
90            Some(existing) => self.adapter.resume_thread(existing).await?,
91            None => self.adapter.start_thread().await?,
92        };
93        meta.runtime_thread_id = Some(thread_id.clone());
94        self.store_io({
95            let artifact_id = artifact_id_owned.clone();
96            let meta_to_store = meta.clone();
97            move |store| store.set_meta(&artifact_id, meta_to_store)
98        })
99        .await?;
100
101        Ok(ArtifactSession {
102            artifact_id: artifact_id_owned,
103            thread_id,
104            format: meta.format,
105            revision: meta.revision,
106        })
107    }
108
109    /// Domain task runner with explicit side-effect boundary:
110    /// runtime RPC call + store read/write.
111    /// Allocation: prompt string + output JSON parse structures.
112    /// Complexity: O(L + e) for DocEdit (L=text size, e=edit count).
113    pub async fn run_task(
114        &self,
115        spec: ArtifactTaskSpec,
116    ) -> Result<ArtifactTaskResult, DomainError> {
117        execution::run_task(self, spec).await
118    }
119
120    fn ensure_contract_compatible(&self) -> Result<(), DomainError> {
121        if let Some(mismatch) = self.contract_mismatch {
122            return Err(DomainError::IncompatibleContract {
123                expected_major: mismatch.expected.major,
124                expected_minor: mismatch.expected.minor,
125                actual_major: mismatch.actual.major,
126                actual_minor: mismatch.actual.minor,
127            });
128        }
129        Ok(())
130    }
131}
132
133fn detect_contract_mismatch(adapter: &dyn ArtifactPluginAdapter) -> Option<ContractMismatch> {
134    let expected = PluginContractVersion::CURRENT;
135    let actual = adapter.plugin_contract_version();
136    if expected.is_compatible_with(actual) {
137        None
138    } else {
139        Some(ContractMismatch { expected, actual })
140    }
141}
142#[cfg(test)]
143mod tests;