Type Epiphany worker launch cargo

This commit is contained in:
Meta Vens 2026-05-12 23:06:58 +01:00
commit 71d14ba245
9 changed files with 335 additions and 136 deletions

View file

@ -190,6 +190,7 @@ pub use surfaces::EpiphanyReorientInput;
pub use surfaces::EpiphanyReorientPressureLevel;
pub use surfaces::EpiphanyReorientReason;
pub use surfaces::EpiphanyReorientStateStatus;
pub use surfaces::EpiphanyReorientWorkerLaunchDocument;
pub use surfaces::EpiphanyRetrievalFreshness;
pub use surfaces::EpiphanyRetrievalFreshnessStatus;
pub use surfaces::EpiphanyRoleAcceptanceFinding;
@ -203,6 +204,7 @@ pub use surfaces::EpiphanyRoleFindingInterpretation;
pub use surfaces::EpiphanyRoleResultRoleId;
pub use surfaces::EpiphanyRoleSelfPersistenceReview;
pub use surfaces::EpiphanyRoleSelfPersistenceStatus;
pub use surfaces::EpiphanyRoleWorkerLaunchDocument;
pub use surfaces::EpiphanyScene;
pub use surfaces::EpiphanySceneAction;
pub use surfaces::EpiphanySceneChurn;
@ -216,6 +218,9 @@ pub use surfaces::EpiphanySceneSource;
pub use surfaces::EpiphanySceneStateStatus;
pub use surfaces::EpiphanySceneStatusCount;
pub use surfaces::EpiphanySceneSubgoal;
pub use surfaces::EpiphanyWorkerLaunchDocument;
pub use surfaces::REORIENT_WORKER_OUTPUT_CONTRACT_ID;
pub use surfaces::ROLE_WORKER_OUTPUT_CONTRACT_ID;
pub use surfaces::build_reorient_acceptance_bundle;
pub use surfaces::build_role_acceptance_bundle;
pub use surfaces::coordinator_automation_action;

View file

@ -9,6 +9,7 @@ mod reorient;
mod role_board;
mod role_result;
mod scene;
mod worker_launch;
pub use coordinator::EpiphanyCoordinatorAction;
pub use coordinator::EpiphanyCoordinatorAutomationAction;
@ -120,3 +121,8 @@ pub use scene::EpiphanySceneStateStatus;
pub use scene::EpiphanySceneStatusCount;
pub use scene::EpiphanySceneSubgoal;
pub use scene::derive_scene;
pub use worker_launch::EpiphanyReorientWorkerLaunchDocument;
pub use worker_launch::EpiphanyRoleWorkerLaunchDocument;
pub use worker_launch::EpiphanyWorkerLaunchDocument;
pub use worker_launch::REORIENT_WORKER_OUTPUT_CONTRACT_ID;
pub use worker_launch::ROLE_WORKER_OUTPUT_CONTRACT_ID;

View file

