Move Epiphany view projection into bridge

This commit is contained in:
Meta Vens 2026-05-13 09:56:06 +01:00
commit 88f8ca2436
6 changed files with 350 additions and 280 deletions

View file

@ -11,6 +11,7 @@ pub mod retrieve;
pub mod runtime_results;
pub mod scene;
pub mod state;
pub mod view;
pub use epiphany_openai_adapter as openai_adapter;
pub use epiphany_openai_codex_spine as openai_codex_spine;

View file

@ -0,0 +1,314 @@
use std::path::Path;
use codex_app_server_protocol::*;
use codex_protocol::protocol::EpiphanyRetrievalState;
use codex_protocol::protocol::EpiphanyThreadState;
use codex_protocol::protocol::TokenUsageInfo as CoreTokenUsageInfo;
use epiphany_core::EpiphanySceneInput;
use epiphany_core::derive_scene;
use crate::context::map_epiphany_planning;
use crate::coordinator::derive_epiphany_coordinator_status;
use crate::coordinator::epiphany_reorient_finding_already_accepted;
use crate::coordinator::map_epiphany_coordinator_view;
use crate::coordinator::map_epiphany_crrc_recommendation;
use crate::coordinator::map_epiphany_roles;
use crate::coordinator::render_epiphany_roles_note;
use crate::jobs::map_epiphany_jobs;
use crate::launch::EPIPHANY_REORIENT_LAUNCH_BINDING_ID;
use crate::pressure::map_epiphany_pressure;
use crate::reorient::EpiphanyFreshnessWatcherSnapshot;
use crate::reorient::map_epiphany_freshness;
use crate::reorient::map_epiphany_reorient;
use crate::runtime_results::load_epiphany_reorient_result_snapshot;
use crate::scene::map_core_epiphany_scene_action;
use crate::scene::map_epiphany_scene;
pub fn default_epiphany_view_lenses() -> Vec<ThreadEpiphanyViewLens> {
vec![
ThreadEpiphanyViewLens::Scene,
ThreadEpiphanyViewLens::Jobs,
ThreadEpiphanyViewLens::Roles,
ThreadEpiphanyViewLens::Planning,
ThreadEpiphanyViewLens::Pressure,
ThreadEpiphanyViewLens::Reorient,
ThreadEpiphanyViewLens::Crrc,
ThreadEpiphanyViewLens::Coordinator,
]
}
pub fn epiphany_view_needs_jobs(lenses: &[ThreadEpiphanyViewLens]) -> bool {
lenses.contains(&ThreadEpiphanyViewLens::Jobs)
|| lenses.contains(&ThreadEpiphanyViewLens::Roles)
|| lenses.contains(&ThreadEpiphanyViewLens::Crrc)
|| lenses.contains(&ThreadEpiphanyViewLens::Coordinator)
}
pub fn epiphany_view_needs_reorientation_inputs(lenses: &[ThreadEpiphanyViewLens]) -> bool {
lenses.contains(&ThreadEpiphanyViewLens::Roles)
|| lenses.contains(&ThreadEpiphanyViewLens::Reorient)
|| lenses.contains(&ThreadEpiphanyViewLens::Crrc)
|| lenses.contains(&ThreadEpiphanyViewLens::Coordinator)
}
pub fn epiphany_view_needs_pressure(lenses: &[ThreadEpiphanyViewLens]) -> bool {
lenses.contains(&ThreadEpiphanyViewLens::Pressure)
|| epiphany_view_needs_reorientation_inputs(lenses)
}
pub fn epiphany_view_needs_runtime_store(lenses: &[ThreadEpiphanyViewLens]) -> bool {
lenses.contains(&ThreadEpiphanyViewLens::Roles)
|| lenses.contains(&ThreadEpiphanyViewLens::Crrc)
|| lenses.contains(&ThreadEpiphanyViewLens::Coordinator)
}
pub struct EpiphanyViewResponseInput<'a> {
pub thread_id: String,
pub lenses: Vec<ThreadEpiphanyViewLens>,
pub loaded: bool,
pub state: Option<&'a EpiphanyThreadState>,
pub retrieval_override: Option<&'a EpiphanyRetrievalState>,
pub watcher_snapshot: Option<EpiphanyFreshnessWatcherSnapshot<'a>>,
pub token_usage_info: Option<&'a CoreTokenUsageInfo>,
pub runtime_store_path: Option<&'a Path>,
}
pub async fn map_epiphany_view_response(
input: EpiphanyViewResponseInput<'_>,
) -> ThreadEpiphanyViewResponse {
let EpiphanyViewResponseInput {
thread_id,
lenses,
loaded,
state,
retrieval_override,
watcher_snapshot,
token_usage_info,
runtime_store_path,
} = input;
let needs_jobs = epiphany_view_needs_jobs(&lenses);
let needs_reorientation_inputs = epiphany_view_needs_reorientation_inputs(&lenses);
let needs_pressure = epiphany_view_needs_pressure(&lenses);
let pressure = needs_pressure.then(|| map_epiphany_pressure(token_usage_info));
let freshness = needs_reorientation_inputs
.then(|| map_epiphany_freshness(state, retrieval_override, watcher_snapshot));
let (state_revision, reorient_state_status, reorient_decision) =
if let (Some((state_revision, retrieval, graph, watcher)), Some(pressure)) =
(freshness.as_ref(), pressure.as_ref())
{
let (state_status, decision) =
map_epiphany_reorient(state, pressure, retrieval, graph, watcher);
(*state_revision, state_status, Some(decision))
} else {
(None, ThreadEpiphanyReorientStateStatus::Missing, None)
};
let jobs = if needs_jobs {
map_epiphany_jobs(state, retrieval_override)
} else {
Vec::new()
};
let reorient_job = jobs
.iter()
.find(|job| job.id == EPIPHANY_REORIENT_LAUNCH_BINDING_ID)
.cloned();
let (reorient_result_status, reorient_finding, reorient_result_note) = if runtime_store_path
.is_some()
|| lenses.contains(&ThreadEpiphanyViewLens::Crrc)
|| lenses.contains(&ThreadEpiphanyViewLens::Coordinator)
{
load_epiphany_reorient_result_snapshot(
state,
runtime_store_path,
EPIPHANY_REORIENT_LAUNCH_BINDING_ID,
)
.await
} else {
(
ThreadEpiphanyReorientResultStatus::MissingState,
None,
"Reorient result was not requested.".to_string(),
)
};
let checkpoint_present = state
.and_then(|state| state.investigation_checkpoint.as_ref())
.is_some();
let reorient_finding_accepted = reorient_finding.as_ref().is_some_and(|finding| {
state.is_some_and(|state| epiphany_reorient_finding_already_accepted(state, finding))
});
let recommendation =
if let (Some(pressure), Some(decision)) = (pressure.as_ref(), reorient_decision.as_ref()) {
Some(map_epiphany_crrc_recommendation(
loaded,
reorient_state_status,
pressure,
decision,
reorient_result_status,
checkpoint_present,
reorient_finding.is_some(),
reorient_finding_accepted,
))
} else {
None
};
let roles = if let (Some(pressure), Some(decision), Some(recommendation)) = (
pressure.as_ref(),
reorient_decision.as_ref(),
recommendation.as_ref(),
) {
Some(map_epiphany_roles(
state,
&jobs,
decision,
pressure,
recommendation,
reorient_result_status,
reorient_job.as_ref(),
))
} else {
None
};
let coordinator_response = if lenses.contains(&ThreadEpiphanyViewLens::Coordinator) {
if let (Some(pressure), Some(recommendation), Some(roles)) =
(pressure.as_ref(), recommendation.as_ref(), roles.clone())
{
let status = derive_epiphany_coordinator_status(
state,
runtime_store_path,
reorient_state_status,
pressure,
recommendation,
roles,
reorient_decision.as_ref(),
reorient_result_status,
reorient_finding.as_ref(),
checkpoint_present,
)
.await;
Some(map_epiphany_coordinator_view(
thread_id.clone(),
loaded,
reorient_state_status,
state_revision,
status,
))
} else {
None
}
} else {
None
};
ThreadEpiphanyViewResponse {
thread_id: thread_id.clone(),
scene: lenses
.contains(&ThreadEpiphanyViewLens::Scene)
.then(|| map_epiphany_scene(state, loaded, EPIPHANY_REORIENT_LAUNCH_BINDING_ID)),
jobs: if lenses.contains(&ThreadEpiphanyViewLens::Jobs) {
jobs.clone()
} else {
Vec::new()
},
roles: lenses.contains(&ThreadEpiphanyViewLens::Roles).then(|| {
let roles = roles.clone().unwrap_or_default();
ThreadEpiphanyViewRoles {
thread_id: thread_id.clone(),
source: if loaded {
ThreadEpiphanyRolesSource::Live
} else {
ThreadEpiphanyRolesSource::Stored
},
state_status: reorient_state_status,
state_revision,
note: render_epiphany_roles_note(
&roles,
reorient_state_status,
recommendation
.as_ref()
.map(|recommendation| recommendation.action)
.unwrap_or(ThreadEpiphanyCrrcAction::Continue),
),
roles,
}
}),
planning: lenses.contains(&ThreadEpiphanyViewLens::Planning).then(|| {
let (state_status, state_revision, planning, summary) = map_epiphany_planning(state);
ThreadEpiphanyViewPlanning {
thread_id: thread_id.clone(),
source: if loaded {
ThreadEpiphanyContextSource::Live
} else {
ThreadEpiphanyContextSource::Stored
},
state_status,
state_revision,
planning,
summary,
}
}),
pressure: lenses
.contains(&ThreadEpiphanyViewLens::Pressure)
.then(|| pressure.clone())
.flatten(),
reorient: lenses
.contains(&ThreadEpiphanyViewLens::Reorient)
.then(|| {
reorient_decision
.clone()
.map(|decision| ThreadEpiphanyViewReorient {
thread_id: thread_id.clone(),
source: if loaded {
ThreadEpiphanyReorientSource::Live
} else {
ThreadEpiphanyReorientSource::Stored
},
state_status: reorient_state_status,
state_revision,
decision,
})
})
.flatten(),
crrc: lenses
.contains(&ThreadEpiphanyViewLens::Crrc)
.then(|| {
let pressure = pressure.clone()?;
let decision = reorient_decision.clone()?;
let recommendation = recommendation.clone()?;
let available_actions = derive_scene(EpiphanySceneInput {
state,
loaded,
reorient_binding_id: EPIPHANY_REORIENT_LAUNCH_BINDING_ID,
})
.available_actions
.into_iter()
.map(map_core_epiphany_scene_action)
.collect();
let note = format!(
"{} Result status: {:?}. {}",
recommendation.reason, reorient_result_status, reorient_result_note
);
Some(ThreadEpiphanyViewCrrc {
thread_id: thread_id.clone(),
source: if loaded {
ThreadEpiphanyReorientSource::Live
} else {
ThreadEpiphanyReorientSource::Stored
},
state_status: reorient_state_status,
state_revision,
pressure,
decision,
recommendation,
reorient_binding_id: EPIPHANY_REORIENT_LAUNCH_BINDING_ID.to_string(),
reorient_result_status,
reorient_job: reorient_job.clone(),
reorient_finding: reorient_finding.clone(),
available_actions,
note,
})
})
.flatten(),
coordinator: coordinator_response,
lenses,
}
}

