From b233d47727cd62a32b4db775b867f3647f0c46da Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 14 Aug 2026 10:34:14 +0200 Subject: [PATCH] Add decision-auditable reasoning context foundation --- Cargo.lock | 2 + .../src/bin/epiphany-runtime-spine.rs | 1 + epiphany-core/src/coordinator_launch.rs | 7 + .../src/coordinator_launch_context.rs | 2 + epiphany-core/src/lib.rs | 2 + epiphany-core/src/reasoning_context.rs | 1030 +++++++++++++++++ epiphany-core/src/resident_self.rs | 2 + epiphany-core/src/runtime_spine.rs | 79 +- epiphany-model-adapter/src/lib.rs | 3 + epiphany-openai-adapter/Cargo.toml | 1 + epiphany-openai-adapter/src/lib.rs | 56 + .../src/bin/epiphany-openai-runtime.rs | 59 +- epiphany-openai-runtime/src/lib.rs | 190 ++- epiphany-tool-adapter/Cargo.toml | 1 + epiphany-tool-adapter/src/lib.rs | 21 + notes/epiphany-current-algorithmic-map.md | 20 + notes/fresh-workspace-handoff.md | 38 +- state/map.yaml | 4 +- 18 files changed, 1377 insertions(+), 141 deletions(-) create mode 100644 epiphany-core/src/reasoning_context.rs diff --git a/Cargo.lock b/Cargo.lock index 771a720d..61a9ca7f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2149,6 +2149,7 @@ name = "epiphany-openai-adapter" version = "0.1.0" dependencies = [ "cultcache-rs", + "epiphany-model-adapter", "serde", ] @@ -2280,6 +2281,7 @@ version = "0.1.0" dependencies = [ "cultcache-rs", "serde", + "serde_json", ] [[package]] diff --git a/epiphany-core/src/bin/epiphany-runtime-spine.rs b/epiphany-core/src/bin/epiphany-runtime-spine.rs index f14adea5..a8799c74 100644 --- a/epiphany-core/src/bin/epiphany-runtime-spine.rs +++ b/epiphany-core/src/bin/epiphany-runtime-spine.rs @@ -290,6 +290,7 @@ fn main() -> Result<()> { next_safe_move, evidence_refs, artifact_refs, + decision_context_id: None, }, )?; println!("runtime job completed"); diff --git a/epiphany-core/src/coordinator_launch.rs b/epiphany-core/src/coordinator_launch.rs index b7bfbccc..e334c5e2 100644 --- a/epiphany-core/src/coordinator_launch.rs +++ b/epiphany-core/src/coordinator_launch.rs @@ -1531,6 +1531,7 @@ pub(crate) mod tests { imagination_consideration_candidate_msgpack: None, admitted_model_direction_consideration_request_id: None, admitted_model_direction_consideration_result_msgpack: None, + decision_context_id: "decision-context-fixture".into(), }) } @@ -1693,6 +1694,7 @@ pub(crate) mod tests { imagination_consideration_candidate_msgpack: Some(rmp_serde::to_vec_named(&candidate)?), admitted_model_direction_consideration_request_id: None, admitted_model_direction_consideration_result_msgpack: None, + decision_context_id: "decision-context-fixture".into(), }) } @@ -1848,6 +1850,7 @@ pub(crate) mod tests { imagination_consideration_candidate_msgpack: None, admitted_model_direction_consideration_request_id: None, admitted_model_direction_consideration_result_msgpack: None, + decision_context_id: "decision-context-fixture".into(), }; put_runtime_role_worker_result(store, &result) .map_err(|error| anyhow!("persist frontier Mind result: {error}"))?; @@ -3216,6 +3219,7 @@ pub(crate) mod tests { imagination_consideration_candidate_msgpack: None, admitted_model_direction_consideration_request_id: None, admitted_model_direction_consideration_result_msgpack: None, + decision_context_id: "decision-context-fixture".into(), }; crate::put_runtime_role_worker_result(&store, &verification_result)?; let soul_verdict = crate::SoulVerdictReceipt { @@ -3356,6 +3360,7 @@ pub(crate) mod tests { imagination_consideration_candidate_msgpack: None, admitted_model_direction_consideration_request_id: None, admitted_model_direction_consideration_result_msgpack: None, + decision_context_id: "decision-context-fixture".into(), }; crate::put_runtime_role_worker_result(&store, &modeling_result)?; let incorporation_review = crate::RepoModelAdmissionReview { @@ -3508,6 +3513,7 @@ pub(crate) mod tests { next_safe_move: "Review before retry.".into(), evidence_refs: Vec::new(), artifact_refs: Vec::new(), + decision_context_id: None, }, )?; @@ -3684,6 +3690,7 @@ pub(crate) mod tests { next_safe_move: "Review before retry.".into(), evidence_refs: Vec::new(), artifact_refs: Vec::new(), + decision_context_id: None, }, )?; assert_eq!( diff --git a/epiphany-core/src/coordinator_launch_context.rs b/epiphany-core/src/coordinator_launch_context.rs index 01f58552..79240127 100644 --- a/epiphany-core/src/coordinator_launch_context.rs +++ b/epiphany-core/src/coordinator_launch_context.rs @@ -1468,6 +1468,7 @@ mod tests { imagination_consideration_candidate_msgpack: None, admitted_model_direction_consideration_request_id: None, admitted_model_direction_consideration_result_msgpack: None, + decision_context_id: "decision-context-fixture".into(), }; crate::put_runtime_role_worker_result(store, &result)?; crate::commit_repo_model_admission( @@ -2174,6 +2175,7 @@ mod tests { imagination_consideration_candidate_msgpack: None, admitted_model_direction_consideration_request_id: None, admitted_model_direction_consideration_result_msgpack: None, + decision_context_id: "decision-context-fixture".into(), }; crate::put_runtime_role_worker_result(&runtime_store, &verification_result)?; crate::put_soul_verdict_receipt( diff --git a/epiphany-core/src/lib.rs b/epiphany-core/src/lib.rs index cba5f556..f8d6b499 100644 --- a/epiphany-core/src/lib.rs +++ b/epiphany-core/src/lib.rs @@ -43,6 +43,7 @@ mod repository_readiness; mod resident_readiness; mod resident_self; mod runtime_spine; +mod reasoning_context; mod runtime_store_backend; mod runtime_store_migration; mod runtime_worker_attempt; @@ -988,6 +989,7 @@ pub use repository_readiness::*; pub use resident_readiness::*; pub use resident_self::*; pub use runtime_spine::ARCHIVED_RUNTIME_SESSION_SCHEMA_VERSION; +pub use reasoning_context::*; pub use runtime_spine::ARCHIVED_RUNTIME_SESSION_TYPE; pub use runtime_spine::COORDINATOR_RUN_RECEIPT_SCHEMA_VERSION; pub use runtime_spine::COORDINATOR_RUN_RECEIPT_TYPE; diff --git a/epiphany-core/src/reasoning_context.rs b/epiphany-core/src/reasoning_context.rs new file mode 100644 index 00000000..502317e1 --- /dev/null +++ b/epiphany-core/src/reasoning_context.rs @@ -0,0 +1,1030 @@ +use crate::runtime_store_backend::runtime_spine_backing_store; +use crate::{ + EpiphanyRoleWorkerLaunchDocument, EpiphanyRuntimeWorkerLaunchRequest, + EpiphanyWorkerLaunchDocument, PersonaInterpreterInput, PersonaProjectorInput, PersonaTurnInput, + runtime_spine_cache, +}; +use anyhow::{Result, anyhow}; +use cultcache_rs::{CacheBackingStore, CultCacheEnvelope, DatabaseEntry}; +use epiphany_model_adapter::EpiphanyModelRequest; +use epiphany_openai_adapter::EpiphanyOpenAiModelRequest; +use epiphany_tool_adapter::{ + EpiphanyToolInvocationIntent, EpiphanyToolInvocationReceipt, receipt_output_for_model, + tool_invocation_intent_key, tool_invocation_receipt_key, +}; +use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; +use std::collections::BTreeSet; +use std::path::Path; + +pub const REASONING_BASIS_SCHEMA_VERSION: &str = "epiphany.reasoning_basis.v1"; +pub const DECISION_CONTEXT_SCHEMA_VERSION: &str = "epiphany.decision_context.v1"; +pub const MIND_COMMIT_RECEIPT_SCHEMA_VERSION: &str = "epiphany.mind_commit_receipt.v1"; +pub const WORKER_REASONING_PROJECTION_POLICY: &str = + "epiphany.reasoning_projection.worker_launch.v1"; + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct EpiphanyMindDocumentVersion { + pub store_id: String, + pub document_type: String, + pub document_key: String, + pub schema_id: Option, + pub payload_msgpack: Vec, + pub payload_sha256: String, +} + +impl EpiphanyMindDocumentVersion { + pub fn from_envelope(store_id: &str, envelope: &CultCacheEnvelope) -> Result { + require_non_empty(store_id, "Mind document store id")?; + require_non_empty(&envelope.r#type, "Mind document type")?; + require_non_empty(&envelope.key, "Mind document key")?; + let payload_sha256 = sha256(&envelope.payload); + Ok(Self { + store_id: store_id.to_string(), + document_type: envelope.r#type.clone(), + document_key: envelope.key.clone(), + schema_id: envelope.schema_id.clone(), + payload_msgpack: envelope.payload.clone(), + payload_sha256, + }) + } + + pub fn validate(&self) -> Result<()> { + require_non_empty(&self.store_id, "Mind document store id")?; + require_non_empty(&self.document_type, "Mind document type")?; + require_non_empty(&self.document_key, "Mind document key")?; + if self.payload_sha256 != sha256(&self.payload_msgpack) { + return Err(anyhow!( + "Mind document {:?}/{:?} payload digest mismatch", + self.document_type, + self.document_key + )); + } + Ok(()) + } + + pub fn identity(&self) -> (&str, &str) { + (&self.document_type, &self.document_key) + } +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(tag = "kind", rename_all = "snake_case")] +pub enum EpiphanyReasoningProjection { + RoleLaunch(EpiphanyRoleWorkerLaunchDocument), + ReorientLaunch(crate::EpiphanyReorientWorkerLaunchDocument), + PersonaProjector(PersonaProjectorInput), + PersonaTurn(PersonaTurnInput), + PersonaInterpreter(PersonaInterpreterInput), +} + +impl From for EpiphanyReasoningProjection { + fn from(value: EpiphanyWorkerLaunchDocument) -> Self { + match value { + EpiphanyWorkerLaunchDocument::Role(document) => Self::RoleLaunch(document), + EpiphanyWorkerLaunchDocument::Reorient(document) => Self::ReorientLaunch(document), + } + } +} + +#[derive(Clone, Debug, PartialEq, DatabaseEntry)] +#[cultcache( + type = "epiphany.reasoning_basis.v1", + schema = "EpiphanyReasoningBasis" +)] +pub struct EpiphanyReasoningBasis { + #[cultcache(key = 0)] + pub schema_version: String, + #[cultcache(key = 1)] + pub basis_id: String, + #[cultcache(key = 2)] + pub pass_id: String, + #[cultcache(key = 3)] + pub organ_id: String, + #[cultcache(key = 4)] + pub projection_policy_id: String, + #[cultcache(key = 5)] + pub source_documents: Vec, + #[cultcache(key = 6)] + pub projection_msgpack: Vec, + #[cultcache(key = 7, default)] + pub predecessor_decision_context_ids: Vec, +} + +impl EpiphanyReasoningBasis { + pub fn new( + pass_id: impl Into, + organ_id: impl Into, + projection_policy_id: impl Into, + mut source_documents: Vec, + projection: EpiphanyReasoningProjection, + ) -> Result { + let pass_id = pass_id.into(); + let organ_id = organ_id.into(); + let projection_policy_id = projection_policy_id.into(); + require_non_empty(&pass_id, "reasoning pass id")?; + require_non_empty(&organ_id, "reasoning organ id")?; + require_non_empty(&projection_policy_id, "reasoning projection policy id")?; + canonicalize_source_documents(&mut source_documents)?; + let projection_msgpack = rmp_serde::to_vec_named(&projection)?; + let mut basis = Self { + schema_version: REASONING_BASIS_SCHEMA_VERSION.to_string(), + basis_id: String::new(), + pass_id, + organ_id, + projection_policy_id, + source_documents, + projection_msgpack, + predecessor_decision_context_ids: Vec::new(), + }; + basis.basis_id = format!("reasoning-basis-{}", digest_without_basis_id(&basis)?); + basis.validate()?; + Ok(basis) + } + + pub fn validate(&self) -> Result<()> { + if self.schema_version != REASONING_BASIS_SCHEMA_VERSION { + return Err(anyhow!("unsupported reasoning basis schema")); + } + require_non_empty(&self.basis_id, "reasoning basis id")?; + require_non_empty(&self.pass_id, "reasoning pass id")?; + require_non_empty(&self.organ_id, "reasoning organ id")?; + require_non_empty(&self.projection_policy_id, "reasoning projection policy id")?; + let mut canonical = self.source_documents.clone(); + canonicalize_source_documents(&mut canonical)?; + if canonical != self.source_documents { + return Err(anyhow!( + "reasoning basis source documents are not canonical" + )); + } + let _: EpiphanyReasoningProjection = rmp_serde::from_slice(&self.projection_msgpack) + .map_err(|error| anyhow!("reasoning projection is invalid: {error}"))?; + if self + .predecessor_decision_context_ids + .iter() + .any(|context_id| context_id.trim().is_empty()) + || self + .predecessor_decision_context_ids + .windows(2) + .any(|pair| pair[0] >= pair[1]) + { + return Err(anyhow!( + "reasoning basis predecessor contexts are not canonical" + )); + } + let expected = format!("reasoning-basis-{}", digest_without_basis_id(self)?); + if self.basis_id != expected { + return Err(anyhow!("reasoning basis identity digest mismatch")); + } + Ok(()) + } + + pub fn projection(&self) -> Result { + rmp_serde::from_slice(&self.projection_msgpack) + .map_err(|error| anyhow!("reasoning projection is invalid: {error}")) + } +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct EpiphanyDecisionToolObservation { + pub intent: EpiphanyToolInvocationIntent, + pub receipt: EpiphanyToolInvocationReceipt, +} + +#[derive(Clone, Debug, PartialEq, DatabaseEntry)] +#[cultcache( + type = "epiphany.decision_context.v1", + schema = "EpiphanyDecisionContext" +)] +pub struct EpiphanyDecisionContext { + #[cultcache(key = 0)] + pub schema_version: String, + #[cultcache(key = 1)] + pub context_id: String, + #[cultcache(key = 2)] + pub basis_id: String, + #[cultcache(key = 3)] + pub terminal_request_id: String, + #[cultcache(key = 4)] + pub native_request_msgpack: Vec, + #[cultcache(key = 5)] + pub provider_request_msgpack: Vec, + #[cultcache(key = 6)] + pub tool_observations_msgpack: Vec, +} + +impl EpiphanyDecisionContext { + pub fn new( + basis: &EpiphanyReasoningBasis, + native_request: EpiphanyModelRequest, + provider_request: EpiphanyOpenAiModelRequest, + tool_observations: Vec, + ) -> Result { + basis.validate()?; + validate_request_pair(basis, &native_request, &provider_request)?; + validate_tool_observations(&native_request, &tool_observations)?; + let mut context = Self { + schema_version: DECISION_CONTEXT_SCHEMA_VERSION.to_string(), + context_id: String::new(), + basis_id: basis.basis_id.clone(), + terminal_request_id: native_request.request_id.clone(), + native_request_msgpack: rmp_serde::to_vec_named(&native_request)?, + provider_request_msgpack: rmp_serde::to_vec_named(&provider_request)?, + tool_observations_msgpack: rmp_serde::to_vec_named(&tool_observations)?, + }; + context.context_id = format!("decision-context-{}", digest_without_context_id(&context)?); + context.validate(basis)?; + Ok(context) + } + + pub fn validate(&self, basis: &EpiphanyReasoningBasis) -> Result<()> { + if self.schema_version != DECISION_CONTEXT_SCHEMA_VERSION { + return Err(anyhow!("unsupported decision context schema")); + } + let native_request = self.native_request()?; + let provider_request = self.provider_request()?; + let tool_observations = self.tool_observations()?; + if self.basis_id != basis.basis_id || self.terminal_request_id != native_request.request_id + { + return Err(anyhow!("decision context ownership mismatch")); + } + validate_request_pair(basis, &native_request, &provider_request)?; + validate_tool_observations(&native_request, &tool_observations)?; + let expected = format!("decision-context-{}", digest_without_context_id(self)?); + if self.context_id != expected { + return Err(anyhow!("decision context identity digest mismatch")); + } + Ok(()) + } + + pub fn native_request(&self) -> Result { + rmp_serde::from_slice(&self.native_request_msgpack) + .map_err(|error| anyhow!("decision native request is invalid: {error}")) + } + + pub fn provider_request(&self) -> Result { + rmp_serde::from_slice(&self.provider_request_msgpack) + .map_err(|error| anyhow!("decision provider request is invalid: {error}")) + } + + pub fn tool_observations(&self) -> Result> { + rmp_serde::from_slice(&self.tool_observations_msgpack) + .map_err(|error| anyhow!("decision tool observations are invalid: {error}")) + } +} + +#[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)] +#[cultcache( + type = "epiphany.mind_commit_receipt.v1", + schema = "EpiphanyMindCommitReceipt" +)] +pub struct EpiphanyMindCommitReceipt { + #[cultcache(key = 0)] + pub schema_version: String, + #[cultcache(key = 1)] + pub receipt_id: String, + #[cultcache(key = 2)] + pub decision_context_id: String, + #[cultcache(key = 3)] + pub invariant_owner: String, + #[cultcache(key = 4)] + pub strong_reads: Vec, + #[cultcache(key = 5)] + pub writes: Vec, + #[cultcache(key = 6)] + pub committed_at: String, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum EpiphanyMindCommitOutcome { + Committed(EpiphanyMindCommitReceipt), + Conflict { + document_identities: Vec<(String, String)>, + }, +} + +pub fn worker_reasoning_basis( + store_path: &Path, + launch: &EpiphanyRuntimeWorkerLaunchRequest, +) -> Result { + let mut cache = runtime_spine_cache(store_path)?; + cache.pull_all_backing_stores()?; + let launch_envelope = cache + .get_envelope::(&launch.job_id)? + .ok_or_else(|| anyhow!("worker reasoning basis lost its launch envelope"))?; + let source_documents = vec![EpiphanyMindDocumentVersion::from_envelope( + "epiphany-mind", + &launch_envelope, + )?]; + EpiphanyReasoningBasis::new( + &launch.job_id, + &launch.role, + WORKER_REASONING_PROJECTION_POLICY, + source_documents, + launch.launch_document()?.into(), + ) +} + +pub fn put_reasoning_basis( + store_path: &Path, + basis: &EpiphanyReasoningBasis, +) -> Result { + basis.validate()?; + let mut cache = runtime_spine_cache(store_path)?; + cache.pull_all_backing_stores()?; + for source in &basis.source_documents { + if source.store_id != "epiphany-mind" { + return Err(anyhow!( + "reasoning basis source store {:?} is not an admitted Mind store", + source.store_id + )); + } + let live = cache + .snapshot_envelopes() + .into_iter() + .find(|envelope| { + envelope.r#type == source.document_type && envelope.key == source.document_key + }) + .ok_or_else(|| { + anyhow!( + "reasoning basis source {:?}/{:?} is absent", + source.document_type, + source.document_key + ) + })?; + if EpiphanyMindDocumentVersion::from_envelope("epiphany-mind", &live)? != *source { + return Err(anyhow!( + "reasoning basis source {:?}/{:?} changed before sealing", + source.document_type, + source.document_key + )); + } + } + if let Some(existing) = cache.get::(&basis.basis_id)? { + if existing != *basis { + return Err(anyhow!("reasoning basis identity collision")); + } + return Ok(existing); + } + let envelope = cache.prepare_entry(&basis.basis_id, basis)?.0; + if !runtime_spine_backing_store(store_path)?.compare_and_swap_batch(&[], vec![envelope])? { + return put_reasoning_basis(store_path, basis); + } + Ok(basis.clone()) +} + +pub fn put_decision_context( + store_path: &Path, + context: &EpiphanyDecisionContext, +) -> Result { + let mut cache = runtime_spine_cache(store_path)?; + cache.pull_all_backing_stores()?; + let basis = cache + .get::(&context.basis_id)? + .ok_or_else(|| anyhow!("decision context lost its reasoning basis"))?; + context.validate(&basis)?; + validate_context_store_ownership(&cache, context)?; + if let Some(existing) = cache.get::(&context.context_id)? { + if existing != *context { + return Err(anyhow!("decision context identity collision")); + } + return Ok(existing); + } + let basis_envelope = cache + .get_envelope::(&basis.basis_id)? + .ok_or_else(|| anyhow!("decision context lost its basis envelope"))?; + let context_envelope = cache.prepare_entry(&context.context_id, context)?.0; + if !runtime_spine_backing_store(store_path)?.compare_and_swap_batch( + &[basis_envelope.clone()], + vec![basis_envelope, context_envelope], + )? { + return put_decision_context(store_path, context); + } + Ok(context.clone()) +} + +pub fn seal_model_decision_context( + store_path: &Path, + terminal_request_id: &str, +) -> Result { + let mut cache = runtime_spine_cache(store_path)?; + cache.pull_all_backing_stores()?; + let native = cache + .get::(terminal_request_id)? + .ok_or_else(|| anyhow!("terminal native model request is absent"))?; + let provider = cache + .get::(terminal_request_id)? + .ok_or_else(|| anyhow!("terminal provider model request is absent"))?; + let basis_id = native + .reasoning_basis_id + .as_deref() + .ok_or_else(|| anyhow!("terminal model request has no reasoning basis"))?; + let basis = cache + .get::(basis_id)? + .ok_or_else(|| anyhow!("terminal model request lost its reasoning basis"))?; + let intents = cache.get_all::()?; + let mut observations = Vec::new(); + for input in &native.input { + let epiphany_model_adapter::EpiphanyModelInputItem::ToolCall { + call_id, + name, + arguments, + } = input + else { + continue; + }; + let mut matches = intents.iter().filter(|intent| { + intent.call_id.as_deref() == Some(call_id.as_str()) + && format!("mcp__{}__{}", intent.server, intent.tool_name) == *name + && intent.arguments_json == *arguments + }); + let intent = matches + .next() + .ok_or_else(|| anyhow!("terminal tool call has no exact governed intent"))?; + if matches.next().is_some() { + return Err(anyhow!( + "terminal tool call has ambiguous governed ownership" + )); + } + let receipt = cache + .get::(&tool_invocation_receipt_key(&intent.intent_id))? + .ok_or_else(|| anyhow!("terminal tool call has no governed receipt"))?; + observations.push(EpiphanyDecisionToolObservation { + intent: intent.clone(), + receipt, + }); + } + let context = EpiphanyDecisionContext::new(&basis, native, provider, observations)?; + put_decision_context(store_path, &context) +} + +fn validate_context_store_ownership( + cache: &cultcache_rs::CultCache, + context: &EpiphanyDecisionContext, +) -> Result<()> { + let native = context.native_request()?; + let provider = context.provider_request()?; + let observations = context.tool_observations()?; + if cache + .get::(&native.request_id)? + .as_ref() + != Some(&native) + || cache + .get::(&provider.request_id)? + .as_ref() + != Some(&provider) + { + return Err(anyhow!( + "decision context terminal request family is absent or substituted" + )); + } + let model_binding = cache + .get::(&native.request_id)? + .ok_or_else(|| anyhow!("decision context terminal request has no runtime binding"))?; + if model_binding.request_id != native.request_id + || model_binding.reasoning_basis_id.as_deref() != Some(context.basis_id.as_str()) + || model_binding.source_worker_job_id != native.source_worker_job_id + { + return Err(anyhow!( + "decision context terminal request has foreign runtime ownership" + )); + } + for observation in observations { + let intent = cache + .get::(&tool_invocation_intent_key( + &observation.intent.intent_id, + ))? + .ok_or_else(|| anyhow!("decision context tool intent is absent"))?; + let receipt = cache + .get::(&tool_invocation_receipt_key( + &observation.intent.intent_id, + ))? + .ok_or_else(|| anyhow!("decision context tool receipt is absent"))?; + let tool_binding = cache + .get::(&observation.intent.intent_id)? + .ok_or_else(|| anyhow!("decision context tool binding is absent"))?; + if intent != observation.intent + || receipt != observation.receipt + || !matches!(receipt.status.as_str(), "completed" | "failed") + || tool_binding.intent_id != intent.intent_id + || tool_binding.model_request_id != intent.model_request_id + { + return Err(anyhow!( + "decision context tool observation has foreign stored ownership" + )); + } + if let Some(request_id) = intent.model_request_id.as_deref() { + let continuation = cache + .get::(request_id)? + .ok_or_else(|| anyhow!("decision tool continuation request is absent"))?; + if continuation.session_id != model_binding.session_id + || continuation.source_worker_job_id != model_binding.source_worker_job_id + || continuation.reasoning_basis_id != model_binding.reasoning_basis_id + || tool_binding.session_id != continuation.session_id + || tool_binding.job_id != continuation.job_id + { + return Err(anyhow!( + "decision tool continuation belongs to another reasoning pass" + )); + } + } else if tool_binding.job_id != native.source_worker_job_id.as_deref().unwrap_or_default() + { + return Err(anyhow!( + "request-owned tool observation belongs to another worker" + )); + } + } + Ok(()) +} + +pub fn commit_mind_mutation( + store_path: &Path, + decision_context_id: &str, + invariant_owner: &str, + strong_reads: Vec, + writes: Vec, + committed_at: &str, +) -> Result { + require_non_empty(decision_context_id, "Mind mutation decision context id")?; + require_non_empty(invariant_owner, "Mind mutation invariant owner")?; + require_non_empty(committed_at, "Mind mutation commit time")?; + chrono::DateTime::parse_from_rfc3339(committed_at) + .map_err(|error| anyhow!("Mind mutation commit time is invalid: {error}"))?; + if writes.is_empty() { + return Err(anyhow!("Mind mutation requires at least one write")); + } + validate_unique_envelope_identities(&strong_reads, "strong read")?; + validate_unique_envelope_identities(&writes, "write")?; + let expected_ids = strong_reads + .iter() + .map(|entry| (entry.r#type.as_str(), entry.key.as_str())) + .collect::>(); + let write_ids = writes + .iter() + .map(|entry| (entry.r#type.as_str(), entry.key.as_str())) + .collect::>(); + if !expected_ids.is_subset(&write_ids) { + return Err(anyhow!( + "Mind mutation must replace every strongly read identity" + )); + } + + let mut cache = runtime_spine_cache(store_path)?; + cache.pull_all_backing_stores()?; + let context = cache + .get::(decision_context_id)? + .ok_or_else(|| anyhow!("Mind mutation decision context does not exist"))?; + let basis = cache + .get::(&context.basis_id)? + .ok_or_else(|| anyhow!("Mind mutation decision context lost its basis"))?; + context.validate(&basis)?; + + let strong_versions = strong_reads + .iter() + .map(|entry| EpiphanyMindDocumentVersion::from_envelope("epiphany-mind", entry)) + .collect::>>()?; + let write_versions = writes + .iter() + .map(|entry| EpiphanyMindDocumentVersion::from_envelope("epiphany-mind", entry)) + .collect::>>()?; + let receipt_id = mind_commit_receipt_id( + decision_context_id, + invariant_owner, + &strong_versions, + &write_versions, + )?; + let receipt = EpiphanyMindCommitReceipt { + schema_version: MIND_COMMIT_RECEIPT_SCHEMA_VERSION.to_string(), + receipt_id: receipt_id.clone(), + decision_context_id: decision_context_id.to_string(), + invariant_owner: invariant_owner.to_string(), + strong_reads: strong_versions, + writes: write_versions, + committed_at: committed_at.to_string(), + }; + if let Some(existing) = cache.get::(&receipt_id)? { + if existing != receipt { + return Err(anyhow!("Mind commit receipt identity collision")); + } + return Ok(EpiphanyMindCommitOutcome::Committed(existing)); + } + let mut replacements = writes; + replacements.push(cache.prepare_entry(&receipt_id, &receipt)?.0); + if runtime_spine_backing_store(store_path)? + .compare_and_swap_batch(&strong_reads, replacements)? + { + return Ok(EpiphanyMindCommitOutcome::Committed(receipt)); + } + let current = runtime_spine_backing_store(store_path)?.pull_all()?; + let mut conflicts = strong_reads + .iter() + .filter(|expected| { + current + .iter() + .find(|entry| entry.r#type == expected.r#type && entry.key == expected.key) + != Some(*expected) + }) + .map(|entry| (entry.r#type.clone(), entry.key.clone())) + .collect::>(); + if conflicts.is_empty() { + conflicts = current_write_collisions(¤t, &receipt.writes, &expected_ids); + } + conflicts.sort(); + conflicts.dedup(); + Ok(EpiphanyMindCommitOutcome::Conflict { + document_identities: conflicts, + }) +} + +fn validate_request_pair( + basis: &EpiphanyReasoningBasis, + native: &EpiphanyModelRequest, + provider: &EpiphanyOpenAiModelRequest, +) -> Result<()> { + if native.reasoning_basis_id.as_deref() != Some(basis.basis_id.as_str()) { + return Err(anyhow!("model request does not bind its reasoning basis")); + } + if provider != &epiphany_openai_adapter::request_from_native(native) { + return Err(anyhow!("native and provider terminal requests diverge")); + } + Ok(()) +} + +fn validate_tool_observations( + request: &EpiphanyModelRequest, + observations: &[EpiphanyDecisionToolObservation], +) -> Result<()> { + let mut observed = BTreeSet::new(); + for observation in observations { + if observation.intent.intent_id != observation.receipt.intent_id + || observation.intent.adapter != observation.receipt.adapter + || observation.intent.server != observation.receipt.server + || observation.intent.tool_name != observation.receipt.tool_name + || observation + .intent + .call_id + .as_deref() + .unwrap_or_default() + .is_empty() + || observation.intent.schema_id + != epiphany_tool_adapter::TOOL_ADAPTER_INVOCATION_INTENT_SCHEMA_ID + || observation.receipt.schema_id + != epiphany_tool_adapter::TOOL_ADAPTER_INVOCATION_RECEIPT_SCHEMA_ID + || observation.receipt.receipt_id.trim().is_empty() + || observation.receipt.status.trim().is_empty() + { + return Err(anyhow!("decision tool observation ownership mismatch")); + } + if !observed.insert(observation.intent.intent_id.clone()) { + return Err(anyhow!("decision context repeats a tool observation")); + } + } + let mut request_observations = Vec::new(); + let mut index = 0usize; + while index < request.input.len() { + match &request.input[index] { + epiphany_model_adapter::EpiphanyModelInputItem::ToolCall { + call_id, + name, + arguments, + } => { + let Some(epiphany_model_adapter::EpiphanyModelInputItem::ToolResult { + call_id: result_call_id, + output, + }) = request.input.get(index + 1) + else { + return Err(anyhow!( + "terminal request tool call is not immediately paired with its result" + )); + }; + if result_call_id != call_id { + return Err(anyhow!("terminal request tool result call id mismatch")); + } + request_observations.push((call_id, name, arguments, output)); + index += 2; + } + epiphany_model_adapter::EpiphanyModelInputItem::ToolResult { .. } => { + return Err(anyhow!("terminal request contains an unpaired tool result")); + } + _ => index += 1, + } + } + if request_observations.len() != observations.len() { + return Err(anyhow!( + "decision context tool observation count does not match terminal request" + )); + } + for ((call_id, name, arguments, output), observation) in + request_observations.into_iter().zip(observations) + { + let expected_name = format!( + "mcp__{}__{}", + observation.intent.server, observation.intent.tool_name + ); + if observation.intent.call_id.as_deref() != Some(call_id.as_str()) + || name != &expected_name + || arguments != &observation.intent.arguments_json + || output != &receipt_output_for_model(&observation.intent, &observation.receipt) + { + return Err(anyhow!( + "decision context tool observation does not match terminal request bytes" + )); + } + } + Ok(()) +} + +fn canonicalize_source_documents(documents: &mut Vec) -> Result<()> { + for document in documents.iter() { + document.validate()?; + } + documents.sort_by(|left, right| { + left.store_id + .cmp(&right.store_id) + .then(left.document_type.cmp(&right.document_type)) + .then(left.document_key.cmp(&right.document_key)) + }); + for pair in documents.windows(2) { + if pair[0].store_id == pair[1].store_id + && pair[0].document_type == pair[1].document_type + && pair[0].document_key == pair[1].document_key + { + return Err(anyhow!( + "reasoning basis repeats a source document identity" + )); + } + } + Ok(()) +} + +fn digest_without_basis_id(basis: &EpiphanyReasoningBasis) -> Result { + let mut canonical = basis.clone(); + canonical.basis_id.clear(); + Ok(sha256(&rmp_serde::to_vec_named(&canonical)?)) +} + +fn digest_without_context_id(context: &EpiphanyDecisionContext) -> Result { + let mut canonical = context.clone(); + canonical.context_id.clear(); + Ok(sha256(&rmp_serde::to_vec_named(&canonical)?)) +} + +fn mind_commit_receipt_id( + context_id: &str, + owner: &str, + strong_reads: &[EpiphanyMindDocumentVersion], + writes: &[EpiphanyMindDocumentVersion], +) -> Result { + Ok(format!( + "mind-commit-{}", + sha256(&rmp_serde::to_vec_named(&( + context_id, + owner, + strong_reads, + writes + ))?) + )) +} + +fn validate_unique_envelope_identities(entries: &[CultCacheEnvelope], label: &str) -> Result<()> { + let mut identities = BTreeSet::new(); + for entry in entries { + if !identities.insert((entry.r#type.as_str(), entry.key.as_str())) { + return Err(anyhow!("Mind mutation repeats {label} identity")); + } + } + Ok(()) +} + +fn current_write_collisions( + current: &[CultCacheEnvelope], + writes: &[EpiphanyMindDocumentVersion], + expected_ids: &BTreeSet<(&str, &str)>, +) -> Vec<(String, String)> { + writes + .iter() + .filter(|write| { + !expected_ids.contains(&(write.document_type.as_str(), write.document_key.as_str())) + && current.iter().any(|entry| { + entry.r#type == write.document_type && entry.key == write.document_key + }) + }) + .map(|write| (write.document_type.clone(), write.document_key.clone())) + .collect() +} + +fn require_non_empty(value: &str, label: &str) -> Result<()> { + if value.trim().is_empty() { + return Err(anyhow!("{label} cannot be empty")); + } + Ok(()) +} + +fn sha256(bytes: &[u8]) -> String { + format!("sha256:{:x}", Sha256::digest(bytes)) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{EpiphanyRuntimeJobStatus, RuntimeSpineInitOptions, initialize_runtime_spine}; + use cultcache_rs::{CacheBackingStore, SingleFileMessagePackBackingStore}; + use epiphany_model_adapter::EpiphanyModelInputItem; + use tempfile::tempdir; + + fn basis() -> Result { + let projection = + EpiphanyReasoningProjection::RoleLaunch(EpiphanyRoleWorkerLaunchDocument { + thread_id: "creation-thread".into(), + role_id: "Modeling".into(), + state_revision: 7, + objective: Some("Map the Body".into()), + dynamic_prompt_context: Some("typed projection".into()), + repository_body_observation_basis: None, + proposal_modeling_context: None, + claim_repair_context: None, + frontier_planning_context: None, + frontier_research_context: None, + frontier_plan_mind_context: None, + imagination_consideration_context: None, + admitted_model_direction_consideration_context: None, + active_subgoal_id: None, + active_subgoals: Vec::new(), + active_graph_node_ids: Vec::new(), + investigation_checkpoint: None, + scratch: None, + invariants: Vec::new(), + graphs: None, + recent_evidence: Vec::new(), + recent_observations: Vec::new(), + graph_frontier: None, + graph_checkpoint: None, + planning: None, + churn: None, + }); + EpiphanyReasoningBasis::new( + "pass-1", + "Modeling", + WORKER_REASONING_PROJECTION_POLICY, + Vec::new(), + projection, + ) + } + + fn requests( + basis: &EpiphanyReasoningBasis, + ) -> (EpiphanyModelRequest, EpiphanyOpenAiModelRequest) { + let mut native = EpiphanyModelRequest::new( + "request-1", + "conversation-1", + "openai-codex", + "gpt-test", + "inspect", + ); + native.reasoning_basis_id = Some(basis.basis_id.clone()); + native.input.push(EpiphanyModelInputItem::UserText { + text: "projection".into(), + }); + let provider = EpiphanyOpenAiModelRequest { + schema_id: epiphany_openai_adapter::OPENAI_ADAPTER_REQUEST_SCHEMA_ID.into(), + request_id: native.request_id.clone(), + conversation_id: native.conversation_id.clone(), + model: native.model.clone(), + instructions: native.instructions.clone(), + input: vec![epiphany_openai_adapter::EpiphanyOpenAiInputItem::UserText { + text: "projection".into(), + }], + reasoning_effort: None, + reasoning_summary: None, + service_tier: None, + output_contract_id: None, + previous_response_id: None, + tools: Vec::new(), + output_schema_json: None, + }; + (native, provider) + } + + #[test] + fn basis_and_context_are_content_addressed_and_reject_substitution() -> Result<()> { + let reasoning_basis = basis()?; + assert_eq!(reasoning_basis, basis()?); + let (native, provider) = requests(&reasoning_basis); + let context = + EpiphanyDecisionContext::new(&reasoning_basis, native.clone(), provider, Vec::new())?; + context.validate(&reasoning_basis)?; + let mut substituted = context.clone(); + substituted.native_request_msgpack.push(0xff); + assert!(substituted.validate(&reasoning_basis).is_err()); + Ok(()) + } + + #[test] + fn disjoint_mind_mutations_merge_and_same_identity_conflicts() -> Result<()> { + let temp = tempdir()?; + let store = temp.path().join("mind.cc"); + initialize_runtime_spine( + &store, + RuntimeSpineInitOptions { + runtime_id: "mind-test".into(), + display_name: "Mind test".into(), + created_at: "2026-08-14T00:00:00Z".into(), + }, + )?; + let basis = put_reasoning_basis(&store, &basis()?)?; + let (native, provider) = requests(&basis); + crate::open_runtime_model_execution( + &store, + crate::RuntimeSpineSessionOptions { + session_id: "session-1".into(), + objective: "Test sealed decision context".into(), + created_at: "2026-08-14T00:00:01Z".into(), + coordinator_note: "reasoning context test".into(), + }, + crate::RuntimeSpineJobOptions { + job_id: "model-job-1".into(), + session_id: "session-1".into(), + role: "model-adapter".into(), + created_at: "2026-08-14T00:00:01Z".into(), + summary: "Test terminal request publication".into(), + artifact_refs: Vec::new(), + }, + &native, + &provider, + "2026-08-14T00:00:01Z", + )?; + let context = put_decision_context( + &store, + &EpiphanyDecisionContext::new(&basis, native, provider, Vec::new())?, + )?; + let backing = SingleFileMessagePackBackingStore::new(&store); + let make = |key: &str, summary: &str| -> Result { + let cache = runtime_spine_cache(&store)?; + let event = crate::EpiphanyRuntimeEvent { + schema_version: crate::runtime_spine::RUNTIME_SPINE_SCHEMA_VERSION.into(), + event_id: key.into(), + occurred_at: "2026-08-14T00:00:01Z".into(), + event_type: "test.mind.mutation".into(), + source: "reasoning-context-test".into(), + session_id: None, + job_id: None, + summary: summary.into(), + metadata: Default::default(), + }; + Ok(cache.prepare_entry(key, &event)?.0) + }; + assert!(matches!( + commit_mind_mutation( + &store, + &context.context_id, + "test-owner", + Vec::new(), + vec![make("persona", "one")?], + "2026-08-14T00:00:02Z" + )?, + EpiphanyMindCommitOutcome::Committed(_) + )); + assert!(matches!( + commit_mind_mutation( + &store, + &context.context_id, + "test-owner", + Vec::new(), + vec![make("hands", "two")?], + "2026-08-14T00:00:03Z" + )?, + EpiphanyMindCommitOutcome::Committed(_) + )); + let current = backing + .pull_all()? + .into_iter() + .find(|entry| { + entry.r#type == crate::EpiphanyRuntimeEvent::TYPE && entry.key == "persona" + }) + .unwrap(); + assert!(matches!( + commit_mind_mutation( + &store, + &context.context_id, + "test-owner", + vec![current.clone()], + vec![make("persona", "winner")?], + "2026-08-14T00:00:04Z" + )?, + EpiphanyMindCommitOutcome::Committed(_) + )); + assert!(matches!( + commit_mind_mutation( + &store, + &context.context_id, + "test-owner", + vec![current], + vec![make("persona", "loser")?], + "2026-08-14T00:00:05Z" + )?, + EpiphanyMindCommitOutcome::Conflict { .. } + )); + let _ = EpiphanyRuntimeJobStatus::Completed; + Ok(()) + } +} diff --git a/epiphany-core/src/resident_self.rs b/epiphany-core/src/resident_self.rs index c5fb4b3d..9b54d89d 100644 --- a/epiphany-core/src/resident_self.rs +++ b/epiphany-core/src/resident_self.rs @@ -3625,6 +3625,7 @@ mod tests { imagination_consideration_candidate_msgpack: None, admitted_model_direction_consideration_request_id: None, admitted_model_direction_consideration_result_msgpack: None, + decision_context_id: "decision-context-fixture".into(), } } @@ -5694,6 +5695,7 @@ mod tests { evidence_refs: Vec::new(), artifact_refs: Vec::new(), metadata: BTreeMap::new(), + decision_context_id: None, })?; runtime.put(job_id, &role_worker_result(job_id, "modeling"))?; diff --git a/epiphany-core/src/runtime_spine.rs b/epiphany-core/src/runtime_spine.rs index db7531dd..875b05c0 100644 --- a/epiphany-core/src/runtime_spine.rs +++ b/epiphany-core/src/runtime_spine.rs @@ -366,6 +366,8 @@ pub struct EpiphanyRuntimeModelExecutionBinding { pub bound_at: String, #[cultcache(key = 7, default)] pub source_worker_job_id: Option, + #[cultcache(key = 8, default)] + pub reasoning_basis_id: Option, } #[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)] @@ -645,6 +647,8 @@ pub struct EpiphanyRuntimeRoleWorkerResult { pub admitted_model_direction_consideration_request_id: Option, #[cultcache(key = 34, default)] pub admitted_model_direction_consideration_result_msgpack: Option>, + #[cultcache(key = 35)] + pub decision_context_id: String, } impl EpiphanyRuntimeRoleWorkerResult { @@ -736,6 +740,8 @@ pub struct EpiphanyRuntimeReorientWorkerResult { pub item_error: Option, #[cultcache(key = 14, default)] pub metadata: BTreeMap, + #[cultcache(key = 15)] + pub decision_context_id: String, } #[derive(Clone, Debug, PartialEq, DatabaseEntry)] @@ -768,6 +774,8 @@ pub struct EpiphanyRuntimeJobResult { pub artifact_refs: Vec, #[cultcache(key = 11, default)] pub metadata: BTreeMap, + #[cultcache(key = 12, default)] + pub decision_context_id: Option, } #[derive(Clone, Debug, PartialEq, DatabaseEntry)] @@ -982,6 +990,7 @@ pub struct RuntimeSpineJobResultOptions { pub next_safe_move: String, pub evidence_refs: Vec, pub artifact_refs: Vec, + pub decision_context_id: Option, } #[derive(Clone, Debug, PartialEq)] @@ -1117,6 +1126,9 @@ pub fn runtime_spine_cache(store_path: impl AsRef) -> Result { cache.register_entry_type::()?; cache.register_entry_type::()?; cache.register_entry_type::()?; + cache.register_entry_type::()?; + cache.register_entry_type::()?; + cache.register_entry_type::()?; cache.register_entry_type::()?; cache.register_entry_type::()?; cache.register_entry_type::()?; @@ -1673,10 +1685,7 @@ pub fn open_runtime_model_execution( validate_non_empty(bound_at, "model execution binding time")?; chrono::DateTime::parse_from_rfc3339(bound_at) .map_err(|error| anyhow!("model execution binding time is invalid: {error}"))?; - if provider_request.request_id != model_request.request_id - || provider_request.conversation_id != model_request.conversation_id - || provider_request.model != model_request.model - { + if provider_request != &epiphany_openai_adapter::request_from_native(model_request) { return Err(anyhow!( "native and provider model requests do not describe one execution" )); @@ -1689,9 +1698,25 @@ pub fn open_runtime_model_execution( require_runtime_identity_not_archived(&cache, "session", &session_options.session_id)?; require_runtime_identity_not_archived(&cache, "job", &job_options.job_id)?; require_runtime_identity_not_archived(&cache, "model-request", &model_request.request_id)?; + let mut reasoning_basis_envelope = None; let source_worker_envelopes = if let Some(worker_job_id) = model_request.source_worker_job_id.as_deref() { + let basis_id = model_request.reasoning_basis_id.as_deref().ok_or_else(|| { + anyhow!("decision-bearing worker model execution has no reasoning basis") + })?; + let basis = cache + .get::(basis_id)? + .ok_or_else(|| anyhow!("model execution reasoning basis is absent"))?; + basis.validate()?; + if basis.pass_id != worker_job_id { + return Err(anyhow!("model execution reasoning basis belongs to another pass")); + } + reasoning_basis_envelope = Some( + cache + .get_envelope::(basis_id)? + .ok_or_else(|| anyhow!("model execution lost its reasoning basis envelope"))?, + ); let launch = cache .get::(worker_job_id)? .ok_or_else(|| { @@ -1819,12 +1844,16 @@ pub fn open_runtime_model_execution( provider: model_request.provider.clone(), bound_at: bound_at.to_string(), source_worker_job_id: model_request.source_worker_job_id.clone(), + reasoning_basis_id: model_request.reasoning_basis_id.clone(), }; let identity_envelope = cache .get_envelope::(RUNTIME_IDENTITY_KEY)? .ok_or_else(|| anyhow!("model execution lost its exact runtime identity envelope"))?; let mut expected = vec![identity_envelope.clone()]; expected.extend(source_worker_envelopes.iter().cloned()); + if let Some(envelope) = reasoning_basis_envelope.as_ref() { + expected.push(envelope.clone()); + } if existing_session.is_some() { expected.push( cache @@ -1848,6 +1877,9 @@ pub fn open_runtime_model_execution( .0, ]; replacements.extend(source_worker_envelopes); + if let Some(envelope) = reasoning_basis_envelope { + replacements.push(envelope); + } if !runtime_spine_backing_store(store_path)?.compare_and_swap_batch(&expected, replacements)? { return Err(anyhow!( "model execution request publication lost its snapshot fence" @@ -4417,11 +4449,13 @@ pub fn put_runtime_role_worker_result( validate_non_empty(&result.job_id, "role worker result job id")?; validate_non_empty(&result.result_id, "role worker result id")?; validate_non_empty(&result.role_id, "role worker result role id")?; + validate_non_empty(&result.decision_context_id, "role worker result decision context id")?; if result.schema_version != RUNTIME_ROLE_WORKER_RESULT_SCHEMA_VERSION { return Err(anyhow!("role worker result schema version mismatch")); } let mut cache = runtime_spine_cache(store_path)?; cache.pull_all_backing_stores()?; + require_worker_decision_context(&cache, &result.decision_context_id, &result.job_id)?; let worker_launch = cache .get::(&result.job_id)? .ok_or_else(|| anyhow!("role worker result requires its immutable worker launch"))?; @@ -9963,12 +9997,28 @@ pub fn put_runtime_reorient_worker_result( validate_non_empty(&result.job_id, "reorient worker result job id")?; validate_non_empty(&result.result_id, "reorient worker result id")?; validate_non_empty(&result.mode, "reorient worker result mode")?; + validate_non_empty(&result.decision_context_id, "reorient worker result decision context id")?; let mut cache = runtime_spine_cache(store_path)?; cache.pull_all_backing_stores()?; + require_worker_decision_context(&cache, &result.decision_context_id, &result.job_id)?; cache.put(&result.job_id, result)?; Ok(()) } +fn require_worker_decision_context( + cache: &cultcache_rs::CultCache, + context_id: &str, + worker_job_id: &str, +) -> Result<()> { + let context = cache + .get::(context_id)? + .ok_or_else(|| anyhow!("worker result decision context is absent"))?; + if context.native_request()?.source_worker_job_id.as_deref() != Some(worker_job_id) { + return Err(anyhow!("worker result decision context belongs to another worker")); + } + Ok(()) +} + pub fn runtime_reorient_worker_result( store_path: impl AsRef, job_id: &str, @@ -12963,6 +13013,7 @@ pub fn complete_runtime_job( evidence_refs: options.evidence_refs, artifact_refs: options.artifact_refs, metadata: BTreeMap::new(), + decision_context_id: options.decision_context_id, }; let event = EpiphanyRuntimeEvent { schema_version: RUNTIME_SPINE_SCHEMA_VERSION.to_string(), @@ -14648,6 +14699,7 @@ pub(crate) mod tests { next_safe_move: "Close the session.".to_string(), evidence_refs: Vec::new(), artifact_refs: Vec::new(), + decision_context_id: None, }, )?; close_runtime_session( @@ -14842,6 +14894,7 @@ pub(crate) mod tests { imagination_consideration_candidate_msgpack: None, admitted_model_direction_consideration_request_id: None, admitted_model_direction_consideration_result_msgpack: None, + decision_context_id: "decision-context-fixture".into(), }; assert!(put_runtime_role_worker_result(&store, &result).is_err()); assert!( @@ -14940,6 +14993,7 @@ pub(crate) mod tests { next_safe_move: "Retry only from this terminal process authority.".into(), evidence_refs: Vec::new(), artifact_refs: Vec::new(), + decision_context_id: None, }, )?; let claim = runtime_worker_process_claim(&store, "worker-failure-job")? @@ -15044,6 +15098,7 @@ pub(crate) mod tests { imagination_consideration_candidate_msgpack: None, admitted_model_direction_consideration_request_id: None, admitted_model_direction_consideration_result_msgpack: None, + decision_context_id: "decision-context-fixture".into(), }; assert!(put_runtime_role_worker_result(&store, &result).is_err()); result.result_id = "other-late-result".into(); @@ -15326,6 +15381,7 @@ pub(crate) mod tests { imagination_consideration_candidate_msgpack: None, admitted_model_direction_consideration_request_id: None, admitted_model_direction_consideration_result_msgpack: None, + decision_context_id: "decision-context-fixture".into(), }; let review = RepoModelAdmissionReview { schema_version: REPO_MODEL_ADMISSION_REVIEW_SCHEMA_VERSION.to_string(), @@ -15991,6 +16047,7 @@ pub(crate) mod tests { imagination_consideration_candidate_msgpack: None, admitted_model_direction_consideration_request_id: None, admitted_model_direction_consideration_result_msgpack: None, + decision_context_id: "decision-context-fixture".into(), }; let review = RepoModelAdmissionReview { schema_version: REPO_MODEL_ADMISSION_REVIEW_SCHEMA_VERSION.into(), @@ -16440,6 +16497,7 @@ pub(crate) mod tests { admitted_model_direction_consideration_result_msgpack: Some(rmp_serde::to_vec_named( &direction, )?), + decision_context_id: "decision-context-fixture".into(), }; put_runtime_role_worker_result(&store, &worker_result)?; let mut companion_cache = runtime_spine_cache(&store)?; @@ -17594,6 +17652,7 @@ pub(crate) mod tests { evidence_refs: Vec::new(), artifact_refs: Vec::new(), metadata: BTreeMap::new(), + decision_context_id: None, }, )?; let continuation_receipt = EpiphanyCoordinatorRunReceipt { @@ -17760,6 +17819,7 @@ pub(crate) mod tests { imagination_consideration_candidate_msgpack: None, admitted_model_direction_consideration_request_id: None, admitted_model_direction_consideration_result_msgpack: None, + decision_context_id: "decision-context-fixture".into(), }, )?; assert_eq!( @@ -18331,6 +18391,7 @@ pub(crate) mod tests { imagination_consideration_candidate_msgpack: None, admitted_model_direction_consideration_request_id: None, admitted_model_direction_consideration_result_msgpack: None, + decision_context_id: "decision-context-fixture".into(), }; put_test_non_modeling_worker_launch(&store, &verification_result.job_id, "verification")?; put_runtime_role_worker_result(&store, &verification_result)?; @@ -18526,6 +18587,7 @@ pub(crate) mod tests { imagination_consideration_candidate_msgpack: None, admitted_model_direction_consideration_request_id: None, admitted_model_direction_consideration_result_msgpack: None, + decision_context_id: "decision-context-fixture".into(), }; let review = RepoModelAdmissionReview { schema_version: REPO_MODEL_ADMISSION_REVIEW_SCHEMA_VERSION.to_string(), @@ -18874,6 +18936,7 @@ pub(crate) mod tests { imagination_consideration_candidate_msgpack: None, admitted_model_direction_consideration_request_id: None, admitted_model_direction_consideration_result_msgpack: None, + decision_context_id: "decision-context-fixture".into(), }; put_test_non_modeling_worker_launch(&fixture.store, &adjacent.job_id, "verification")?; put_runtime_role_worker_result(&fixture.store, &adjacent)?; @@ -18964,6 +19027,7 @@ pub(crate) mod tests { next_safe_move: "Launch the next isolated fixture.".into(), evidence_refs: Vec::new(), artifact_refs: Vec::new(), + decision_context_id: None, }, )?; let mut cache = runtime_spine_cache(&fixture.store)?; @@ -20497,6 +20561,7 @@ pub(crate) mod tests { next_safe_move: "Complete the session.".to_string(), evidence_refs: Vec::new(), artifact_refs: Vec::new(), + decision_context_id: None, }, )?; @@ -21104,6 +21169,7 @@ pub(crate) mod tests { next_safe_move: "Authenticate the terminal attempt evidence.".to_string(), evidence_refs: vec![format!("eyes-source-{}", exact.intent_id)], artifact_refs: Vec::new(), + decision_context_id: None, }, )?; assert!(require_runtime_tool_execution_binding(&store, &exact.intent_id).is_err()); @@ -21279,6 +21345,7 @@ pub(crate) mod tests { next_safe_move: "Close the session.".to_string(), evidence_refs: Vec::new(), artifact_refs: Vec::new(), + decision_context_id: None, }, )?; close_runtime_session( @@ -21541,6 +21608,7 @@ pub(crate) mod tests { next_safe_move: "Launch verification.".to_string(), evidence_refs: vec!["evidence:model".to_string()], artifact_refs: vec!["artifact:model".to_string()], + decision_context_id: None, }, )?; assert_eq!(result.role, "modeling"); @@ -22735,6 +22803,7 @@ pub(crate) mod tests { completed_at: "2026-08-10T20:00:02Z".into(), verdict: "failed".into(), summary: "Typed worker attempt failed.".into(), next_safe_move: "Retry after resident closure.".into(), evidence_refs: Vec::new(), artifact_refs: Vec::new(), + decision_context_id: None, })?; let before = std::fs::read(&store)?; assert!(archive_failed_runtime_worker_attempt(&store, &job_id, @@ -22799,6 +22868,7 @@ pub(crate) mod tests { summary: "Newer typed worker attempt failed.".into(), next_safe_move: "Preserve the newest failed attempt.".into(), evidence_refs: Vec::new(), artifact_refs: Vec::new(), + decision_context_id: None, })?; let retained = retain_failed_runtime_worker_attempts( &store, 1, &BTreeSet::new(), "2026-08-10T20:00:06Z")?; @@ -22836,6 +22906,7 @@ pub(crate) mod tests { completed_at: "2026-08-10T21:00:02Z".into(), verdict: "pass".into(), summary: "Typed worker attempt completed.".into(), next_safe_move: "Retire worker scaffolding.".into(), evidence_refs: Vec::new(), artifact_refs: Vec::new(), + decision_context_id: None, })?; let expected = runtime_typed_request_fulfillment( &store, RuntimeTypedRequestRef::ProposalModeling(&request_id))?.expect("live fulfillment"); diff --git a/epiphany-model-adapter/src/lib.rs b/epiphany-model-adapter/src/lib.rs index 7b23b704..2cb5e668 100644 --- a/epiphany-model-adapter/src/lib.rs +++ b/epiphany-model-adapter/src/lib.rs @@ -60,6 +60,8 @@ pub struct EpiphanyModelRequest { pub output_schema_json: Option, #[cultcache(key = 14, default)] pub source_worker_job_id: Option, + #[cultcache(key = 15, default)] + pub reasoning_basis_id: Option, } impl EpiphanyModelRequest { @@ -86,6 +88,7 @@ impl EpiphanyModelRequest { tools: Vec::new(), output_schema_json: None, source_worker_job_id: None, + reasoning_basis_id: None, } } } diff --git a/epiphany-openai-adapter/Cargo.toml b/epiphany-openai-adapter/Cargo.toml index afc39f0d..9875b49d 100644 --- a/epiphany-openai-adapter/Cargo.toml +++ b/epiphany-openai-adapter/Cargo.toml @@ -11,4 +11,5 @@ path = "src/lib.rs" [dependencies] cultcache-rs = { path = "../vendor/cultcache-rs" } +epiphany-model-adapter = { path = "../epiphany-model-adapter" } serde = { version = "1", features = ["derive"] } diff --git a/epiphany-openai-adapter/src/lib.rs b/epiphany-openai-adapter/src/lib.rs index 0a14d81a..6054bc19 100644 --- a/epiphany-openai-adapter/src/lib.rs +++ b/epiphany-openai-adapter/src/lib.rs @@ -1,4 +1,5 @@ use cultcache_rs::DatabaseEntry; +use epiphany_model_adapter::{EpiphanyModelInputItem, EpiphanyModelRequest}; use serde::Deserialize; use serde::Serialize; @@ -98,6 +99,61 @@ impl EpiphanyOpenAiModelRequest { } } +/// The only lowering from a native model request to the OpenAI transport. +/// Internal audit identity stays on the native request; every provider-bearing +/// byte is derived here. +pub fn request_from_native(request: &EpiphanyModelRequest) -> EpiphanyOpenAiModelRequest { + EpiphanyOpenAiModelRequest { + schema_id: OPENAI_ADAPTER_REQUEST_SCHEMA_ID.to_string(), + request_id: request.request_id.clone(), + conversation_id: request.conversation_id.clone(), + model: request.model.clone(), + instructions: request.instructions.clone(), + input: request.input.iter().map(input_from_native).collect(), + reasoning_effort: request.reasoning_effort.clone(), + reasoning_summary: request.reasoning_summary.clone(), + service_tier: request.service_tier.clone(), + output_contract_id: request.output_contract_id.clone(), + previous_response_id: request.previous_response_id.clone(), + tools: request + .tools + .iter() + .map(|tool| EpiphanyOpenAiToolDefinition { + name: tool.name.clone(), + description: tool.description.clone(), + parameters_json: tool.parameters_json.clone(), + }) + .collect(), + output_schema_json: request.output_schema_json.clone(), + } +} + +fn input_from_native(input: &EpiphanyModelInputItem) -> EpiphanyOpenAiInputItem { + match input { + EpiphanyModelInputItem::UserText { text } => { + EpiphanyOpenAiInputItem::UserText { text: text.clone() } + } + EpiphanyModelInputItem::AssistantText { text } => { + EpiphanyOpenAiInputItem::AssistantText { text: text.clone() } + } + EpiphanyModelInputItem::ToolCall { + call_id, + name, + arguments, + } => EpiphanyOpenAiInputItem::ToolCall { + call_id: call_id.clone(), + name: name.clone(), + arguments: arguments.clone(), + }, + EpiphanyModelInputItem::ToolResult { call_id, output } => { + EpiphanyOpenAiInputItem::ToolResult { + call_id: call_id.clone(), + output: output.clone(), + } + } + } +} + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct EpiphanyOpenAiToolDefinition { pub name: String, diff --git a/epiphany-openai-runtime/src/bin/epiphany-openai-runtime.rs b/epiphany-openai-runtime/src/bin/epiphany-openai-runtime.rs index 583c2504..c3322c26 100644 --- a/epiphany-openai-runtime/src/bin/epiphany-openai-runtime.rs +++ b/epiphany-openai-runtime/src/bin/epiphany-openai-runtime.rs @@ -25,8 +25,8 @@ use epiphany_openai_adapter::OPENAI_ADAPTER_REQUEST_SCHEMA_ID; use epiphany_openai_runtime::EpiphanyOpenAiRuntimeOptions; use epiphany_openai_runtime::EpiphanyWorkerRuntimeOptions; use epiphany_openai_runtime::OPENAI_RUNTIME_ROLE; -use epiphany_openai_runtime::assistant_text_from_model_events; use epiphany_openai_runtime::append_requested_public_source_receipts; +use epiphany_openai_runtime::assistant_text_from_model_events; use epiphany_openai_runtime::build_tool_followup_model_request; use epiphany_openai_runtime::build_worker_model_request; use epiphany_openai_runtime::complete_worker_job_from_assistant_text; @@ -658,8 +658,7 @@ fn parse_run_worker_options(args: Vec) -> Result { max_runtime_seconds = Some(next_value(&mut iter, "--max-runtime-seconds")?.parse()?) } "--activation-token-sha256" => { - activation_token_sha256 = - Some(next_value(&mut iter, "--activation-token-sha256")?) + activation_token_sha256 = Some(next_value(&mut iter, "--activation-token-sha256")?) } other => return Err(anyhow!("unknown run-worker argument: {other}")), } @@ -693,16 +692,16 @@ fn claim_and_wait_for_worker_activation(options: &RunWorkerCliOptions) -> Result )?; let deadline = std::time::Instant::now() + Duration::from_secs(10); loop { - let claim = epiphany_core::runtime_worker_process_claim( - &options.store_path, - &options.job_id, - )? - .ok_or_else(|| anyhow!("worker activation gate lost its process claim"))?; + let claim = + epiphany_core::runtime_worker_process_claim(&options.store_path, &options.job_id)? + .ok_or_else(|| anyhow!("worker activation gate lost its process claim"))?; if claim.process_id != process.process_id || claim.process_creation_token != process.creation_token || claim.process_executable_path != process.executable_path.display().to_string() { - return Err(anyhow!("worker activation gate observed a substituted process claim")); + return Err(anyhow!( + "worker activation gate observed a substituted process claim" + )); } match claim.status.as_str() { "active" => return Ok(()), @@ -723,7 +722,9 @@ fn claim_and_wait_for_worker_activation(options: &RunWorkerCliOptions) -> Result "Runtime Continuity may supersede only from the terminal unactivated process claim." .into(), )?; - return Err(anyhow!("worker activation gate expired before model/tool work")); + return Err(anyhow!( + "worker activation gate expired before model/tool work" + )); } status => { return Err(anyhow!( @@ -875,20 +876,24 @@ async fn run_worker_launch_with_tool_continuation( .clone() .context("run-worker --auto-tools requires --tool-adapter-bin")?; let launch_request = load_worker_launch_request(&options.store_path, &options.job_id)?; - let mut initial_request = - build_worker_model_request(&launch_request, &options.provider, &options.model)?; - let requested_public_source_intents = if launch_request - .repo_frontier_research_request_id - .is_some() - { - epiphany_core::put_runtime_requested_public_source_intents( - &options.store_path, - &options.job_id, - &now(), - )? - } else { - Vec::new() - }; + let basis = epiphany_core::worker_reasoning_basis(&options.store_path, &launch_request)?; + epiphany_core::put_reasoning_basis(&options.store_path, &basis)?; + let mut initial_request = build_worker_model_request( + &launch_request, + &options.provider, + &options.model, + &basis.basis_id, + )?; + let requested_public_source_intents = + if launch_request.repo_frontier_research_request_id.is_some() { + epiphany_core::put_runtime_requested_public_source_intents( + &options.store_path, + &options.job_id, + &now(), + )? + } else { + Vec::new() + }; let mut requested_public_source_runs = Vec::new(); for intent in &requested_public_source_intents { requested_public_source_runs.push(run_tool_adapter( @@ -1241,11 +1246,7 @@ mod tests { #[test] fn worker_cli_requires_activation_capability_before_runtime_path() -> Result<()> { assert!( - parse_run_worker_options(vec![ - "--job-id".into(), - "job-without-gate".into(), - ]) - .is_err() + parse_run_worker_options(vec!["--job-id".into(), "job-without-gate".into(),]).is_err() ); let options = parse_run_worker_options(vec![ "--job-id".into(), diff --git a/epiphany-openai-runtime/src/lib.rs b/epiphany-openai-runtime/src/lib.rs index e80c5ed0..4e62f236 100644 --- a/epiphany-openai-runtime/src/lib.rs +++ b/epiphany-openai-runtime/src/lib.rs @@ -37,7 +37,6 @@ use epiphany_openai_adapter::EpiphanyOpenAiInputItem; use epiphany_openai_adapter::EpiphanyOpenAiModelRequest; use epiphany_openai_adapter::EpiphanyOpenAiStreamEvent; use epiphany_openai_adapter::EpiphanyOpenAiStreamPayload; -use epiphany_openai_adapter::EpiphanyOpenAiToolDefinition; use epiphany_openai_codex_spine::EpiphanyCodexOpenAiTransport; use epiphany_openai_codex_spine::EpiphanyResponsesFrameObservation; use epiphany_openai_codex_spine::auth_manager; @@ -46,11 +45,12 @@ use epiphany_openai_codex_spine::status_from_auth_manager; use epiphany_tool_adapter::EPIPHANY_TOOL_RUNTIME_ADAPTER_ID; use epiphany_tool_adapter::EpiphanyToolInvocationIntent; use epiphany_tool_adapter::EpiphanyToolInvocationReceipt; +use epiphany_tool_adapter::receipt_output_for_model; use epiphany_tool_adapter::tool_invocation_receipt_key; use serde::Deserialize; +use serde::de::DeserializeOwned; use serde::de::MapAccess; use serde::de::SeqAccess; -use serde::de::DeserializeOwned; use serde::de::Visitor; use sha2::Digest; @@ -332,8 +332,14 @@ pub async fn run_worker_launch( options.job_id )); } - let model_request = - build_worker_model_request(&launch_request, &options.provider, &options.model)?; + let basis = epiphany_core::worker_reasoning_basis(&options.store_path, &launch_request)?; + epiphany_core::put_reasoning_basis(&options.store_path, &basis)?; + let model_request = build_worker_model_request( + &launch_request, + &options.provider, + &options.model, + &basis.basis_id, + )?; let openai_options = EpiphanyOpenAiRuntimeOptions { store_path: options.store_path.clone(), codex_home: options.codex_home, @@ -489,6 +495,7 @@ pub fn record_openai_events( .to_string(), evidence_refs: Vec::new(), artifact_refs: Vec::new(), + decision_context_id: None, }, )?; @@ -597,7 +604,11 @@ pub fn build_worker_model_request( launch_request: &EpiphanyRuntimeWorkerLaunchRequest, provider: &str, model: &str, + reasoning_basis_id: &str, ) -> Result { + if reasoning_basis_id.trim().is_empty() { + return Err(anyhow!("worker model request requires a reasoning basis")); + } let launch_document = launch_request.launch_document()?; let output_schema_json = worker_output_schema_json(launch_request, &launch_document)?; let request_id = format!( @@ -632,6 +643,7 @@ pub fn build_worker_model_request( request.output_contract_id = Some(launch_request.output_contract_id.clone()); request.output_schema_json = Some(output_schema_json); request.source_worker_job_id = Some(launch_request.job_id.clone()); + request.reasoning_basis_id = Some(reasoning_basis_id.to_string()); if matches!( launch_request.binding_id.as_str(), epiphany_core::EPIPHANY_RESEARCH_ROLE_BINDING_ID @@ -650,6 +662,8 @@ pub fn complete_worker_job_from_assistant_text( openai_summary: &EpiphanyOpenAiRuntimeRunSummary, assistant_text: &str, ) -> Result { + let decision_context = + epiphany_core::seal_model_decision_context(store_path.as_ref(), openai_request_id)?; let launch_document = launch_request.launch_document()?; let parsed_result = parse_worker_result_ingress(&launch_document, assistant_text); let parse_error = parsed_result @@ -718,6 +732,7 @@ pub fn complete_worker_job_from_assistant_text( .as_ref(), &completed_at, &result_id, + &decision_context.context_id, parsed, evidence_refs.clone(), artifact_refs.clone(), @@ -728,6 +743,7 @@ pub fn complete_worker_job_from_assistant_text( let typed_result = reorient_worker_result_from_ingress( launch_request, &result_id, + &decision_context.context_id, parsed, artifact_refs.clone(), ); @@ -751,6 +767,7 @@ pub fn complete_worker_job_from_assistant_text( next_safe_move, evidence_refs, artifact_refs, + decision_context_id: Some(decision_context.context_id), }, ) } @@ -772,6 +789,7 @@ pub fn fail_worker_job( next_safe_move, evidence_refs: Vec::new(), artifact_refs: Vec::new(), + decision_context_id: None, }, ) } @@ -938,7 +956,7 @@ pub fn build_tool_followup_model_request( }); input.push(EpiphanyModelInputItem::ToolResult { call_id, - output: tool_receipt_output_for_model(&intent, &receipt), + output: receipt_output_for_model(&intent, &receipt), }); } followup.input = input; @@ -1018,9 +1036,7 @@ pub fn append_requested_public_source_receipts( .as_deref() .ok_or_else(|| anyhow!("requested public source intent has no call id"))?; let receipt = cache - .get::(&tool_invocation_receipt_key( - &intent.intent_id, - ))? + .get::(&tool_invocation_receipt_key(&intent.intent_id))? .ok_or_else(|| { anyhow!( "requested public source intent {:?} has no terminal receipt", @@ -1044,31 +1060,12 @@ pub fn append_requested_public_source_receipts( }); request.input.push(EpiphanyModelInputItem::ToolResult { call_id: call_id.to_string(), - output: tool_receipt_output_for_model(intent, &receipt), + output: receipt_output_for_model(intent, &receipt), }); } Ok(()) } -fn tool_receipt_output_for_model( - intent: &EpiphanyToolInvocationIntent, - receipt: &EpiphanyToolInvocationReceipt, -) -> String { - if let Some(result) = receipt.result_json.as_ref() { - return result.clone(); - } - serde_json::json!({ - "status": receipt.status, - "adapter": receipt.adapter, - "server": receipt.server, - "toolName": receipt.tool_name, - "intentId": intent.intent_id, - "receiptId": receipt.receipt_id, - "error": receipt.error, - }) - .to_string() -} - pub fn default_options( store_path: PathBuf, codex_home: PathBuf, @@ -1170,6 +1167,7 @@ pub fn model_request_from_openai_request( previous_response_id: request.previous_response_id.clone(), output_schema_json: request.output_schema_json.clone(), source_worker_job_id: None, + reasoning_basis_id: None, tools: request .tools .iter() @@ -1185,33 +1183,7 @@ pub fn model_request_from_openai_request( pub fn openai_request_from_model_request( request: &EpiphanyModelRequest, ) -> EpiphanyOpenAiModelRequest { - EpiphanyOpenAiModelRequest { - schema_id: epiphany_openai_adapter::OPENAI_ADAPTER_REQUEST_SCHEMA_ID.to_string(), - request_id: request.request_id.clone(), - conversation_id: request.conversation_id.clone(), - model: request.model.clone(), - instructions: request.instructions.clone(), - input: request - .input - .iter() - .map(openai_input_from_model_input) - .collect(), - reasoning_effort: request.reasoning_effort.clone(), - reasoning_summary: request.reasoning_summary.clone(), - service_tier: request.service_tier.clone(), - output_contract_id: request.output_contract_id.clone(), - previous_response_id: request.previous_response_id.clone(), - output_schema_json: request.output_schema_json.clone(), - tools: request - .tools - .iter() - .map(|tool| EpiphanyOpenAiToolDefinition { - name: tool.name.clone(), - description: tool.description.clone(), - parameters_json: tool.parameters_json.clone(), - }) - .collect(), - } + epiphany_openai_adapter::request_from_native(request) } fn repository_source_tools() -> Vec { @@ -1318,32 +1290,6 @@ fn model_input_from_openai_input(input: &EpiphanyOpenAiInputItem) -> EpiphanyMod } } -fn openai_input_from_model_input(input: &EpiphanyModelInputItem) -> EpiphanyOpenAiInputItem { - match input { - EpiphanyModelInputItem::UserText { text } => { - EpiphanyOpenAiInputItem::UserText { text: text.clone() } - } - EpiphanyModelInputItem::AssistantText { text } => { - EpiphanyOpenAiInputItem::AssistantText { text: text.clone() } - } - EpiphanyModelInputItem::ToolCall { - call_id, - name, - arguments, - } => EpiphanyOpenAiInputItem::ToolCall { - call_id: call_id.clone(), - name: name.clone(), - arguments: arguments.clone(), - }, - EpiphanyModelInputItem::ToolResult { call_id, output } => { - EpiphanyOpenAiInputItem::ToolResult { - call_id: call_id.clone(), - output: output.clone(), - } - } - } -} - pub fn model_event_from_openai_event( provider: &str, event: &EpiphanyOpenAiStreamEvent, @@ -1802,6 +1748,7 @@ fn role_worker_result_from_ingress( >, completed_at: &str, result_id: &str, + decision_context_id: &str, result: &RoleWorkerResultIngress, runtime_evidence_ids: Vec, artifact_refs: Vec, @@ -2121,6 +2068,7 @@ fn role_worker_result_from_ingress( admitted_model_direction_consideration_context .map(|context| context.request.request_id.clone()), admitted_model_direction_consideration_result_msgpack, + decision_context_id: decision_context_id.to_string(), } } @@ -2181,12 +2129,14 @@ pub fn failed_frontier_planning_role_result( imagination_consideration_candidate_msgpack: None, admitted_model_direction_consideration_request_id: None, admitted_model_direction_consideration_result_msgpack: None, + decision_context_id: String::new(), })) } fn reorient_worker_result_from_ingress( launch_request: &EpiphanyRuntimeWorkerLaunchRequest, result_id: &str, + decision_context_id: &str, result: &ReorientWorkerResultIngress, artifact_refs: Vec, ) -> EpiphanyRuntimeReorientWorkerResult { @@ -2211,6 +2161,7 @@ fn reorient_worker_result_from_ingress( continuity_risks: clean_string_vec(&result.continuity_risks), item_error: None, metadata: std::collections::BTreeMap::new(), + decision_context_id: decision_context_id.to_string(), } } @@ -2301,7 +2252,9 @@ impl<'de> Visitor<'de> for UniqueJsonValueVisitor { } fn visit_str(self, value: &str) -> Result { - Ok(UniqueJsonValue(serde_json::Value::String(value.to_string()))) + Ok(UniqueJsonValue(serde_json::Value::String( + value.to_string(), + ))) } fn visit_string(self, value: String) -> Result { @@ -2334,9 +2287,7 @@ impl<'de> Visitor<'de> for UniqueJsonValueVisitor { let mut values = serde_json::Map::new(); while let Some((key, value)) = object.next_entry::()? { if values.insert(key.clone(), value.0).is_some() { - return Err(serde::de::Error::custom(format!( - "duplicate field `{key}`" - ))); + return Err(serde::de::Error::custom(format!("duplicate field `{key}`"))); } } Ok(UniqueJsonValue(serde_json::Value::Object(values))) @@ -2642,6 +2593,7 @@ mod tests { None, "2026-08-11T08:00:02Z", "result-proposal-job-1", + "decision-context-test", &ingress, vec!["openai-request:proposal".into()], Vec::new(), @@ -2758,7 +2710,12 @@ mod tests { repo_frontier_verdict_modeling_authority_msgpack: None, }; - let request = build_worker_model_request(&launch, DEFAULT_MODEL_PROVIDER, "gpt-5.4")?; + let request = build_worker_model_request( + &launch, + DEFAULT_MODEL_PROVIDER, + "gpt-5.4", + "reasoning-basis-test", + )?; let schema: serde_json::Value = serde_json::from_str( request .output_schema_json @@ -2818,6 +2775,7 @@ mod tests { None, "2026-07-15T10:00:00Z", "verification-result-1", + "decision-context-test", &parsed, vec!["openai-request:verification-request-1".to_string()], Vec::new(), @@ -2899,6 +2857,7 @@ mod tests { None, "2026-07-15T10:00:00Z", "planning-result-1", + "decision-context-test", &parsed, Vec::new(), Vec::new(), @@ -3032,6 +2991,7 @@ mod tests { None, "2026-07-15T10:00:00Z", "consideration-result-1", + "decision-context-test", &parsed, Vec::new(), Vec::new(), @@ -3227,6 +3187,7 @@ mod tests { None, "2026-07-15T10:00:00Z", "mind-result-1", + "decision-context-test", &parsed, Vec::new(), Vec::new(), @@ -3521,8 +3482,35 @@ mod tests { }, )?; let launch_request = load_worker_launch_request(&store, "worker-job-1")?; - let model_request = - build_worker_model_request(&launch_request, DEFAULT_MODEL_PROVIDER, "gpt-5.4")?; + let reasoning_basis = epiphany_core::worker_reasoning_basis(&store, &launch_request)?; + epiphany_core::put_reasoning_basis(&store, &reasoning_basis)?; + let model_request = build_worker_model_request( + &launch_request, + DEFAULT_MODEL_PROVIDER, + "gpt-5.4", + &reasoning_basis.basis_id, + )?; + let provider_request = openai_request_from_model_request(&model_request); + epiphany_core::open_runtime_model_execution( + &store, + RuntimeSpineSessionOptions { + session_id: "openai-worker-session-modeling-checkpoint-worker".into(), + objective: "Run the exact Modeling pass.".into(), + created_at: "2026-08-14T00:00:00Z".into(), + coordinator_note: "decision context fixture".into(), + }, + RuntimeSpineJobOptions { + job_id: "openai-worker-worker-job-1".into(), + session_id: "openai-worker-session-modeling-checkpoint-worker".into(), + role: "openai-model-adapter".into(), + created_at: "2026-08-14T00:00:00Z".into(), + summary: "Execute the Modeling request.".into(), + artifact_refs: Vec::new(), + }, + &model_request, + &provider_request, + "2026-08-14T00:00:00Z", + )?; assert_eq!( model_request.output_contract_id.as_deref(), Some(epiphany_core::ROLE_WORKER_OUTPUT_CONTRACT_ID) @@ -3718,8 +3706,12 @@ mod tests { }, )?; let launch_request = load_worker_launch_request(&store, "verification-job-1")?; - let model_request = - build_worker_model_request(&launch_request, DEFAULT_MODEL_PROVIDER, "gpt-5.4")?; + let model_request = build_worker_model_request( + &launch_request, + DEFAULT_MODEL_PROVIDER, + "gpt-5.4", + "reasoning-basis-test", + )?; let tool_names = model_request .tools .iter() @@ -3752,12 +3744,18 @@ mod tests { research_launch.binding_id = epiphany_core::EPIPHANY_RESEARCH_ROLE_BINDING_ID.to_string(); research_launch.role = epiphany_core::EPIPHANY_RESEARCH_OWNER_ROLE.to_string(); research_launch.authority_scope = "epiphany.role.research".to_string(); - let research_request = - build_worker_model_request(&research_launch, DEFAULT_MODEL_PROVIDER, "gpt-5.4")?; - assert!(!research_request - .tools - .iter() - .any(|tool| tool.name == "mcp__epiphany_public__github_file")); + let research_request = build_worker_model_request( + &research_launch, + DEFAULT_MODEL_PROVIDER, + "gpt-5.4", + "reasoning-basis-test", + )?; + assert!( + !research_request + .tools + .iter() + .any(|tool| tool.name == "mcp__epiphany_public__github_file") + ); assert!( research_request .instructions diff --git a/epiphany-tool-adapter/Cargo.toml b/epiphany-tool-adapter/Cargo.toml index 38688921..afb73f38 100644 --- a/epiphany-tool-adapter/Cargo.toml +++ b/epiphany-tool-adapter/Cargo.toml @@ -10,3 +10,4 @@ path = "src/lib.rs" [dependencies] cultcache-rs = { path = "../vendor/cultcache-rs" } serde = { version = "1", features = ["derive"] } +serde_json = "1" diff --git a/epiphany-tool-adapter/src/lib.rs b/epiphany-tool-adapter/src/lib.rs index c82caa55..4a444dd4 100644 --- a/epiphany-tool-adapter/src/lib.rs +++ b/epiphany-tool-adapter/src/lib.rs @@ -13,6 +13,27 @@ pub fn tool_invocation_receipt_key(intent_id: &str) -> String { format!("receipt:{intent_id}") } +/// Canonical lowering of a governed receipt into the exact tool-result text +/// supplied to a model. Decision audit and execution must share this owner. +pub fn receipt_output_for_model( + intent: &EpiphanyToolInvocationIntent, + receipt: &EpiphanyToolInvocationReceipt, +) -> String { + if let Some(result) = receipt.result_json.as_ref() { + return result.clone(); + } + serde_json::json!({ + "status": receipt.status, + "adapter": receipt.adapter, + "server": receipt.server, + "toolName": receipt.tool_name, + "intentId": intent.intent_id, + "receiptId": receipt.receipt_id, + "error": receipt.error, + }) + .to_string() +} + #[derive(Debug, Clone, PartialEq, Eq, DatabaseEntry)] #[cultcache( type = "epiphany.tool_capability.v0", diff --git a/notes/epiphany-current-algorithmic-map.md b/notes/epiphany-current-algorithmic-map.md index 448aa22f..55e2494c 100644 --- a/notes/epiphany-current-algorithmic-map.md +++ b/notes/epiphany-current-algorithmic-map.md @@ -4516,3 +4516,23 @@ zero. before network under `--network none`, preserved byte-identical runtime state, and emitted no receipt. Together these close the immutable public Eyes success/denial pair. +## Decision-auditable concurrent Mind foundation (2026-08-14) + +The migration seals a reasoning pass before inference. One +`EpiphanyReasoningBasis` content-addresses the exact typed source envelopes and +one closed projection variant. Every decision-bearing native model request +binds that basis. The OpenAI request is derived from the native request by the +adapter; callers cannot author an independent provider request. + +At terminal inference, `EpiphanyDecisionContext` records the final native and +provider requests plus the ordered governed tool intent/receipt versions that +the final request actually contains. Validation compares exact call identity, +tool name, arguments, lowered result bytes, runtime model binding, worker, +session, and basis. Stream deltas are not decision authority. + +`commit_mind_mutation` accepts exact strong-read envelopes and exact writes. +CultCache batch CAS merges disjoint identities, refuses changed strong reads or +same-identity divergence, and writes an immutable Mind commit receipt with the +mutation. It never rebases model output. This foundation does not yet replace +persisted thread state or aggregate RepoModel; those obsolete authorities are +the next hard deletion line. diff --git a/notes/fresh-workspace-handoff.md b/notes/fresh-workspace-handoff.md index 5dd911d6..9ead485f 100644 --- a/notes/fresh-workspace-handoff.md +++ b/notes/fresh-workspace-handoff.md @@ -8,6 +8,17 @@ The five-day shakedown remains paused for architectural consolidation. The latest behavior cut is clean and pushed on `codex/epiphany-shakedown-live` at exact `f90f1186`. +The active campaign is now the hard Decision-Auditable Concurrent Mind +migration. Its first foundation is implemented locally: typed content-addressed +reasoning bases preserve exact source envelopes and a closed projection; +terminal decision contexts preserve the exact native request, the provider +request derived from it, and every ordered governed tool intent/receipt actually +fed to the model. Worker model requests require a sealed basis, and structured +role/reorient results plus their generic job result bind the terminal context. +Exact-envelope batch CAS merges disjoint Mind documents and explicitly refuses +same-identity or changed-strong-read conflicts. Model failures, Persona stages, +retention, keyed canonical Mind state, and keyed RepoModel remain to be cut. + Operational correction: the original c011 resident and Heartbeat containers are not alive; both exited 255 on 2026-08-11. Their named volumes are quiescent and remain untouched. Historical c011 zero-zombie and organ-circuit evidence is @@ -109,16 +120,23 @@ unchanged; the lifecycle authorities above remain separate. ## Acceptance gates before shakedown resumes -1. Generalize exact current-work lifecycle ownership across Modeling and - Verification so neither family can repeat Research's stale-role-lane split. -2. Run one fresh-repository source -> Hands -> Soul -> Modeling -> Mind -> Self - capstone before claiming organism-level readiness. +1. Every model-authored terminal decision or typed failure recovers its exact + reasoning basis and terminal request after transcript/session retention. +2. Persisted thread state, aggregate RepoModel revision/hash, timestamps, + events, and role lanes can no longer manufacture or suppress current work. +3. Concurrent disjoint Persona, Hands, Modeling, evidence, and Verification + mutations merge; same semantic identity and changed strong reads refuse + without partial writes or silent model-output rebasing. +4. Run the fresh-store concurrent decision-audit capstone, including Body -> + Modeling without Eyes and Eyes only for explicit external-evidence work. ## Immediate next action -Map Modeling and Verification's exact causal currency, then give each family -one typed continuation owner consumed by Resident Self, coordinator policy, and -status. Preserve their distinct invariants; do not create a generic registry or -let the mutable role-board projection decide current work. Replay the accepted -circuits before the fresh-repository capstone. Do not touch the original c011 -volumes, add capability surfaces, or build on Yggdrasil. +Finish decision-context ownership before normalizing state: bind model-backed +failures and all three Persona stages, make their terminal commits atomic, and +make session/worker/Persona retention preserve the Basis -> Context -> decision +route after streams and transport scaffolding disappear. Then delete persisted +thread-state authority and split canonical Mind and RepoModel into semantic-key +documents under exact-envelope batch CAS. There is no writable-store migrator +or compatibility read path. Do not touch the original c011 volumes, resume the +shakedown, add capability surfaces, or build on Yggdrasil. diff --git a/state/map.yaml b/state/map.yaml index 4dda4664..81e3f82e 100644 --- a/state/map.yaml +++ b/state/map.yaml @@ -4,7 +4,7 @@ experiment: purpose: Native, inspectable agent organism with typed state and explicit organ authority. objective: - current: Consolidate the architecture exposed by the paused five-day shakedown before any new capability or live-body replacement. + current: Migrate the paused shakedown body to a decision-auditable concurrent Mind before any new capability or live-body replacement. architecture: body: Repository, runtime, service processes, stores, interfaces, and deployment substrate. @@ -70,7 +70,7 @@ current_status: yggdrasil_epiphany_service_contract: repaired_in_gamecult_ops_812b427_not_installed resident_physiology: historical_c011_exact_465af24d_zero_zombie_proof_accepted_current_containers_exited_255 unresolved_authority: Coordinator receipt, Heartbeat pulse-artifact, genuine-idle and active resource plateaus, exact-grant terminality, restart-persistent cooldown refusal, schedule/action issuance cardinality, runtime-owned typed fulfillment, all three packaged fulfilled-before-receipt lanes, genuine packaged Imagination option generation, narrow model-generation retention, published model-turn JSON ingress, semantic logical/physical retention, Persona conversation retention, stable Starfire firewall ownership, completed model-adapter session archival, normal coordinator lifecycle opening/error closure/legacy repair/archival, co-resident resident lifecycle retention, authenticated outer-worker attempt retention, deployed supervisor/projector journald ownership, Docker named-volume atomic snapshot visibility, deterministic incremental release construction, and canonical-brake refusal of direct daemon lifecycle actuation are accepted. Starfire's Docker Desktop F: bind is rejected for mutable authority after an exact packaged lockless reader observed an empty published snapshot during writer interruption. Historical migrated semantic attempts without recorded physical namespace remain durable and non-retirable. One completed Discord consequence remains blocked by the absent Yggdrasil credential. Exact aa841873 also makes legacy coordinator repair one full-snapshot transaction so a later hostile family cannot leave an earlier partial repair. - next_action: Generalize current-work lifecycle ownership for Modeling and Verification before another live discovery dresses the stale-role-lane bug in a new suit. Map each family's exact causal currency, then make coordinator, status, and Resident Self consume one typed owner per family without a generic registry or shared mutable role projection. Replay the accepted circuits, then run the fresh-repository source-to-Hands-to-Soul-to-Modeling-to-Mind-to-Self capstone. + next_action: Complete the hard Decision-Auditable Concurrent Mind cut. The first foundation now seals content-addressed typed reasoning bases and terminal decision contexts, derives the provider request from the native request through one lowering owner, authenticates exact governed tool observations, and gives disjoint Mind writes exact-envelope batch CAS. Next bind model-backed failure and Persona stage outcomes, preserve contexts through retention, then replace persisted thread state and aggregate RepoModel authority with keyed Mind documents and state-derived obligations. No migrator or dual-read path; historical stores remain read-only. active_subgoals: - Prove Persona, Self, Eyes, Modeling, Imagination, Hands, Soul, Mind, Continuity, and nervous-system behavior individually.