@ -0,0 +1,122 @@
use codex_protocol::protocol::EpiphanyChurnState;
use codex_protocol::protocol::EpiphanyCodeRef;
use codex_protocol::protocol::EpiphanyEvidenceRecord;
use codex_protocol::protocol::EpiphanyGraphCheckpoint;
use codex_protocol::protocol::EpiphanyGraphFrontier;
use codex_protocol::protocol::EpiphanyGraphs;
use codex_protocol::protocol::EpiphanyInvariant;
use codex_protocol::protocol::EpiphanyInvestigationCheckpoint;
use codex_protocol::protocol::EpiphanyObservation;
use codex_protocol::protocol::EpiphanyPlanningState;
use codex_protocol::protocol::EpiphanyScratchPad;
use codex_protocol::protocol::EpiphanySubgoal;
use serde::Deserialize;
use serde::Serialize;
pub const ROLE_WORKER_OUTPUT_CONTRACT_ID: &str = "epiphany.worker.role_result.v0";
pub const REORIENT_WORKER_OUTPUT_CONTRACT_ID: &str = "epiphany.worker.reorient_result.v0";
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", tag = "documentKind")]
pub enum EpiphanyWorkerLaunchDocument {
Role(EpiphanyRoleWorkerLaunchDocument),
Reorient(EpiphanyReorientWorkerLaunchDocument),
}
impl EpiphanyWorkerLaunchDocument {
pub fn output_contract_id(&self) -> &'static str {
match self {
Self::Role(_) => ROLE_WORKER_OUTPUT_CONTRACT_ID,
Self::Reorient(_) => REORIENT_WORKER_OUTPUT_CONTRACT_ID,
}
}
pub fn thread_id(&self) -> &str {
match self {
Self::Role(document) => &document.thread_id,
Self::Reorient(document) => &document.thread_id,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EpiphanyRoleWorkerLaunchDocument {
pub thread_id: String,
pub role_id: String,
pub state_revision: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub objective: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub active_subgoal_id: Option<String>,
#[serde(default)]
pub active_subgoals: Vec<EpiphanySubgoal>,
#[serde(default)]
pub active_graph_node_ids: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub investigation_checkpoint: Option<EpiphanyInvestigationCheckpoint>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub scratch: Option<EpiphanyScratchPad>,
#[serde(default)]
pub invariants: Vec<EpiphanyInvariant>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub graphs: Option<EpiphanyGraphs>,
#[serde(default)]
pub recent_evidence: Vec<EpiphanyEvidenceRecord>,
#[serde(default)]
pub recent_observations: Vec<EpiphanyObservation>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub graph_frontier: Option<EpiphanyGraphFrontier>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub graph_checkpoint: Option<EpiphanyGraphCheckpoint>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub planning: Option<EpiphanyPlanningState>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub churn: Option<EpiphanyChurnState>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EpiphanyReorientWorkerLaunchDocument {
pub thread_id: String,
pub mode: String,
pub checkpoint_id: String,
pub checkpoint_kind: String,
pub checkpoint_disposition: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub checkpoint_focus: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub checkpoint_summary: Option<String>,
pub checkpoint_next_action: String,
#[serde(default)]
pub checkpoint_open_questions: Vec<String>,
#[serde(default)]
pub checkpoint_evidence_ids: Vec<String>,
#[serde(default)]
pub checkpoint_code_refs: Vec<EpiphanyCodeRef>,
#[serde(default)]
pub decision_reasons: Vec<String>,
pub decision_note: String,
pub pressure_level: String,
pub retrieval_status: String,
pub graph_status: String,
pub watcher_status: String,
#[serde(default)]
pub checkpoint_dirty_paths: Vec<String>,
#[serde(default)]
pub checkpoint_changed_paths: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub scratch: Option<EpiphanyScratchPad>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub graphs: Option<EpiphanyGraphs>,
#[serde(default)]
pub recent_evidence: Vec<EpiphanyEvidenceRecord>,
#[serde(default)]
pub recent_observations: Vec<EpiphanyObservation>,
#[serde(default)]
pub active_frontier_node_ids: Vec<String>,
#[serde(default)]
pub linked_subgoal_ids: Vec<String>,
#[serde(default)]
pub linked_graph_node_ids: Vec<String>,
}

View file

@ -381,14 +381,20 @@ inventory and ledger now exist:
`AgentSelfPatch` document reviewed by the same contract used by agent memory.
The legacy app-server protocol projection serializes it back to JSON only at
the quarantine wall.
- Second cut landed: `EpiphanyJobLaunchRequest` in core no longer accepts
`input_json` or `output_schema_json`. It carries a typed
`EpiphanyWorkerLaunchDocument` plus `output_contract_id`. Role and reorient
launch helpers build typed documents directly; the old app-server protocol
`input_json` is now hostile ingress that must parse into the typed document
before core sees it.
Continue with worker launch/result cargo. Replace
`EpiphanyJobLaunchRequest.input_json` and `output_schema_json` with typed
launch/result documents plus schema ids in `epiphany-core`, then make
`codex_message_processor.rs` project those documents instead of assembling
loose JSON blobs. Do not resume Rider, Unity, Aquarium, Face, dogfood,
planning, app, skill, marketplace, or bridge expansion until this organ is
being cut cleanly.
Continue at the protocol edge. Replace
`ThreadEpiphanyJobLaunchParams.input_json` / `output_schema_json` with CultNet
typed launch-intent messages, then replace runtime job result JSON projections
(`runtime_job_result_to_role_json` and `runtime_job_result_to_reorient_json`)
with typed role/reorient finding documents. Do not resume Rider, Unity,
Aquarium, Face, dogfood, planning, app, skill, marketplace, or bridge expansion
until this organ is being cut cleanly.
The Phase 6 freshness slice is landed. It exposes read-only
`thread/epiphany/freshness` from live retrieval summaries plus graph

View file

@ -19,12 +19,12 @@ Rule: JSON is allowed at external schema/wire boundaries. Inside Epiphany, live
## Internal JSON To Purge
- `vendor/codex/codex-rs/core/src/codex_thread.rs`
- Current mechanism: `EpiphanyJobLaunchRequest.input_json` and `output_schema_json`.
- Former mechanism: `EpiphanyJobLaunchRequest.input_json` and `output_schema_json`.
- Real need: launch role workers with structured instructions and expected response shape.
- What breaks if deleted: current job launch path cannot carry worker input/schema to Codex runtime.
- Essential or bad ownership: bad ownership. Worker launch intent should be a typed document plus schema id, not two loose blobs.
- Simpler architecture: `EpiphanyWorkerLaunchDocument` in CultCache/CultNet, with typed payload and `output_contract_id`.
- Files: `vendor/codex/codex-rs/core/src/codex_thread.rs`, `vendor/codex/codex-rs/app-server/src/codex_message_processor.rs`, `vendor/codex/codex-rs/app-server-protocol/src/protocol/v2.rs`, `epiphany-core/src/runtime_spine.rs`.
- What broke when deleted: app-server helper tests and the manual launch handler had to stop treating launch cargo as loose JSON.
- Essential or bad ownership: bad ownership. The core request now carries `EpiphanyWorkerLaunchDocument` plus `output_contract_id`.
- Remaining contamination: `ThreadEpiphanyJobLaunchParams` still accepts `input_json` at the legacy app-server protocol edge and immediately parses it as hostile ingress. Final shape should be CultNet typed launch intent, not app-server JSON-RPC.
- Files: `epiphany-core/src/surfaces/worker_launch.rs`, `vendor/codex/codex-rs/core/src/codex_thread.rs`, `vendor/codex/codex-rs/app-server/src/codex_message_processor.rs`, `vendor/codex/codex-rs/app-server-protocol/src/protocol/v2.rs`.
- `epiphany-core/src/surfaces/role_result.rs`
- Current mechanism: `state_patch` and `self_patch` are `serde_json::Value`; `selfPatch` is manually revalidated here.
- Real need: interpret worker findings and review bounded lane-local memory petitions.
@ -58,7 +58,7 @@ Rule: JSON is allowed at external schema/wire boundaries. Inside Epiphany, live
## Ranking
1. Done: purge `selfPatch` as internal JSON. Role-result interpretation now stores the typed `AgentSelfPatch` document and shares the agent-memory contract; JSON remains only at the legacy app-server projection.
2. Replace worker launch `input_json` and `output_schema_json` with typed launch documents and schema ids.
2. Partly done: core worker launch `input_json` and `output_schema_json` are replaced by typed launch documents and output contract ids. Remaining cut is the protocol edge.
3. Replace role `statePatch` JSON with typed map/planning/graph patch documents.
4. Seal MCP JSON behind a CultNet MCP adapter.
5. Audit heartbeat cognition values and either type them or explicitly expire them.

Binary file not shown.

View file

@ -278,5 +278,5 @@ rejected_paths:
reason: making Epiphany cleaner inside Codex still preserves the wrong organ; Codex must be starved down to OpenAI subscription auth/model transport, while Epiphany data and subsystem communication move to typed CultCache/CultNet.
current_status:
summary: The Codex starvation pass has its first receipts. `notes/codex-auth-spine-inventory.md` maps the minimal vendored OpenAI auth/model-call spine; `notes/json-contamination-ledger.md` classifies the major Epiphany JSON uses by allowed edge, quarantine, or internal contamination. The first cut moved role-result `selfPatch` handling from raw JSON plus duplicated validators to the typed `AgentSelfPatch` document contract, with JSON serialization retained only at the legacy app-server protocol projection.
next_action: Continue the liberation pass by replacing worker launch/result cargo: remove `EpiphanyJobLaunchRequest.input_json` and `output_schema_json` as internal authority, define typed launch/result documents plus schema ids in `epiphany-core`, and make `codex_message_processor.rs` project those documents instead of assembling loose JSON blobs.
summary: The Codex starvation pass has two receipts. `notes/codex-auth-spine-inventory.md` maps the minimal vendored OpenAI auth/model-call spine; `notes/json-contamination-ledger.md` classifies the major Epiphany JSON uses by allowed edge, quarantine, or internal contamination. Role-result `selfPatch` now uses typed `AgentSelfPatch` internally. Core worker launch now uses `EpiphanyWorkerLaunchDocument` plus `output_contract_id` instead of `input_json` / `output_schema_json`; legacy app-server JSON is restricted to hostile ingress pending CultNet replacement.
next_action: Continue the liberation pass at the protocol edge: replace `ThreadEpiphanyJobLaunchParams.input_json` / `output_schema_json` with CultNet typed launch-intent messages, then replace runtime job result JSON projections with typed role/reorient finding documents instead of `runtime_job_result_to_role_json` and `runtime_job_result_to_reorient_json`.

View file

@ -549,6 +549,7 @@ use epiphany_core::EpiphanyReorientInput;
use epiphany_core::EpiphanyReorientPressureLevel as CoreEpiphanyReorientPressureLevel;
use epiphany_core::EpiphanyReorientReason as CoreEpiphanyReorientReason;
use epiphany_core::EpiphanyReorientStateStatus as CoreEpiphanyReorientStateStatus;
use epiphany_core::EpiphanyReorientWorkerLaunchDocument;
use epiphany_core::EpiphanyRetrievalFreshness as CoreEpiphanyRetrievalFreshness;
use epiphany_core::EpiphanyRetrievalFreshnessStatus as CoreEpiphanyRetrievalFreshnessStatus;
use epiphany_core::EpiphanyRoleAcceptanceFinding;
@ -562,6 +563,7 @@ use epiphany_core::EpiphanyRoleFindingInterpretation;
use epiphany_core::EpiphanyRoleResultRoleId;
use epiphany_core::EpiphanyRoleSelfPersistenceReview as CoreEpiphanyRoleSelfPersistenceReview;
use epiphany_core::EpiphanyRoleSelfPersistenceStatus as CoreEpiphanyRoleSelfPersistenceStatus;
use epiphany_core::EpiphanyRoleWorkerLaunchDocument;
use epiphany_core::EpiphanyRuntimeJobResult;
use epiphany_core::EpiphanyRuntimeJobSnapshot;
use epiphany_core::EpiphanyRuntimeJobStatus;
@ -578,6 +580,7 @@ use epiphany_core::EpiphanySceneSource as CoreEpiphanySceneSource;
use epiphany_core::EpiphanySceneStateStatus as CoreEpiphanySceneStateStatus;
use epiphany_core::EpiphanySceneStatusCount as CoreEpiphanySceneStatusCount;
use epiphany_core::EpiphanySceneSubgoal as CoreEpiphanySceneSubgoal;
use epiphany_core::EpiphanyWorkerLaunchDocument;
use epiphany_core::build_reorient_acceptance_bundle;
use epiphany_core::build_role_acceptance_bundle;
use epiphany_core::coordinator_automation_action;
@ -6381,7 +6384,7 @@ impl CodexMessageProcessor {
linked_graph_node_ids,
instruction,
input_json,
output_schema_json,
output_schema_json: _,
max_runtime_seconds,
} = params;
@ -6406,6 +6409,19 @@ impl CodexMessageProcessor {
}
};
let launch_document =
match serde_json::from_value::<EpiphanyWorkerLaunchDocument>(input_json) {
Ok(document) => document,
Err(err) => {
self.send_invalid_request_error(
request_id,
format!("invalid Epiphany job launch document: {err}"),
)
.await;
return;
}
};
let output_contract_id = launch_document.output_contract_id().to_string();
let changed_fields = epiphany_job_launch_changed_fields();
let launched = match thread
.epiphany_launch_job(EpiphanyJobLaunchRequest {
@ -6418,8 +6434,8 @@ impl CodexMessageProcessor {
linked_subgoal_ids,
linked_graph_node_ids,
instruction,
input_json,
output_schema_json,
launch_document,
output_contract_id,
max_runtime_seconds,
})
.await
@ -12841,52 +12857,51 @@ fn build_epiphany_role_launch_request(
let owner_role = epiphany_role_owner(role_id)?;
let linked_subgoal_ids = epiphany_active_subgoal_ids(Some(state));
let linked_graph_node_ids = epiphany_active_graph_node_ids(Some(state));
let (scope, authority_scope, instruction, output_schema_json) = match role_id {
let (scope, authority_scope, instruction) = match role_id {
ThreadEpiphanyRoleId::Imagination => (
"role-scoped planning synthesis",
"epiphany.role.imagination",
build_epiphany_role_launch_instruction(role_id),
epiphany_role_launch_output_schema(role_id),
),
ThreadEpiphanyRoleId::Modeling => (
"role-scoped modeling/checkpoint maintenance",
"epiphany.role.modeling",
build_epiphany_role_launch_instruction(role_id),
epiphany_role_launch_output_schema(role_id),
),
ThreadEpiphanyRoleId::Verification => (
"role-scoped verification/review",
"epiphany.role.verification",
build_epiphany_role_launch_instruction(role_id),
epiphany_role_launch_output_schema(role_id),
),
ThreadEpiphanyRoleId::Implementation | ThreadEpiphanyRoleId::Reorientation => {
return Err(epiphany_role_binding_id(role_id).unwrap_err());
}
};
let input_json = serde_json::json!({
"threadId": thread_id,
"roleId": epiphany_role_label(role_id),
"stateRevision": state.revision,
"objective": &state.objective,
"activeSubgoalId": &state.active_subgoal_id,
"activeSubgoals": state
let launch_document = EpiphanyWorkerLaunchDocument::Role(EpiphanyRoleWorkerLaunchDocument {
thread_id: thread_id.to_string(),
role_id: epiphany_role_label(role_id).to_string(),
state_revision: state.revision,
objective: state.objective.clone(),
active_subgoal_id: state.active_subgoal_id.clone(),
active_subgoals: state
.subgoals
.iter()
.filter(|subgoal| Some(subgoal.id.as_str()) == state.active_subgoal_id.as_deref())
.collect::<Vec<_>>(),
"activeGraphNodeIds": linked_graph_node_ids,
"investigationCheckpoint": &state.investigation_checkpoint,
"scratch": &state.scratch,
"invariants": &state.invariants,
"graphs": &state.graphs,
"recentEvidence": state.recent_evidence.iter().take(8).collect::<Vec<_>>(),
"recentObservations": state.observations.iter().take(8).collect::<Vec<_>>(),
"graphFrontier": &state.graph_frontier,
"graphCheckpoint": &state.graph_checkpoint,
"planning": &state.planning,
"churn": &state.churn,
.cloned()
.collect(),
active_graph_node_ids: linked_graph_node_ids.clone(),
investigation_checkpoint: state.investigation_checkpoint.clone(),
scratch: state.scratch.clone(),
invariants: state.invariants.clone(),
graphs: Some(state.graphs.clone()),
recent_evidence: state.recent_evidence.iter().take(8).cloned().collect(),
recent_observations: state.observations.iter().take(8).cloned().collect(),
graph_frontier: state.graph_frontier.clone(),
graph_checkpoint: state.graph_checkpoint.clone(),
planning: Some(state.planning.clone()),
churn: state.churn.clone(),
});
let output_contract_id = launch_document.output_contract_id().to_string();
Ok(EpiphanyJobLaunchRequest {
expected_revision,
@ -12898,8 +12913,8 @@ fn build_epiphany_role_launch_request(
linked_subgoal_ids,
linked_graph_node_ids: epiphany_active_graph_node_ids(Some(state)),
instruction,
input_json,
output_schema_json: Some(output_schema_json),
launch_document,
output_contract_id,
max_runtime_seconds,
})
}
@ -13149,50 +13164,50 @@ fn build_epiphany_reorient_launch_request(
ThreadEpiphanyReorientAction::Regather => "reorient-guided checkpoint regather",
};
let instruction = build_epiphany_reorient_launch_instruction(decision.action);
let input_json = serde_json::json!({
"threadId": thread_id,
"mode": reorient_action_label(decision.action),
"checkpointId": checkpoint.checkpoint_id.clone(),
"checkpointKind": checkpoint.kind.clone(),
"checkpointDisposition": investigation_disposition_label(checkpoint.disposition),
"checkpointFocus": checkpoint.focus.clone(),
"checkpointSummary": checkpoint.summary.clone(),
"checkpointNextAction": checkpoint_next_action,
"checkpointOpenQuestions": checkpoint.open_questions.clone(),
"checkpointEvidenceIds": checkpoint.evidence_ids.clone(),
"checkpointCodeRefs": checkpoint
.code_refs
.iter()
.map(epiphany_code_ref_json)
.collect::<Vec<_>>(),
"decisionReasons": decision
.reasons
.iter()
.map(|reason| reorient_reason_label(*reason))
.collect::<Vec<_>>(),
"decisionNote": decision.note.clone(),
"pressureLevel": pressure_level_label(decision.pressure_level),
"retrievalStatus": retrieval_freshness_status_label(decision.retrieval_status),
"graphStatus": graph_freshness_status_label(decision.graph_status),
"watcherStatus": invalidation_status_label(decision.watcher_status),
"checkpointDirtyPaths": decision
.checkpoint_dirty_paths
.iter()
.map(path_to_display_string)
.collect::<Vec<_>>(),
"checkpointChangedPaths": decision
.checkpoint_changed_paths
.iter()
.map(path_to_display_string)
.collect::<Vec<_>>(),
"scratch": &state.scratch,
"graphs": &state.graphs,
"recentEvidence": state.recent_evidence.iter().take(8).collect::<Vec<_>>(),
"recentObservations": state.observations.iter().take(8).collect::<Vec<_>>(),
"activeFrontierNodeIds": decision.active_frontier_node_ids.clone(),
"linkedSubgoalIds": linked_subgoal_ids,
"linkedGraphNodeIds": linked_graph_node_ids,
});
let launch_document =
EpiphanyWorkerLaunchDocument::Reorient(EpiphanyReorientWorkerLaunchDocument {
thread_id: thread_id.to_string(),
mode: reorient_action_label(decision.action).to_string(),
checkpoint_id: checkpoint.checkpoint_id.clone(),
checkpoint_kind: checkpoint.kind.clone(),
checkpoint_disposition: investigation_disposition_label(checkpoint.disposition)
.to_string(),
checkpoint_focus: Some(checkpoint.focus.clone()),
checkpoint_summary: checkpoint.summary.clone(),
checkpoint_next_action,
checkpoint_open_questions: checkpoint.open_questions.clone(),
checkpoint_evidence_ids: checkpoint.evidence_ids.clone(),
checkpoint_code_refs: checkpoint.code_refs.clone(),
decision_reasons: decision
.reasons
.iter()
.map(|reason| reorient_reason_label(*reason).to_string())
.collect(),
decision_note: decision.note.clone(),
pressure_level: pressure_level_label(decision.pressure_level).to_string(),
retrieval_status: retrieval_freshness_status_label(decision.retrieval_status)
.to_string(),
graph_status: graph_freshness_status_label(decision.graph_status).to_string(),
watcher_status: invalidation_status_label(decision.watcher_status).to_string(),
checkpoint_dirty_paths: decision
.checkpoint_dirty_paths
.iter()
.map(path_to_display_string)
.collect(),
checkpoint_changed_paths: decision
.checkpoint_changed_paths
.iter()
.map(path_to_display_string)
.collect(),
scratch: state.scratch.clone(),
graphs: Some(state.graphs.clone()),
recent_evidence: state.recent_evidence.iter().take(8).cloned().collect(),
recent_observations: state.observations.iter().take(8).cloned().collect(),
active_frontier_node_ids: decision.active_frontier_node_ids.clone(),
linked_subgoal_ids: linked_subgoal_ids.clone(),
linked_graph_node_ids: linked_graph_node_ids.clone(),
});
let output_contract_id = launch_document.output_contract_id().to_string();
EpiphanyJobLaunchRequest {
expected_revision,
@ -13208,8 +13223,8 @@ fn build_epiphany_reorient_launch_request(
.chain(decision.active_frontier_node_ids.iter().cloned()),
),
instruction,
input_json,
output_schema_json: Some(epiphany_reorient_launch_output_schema()),
launch_document,
output_contract_id,
max_runtime_seconds,
}
}
@ -14873,18 +14888,6 @@ fn reorient_finding_investigation_checkpoint(
checkpoint
}
fn epiphany_code_ref_json(
code_ref: &codex_protocol::protocol::EpiphanyCodeRef,
) -> serde_json::Value {
serde_json::json!({
"path": path_to_display_string(code_ref.path.as_path()),
"startLine": code_ref.start_line,
"endLine": code_ref.end_line,
"symbol": code_ref.symbol,
"note": code_ref.note,
})
}
fn path_to_display_string(path: impl AsRef<Path>) -> String {
path.as_ref().to_string_lossy().into_owned()
}
@ -19284,8 +19287,12 @@ mod tests {
assert_eq!(imagination.binding_id, EPIPHANY_IMAGINATION_ROLE_BINDING_ID);
assert_eq!(imagination.owner_role, EPIPHANY_IMAGINATION_OWNER_ROLE);
assert_eq!(imagination.authority_scope, "epiphany.role.imagination");
assert_eq!(imagination.input_json["roleId"], "imagination");
assert!(imagination.input_json.get("planning").is_some());
let EpiphanyWorkerLaunchDocument::Role(imagination_document) = &imagination.launch_document
else {
panic!("imagination should build a role launch document");
};
assert_eq!(imagination_document.role_id, "imagination");
assert!(imagination_document.planning.is_some());
assert!(
imagination
.instruction
@ -19296,13 +19303,17 @@ mod tests {
.instruction
.contains("Imagination of the machine")
);
assert!(imagination.instruction.contains("best version of itself"));
assert!(
imagination
.instruction
.contains("reviewable `thread/epiphany/update` patch")
);
let imagination_schema = imagination.output_schema_json.as_ref().unwrap();
assert_eq!(
imagination.output_contract_id,
"epiphany.worker.role_result.v0"
);
let imagination_schema =
epiphany_role_launch_output_schema(ThreadEpiphanyRoleId::Imagination);
assert!(imagination_schema["properties"].get("selfPatch").is_some());
assert!(imagination_schema["properties"].get("statePatch").is_some());
assert_eq!(
@ -19328,9 +19339,13 @@ mod tests {
assert_eq!(modeling.authority_scope, "epiphany.role.modeling");
assert_eq!(modeling.linked_subgoal_ids, vec!["sg-1".to_string()]);
assert_eq!(modeling.linked_graph_node_ids, vec!["node-1".to_string()]);
assert_eq!(modeling.input_json["roleId"], "modeling");
assert!(modeling.input_json.get("graphs").is_some());
assert!(modeling.input_json.get("recentObservations").is_some());
let EpiphanyWorkerLaunchDocument::Role(modeling_document) = &modeling.launch_document
else {
panic!("modeling should build a role launch document");
};
assert_eq!(modeling_document.role_id, "modeling");
assert!(modeling_document.graphs.is_some());
assert!(modeling_document.recent_observations.is_empty());
assert!(modeling.instruction.contains("Epiphany Persistent Memory"));
assert!(modeling.instruction.contains("Body of the machine"));
assert!(modeling.instruction.contains("coherent anatomy"));
@ -19340,7 +19355,11 @@ mod tests {
.contains("`statePatch` is part of the modeling job")
);
assert!(modeling.instruction.contains("optional `selfPatch`"));
let modeling_schema = modeling.output_schema_json.as_ref().unwrap();
assert_eq!(
modeling.output_contract_id,
"epiphany.worker.role_result.v0"
);
let modeling_schema = epiphany_role_launch_output_schema(ThreadEpiphanyRoleId::Modeling);
assert!(modeling_schema["properties"].get("openQuestions").is_some());
assert!(modeling_schema["properties"].get("selfPatch").is_some());
assert!(modeling_schema["properties"].get("statePatch").is_some());
@ -19376,7 +19395,12 @@ mod tests {
);
assert_eq!(verification.owner_role, EPIPHANY_VERIFICATION_OWNER_ROLE);
assert_eq!(verification.authority_scope, "epiphany.role.verification");
assert_eq!(verification.input_json["roleId"], "verification");
let EpiphanyWorkerLaunchDocument::Role(verification_document) =
&verification.launch_document
else {
panic!("verification should build a role launch document");
};
assert_eq!(verification_document.role_id, "verification");
assert!(
verification
.instruction
@ -19384,23 +19408,17 @@ mod tests {
);
assert!(verification.instruction.contains("Soul of the machine"));
assert!(verification.instruction.contains("falsify"));
let verification_schema =
epiphany_role_launch_output_schema(ThreadEpiphanyRoleId::Verification);
assert!(
verification.output_schema_json.as_ref().unwrap()["properties"]
verification_schema["properties"]
.get("evidenceGaps")
.is_some()
);
assert!(verification_schema["properties"].get("risks").is_some());
assert!(verification_schema["properties"].get("selfPatch").is_some());
assert!(
verification.output_schema_json.as_ref().unwrap()["properties"]
.get("risks")
.is_some()
);
assert!(
verification.output_schema_json.as_ref().unwrap()["properties"]
.get("selfPatch")
.is_some()
);
assert!(
verification.output_schema_json.as_ref().unwrap()["properties"]
verification_schema["properties"]
.get("statePatch")
.is_none()
);
@ -19456,17 +19474,28 @@ mod tests {
assert_eq!(request.binding_id, EPIPHANY_REORIENT_LAUNCH_BINDING_ID);
assert_eq!(request.owner_role, EPIPHANY_REORIENT_OWNER_ROLE);
assert_eq!(request.authority_scope, "epiphany.reorient.resume");
let EpiphanyWorkerLaunchDocument::Reorient(reorient_document) = &request.launch_document
else {
panic!("reorient should build a reorient launch document");
};
assert_eq!(
request.input_json["scratch"]["summary"],
"Carry the current seam across compaction."
reorient_document
.scratch
.as_ref()
.and_then(|scratch| scratch.summary.as_deref()),
Some("Carry the current seam across compaction.")
);
assert!(request.input_json.get("graphs").is_some());
assert!(request.input_json.get("recentEvidence").is_some());
assert!(request.input_json.get("recentObservations").is_some());
assert!(reorient_document.graphs.is_some());
assert!(reorient_document.recent_evidence.is_empty());
assert!(reorient_document.recent_observations.is_empty());
assert!(request.instruction.contains("Epiphany Persistent Memory"));
assert!(request.instruction.contains("Life across sleep"));
assert!(request.instruction.contains("checkpoint as the ember"));
let schema = request.output_schema_json.as_ref().unwrap();
assert_eq!(
request.output_contract_id,
"epiphany.worker.reorient_result.v0"
);
let schema = epiphany_reorient_launch_output_schema();
assert!(schema["properties"].get("openQuestions").is_some());
assert!(schema["properties"].get("continuityRisks").is_some());

View file

@ -56,6 +56,7 @@ use codex_protocol::protocol::TokenUsageInfo;
use codex_protocol::protocol::W3cTraceContext;
use codex_protocol::user_input::UserInput;
use codex_utils_absolute_path::AbsolutePathBuf;
use epiphany_core::EpiphanyWorkerLaunchDocument;
use epiphany_core::RuntimeSpineInitOptions;
use epiphany_core::RuntimeSpineJobOptions;
use epiphany_core::RuntimeSpineSessionOptions;
@ -70,7 +71,6 @@ use tokio::sync::Mutex;
use tokio::sync::watch;
use codex_rollout::state_db::StateDbHandle;
use serde_json::Value;
use uuid::Uuid;
#[derive(Clone, Debug)]
@ -146,8 +146,8 @@ pub struct EpiphanyJobLaunchRequest {
pub linked_subgoal_ids: Vec<String>,
pub linked_graph_node_ids: Vec<String>,
pub instruction: String,
pub input_json: Value,
pub output_schema_json: Option<Value>,
pub launch_document: EpiphanyWorkerLaunchDocument,
pub output_contract_id: String,
pub max_runtime_seconds: Option<u64>,
}
@ -793,9 +793,20 @@ fn validate_epiphany_job_launch_request(request: &EpiphanyJobLaunchRequest) -> C
"epiphany job launch instruction must be non-empty".to_string(),
));
}
if !request.input_json.is_object() {
if request.launch_document.thread_id().trim().is_empty() {
return Err(CodexErr::InvalidRequest(
"epiphany job launch input_json must be a JSON object".to_string(),
"epiphany job launch document must include a thread id".to_string(),
));
}
if request.output_contract_id.trim().is_empty() {
return Err(CodexErr::InvalidRequest(
"epiphany job launch output_contract_id must be non-empty".to_string(),
));
}
if request.output_contract_id != request.launch_document.output_contract_id() {
return Err(CodexErr::InvalidRequest(
"epiphany job launch output_contract_id must match the typed launch document"
.to_string(),
));
}
if let Some(max_runtime_seconds) = request.max_runtime_seconds
@ -2020,8 +2031,28 @@ mod epiphany_update_tests {
linked_subgoal_ids: vec!["phase-6".to_string()],
linked_graph_node_ids: vec!["runtime-spine".to_string()],
instruction: "Model the target before implementation.".to_string(),
input_json: serde_json::json!({ "objective": "keep state typed" }),
output_schema_json: None,
launch_document: EpiphanyWorkerLaunchDocument::Role(
epiphany_core::EpiphanyRoleWorkerLaunchDocument {
thread_id: "thread-1".to_string(),
role_id: "modeling".to_string(),
state_revision: 7,
objective: Some("keep state typed".to_string()),
active_subgoal_id: None,
active_subgoals: Vec::new(),
active_graph_node_ids: vec!["runtime-spine".to_string()],
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,
},
),
output_contract_id: "epiphany.worker.role_result.v0".to_string(),
max_runtime_seconds: Some(60),
};