View file

@ -698,6 +698,17 @@ longer owns that coordinator policy. The root `codex_message_processor.rs`
imports for the evacuated Epiphany protocol/policy registry were cut as proof
that those names no longer belong to the host file.
The full `thread/epiphany/view` projection has now followed. App-server still
parses the thread id, loads live/stored thread state, registers watcher input,
fetches token usage, and obtains the runtime-spine store path. The actual lens
selection, freshness/pressure/reorient/CRRC/roles/coordinator composition,
scene/planning/job projection, and `ThreadEpiphanyViewResponse` construction
now live in `epiphany-codex-bridge::view`. `epiphany_read_routes.rs` is about
722 lines after this cut. The remaining read-route bodies are mostly individual
legacy read verbs (`roleResult`, `freshness`, `context`, `graphQuery`,
`reorientResult`, `retrieve`, `distill`, `propose`) plus host loading and
response/error shaping.
Also: MCP itself is allowed to be JSON. The target is not "replace MCP JSON";
the target is an Epiphany-owned boundary that speaks typed Epiphany
intent/result/receipt documents internally and normal MCP JSON-RPC externally.

Binary file not shown.

File diff suppressed because one or more lines are too long

View file

@ -8,26 +8,20 @@ use codex_core::distill_observation;
use codex_protocol::ThreadId;
use epiphany_codex_bridge::context::map_epiphany_context;
use epiphany_codex_bridge::context::map_epiphany_graph_query;
use epiphany_codex_bridge::context::map_epiphany_planning;
use epiphany_codex_bridge::coordinator::derive_epiphany_coordinator_status;
use epiphany_codex_bridge::coordinator::epiphany_reorient_finding_already_accepted;
use epiphany_codex_bridge::coordinator::map_epiphany_coordinator_view;
use epiphany_codex_bridge::coordinator::map_epiphany_crrc_recommendation;
use epiphany_codex_bridge::coordinator::map_epiphany_roles;
use epiphany_codex_bridge::coordinator::render_epiphany_roles_note;
use epiphany_codex_bridge::jobs::map_epiphany_jobs;
use epiphany_codex_bridge::launch::EPIPHANY_REORIENT_LAUNCH_BINDING_ID;
use epiphany_codex_bridge::launch::epiphany_role_binding_id;
use epiphany_codex_bridge::pressure::map_epiphany_pressure;
use epiphany_codex_bridge::reorient::map_epiphany_freshness;
use epiphany_codex_bridge::reorient::map_epiphany_reorient;
use epiphany_codex_bridge::retrieve::map_epiphany_retrieve_response;
use epiphany_codex_bridge::runtime_results::load_epiphany_reorient_result_snapshot;
use epiphany_codex_bridge::runtime_results::load_epiphany_role_result_snapshot;
use epiphany_codex_bridge::scene::map_core_epiphany_scene_action;
use epiphany_codex_bridge::scene::map_epiphany_scene;
use epiphany_core::EpiphanySceneInput;
use epiphany_core::derive_scene;
use epiphany_codex_bridge::view::EpiphanyViewResponseInput;
use epiphany_codex_bridge::view::default_epiphany_view_lenses;
use epiphany_codex_bridge::view::epiphany_view_needs_jobs;
use epiphany_codex_bridge::view::epiphany_view_needs_pressure;
use epiphany_codex_bridge::view::epiphany_view_needs_reorientation_inputs;
use epiphany_codex_bridge::view::epiphany_view_needs_runtime_store;
use epiphany_codex_bridge::view::map_epiphany_view_response;
use super::CodexMessageProcessor;
use super::ConnectionRequestId;
@ -43,16 +37,7 @@ impl CodexMessageProcessor {
) {
let ThreadEpiphanyViewParams { thread_id, lenses } = params;
let lenses = if lenses.is_empty() {
vec![
ThreadEpiphanyViewLens::Scene,
ThreadEpiphanyViewLens::Jobs,
ThreadEpiphanyViewLens::Roles,
ThreadEpiphanyViewLens::Planning,
ThreadEpiphanyViewLens::Pressure,
ThreadEpiphanyViewLens::Reorient,
ThreadEpiphanyViewLens::Crrc,
ThreadEpiphanyViewLens::Coordinator,
]
default_epiphany_view_lenses()
} else {
lenses
};
@ -80,16 +65,9 @@ impl CodexMessageProcessor {
}
};
let needs_jobs = lenses.contains(&ThreadEpiphanyViewLens::Jobs)
|| lenses.contains(&ThreadEpiphanyViewLens::Roles)
|| lenses.contains(&ThreadEpiphanyViewLens::Crrc)
|| lenses.contains(&ThreadEpiphanyViewLens::Coordinator);
let needs_reorientation_inputs = lenses.contains(&ThreadEpiphanyViewLens::Roles)
|| lenses.contains(&ThreadEpiphanyViewLens::Reorient)
|| lenses.contains(&ThreadEpiphanyViewLens::Crrc)
|| lenses.contains(&ThreadEpiphanyViewLens::Coordinator);
let needs_pressure =
lenses.contains(&ThreadEpiphanyViewLens::Pressure) || needs_reorientation_inputs;
let needs_jobs = epiphany_view_needs_jobs(&lenses);
let needs_reorientation_inputs = epiphany_view_needs_reorientation_inputs(&lenses);
let needs_pressure = epiphany_view_needs_pressure(&lenses);
let retrieval_override = if (needs_jobs || needs_reorientation_inputs)
&& thread
.epiphany_state
@ -134,40 +112,7 @@ impl CodexMessageProcessor {
} else {
None
};
let pressure = needs_pressure.then(|| map_epiphany_pressure(token_usage_info.as_ref()));
let freshness = needs_reorientation_inputs.then(|| {
map_epiphany_freshness(
thread.epiphany_state.as_ref(),
retrieval_override.as_ref(),
watcher_snapshot
.as_ref()
.map(epiphany_freshness_watcher_snapshot),
)
});
let (state_revision, reorient_state_status, reorient_decision) =
if let (Some((state_revision, retrieval, graph, watcher)), Some(pressure)) =
(freshness.as_ref(), pressure.as_ref())
{
let (state_status, decision) = map_epiphany_reorient(
thread.epiphany_state.as_ref(),
pressure,
retrieval,
graph,
watcher,
);
(*state_revision, state_status, Some(decision))
} else {
(None, ThreadEpiphanyReorientStateStatus::Missing, None)
};
let jobs = if needs_jobs {
map_epiphany_jobs(thread.epiphany_state.as_ref(), retrieval_override.as_ref())
} else {
Vec::new()
};
let runtime_store_path = if lenses.contains(&ThreadEpiphanyViewLens::Roles)
|| lenses.contains(&ThreadEpiphanyViewLens::Crrc)
|| lenses.contains(&ThreadEpiphanyViewLens::Coordinator)
{
let runtime_store_path = if epiphany_view_needs_runtime_store(&lenses) {
if let Some(loaded_thread) = loaded_thread.as_ref() {
Some(loaded_thread.epiphany_runtime_spine_store_path().await)
} else {
@ -176,220 +121,19 @@ impl CodexMessageProcessor {
} else {
None
};
let reorient_job = jobs
.iter()
.find(|job| job.id == EPIPHANY_REORIENT_LAUNCH_BINDING_ID)
.cloned();
let (reorient_result_status, reorient_finding, reorient_result_note) = if runtime_store_path
.is_some()
|| lenses.contains(&ThreadEpiphanyViewLens::Crrc)
|| lenses.contains(&ThreadEpiphanyViewLens::Coordinator)
{
load_epiphany_reorient_result_snapshot(
thread.epiphany_state.as_ref(),
runtime_store_path.as_deref(),
EPIPHANY_REORIENT_LAUNCH_BINDING_ID,
)
.await
} else {
(
ThreadEpiphanyReorientResultStatus::MissingState,
None,
"Reorient result was not requested.".to_string(),
)
};
let checkpoint_present = thread
.epiphany_state
.as_ref()
.and_then(|state| state.investigation_checkpoint.as_ref())
.is_some();
let reorient_finding_accepted = reorient_finding.as_ref().is_some_and(|finding| {
thread
.epiphany_state
.as_ref()
.is_some_and(|state| epiphany_reorient_finding_already_accepted(state, finding))
});
let recommendation = if let (Some(pressure), Some(decision)) =
(pressure.as_ref(), reorient_decision.as_ref())
{
Some(map_epiphany_crrc_recommendation(
loaded,
reorient_state_status,
pressure,
decision,
reorient_result_status,
checkpoint_present,
reorient_finding.is_some(),
reorient_finding_accepted,
))
} else {
None
};
let roles = if let (Some(pressure), Some(decision), Some(recommendation)) = (
pressure.as_ref(),
reorient_decision.as_ref(),
recommendation.as_ref(),
) {
Some(map_epiphany_roles(
thread.epiphany_state.as_ref(),
&jobs,
decision,
pressure,
recommendation,
reorient_result_status,
reorient_job.as_ref(),
))
} else {
None
};
let coordinator_response = if lenses.contains(&ThreadEpiphanyViewLens::Coordinator) {
if let (Some(pressure), Some(recommendation), Some(roles)) =
(pressure.as_ref(), recommendation.as_ref(), roles.clone())
{
let status = derive_epiphany_coordinator_status(
thread.epiphany_state.as_ref(),
runtime_store_path.as_deref(),
reorient_state_status,
pressure,
recommendation,
roles,
reorient_decision.as_ref(),
reorient_result_status,
reorient_finding.as_ref(),
checkpoint_present,
)
.await;
Some(map_epiphany_coordinator_view(
thread_id.clone(),
loaded,
reorient_state_status,
state_revision,
status,
))
} else {
None
}
} else {
None
};
let response = ThreadEpiphanyViewResponse {
let response = map_epiphany_view_response(EpiphanyViewResponseInput {
thread_id: thread_id.clone(),
scene: lenses.contains(&ThreadEpiphanyViewLens::Scene).then(|| {
map_epiphany_scene(
thread.epiphany_state.as_ref(),
loaded,
EPIPHANY_REORIENT_LAUNCH_BINDING_ID,
)
}),
jobs: if lenses.contains(&ThreadEpiphanyViewLens::Jobs) {
jobs.clone()
} else {
Vec::new()
},
roles: lenses.contains(&ThreadEpiphanyViewLens::Roles).then(|| {
let roles = roles.clone().unwrap_or_default();
ThreadEpiphanyViewRoles {
thread_id: thread_id.clone(),
source: if loaded {
ThreadEpiphanyRolesSource::Live
} else {
ThreadEpiphanyRolesSource::Stored
},
state_status: reorient_state_status,
state_revision,
note: render_epiphany_roles_note(
&roles,
reorient_state_status,
recommendation
.as_ref()
.map(|recommendation| recommendation.action)
.unwrap_or(ThreadEpiphanyCrrcAction::Continue),
),
roles,
}
}),
planning: lenses.contains(&ThreadEpiphanyViewLens::Planning).then(|| {
let (state_status, state_revision, planning, summary) =
map_epiphany_planning(thread.epiphany_state.as_ref());
ThreadEpiphanyViewPlanning {
thread_id: thread_id.clone(),
source: if loaded {
ThreadEpiphanyContextSource::Live
} else {
ThreadEpiphanyContextSource::Stored
},
state_status,
state_revision,
planning,
summary,
}
}),
pressure: lenses
.contains(&ThreadEpiphanyViewLens::Pressure)
.then(|| pressure.clone())
.flatten(),
reorient: lenses
.contains(&ThreadEpiphanyViewLens::Reorient)
.then(|| {
reorient_decision
.clone()
.map(|decision| ThreadEpiphanyViewReorient {
thread_id: thread_id.clone(),
source: if loaded {
ThreadEpiphanyReorientSource::Live
} else {
ThreadEpiphanyReorientSource::Stored
},
state_status: reorient_state_status,
state_revision,
decision,
})
})
.flatten(),
crrc: lenses
.contains(&ThreadEpiphanyViewLens::Crrc)
.then(|| {
let pressure = pressure.clone()?;
let decision = reorient_decision.clone()?;
let recommendation = recommendation.clone()?;
let available_actions = derive_scene(EpiphanySceneInput {
state: thread.epiphany_state.as_ref(),
loaded,
reorient_binding_id: EPIPHANY_REORIENT_LAUNCH_BINDING_ID,
})
.available_actions
.into_iter()
.map(map_core_epiphany_scene_action)
.collect();
let note = format!(
"{} Result status: {:?}. {}",
recommendation.reason, reorient_result_status, reorient_result_note
);
Some(ThreadEpiphanyViewCrrc {
thread_id: thread_id.clone(),
source: if loaded {
ThreadEpiphanyReorientSource::Live
} else {
ThreadEpiphanyReorientSource::Stored
},
state_status: reorient_state_status,
state_revision,
pressure,
decision,
recommendation,
reorient_binding_id: EPIPHANY_REORIENT_LAUNCH_BINDING_ID.to_string(),
reorient_result_status,
reorient_job: reorient_job.clone(),
reorient_finding: reorient_finding.clone(),
available_actions,
note,
})
})
.flatten(),
coordinator: coordinator_response,
lenses,
};
loaded,
state: thread.epiphany_state.as_ref(),
retrieval_override: retrieval_override.as_ref(),
watcher_snapshot: watcher_snapshot
.as_ref()
.map(epiphany_freshness_watcher_snapshot),
token_usage_info: token_usage_info.as_ref(),
runtime_store_path: runtime_store_path.as_deref(),
})
.await;
self.outgoing.send_response(request_id, response).await;
}