Add Epiphany runtime links
This commit is contained in:
parent
d9a63883cb
commit
7eba3e6d6d
5 changed files with 130 additions and 0 deletions
|
|
@ -76,6 +76,7 @@ use codex_protocol::protocol::EpiphanyModeState as CoreEpiphanyModeState;
|
|||
use codex_protocol::protocol::EpiphanyObservation as CoreEpiphanyObservation;
|
||||
use codex_protocol::protocol::EpiphanyPlanningState as CoreEpiphanyPlanningState;
|
||||
use codex_protocol::protocol::EpiphanyRetrievalStatus as CoreEpiphanyRetrievalStatus;
|
||||
use codex_protocol::protocol::EpiphanyRuntimeLink as CoreEpiphanyRuntimeLink;
|
||||
use codex_protocol::protocol::EpiphanyScratchPad as CoreEpiphanyScratchPad;
|
||||
use codex_protocol::protocol::EpiphanySubgoal as CoreEpiphanySubgoal;
|
||||
use codex_protocol::protocol::EpiphanyThreadState as CoreEpiphanyThreadState;
|
||||
|
|
@ -5426,6 +5427,8 @@ pub struct ThreadEpiphanyUpdatePatch {
|
|||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub acceptance_receipts: Vec<CoreEpiphanyAcceptanceReceipt>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub runtime_links: Vec<CoreEpiphanyRuntimeLink>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub observations: Vec<CoreEpiphanyObservation>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub evidence: Vec<CoreEpiphanyEvidenceRecord>,
|
||||
|
|
@ -5487,6 +5490,7 @@ pub enum ThreadEpiphanyStateUpdatedField {
|
|||
InvestigationCheckpoint,
|
||||
JobBindings,
|
||||
AcceptanceReceipts,
|
||||
RuntimeLinks,
|
||||
Observations,
|
||||
Evidence,
|
||||
Churn,
|
||||
|
|
|
|||
|
|
@ -5207,6 +5207,7 @@ impl CodexMessageProcessor {
|
|||
investigation_checkpoint: patch.investigation_checkpoint,
|
||||
job_bindings: patch.job_bindings,
|
||||
acceptance_receipts: patch.acceptance_receipts,
|
||||
runtime_links: patch.runtime_links,
|
||||
observations: patch.observations,
|
||||
evidence: patch.evidence,
|
||||
churn: patch.churn,
|
||||
|
|
@ -6567,6 +6568,7 @@ impl CodexMessageProcessor {
|
|||
investigation_checkpoint: patch.investigation_checkpoint,
|
||||
job_bindings: patch.job_bindings,
|
||||
acceptance_receipts: patch.acceptance_receipts,
|
||||
runtime_links: patch.runtime_links,
|
||||
observations: patch.observations,
|
||||
evidence,
|
||||
churn: patch.churn,
|
||||
|
|
@ -6658,6 +6660,7 @@ impl CodexMessageProcessor {
|
|||
investigation_checkpoint: patch.investigation_checkpoint,
|
||||
job_bindings: patch.job_bindings,
|
||||
acceptance_receipts: patch.acceptance_receipts,
|
||||
runtime_links: patch.runtime_links,
|
||||
observations: patch.observations,
|
||||
evidence: patch.evidence,
|
||||
churn: patch.churn,
|
||||
|
|
@ -15691,6 +15694,12 @@ fn imagination_role_accept_patch_errors(patch: &ThreadEpiphanyUpdatePatch) -> Ve
|
|||
"acceptance receipt changes are owned by roleAccept, not worker statePatch".to_string(),
|
||||
);
|
||||
}
|
||||
if !patch.runtime_links.is_empty() {
|
||||
errors.push(
|
||||
"runtime link changes are owned by launch/read-back surfaces, not worker statePatch"
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
if patch.churn.is_some() || patch.mode.is_some() {
|
||||
errors.push(
|
||||
"churn or mode changes are not allowed through imagination role acceptance".to_string(),
|
||||
|
|
@ -15753,6 +15762,12 @@ fn modeling_role_accept_patch_errors(patch: &ThreadEpiphanyUpdatePatch) -> Vec<S
|
|||
"acceptance receipt changes are owned by roleAccept, not worker statePatch".to_string(),
|
||||
);
|
||||
}
|
||||
if !patch.runtime_links.is_empty() {
|
||||
errors.push(
|
||||
"runtime link changes are owned by launch/read-back surfaces, not worker statePatch"
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
if patch.planning.is_some() {
|
||||
errors
|
||||
.push("planning changes are not allowed through modeling role acceptance".to_string());
|
||||
|
|
@ -17956,6 +17971,7 @@ fn thread_epiphany_patch_has_state_replacements(patch: &ThreadEpiphanyUpdatePatc
|
|||
|| patch.investigation_checkpoint.is_some()
|
||||
|| patch.job_bindings.is_some()
|
||||
|| !patch.acceptance_receipts.is_empty()
|
||||
|| !patch.runtime_links.is_empty()
|
||||
|| patch.churn.is_some()
|
||||
|| patch.mode.is_some()
|
||||
|| patch.planning.is_some()
|
||||
|
|
@ -18064,6 +18080,9 @@ fn epiphany_update_patch_changed_fields(
|
|||
if !patch.acceptance_receipts.is_empty() {
|
||||
fields.push(ThreadEpiphanyStateUpdatedField::AcceptanceReceipts);
|
||||
}
|
||||
if !patch.runtime_links.is_empty() {
|
||||
fields.push(ThreadEpiphanyStateUpdatedField::RuntimeLinks);
|
||||
}
|
||||
if !patch.observations.is_empty() {
|
||||
fields.push(ThreadEpiphanyStateUpdatedField::Observations);
|
||||
}
|
||||
|
|
|
|||
86
vendor/codex/codex-rs/core/src/codex_thread.rs
vendored
86
vendor/codex/codex-rs/core/src/codex_thread.rs
vendored
|
|
@ -40,6 +40,7 @@ use codex_protocol::protocol::EpiphanyModeState;
|
|||
use codex_protocol::protocol::EpiphanyObservation;
|
||||
use codex_protocol::protocol::EpiphanyPlanningState;
|
||||
use codex_protocol::protocol::EpiphanyRetrievalState;
|
||||
use codex_protocol::protocol::EpiphanyRuntimeLink;
|
||||
use codex_protocol::protocol::EpiphanyScratchPad;
|
||||
use codex_protocol::protocol::EpiphanyStateItem;
|
||||
use codex_protocol::protocol::EpiphanySubgoal;
|
||||
|
|
@ -127,6 +128,7 @@ pub struct EpiphanyStateUpdate {
|
|||
pub investigation_checkpoint: Option<EpiphanyInvestigationCheckpoint>,
|
||||
pub job_bindings: Option<Vec<EpiphanyJobBinding>>,
|
||||
pub acceptance_receipts: Vec<EpiphanyAcceptanceReceipt>,
|
||||
pub runtime_links: Vec<EpiphanyRuntimeLink>,
|
||||
pub observations: Vec<EpiphanyObservation>,
|
||||
pub evidence: Vec<EpiphanyEvidenceRecord>,
|
||||
pub churn: Option<EpiphanyChurnState>,
|
||||
|
|
@ -186,6 +188,7 @@ impl EpiphanyStateUpdate {
|
|||
&& self.investigation_checkpoint.is_none()
|
||||
&& self.job_bindings.is_none()
|
||||
&& self.acceptance_receipts.is_empty()
|
||||
&& self.runtime_links.is_empty()
|
||||
&& self.observations.is_empty()
|
||||
&& self.evidence.is_empty()
|
||||
&& self.churn.is_none()
|
||||
|
|
@ -525,15 +528,22 @@ impl CodexThread {
|
|||
launcher_job_id.as_str(),
|
||||
backend_job_id.as_str(),
|
||||
);
|
||||
let replacement_runtime_link =
|
||||
build_epiphany_runtime_link(&request, backend_job_id.as_str());
|
||||
let next_job_bindings = replace_or_append_epiphany_job_binding(
|
||||
current_state.job_bindings.clone(),
|
||||
replacement_binding,
|
||||
);
|
||||
let next_runtime_links = replace_or_append_epiphany_runtime_link(
|
||||
current_state.runtime_links.clone(),
|
||||
replacement_runtime_link,
|
||||
);
|
||||
|
||||
let validation_errors = epiphany_state_update_validation_errors(
|
||||
¤t_state,
|
||||
&EpiphanyStateUpdate {
|
||||
job_bindings: Some(next_job_bindings.clone()),
|
||||
runtime_links: next_runtime_links.clone(),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
|
@ -548,6 +558,7 @@ impl CodexThread {
|
|||
.epiphany_update_state(EpiphanyStateUpdate {
|
||||
expected_revision: request.expected_revision,
|
||||
job_bindings: Some(next_job_bindings),
|
||||
runtime_links: next_runtime_links,
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
|
|
@ -852,6 +863,23 @@ fn build_epiphany_job_launch_binding(
|
|||
}
|
||||
}
|
||||
|
||||
fn build_epiphany_runtime_link(
|
||||
request: &EpiphanyJobLaunchRequest,
|
||||
runtime_job_id: &str,
|
||||
) -> EpiphanyRuntimeLink {
|
||||
EpiphanyRuntimeLink {
|
||||
id: format!("runtime-link-{}-{runtime_job_id}", request.binding_id),
|
||||
binding_id: request.binding_id.clone(),
|
||||
surface: "jobLaunch".to_string(),
|
||||
role_id: request.owner_role.clone(),
|
||||
authority_scope: request.authority_scope.clone(),
|
||||
runtime_job_id: runtime_job_id.to_string(),
|
||||
runtime_result_id: None,
|
||||
linked_subgoal_ids: request.linked_subgoal_ids.clone(),
|
||||
linked_graph_node_ids: request.linked_graph_node_ids.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn open_epiphany_runtime_spine_job(
|
||||
store_path: &std::path::Path,
|
||||
state: &EpiphanyThreadState,
|
||||
|
|
@ -931,6 +959,18 @@ fn replace_or_append_epiphany_job_binding(
|
|||
bindings
|
||||
}
|
||||
|
||||
fn replace_or_append_epiphany_runtime_link(
|
||||
mut links: Vec<EpiphanyRuntimeLink>,
|
||||
replacement: EpiphanyRuntimeLink,
|
||||
) -> Vec<EpiphanyRuntimeLink> {
|
||||
if let Some(existing) = links.iter_mut().find(|link| link.id == replacement.id) {
|
||||
*existing = replacement;
|
||||
return links;
|
||||
}
|
||||
links.push(replacement);
|
||||
links
|
||||
}
|
||||
|
||||
fn clear_epiphany_job_binding_backend(
|
||||
mut bindings: Vec<EpiphanyJobBinding>,
|
||||
binding_index: usize,
|
||||
|
|
@ -1050,6 +1090,12 @@ fn epiphany_state_update_validation_errors(
|
|||
&known_evidence_ids,
|
||||
));
|
||||
}
|
||||
if !update.runtime_links.is_empty() {
|
||||
errors.extend(validate_epiphany_runtime_links(
|
||||
&state.runtime_links,
|
||||
&update.runtime_links,
|
||||
));
|
||||
}
|
||||
if let Some(planning) = update.planning.as_ref() {
|
||||
errors.extend(validate_epiphany_planning_state(planning));
|
||||
}
|
||||
|
|
@ -1058,6 +1104,37 @@ fn epiphany_state_update_validation_errors(
|
|||
errors
|
||||
}
|
||||
|
||||
fn validate_epiphany_runtime_links(
|
||||
existing: &[EpiphanyRuntimeLink],
|
||||
links: &[EpiphanyRuntimeLink],
|
||||
) -> Vec<String> {
|
||||
let mut errors = Vec::new();
|
||||
let mut seen_ids = existing
|
||||
.iter()
|
||||
.map(|link| link.id.as_str())
|
||||
.collect::<HashSet<_>>();
|
||||
for link in links {
|
||||
require_nonempty_update(&link.id, "runtime_link.id", &mut errors);
|
||||
require_nonempty_update(&link.binding_id, "runtime_link.binding_id", &mut errors);
|
||||
require_nonempty_update(&link.surface, "runtime_link.surface", &mut errors);
|
||||
require_nonempty_update(&link.role_id, "runtime_link.role_id", &mut errors);
|
||||
require_nonempty_update(
|
||||
&link.authority_scope,
|
||||
"runtime_link.authority_scope",
|
||||
&mut errors,
|
||||
);
|
||||
require_nonempty_update(
|
||||
&link.runtime_job_id,
|
||||
"runtime_link.runtime_job_id",
|
||||
&mut errors,
|
||||
);
|
||||
if !seen_ids.insert(link.id.as_str()) {
|
||||
errors.push(format!("runtime link id {:?} is duplicated", link.id));
|
||||
}
|
||||
}
|
||||
errors
|
||||
}
|
||||
|
||||
fn validate_epiphany_acceptance_receipts(
|
||||
existing: &[EpiphanyAcceptanceReceipt],
|
||||
receipts: &[EpiphanyAcceptanceReceipt],
|
||||
|
|
@ -1407,6 +1484,7 @@ fn apply_epiphany_state_update(
|
|||
state.job_bindings = job_bindings;
|
||||
}
|
||||
prepend_recent(&mut state.acceptance_receipts, update.acceptance_receipts);
|
||||
prepend_recent(&mut state.runtime_links, update.runtime_links);
|
||||
if let Some(churn) = update.churn {
|
||||
state.churn = Some(churn);
|
||||
}
|
||||
|
|
@ -2072,6 +2150,7 @@ mod epiphany_update_tests {
|
|||
|
||||
let binding =
|
||||
build_epiphany_job_launch_binding(&request, "epiphany-heartbeat-launch-1", "turn-1");
|
||||
let runtime_link = build_epiphany_runtime_link(&request, "turn-1");
|
||||
|
||||
assert_eq!(
|
||||
binding.backend_kind,
|
||||
|
|
@ -2084,6 +2163,13 @@ mod epiphany_update_tests {
|
|||
.as_deref()
|
||||
.is_some_and(|note| note.contains("heartbeat activation"))
|
||||
);
|
||||
assert_eq!(
|
||||
runtime_link.id,
|
||||
"runtime-link-modeling-checkpoint-worker-turn-1"
|
||||
);
|
||||
assert_eq!(runtime_link.runtime_job_id, "turn-1");
|
||||
assert_eq!(runtime_link.runtime_result_id, None);
|
||||
assert_eq!(runtime_link.role_id, "epiphany-modeler");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -339,6 +339,7 @@ fn sample_epiphany_state_for_prompt() -> EpiphanyThreadState {
|
|||
investigation_checkpoint: None,
|
||||
job_bindings: Vec::new(),
|
||||
acceptance_receipts: Vec::new(),
|
||||
runtime_links: Vec::new(),
|
||||
scratch: Some(EpiphanyScratchPad {
|
||||
summary: Some("Inject the state as a developer fragment, not as a user message.".to_string()),
|
||||
hypothesis: Some("Keeping the block bounded will help resume without making prompt sludge.".to_string()),
|
||||
|
|
|
|||
20
vendor/codex/codex-rs/protocol/src/protocol.rs
vendored
20
vendor/codex/codex-rs/protocol/src/protocol.rs
vendored
|
|
@ -3027,6 +3027,9 @@ pub struct EpiphanyThreadState {
|
|||
#[ts(type = "Array<EpiphanyAcceptanceReceipt>")]
|
||||
pub acceptance_receipts: Vec<EpiphanyAcceptanceReceipt>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
#[ts(type = "Array<EpiphanyRuntimeLink>")]
|
||||
pub runtime_links: Vec<EpiphanyRuntimeLink>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
#[ts(type = "Array<EpiphanyObservation>")]
|
||||
pub observations: Vec<EpiphanyObservation>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
|
|
@ -3067,6 +3070,23 @@ pub struct EpiphanyAcceptanceReceipt {
|
|||
pub summary: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS, Default)]
|
||||
pub struct EpiphanyRuntimeLink {
|
||||
pub id: String,
|
||||
pub binding_id: String,
|
||||
pub surface: String,
|
||||
pub role_id: String,
|
||||
pub authority_scope: String,
|
||||
pub runtime_job_id: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(type = "string | null")]
|
||||
pub runtime_result_id: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub linked_subgoal_ids: Vec<String>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub linked_graph_node_ids: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS, Default)]
|
||||
pub struct EpiphanySubgoal {
|
||||
pub id: String,
|
||||
|
|
|
|||
Reference in a new issue