diff --git a/epiphany-core/src/lib.rs b/epiphany-core/src/lib.rs index fcf09966..9a49ecf5 100644 --- a/epiphany-core/src/lib.rs +++ b/epiphany-core/src/lib.rs @@ -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; diff --git a/epiphany-core/src/surfaces/mod.rs b/epiphany-core/src/surfaces/mod.rs index 8ab2f66a..e46bd340 100644 --- a/epiphany-core/src/surfaces/mod.rs +++ b/epiphany-core/src/surfaces/mod.rs @@ -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; diff --git a/epiphany-core/src/surfaces/worker_launch.rs b/epiphany-core/src/surfaces/worker_launch.rs new file mode 100644 index 00000000..7886f3cb --- /dev/null +++ b/epiphany-core/src/surfaces/worker_launch.rs @@ -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, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub active_subgoal_id: Option, + #[serde(default)] + pub active_subgoals: Vec, + #[serde(default)] + pub active_graph_node_ids: Vec, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub investigation_checkpoint: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub scratch: Option, + #[serde(default)] + pub invariants: Vec, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub graphs: Option, + #[serde(default)] + pub recent_evidence: Vec, + #[serde(default)] + pub recent_observations: Vec, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub graph_frontier: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub graph_checkpoint: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub planning: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub churn: Option, +} + +#[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, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub checkpoint_summary: Option, + pub checkpoint_next_action: String, + #[serde(default)] + pub checkpoint_open_questions: Vec, + #[serde(default)] + pub checkpoint_evidence_ids: Vec, + #[serde(default)] + pub checkpoint_code_refs: Vec, + #[serde(default)] + pub decision_reasons: Vec, + 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, + #[serde(default)] + pub checkpoint_changed_paths: Vec, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub scratch: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub graphs: Option, + #[serde(default)] + pub recent_evidence: Vec, + #[serde(default)] + pub recent_observations: Vec, + #[serde(default)] + pub active_frontier_node_ids: Vec, + #[serde(default)] + pub linked_subgoal_ids: Vec, + #[serde(default)] + pub linked_graph_node_ids: Vec, +} diff --git a/notes/fresh-workspace-handoff.md b/notes/fresh-workspace-handoff.md index 3c4f977d..80b28fd9 100644 --- a/notes/fresh-workspace-handoff.md +++ b/notes/fresh-workspace-handoff.md @@ -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 diff --git a/notes/json-contamination-ledger.md b/notes/json-contamination-ledger.md index 27ea59af..2ce679aa 100644 --- a/notes/json-contamination-ledger.md +++ b/notes/json-contamination-ledger.md @@ -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. diff --git a/state/ledgers.msgpack b/state/ledgers.msgpack index 6155f680..128f2a71 100644 Binary files a/state/ledgers.msgpack and b/state/ledgers.msgpack differ diff --git a/state/map.yaml b/state/map.yaml index 53503e26..2f7c31c5 100644 --- a/state/map.yaml +++ b/state/map.yaml @@ -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`. diff --git a/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs b/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs index 17df20f6..42c908c5 100644 --- a/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs +++ b/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs @@ -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::(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::>(), - "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::>(), - "recentObservations": state.observations.iter().take(8).collect::>(), - "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::>(), - "decisionReasons": decision - .reasons - .iter() - .map(|reason| reorient_reason_label(*reason)) - .collect::>(), - "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::>(), - "checkpointChangedPaths": decision - .checkpoint_changed_paths - .iter() - .map(path_to_display_string) - .collect::>(), - "scratch": &state.scratch, - "graphs": &state.graphs, - "recentEvidence": state.recent_evidence.iter().take(8).collect::>(), - "recentObservations": state.observations.iter().take(8).collect::>(), - "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) -> 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()); diff --git a/vendor/codex/codex-rs/core/src/codex_thread.rs b/vendor/codex/codex-rs/core/src/codex_thread.rs index 0465cc93..b1788ac3 100644 --- a/vendor/codex/codex-rs/core/src/codex_thread.rs +++ b/vendor/codex/codex-rs/core/src/codex_thread.rs @@ -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, pub linked_graph_node_ids: Vec, pub instruction: String, - pub input_json: Value, - pub output_schema_json: Option, + pub launch_document: EpiphanyWorkerLaunchDocument, + pub output_contract_id: String, pub max_runtime_seconds: Option, } @@ -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), };