diff --git a/notes/fresh-workspace-handoff.md b/notes/fresh-workspace-handoff.md index bccaf788..91b05930 100644 --- a/notes/fresh-workspace-handoff.md +++ b/notes/fresh-workspace-handoff.md @@ -402,10 +402,26 @@ inventory and ledger now exist: `output_contract_id`, then maps that document into the native `EpiphanyWorkerLaunchDocument` for core. -Continue by sealing MCP JSON behind a CultNet MCP adapter, then audit heartbeat -cognition quarantine values for typing or explicit expiration. Do not resume -Rider, Unity, Aquarium, Face, dogfood, planning, app, skill, marketplace, or -bridge expansion until this organ is being cut cleanly. +Correction after user challenge: the typing cuts above are useful receipts, but +they were not enough to make the Codex organ materially smaller. The first real +carcass cut has now moved Epiphany launch doctrine, prompt config, launch +request builders, output schemas, binding ids, and launch-specific labels into +`vendor/codex/codex-rs/app-server/src/epiphany_launch.rs`. The processor is +down from about 21,263 lines to 20,596 lines. This is progress, not absolution: +handlers, mappers, route orchestration, result projection, coordinator glue, +and tests still keep too much Epiphany inside Codex. + +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. + +Continue with the actual whale-carcass cut: extract Epiphany route handlers, +result/projection mappers, coordinator glue, and remaining policy-shaped code +out of `codex_message_processor.rs` into Epiphany-owned app-server modules, +moving logic further into `epiphany-core` whenever it owns real policy. Success +is a visibly smaller `codex_message_processor.rs`, not merely nicer payload +names. 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 bbed94ca..a14116ea 100644 --- a/notes/json-contamination-ledger.md +++ b/notes/json-contamination-ledger.md @@ -48,8 +48,8 @@ Rule: JSON is allowed at external schema/wire boundaries. Inside Epiphany, live - MCP tool call arguments/results in `vendor/codex/codex-rs/core/src/mcp_tool_call.rs`. - Current mechanism: arbitrary MCP payloads are raw JSON. - Real need: MCP is an external protocol with schema-bearing tools. - - Verdict: edge adapter. It does not belong in the Codex auth organ. - - Simpler architecture: CultNet MCP adapter with typed request/result wrappers and raw JSON sealed at the MCP edge. + - Verdict: allowed MCP wire JSON, but not allowed as Epiphany durable state or internal authority. + - Simpler architecture: an Epiphany-owned boundary with typed intent/result/receipt documents on the Epiphany side and normal MCP JSON-RPC on the MCP side. Do not pretend MCP itself should stop being JSON. - Unity/Rider/void-memory CLI outputs. - Current mechanism: JSON command output and ad hoc parsing. - Real need: bridge external tools while native surfaces mature. @@ -62,5 +62,6 @@ Rule: JSON is allowed at external schema/wire boundaries. Inside Epiphany, live 3. Done: replace role `statePatch` JSON with typed map/planning/graph patch documents. `EpiphanyRoleFindingInterpretation` now carries `EpiphanyRoleStatePatchDocument`; app-server maps it to the legacy protocol patch without JSON round-tripping. 4. Done: replace runtime job result JSON projections. Runtime-spine results now project through typed role/reorient interpreters instead of `runtime_job_result_to_role_json` and `runtime_job_result_to_reorient_json`. 5. Done: replace app-server launch protocol `input_json` / `output_schema_json` with typed launch documents and `output_contract_id`. -6. Seal MCP JSON behind a CultNet MCP adapter. -7. Audit heartbeat cognition values and either type them or explicitly expire them. +6. Continue the whale-carcass cut: the first cut moved Epiphany launch doctrine into `epiphany_launch.rs` and reduced `codex_message_processor.rs` from about 21,263 to 20,596 lines. Next move route handlers, result/projection mappers, coordinator glue, and policy-shaped code out of the processor; success means the processor keeps shrinking and ownership gets clearer, not merely better typed. +7. Seal MCP payloads behind an Epiphany boundary that speaks typed Epiphany documents internally and MCP JSON externally. +8. Audit heartbeat cognition values and either type them or explicitly expire them. diff --git a/state/ledgers.msgpack b/state/ledgers.msgpack index dcc02a5c..00fd626d 100644 Binary files a/state/ledgers.msgpack and b/state/ledgers.msgpack differ diff --git a/state/map.yaml b/state/map.yaml index 6be95e76..fb306093 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 five 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` and `statePatch` now use typed Epiphany documents internally. Core worker launch and app-server launch params now use typed worker launch documents plus `output_contract_id` instead of `input_json` / `output_schema_json`. Runtime job results now project through typed role/reorient interpreters instead of JSON round-tripping. Legacy app-server JSON remains in smaller compatibility pockets pending CultNet replacement. - next_action: Continue the liberation pass by sealing MCP JSON behind a CultNet MCP adapter instead of letting Codex app/tool plumbing own it, then audit heartbeat cognition quarantine values for typing or explicit expiration. + summary: The Codex starvation pass has five typing receipts plus the first real carcass cut: Epiphany launch doctrine, prompt config, launch request builders, output schemas, binding ids, and launch-specific labels moved from `vendor/codex/codex-rs/app-server/src/codex_message_processor.rs` into `vendor/codex/codex-rs/app-server/src/epiphany_launch.rs`. The processor is down from about 21,263 lines to 20,596 lines. This is progress, not absolution: handlers, mappers, route orchestration, result projection, coordinator glue, and tests still keep too much Epiphany inside Codex. MCP itself is correctly JSON at its protocol edge; the target is typed Epiphany intent/result/receipt wrappers around MCP, not replacing MCP JSON. + next_action: Continue the actual whale-carcass cut: extract Epiphany route handlers and result/projection mappers out of `codex_message_processor.rs` into Epiphany-owned app-server modules, moving logic further into `epiphany-core` whenever it owns real policy. Success metric remains a visibly smaller processor and clearer ownership, not merely typed payloads. 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 042a695c..63337f8c 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 @@ -5,6 +5,25 @@ use crate::command_exec::StartCommandExecParams; use crate::config_manager::ConfigManager; use crate::epiphany_invalidation::EpiphanyInvalidationManager; use crate::epiphany_invalidation::EpiphanyInvalidationSnapshot; +use crate::epiphany_launch::EPIPHANY_IMAGINATION_OWNER_ROLE; +use crate::epiphany_launch::EPIPHANY_IMAGINATION_ROLE_BINDING_ID; +use crate::epiphany_launch::EPIPHANY_MODELING_OWNER_ROLE; +use crate::epiphany_launch::EPIPHANY_MODELING_ROLE_BINDING_ID; +use crate::epiphany_launch::EPIPHANY_REORIENT_LAUNCH_BINDING_ID; +use crate::epiphany_launch::EPIPHANY_REORIENT_OWNER_ROLE; +use crate::epiphany_launch::EPIPHANY_VERIFICATION_OWNER_ROLE; +use crate::epiphany_launch::EPIPHANY_VERIFICATION_ROLE_BINDING_ID; +use crate::epiphany_launch::build_epiphany_reorient_launch_instruction; +use crate::epiphany_launch::build_epiphany_reorient_launch_request; +use crate::epiphany_launch::build_epiphany_role_launch_request; +use crate::epiphany_launch::epiphany_agent_prompt_with_memory; +use crate::epiphany_launch::epiphany_reorient_launch_output_schema; +use crate::epiphany_launch::epiphany_role_binding_id; +use crate::epiphany_launch::epiphany_role_label; +use crate::epiphany_launch::epiphany_role_launch_output_schema; +use crate::epiphany_launch::epiphany_specialist_prompt_config; +use crate::epiphany_launch::pressure_level_label; +use crate::epiphany_launch::render_epiphany_coordinator_note; use crate::error_code::INPUT_TOO_LARGE_ERROR_CODE; use crate::error_code::INTERNAL_ERROR_CODE; use crate::error_code::INVALID_PARAMS_ERROR_CODE; @@ -613,7 +632,6 @@ use std::io::Error as IoError; use std::path::Path; use std::path::PathBuf; use std::sync::Arc; -use std::sync::OnceLock; use std::sync::atomic::AtomicBool; use std::sync::atomic::Ordering; use std::time::Duration; @@ -12714,592 +12732,6 @@ fn map_core_epiphany_scene_churn(churn: CoreEpiphanySceneChurn) -> ThreadEpiphan } } -const EPIPHANY_IMAGINATION_ROLE_BINDING_ID: &str = "planning-synthesis-worker"; -const EPIPHANY_IMAGINATION_OWNER_ROLE: &str = "epiphany-imagination"; -const EPIPHANY_MODELING_ROLE_BINDING_ID: &str = "modeling-checkpoint-worker"; -const EPIPHANY_MODELING_OWNER_ROLE: &str = "epiphany-modeler"; -const EPIPHANY_VERIFICATION_ROLE_BINDING_ID: &str = "verification-review-worker"; -const EPIPHANY_VERIFICATION_OWNER_ROLE: &str = "epiphany-verifier"; -const EPIPHANY_REORIENT_LAUNCH_BINDING_ID: &str = "reorient-worker"; -const EPIPHANY_REORIENT_OWNER_ROLE: &str = "epiphany-reorient"; - -fn epiphany_role_binding_id(role_id: ThreadEpiphanyRoleId) -> Result<&'static str, String> { - match role_id { - ThreadEpiphanyRoleId::Imagination => Ok(EPIPHANY_IMAGINATION_ROLE_BINDING_ID), - ThreadEpiphanyRoleId::Modeling => Ok(EPIPHANY_MODELING_ROLE_BINDING_ID), - ThreadEpiphanyRoleId::Verification => Ok(EPIPHANY_VERIFICATION_ROLE_BINDING_ID), - ThreadEpiphanyRoleId::Implementation => Err( - "implementation is owned by the main coding agent; no role specialist launch template exists" - .to_string(), - ), - ThreadEpiphanyRoleId::Reorientation => Err( - "reorientation uses thread/epiphany/reorientLaunch and thread/epiphany/reorientResult" - .to_string(), - ), - } -} - -fn epiphany_role_owner(role_id: ThreadEpiphanyRoleId) -> Result<&'static str, String> { - match role_id { - ThreadEpiphanyRoleId::Imagination => Ok(EPIPHANY_IMAGINATION_OWNER_ROLE), - ThreadEpiphanyRoleId::Modeling => Ok(EPIPHANY_MODELING_OWNER_ROLE), - ThreadEpiphanyRoleId::Verification => Ok(EPIPHANY_VERIFICATION_OWNER_ROLE), - ThreadEpiphanyRoleId::Implementation | ThreadEpiphanyRoleId::Reorientation => { - Err(epiphany_role_binding_id(role_id).unwrap_err()) - } - } -} - -fn epiphany_role_label(role_id: ThreadEpiphanyRoleId) -> &'static str { - match role_id { - ThreadEpiphanyRoleId::Implementation => "implementation", - ThreadEpiphanyRoleId::Imagination => "imagination", - ThreadEpiphanyRoleId::Modeling => "modeling", - ThreadEpiphanyRoleId::Verification => "verification", - ThreadEpiphanyRoleId::Reorientation => "reorientation", - } -} - -const EPIPHANY_SPECIALIST_PROMPTS_TOML: &str = include_str!("prompts/epiphany_specialists.toml"); - -#[derive(Debug, serde::Deserialize)] -struct EpiphanySpecialistPromptConfig { - shared: EpiphanySharedPromptConfig, - roles: EpiphanyRolePromptConfig, - // Parsed here so the bundled prompt config fails fast even though the GUI runner consumes it. - #[allow(dead_code)] - implementation: EpiphanyImplementationPromptConfig, - reorientation: EpiphanyReorientationPromptConfig, - coordinator: EpiphanyCoordinatorPromptConfig, - crrc: EpiphanyCrrcPromptConfig, -} - -#[derive(Debug, serde::Deserialize)] -struct EpiphanySharedPromptConfig { - persistent_memory: String, -} - -#[derive(Debug, serde::Deserialize)] -struct EpiphanyRolePromptConfig { - imagination: String, - modeling: String, - verification: String, - #[allow(dead_code)] - research: String, - #[allow(dead_code)] - repo_personality: String, - #[allow(dead_code)] - repo_memory: String, - #[allow(dead_code)] - face: String, -} - -#[derive(Debug, serde::Deserialize)] -struct EpiphanyImplementationPromptConfig { - #[allow(dead_code)] - continue_template: String, -} - -#[derive(Debug, serde::Deserialize)] -struct EpiphanyReorientationPromptConfig { - resume: String, - regather: String, -} - -#[derive(Debug, serde::Deserialize)] -struct EpiphanyCoordinatorPromptConfig { - note_template: String, -} - -#[derive(Debug, serde::Deserialize)] -struct EpiphanyCrrcPromptConfig { - pre_compaction_checkpoint_intervention: String, -} - -fn epiphany_specialist_prompt_config() -> &'static EpiphanySpecialistPromptConfig { - static CONFIG: OnceLock = OnceLock::new(); - CONFIG.get_or_init(|| { - toml::from_str(EPIPHANY_SPECIALIST_PROMPTS_TOML) - .expect("bundled Epiphany specialist prompt config must parse") - }) -} - -fn epiphany_agent_prompt_with_memory(body: &str) -> String { - let memory = epiphany_specialist_prompt_config() - .shared - .persistent_memory - .trim(); - let body = body.trim(); - if memory.is_empty() { - body.to_string() - } else if body.is_empty() { - memory.to_string() - } else { - format!("{memory}\n\n{body}") - } -} - -fn build_epiphany_role_launch_request( - thread_id: &str, - role_id: ThreadEpiphanyRoleId, - expected_revision: Option, - max_runtime_seconds: Option, - state: &EpiphanyThreadState, -) -> Result { - let binding_id = epiphany_role_binding_id(role_id)?; - 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) = match role_id { - ThreadEpiphanyRoleId::Imagination => ( - "role-scoped planning synthesis", - "epiphany.role.imagination", - build_epiphany_role_launch_instruction(role_id), - ), - ThreadEpiphanyRoleId::Modeling => ( - "role-scoped modeling/checkpoint maintenance", - "epiphany.role.modeling", - build_epiphany_role_launch_instruction(role_id), - ), - ThreadEpiphanyRoleId::Verification => ( - "role-scoped verification/review", - "epiphany.role.verification", - build_epiphany_role_launch_instruction(role_id), - ), - ThreadEpiphanyRoleId::Implementation | ThreadEpiphanyRoleId::Reorientation => { - return Err(epiphany_role_binding_id(role_id).unwrap_err()); - } - }; - 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()) - .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, - binding_id: binding_id.to_string(), - kind: CoreEpiphanyJobKind::Specialist, - scope: scope.to_string(), - owner_role: owner_role.to_string(), - authority_scope: authority_scope.to_string(), - linked_subgoal_ids, - linked_graph_node_ids: epiphany_active_graph_node_ids(Some(state)), - instruction, - launch_document, - output_contract_id, - max_runtime_seconds, - }) -} - -fn build_epiphany_role_launch_instruction(role_id: ThreadEpiphanyRoleId) -> String { - let prompts = &epiphany_specialist_prompt_config().roles; - let body = match role_id { - ThreadEpiphanyRoleId::Imagination => prompts.imagination.as_str(), - ThreadEpiphanyRoleId::Modeling => prompts.modeling.as_str(), - ThreadEpiphanyRoleId::Verification => prompts.verification.as_str(), - ThreadEpiphanyRoleId::Implementation | ThreadEpiphanyRoleId::Reorientation => { - "Unsupported Epiphany role specialist template." - } - }; - epiphany_agent_prompt_with_memory(body) -} - -fn epiphany_role_launch_output_schema(role_id: ThreadEpiphanyRoleId) -> serde_json::Value { - let verdict_enum = match role_id { - ThreadEpiphanyRoleId::Imagination => { - vec!["draft-ready", "planning-update-needed", "blocked"] - } - ThreadEpiphanyRoleId::Modeling => { - vec![ - "checkpoint-ready", - "checkpoint-update-needed", - "regather-needed", - ] - } - ThreadEpiphanyRoleId::Verification => { - vec!["pass", "needs-review", "needs-evidence", "fail"] - } - ThreadEpiphanyRoleId::Implementation | ThreadEpiphanyRoleId::Reorientation => vec![], - }; - let mut properties = serde_json::json!({ - "roleId": { - "type": "string", - "enum": [epiphany_role_label(role_id)] - }, - "verdict": { - "type": "string", - "enum": verdict_enum - }, - "summary": {"type": "string"}, - "nextSafeMove": {"type": "string"}, - "checkpointSummary": {"type": "string"}, - "scratchSummary": {"type": "string"}, - "filesInspected": { - "type": "array", - "items": {"type": "string"} - }, - "frontierNodeIds": { - "type": "array", - "items": {"type": "string"} - }, - "evidenceIds": { - "type": "array", - "items": {"type": "string"} - }, - "openQuestions": { - "type": "array", - "items": {"type": "string"} - }, - "evidenceGaps": { - "type": "array", - "items": {"type": "string"} - }, - "risks": { - "type": "array", - "items": {"type": "string"} - }, - "selfPatch": { - "type": "object", - "description": "Optional bounded request to update this role's Ghostlight-shaped persistent memory file. This is for lane habits, durable lessons, personality pressure, goals, or values only. It must not contain project truth, code edits, job authority, graph/frontier/checkpoint/planning changes, or objective changes.", - "required": ["agentId", "reason"], - "properties": { - "agentId": { - "type": "string", - "description": "Expected target persistent agent id for this lane, such as epiphany.body or epiphany.soul." - }, - "reason": { - "type": "string", - "description": "Why this memory mutation makes the lane sharper for future work." - }, - "evidenceIds": { - "type": "array", - "description": "Optional accepted/project evidence ids that ground the memory request. These do not count as a memory mutation by themselves.", - "items": {"type": "string"} - }, - "semanticMemories": { - "type": "array", - "items": { - "type": "object", - "required": ["memoryId", "summary", "salience", "confidence"], - "additionalProperties": true - } - }, - "episodicMemories": { - "type": "array", - "items": { - "type": "object", - "required": ["memoryId", "summary", "salience", "confidence"], - "additionalProperties": true - } - }, - "relationshipMemories": { - "type": "array", - "items": { - "type": "object", - "required": ["memoryId", "summary", "salience", "confidence"], - "additionalProperties": true - } - }, - "goals": { - "type": "array", - "items": { - "type": "object", - "required": ["goalId", "description", "scope", "priority", "emotionalStake", "status"], - "additionalProperties": true - } - }, - "values": { - "type": "array", - "items": { - "type": "object", - "required": ["valueId", "label", "priority", "unforgivableIfBetrayed"], - "additionalProperties": true - } - }, - "privateNotes": { - "type": "array", - "items": {"type": "string"} - } - }, - "additionalProperties": false - } - }); - let mut required = vec![ - "roleId", - "verdict", - "summary", - "nextSafeMove", - "filesInspected", - ]; - if role_id == ThreadEpiphanyRoleId::Imagination { - if let Some(map) = properties.as_object_mut() { - map.insert( - "statePatch".to_string(), - serde_json::json!({ - "type": "object", - "description": "Required reviewable thread/epiphany/update patch for Imagination. Use only planning plus optional observations/evidence. planning is a full replacement object and must include at least one objective_drafts entry with status draft.", - "required": ["planning"], - "properties": { - "planning": { - "type": "object", - "required": ["objective_drafts"], - "properties": { - "objective_drafts": { - "type": "array", - "minItems": 1, - "items": { - "type": "object", - "required": ["id", "title", "summary", "acceptance_criteria", "status"], - "properties": { - "status": { - "type": "string", - "enum": ["draft"] - } - }, - "additionalProperties": true - } - } - }, - "additionalProperties": true - } - }, - "additionalProperties": true - }), - ); - } - required.push("statePatch"); - } else if role_id == ThreadEpiphanyRoleId::Modeling { - if let Some(map) = properties.as_object_mut() { - map.insert( - "statePatch".to_string(), - serde_json::json!({ - "type": "object", - "description": "Required reviewable thread/epiphany/update patch for modeling. Use only graphs, graphFrontier, graphCheckpoint, scratch, investigationCheckpoint, observations, and evidence. The patch must include at least one durable modeling field, not observations/evidence alone.", - "anyOf": [ - {"required": ["graphs"]}, - {"required": ["graphFrontier"]}, - {"required": ["graphCheckpoint"]}, - {"required": ["scratch"]}, - {"required": ["investigationCheckpoint"]} - ], - "properties": { - "investigationCheckpoint": { - "type": "object", - "properties": { - "disposition": { - "type": "string", - "enum": ["resume_ready", "regather_required"] - } - }, - "additionalProperties": true - } - }, - "additionalProperties": true - }), - ); - } - required.push("frontierNodeIds"); - required.push("statePatch"); - } - serde_json::json!({ - "type": "object", - "properties": properties, - "required": required, - "additionalProperties": true - }) -} - -fn build_epiphany_reorient_launch_request( - thread_id: &str, - expected_revision: Option, - max_runtime_seconds: Option, - state: &EpiphanyThreadState, - checkpoint: &codex_protocol::protocol::EpiphanyInvestigationCheckpoint, - decision: &ThreadEpiphanyReorientDecision, -) -> EpiphanyJobLaunchRequest { - let linked_subgoal_ids = epiphany_active_subgoal_ids(Some(state)); - let linked_graph_node_ids = unique_strings( - epiphany_active_graph_node_ids(Some(state)) - .into_iter() - .chain(decision.active_frontier_node_ids.iter().cloned()), - ); - let checkpoint_next_action = checkpoint - .next_action - .clone() - .unwrap_or_else(|| decision.next_action.clone()); - let authority_scope = match decision.action { - ThreadEpiphanyReorientAction::Resume => "epiphany.reorient.resume", - ThreadEpiphanyReorientAction::Regather => "epiphany.reorient.regather", - }; - let scope = match decision.action { - ThreadEpiphanyReorientAction::Resume => "reorient-guided checkpoint resume", - ThreadEpiphanyReorientAction::Regather => "reorient-guided checkpoint regather", - }; - let instruction = build_epiphany_reorient_launch_instruction(decision.action); - 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, - binding_id: EPIPHANY_REORIENT_LAUNCH_BINDING_ID.to_string(), - kind: CoreEpiphanyJobKind::Specialist, - scope: scope.to_string(), - owner_role: EPIPHANY_REORIENT_OWNER_ROLE.to_string(), - authority_scope: authority_scope.to_string(), - linked_subgoal_ids: epiphany_active_subgoal_ids(Some(state)), - linked_graph_node_ids: unique_strings( - epiphany_active_graph_node_ids(Some(state)) - .into_iter() - .chain(decision.active_frontier_node_ids.iter().cloned()), - ), - instruction, - launch_document, - output_contract_id, - max_runtime_seconds, - } -} - -fn build_epiphany_reorient_launch_instruction(action: ThreadEpiphanyReorientAction) -> String { - let prompts = &epiphany_specialist_prompt_config().reorientation; - let body = match action { - ThreadEpiphanyReorientAction::Resume => prompts.resume.as_str(), - ThreadEpiphanyReorientAction::Regather => prompts.regather.as_str(), - }; - epiphany_agent_prompt_with_memory(body) -} - -fn render_epiphany_coordinator_note( - crrc_action: ThreadEpiphanyCrrcAction, - pressure_level: ThreadEpiphanyPressureLevel, - modeling_result_status: ThreadEpiphanyRoleResultStatus, - verification_result_status: ThreadEpiphanyRoleResultStatus, - reorient_result_status: ThreadEpiphanyReorientResultStatus, - coordinator_action: ThreadEpiphanyCoordinatorAction, -) -> String { - let template = epiphany_agent_prompt_with_memory( - &epiphany_specialist_prompt_config() - .coordinator - .note_template, - ); - template - .trim() - .replace("{crrc_action}", &format!("{crrc_action:?}")) - .replace("{pressure_level}", &format!("{pressure_level:?}")) - .replace( - "{modeling_result_status}", - &format!("{modeling_result_status:?}"), - ) - .replace( - "{verification_result_status}", - &format!("{verification_result_status:?}"), - ) - .replace( - "{reorient_result_status}", - &format!("{reorient_result_status:?}"), - ) - .replace("{coordinator_action}", &format!("{coordinator_action:?}")) -} - -fn epiphany_reorient_launch_output_schema() -> serde_json::Value { - serde_json::json!({ - "type": "object", - "properties": { - "mode": { - "type": "string", - "enum": ["resume", "regather"] - }, - "summary": {"type": "string"}, - "nextSafeMove": {"type": "string"}, - "checkpointStillValid": {"type": "boolean"}, - "filesInspected": { - "type": "array", - "items": {"type": "string"} - }, - "frontierNodeIds": { - "type": "array", - "items": {"type": "string"} - }, - "evidenceIds": { - "type": "array", - "items": {"type": "string"} - }, - "openQuestions": { - "type": "array", - "items": {"type": "string"} - }, - "continuityRisks": { - "type": "array", - "items": {"type": "string"} - } - }, - "required": ["mode", "summary", "nextSafeMove"], - "additionalProperties": true - }) -} - fn map_epiphany_reorient_finding( raw_result: serde_json::Value, job_error: Option, @@ -14966,77 +14398,6 @@ fn reorient_finding_investigation_checkpoint( checkpoint } -fn path_to_display_string(path: impl AsRef) -> String { - path.as_ref().to_string_lossy().into_owned() -} - -fn reorient_action_label(action: ThreadEpiphanyReorientAction) -> &'static str { - match action { - ThreadEpiphanyReorientAction::Resume => "resume", - ThreadEpiphanyReorientAction::Regather => "regather", - } -} - -fn investigation_disposition_label(disposition: EpiphanyInvestigationDisposition) -> &'static str { - match disposition { - EpiphanyInvestigationDisposition::ResumeReady => "resume_ready", - EpiphanyInvestigationDisposition::RegatherRequired => "regather_required", - } -} - -fn reorient_reason_label(reason: ThreadEpiphanyReorientReason) -> &'static str { - match reason { - ThreadEpiphanyReorientReason::MissingState => "missingState", - ThreadEpiphanyReorientReason::MissingCheckpoint => "missingCheckpoint", - ThreadEpiphanyReorientReason::CheckpointRequestedRegather => "checkpointRequestedRegather", - ThreadEpiphanyReorientReason::CheckpointPathsDirty => "checkpointPathsDirty", - ThreadEpiphanyReorientReason::CheckpointPathsChanged => "checkpointPathsChanged", - ThreadEpiphanyReorientReason::FrontierChanged => "frontierChanged", - ThreadEpiphanyReorientReason::UnanchoredCheckpointWhileStateStale => { - "unanchoredCheckpointWhileStateStale" - } - ThreadEpiphanyReorientReason::CheckpointReady => "checkpointReady", - } -} - -fn pressure_level_label(level: ThreadEpiphanyPressureLevel) -> &'static str { - match level { - ThreadEpiphanyPressureLevel::Unknown => "unknown", - ThreadEpiphanyPressureLevel::Low => "low", - ThreadEpiphanyPressureLevel::Elevated => "elevated", - ThreadEpiphanyPressureLevel::High => "high", - ThreadEpiphanyPressureLevel::Critical => "critical", - } -} - -fn retrieval_freshness_status_label( - status: ThreadEpiphanyRetrievalFreshnessStatus, -) -> &'static str { - match status { - ThreadEpiphanyRetrievalFreshnessStatus::Missing => "missing", - ThreadEpiphanyRetrievalFreshnessStatus::Ready => "ready", - ThreadEpiphanyRetrievalFreshnessStatus::Stale => "stale", - ThreadEpiphanyRetrievalFreshnessStatus::Indexing => "indexing", - ThreadEpiphanyRetrievalFreshnessStatus::Unavailable => "unavailable", - } -} - -fn graph_freshness_status_label(status: ThreadEpiphanyGraphFreshnessStatus) -> &'static str { - match status { - ThreadEpiphanyGraphFreshnessStatus::Missing => "missing", - ThreadEpiphanyGraphFreshnessStatus::Ready => "ready", - ThreadEpiphanyGraphFreshnessStatus::Stale => "stale", - } -} - -fn invalidation_status_label(status: ThreadEpiphanyInvalidationStatus) -> &'static str { - match status { - ThreadEpiphanyInvalidationStatus::Unavailable => "unavailable", - ThreadEpiphanyInvalidationStatus::Clean => "clean", - ThreadEpiphanyInvalidationStatus::Changed => "changed", - } -} - fn map_epiphany_freshness( state: Option<&EpiphanyThreadState>, retrieval_override: Option<&EpiphanyRetrievalState>, @@ -15666,20 +15027,6 @@ fn map_core_epiphany_graph_query_missing( } } -fn unique_strings(values: impl IntoIterator) -> Vec { - let mut unique = Vec::new(); - extend_unique_strings(&mut unique, values); - unique -} - -fn extend_unique_strings(target: &mut Vec, values: impl IntoIterator) { - for value in values { - if !target.iter().any(|existing| existing == &value) { - target.push(value); - } - } -} - pub(crate) async fn maybe_run_epiphany_coordinator_automation_for_turn_boundary( thread_id: ThreadId, thread: Arc, @@ -16014,20 +15361,6 @@ fn epiphany_blocked_state_job( } } -fn epiphany_active_subgoal_ids(state: Option<&EpiphanyThreadState>) -> Vec { - state - .and_then(|state| state.active_subgoal_id.clone()) - .map(|id| vec![id]) - .unwrap_or_default() -} - -fn epiphany_active_graph_node_ids(state: Option<&EpiphanyThreadState>) -> Vec { - state - .and_then(|state| state.graph_frontier.as_ref()) - .map(|frontier| frontier.active_node_ids.clone()) - .unwrap_or_default() -} - fn thread_epiphany_patch_has_state_replacements(patch: &ThreadEpiphanyUpdatePatch) -> bool { patch.objective.is_some() || patch.active_subgoal_id.is_some() @@ -19253,7 +18586,7 @@ mod tests { prompts .shared .persistent_memory - .contains("best version of itself") + .contains("First law of the lane") ); assert!( prompts diff --git a/vendor/codex/codex-rs/app-server/src/epiphany_launch.rs b/vendor/codex/codex-rs/app-server/src/epiphany_launch.rs new file mode 100644 index 00000000..21175d3e --- /dev/null +++ b/vendor/codex/codex-rs/app-server/src/epiphany_launch.rs @@ -0,0 +1,710 @@ +use std::path::Path; +use std::sync::OnceLock; + +use codex_app_server_protocol::ThreadEpiphanyGraphFreshnessStatus; +use codex_app_server_protocol::ThreadEpiphanyInvalidationStatus; +use codex_app_server_protocol::ThreadEpiphanyPressureLevel; +use codex_app_server_protocol::ThreadEpiphanyReorientAction; +use codex_app_server_protocol::ThreadEpiphanyReorientDecision; +use codex_app_server_protocol::ThreadEpiphanyReorientReason; +use codex_app_server_protocol::ThreadEpiphanyRetrievalFreshnessStatus; +use codex_app_server_protocol::ThreadEpiphanyRoleId; +use codex_core::EpiphanyJobLaunchRequest; +use codex_protocol::protocol::EpiphanyInvestigationCheckpoint; +use codex_protocol::protocol::EpiphanyInvestigationDisposition; +use codex_protocol::protocol::EpiphanyJobKind as CoreEpiphanyJobKind; +use codex_protocol::protocol::EpiphanyThreadState; +use epiphany_core::EpiphanyReorientWorkerLaunchDocument; +use epiphany_core::EpiphanyRoleWorkerLaunchDocument; +use epiphany_core::EpiphanyWorkerLaunchDocument; + +pub(super) const EPIPHANY_IMAGINATION_ROLE_BINDING_ID: &str = "planning-synthesis-worker"; +pub(super) const EPIPHANY_IMAGINATION_OWNER_ROLE: &str = "epiphany-imagination"; +pub(super) const EPIPHANY_MODELING_ROLE_BINDING_ID: &str = "modeling-checkpoint-worker"; +pub(super) const EPIPHANY_MODELING_OWNER_ROLE: &str = "epiphany-modeler"; +pub(super) const EPIPHANY_VERIFICATION_ROLE_BINDING_ID: &str = "verification-review-worker"; +pub(super) const EPIPHANY_VERIFICATION_OWNER_ROLE: &str = "epiphany-verifier"; +pub(super) const EPIPHANY_REORIENT_LAUNCH_BINDING_ID: &str = "reorient-worker"; +pub(super) const EPIPHANY_REORIENT_OWNER_ROLE: &str = "epiphany-reorient"; + +pub(super) fn epiphany_role_binding_id( + role_id: ThreadEpiphanyRoleId, +) -> Result<&'static str, String> { + match role_id { + ThreadEpiphanyRoleId::Imagination => Ok(EPIPHANY_IMAGINATION_ROLE_BINDING_ID), + ThreadEpiphanyRoleId::Modeling => Ok(EPIPHANY_MODELING_ROLE_BINDING_ID), + ThreadEpiphanyRoleId::Verification => Ok(EPIPHANY_VERIFICATION_ROLE_BINDING_ID), + ThreadEpiphanyRoleId::Implementation => Err( + "implementation is owned by the main coding agent; no role specialist launch template exists" + .to_string(), + ), + ThreadEpiphanyRoleId::Reorientation => Err( + "reorientation uses thread/epiphany/reorientLaunch and thread/epiphany/reorientResult" + .to_string(), + ), + } +} + +pub(super) fn epiphany_role_owner(role_id: ThreadEpiphanyRoleId) -> Result<&'static str, String> { + match role_id { + ThreadEpiphanyRoleId::Imagination => Ok(EPIPHANY_IMAGINATION_OWNER_ROLE), + ThreadEpiphanyRoleId::Modeling => Ok(EPIPHANY_MODELING_OWNER_ROLE), + ThreadEpiphanyRoleId::Verification => Ok(EPIPHANY_VERIFICATION_OWNER_ROLE), + ThreadEpiphanyRoleId::Implementation | ThreadEpiphanyRoleId::Reorientation => { + Err(epiphany_role_binding_id(role_id).unwrap_err()) + } + } +} + +pub(super) fn epiphany_role_label(role_id: ThreadEpiphanyRoleId) -> &'static str { + match role_id { + ThreadEpiphanyRoleId::Implementation => "implementation", + ThreadEpiphanyRoleId::Imagination => "imagination", + ThreadEpiphanyRoleId::Modeling => "modeling", + ThreadEpiphanyRoleId::Verification => "verification", + ThreadEpiphanyRoleId::Reorientation => "reorientation", + } +} + +pub(super) fn epiphany_role_launch_output_schema( + role_id: ThreadEpiphanyRoleId, +) -> serde_json::Value { + let verdict_enum = match role_id { + ThreadEpiphanyRoleId::Imagination => { + vec!["draft-ready", "planning-update-needed", "blocked"] + } + ThreadEpiphanyRoleId::Modeling => { + vec![ + "checkpoint-ready", + "checkpoint-update-needed", + "regather-needed", + ] + } + ThreadEpiphanyRoleId::Verification => { + vec!["pass", "needs-review", "needs-evidence", "fail"] + } + ThreadEpiphanyRoleId::Implementation | ThreadEpiphanyRoleId::Reorientation => vec![], + }; + let mut properties = serde_json::json!({ + "roleId": { + "type": "string", + "enum": [epiphany_role_label(role_id)] + }, + "verdict": { + "type": "string", + "enum": verdict_enum + }, + "summary": {"type": "string"}, + "nextSafeMove": {"type": "string"}, + "checkpointSummary": {"type": "string"}, + "scratchSummary": {"type": "string"}, + "filesInspected": { + "type": "array", + "items": {"type": "string"} + }, + "frontierNodeIds": { + "type": "array", + "items": {"type": "string"} + }, + "evidenceIds": { + "type": "array", + "items": {"type": "string"} + }, + "openQuestions": { + "type": "array", + "items": {"type": "string"} + }, + "evidenceGaps": { + "type": "array", + "items": {"type": "string"} + }, + "risks": { + "type": "array", + "items": {"type": "string"} + }, + "selfPatch": { + "type": "object", + "description": "Optional bounded request to update this role's Ghostlight-shaped persistent memory file. This is for lane habits, durable lessons, personality pressure, goals, or values only. It must not contain project truth, code edits, job authority, graph/frontier/checkpoint/planning changes, or objective changes.", + "required": ["agentId", "reason"], + "properties": { + "agentId": { + "type": "string", + "description": "Expected target persistent agent id for this lane, such as epiphany.body or epiphany.soul." + }, + "reason": { + "type": "string", + "description": "Why this memory mutation makes the lane sharper for future work." + }, + "evidenceIds": { + "type": "array", + "description": "Optional accepted/project evidence ids that ground the memory request. These do not count as a memory mutation by themselves.", + "items": {"type": "string"} + }, + "semanticMemories": { + "type": "array", + "items": { + "type": "object", + "required": ["memoryId", "summary", "salience", "confidence"], + "additionalProperties": true + } + }, + "episodicMemories": { + "type": "array", + "items": { + "type": "object", + "required": ["memoryId", "summary", "salience", "confidence"], + "additionalProperties": true + } + }, + "relationshipMemories": { + "type": "array", + "items": { + "type": "object", + "required": ["memoryId", "summary", "salience", "confidence"], + "additionalProperties": true + } + }, + "goals": { + "type": "array", + "items": { + "type": "object", + "required": ["goalId", "description", "scope", "priority", "emotionalStake", "status"], + "additionalProperties": true + } + }, + "values": { + "type": "array", + "items": { + "type": "object", + "required": ["valueId", "label", "priority", "unforgivableIfBetrayed"], + "additionalProperties": true + } + }, + "privateNotes": { + "type": "array", + "items": {"type": "string"} + } + }, + "additionalProperties": false + } + }); + let mut required = vec![ + "roleId", + "verdict", + "summary", + "nextSafeMove", + "filesInspected", + ]; + if role_id == ThreadEpiphanyRoleId::Imagination { + if let Some(map) = properties.as_object_mut() { + map.insert( + "statePatch".to_string(), + serde_json::json!({ + "type": "object", + "description": "Required reviewable thread/epiphany/update patch for Imagination. Use only planning plus optional observations/evidence. planning is a full replacement object and must include at least one objective_drafts entry with status draft.", + "required": ["planning"], + "properties": { + "planning": { + "type": "object", + "required": ["objective_drafts"], + "properties": { + "objective_drafts": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "required": ["id", "title", "summary", "acceptance_criteria", "status"], + "properties": { + "status": { + "type": "string", + "enum": ["draft"] + } + }, + "additionalProperties": true + } + } + }, + "additionalProperties": true + } + }, + "additionalProperties": true + }), + ); + } + required.push("statePatch"); + } else if role_id == ThreadEpiphanyRoleId::Modeling { + if let Some(map) = properties.as_object_mut() { + map.insert( + "statePatch".to_string(), + serde_json::json!({ + "type": "object", + "description": "Required reviewable thread/epiphany/update patch for modeling. Use only graphs, graphFrontier, graphCheckpoint, scratch, investigationCheckpoint, observations, and evidence. The patch must include at least one durable modeling field, not observations/evidence alone.", + "anyOf": [ + {"required": ["graphs"]}, + {"required": ["graphFrontier"]}, + {"required": ["graphCheckpoint"]}, + {"required": ["scratch"]}, + {"required": ["investigationCheckpoint"]} + ], + "properties": { + "investigationCheckpoint": { + "type": "object", + "properties": { + "disposition": { + "type": "string", + "enum": ["resume_ready", "regather_required"] + } + }, + "additionalProperties": true + } + }, + "additionalProperties": true + }), + ); + } + required.push("frontierNodeIds"); + required.push("statePatch"); + } + serde_json::json!({ + "type": "object", + "properties": properties, + "required": required, + "additionalProperties": true + }) +} + +pub(super) fn epiphany_reorient_launch_output_schema() -> serde_json::Value { + serde_json::json!({ + "type": "object", + "properties": { + "mode": { + "type": "string", + "enum": ["resume", "regather"] + }, + "summary": {"type": "string"}, + "nextSafeMove": {"type": "string"}, + "checkpointStillValid": {"type": "boolean"}, + "filesInspected": { + "type": "array", + "items": {"type": "string"} + }, + "frontierNodeIds": { + "type": "array", + "items": {"type": "string"} + }, + "evidenceIds": { + "type": "array", + "items": {"type": "string"} + }, + "openQuestions": { + "type": "array", + "items": {"type": "string"} + }, + "continuityRisks": { + "type": "array", + "items": {"type": "string"} + } + }, + "required": ["mode", "summary", "nextSafeMove"], + "additionalProperties": true + }) +} + +const EPIPHANY_SPECIALIST_PROMPTS_TOML: &str = include_str!("prompts/epiphany_specialists.toml"); + +#[derive(Debug, serde::Deserialize)] +pub(super) struct EpiphanySpecialistPromptConfig { + pub(super) shared: EpiphanySharedPromptConfig, + pub(super) roles: EpiphanyRolePromptConfig, + // Parsed here so the bundled prompt config fails fast even though the GUI runner consumes it. + #[allow(dead_code)] + pub(super) implementation: EpiphanyImplementationPromptConfig, + pub(super) reorientation: EpiphanyReorientationPromptConfig, + pub(super) coordinator: EpiphanyCoordinatorPromptConfig, + pub(super) crrc: EpiphanyCrrcPromptConfig, +} + +#[derive(Debug, serde::Deserialize)] +pub(super) struct EpiphanySharedPromptConfig { + pub(super) persistent_memory: String, +} + +#[derive(Debug, serde::Deserialize)] +pub(super) struct EpiphanyRolePromptConfig { + pub(super) imagination: String, + pub(super) modeling: String, + pub(super) verification: String, + #[allow(dead_code)] + pub(super) research: String, + #[allow(dead_code)] + pub(super) repo_personality: String, + #[allow(dead_code)] + pub(super) repo_memory: String, + #[allow(dead_code)] + pub(super) face: String, +} + +#[derive(Debug, serde::Deserialize)] +pub(super) struct EpiphanyImplementationPromptConfig { + #[allow(dead_code)] + pub(super) continue_template: String, +} + +#[derive(Debug, serde::Deserialize)] +pub(super) struct EpiphanyReorientationPromptConfig { + pub(super) resume: String, + pub(super) regather: String, +} + +#[derive(Debug, serde::Deserialize)] +pub(super) struct EpiphanyCoordinatorPromptConfig { + pub(super) note_template: String, +} + +#[derive(Debug, serde::Deserialize)] +pub(super) struct EpiphanyCrrcPromptConfig { + pub(super) pre_compaction_checkpoint_intervention: String, +} + +pub(super) fn epiphany_specialist_prompt_config() -> &'static EpiphanySpecialistPromptConfig { + static CONFIG: OnceLock = OnceLock::new(); + CONFIG.get_or_init(|| { + toml::from_str(EPIPHANY_SPECIALIST_PROMPTS_TOML) + .expect("bundled Epiphany specialist prompt config must parse") + }) +} + +pub(super) fn epiphany_agent_prompt_with_memory(body: &str) -> String { + let memory = epiphany_specialist_prompt_config() + .shared + .persistent_memory + .trim(); + let body = body.trim(); + if memory.is_empty() { + body.to_string() + } else if body.is_empty() { + memory.to_string() + } else { + format!("{memory}\n\n{body}") + } +} + +pub(super) fn build_epiphany_role_launch_request( + thread_id: &str, + role_id: ThreadEpiphanyRoleId, + expected_revision: Option, + max_runtime_seconds: Option, + state: &EpiphanyThreadState, +) -> Result { + let binding_id = epiphany_role_binding_id(role_id)?; + 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) = match role_id { + ThreadEpiphanyRoleId::Imagination => ( + "role-scoped planning synthesis", + "epiphany.role.imagination", + build_epiphany_role_launch_instruction(role_id), + ), + ThreadEpiphanyRoleId::Modeling => ( + "role-scoped modeling/checkpoint maintenance", + "epiphany.role.modeling", + build_epiphany_role_launch_instruction(role_id), + ), + ThreadEpiphanyRoleId::Verification => ( + "role-scoped verification/review", + "epiphany.role.verification", + build_epiphany_role_launch_instruction(role_id), + ), + ThreadEpiphanyRoleId::Implementation | ThreadEpiphanyRoleId::Reorientation => { + return Err(epiphany_role_binding_id(role_id).unwrap_err()); + } + }; + 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()) + .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, + binding_id: binding_id.to_string(), + kind: CoreEpiphanyJobKind::Specialist, + scope: scope.to_string(), + owner_role: owner_role.to_string(), + authority_scope: authority_scope.to_string(), + linked_subgoal_ids, + linked_graph_node_ids: epiphany_active_graph_node_ids(Some(state)), + instruction, + launch_document, + output_contract_id, + max_runtime_seconds, + }) +} + +fn build_epiphany_role_launch_instruction(role_id: ThreadEpiphanyRoleId) -> String { + let prompts = &epiphany_specialist_prompt_config().roles; + let body = match role_id { + ThreadEpiphanyRoleId::Imagination => prompts.imagination.as_str(), + ThreadEpiphanyRoleId::Modeling => prompts.modeling.as_str(), + ThreadEpiphanyRoleId::Verification => prompts.verification.as_str(), + ThreadEpiphanyRoleId::Implementation | ThreadEpiphanyRoleId::Reorientation => { + "Unsupported Epiphany role specialist template." + } + }; + epiphany_agent_prompt_with_memory(body) +} + +pub(super) fn build_epiphany_reorient_launch_request( + thread_id: &str, + expected_revision: Option, + max_runtime_seconds: Option, + state: &EpiphanyThreadState, + checkpoint: &EpiphanyInvestigationCheckpoint, + decision: &ThreadEpiphanyReorientDecision, +) -> EpiphanyJobLaunchRequest { + let linked_subgoal_ids = epiphany_active_subgoal_ids(Some(state)); + let linked_graph_node_ids = unique_strings( + epiphany_active_graph_node_ids(Some(state)) + .into_iter() + .chain(decision.active_frontier_node_ids.iter().cloned()), + ); + let checkpoint_next_action = checkpoint + .next_action + .clone() + .unwrap_or_else(|| decision.next_action.clone()); + let authority_scope = match decision.action { + ThreadEpiphanyReorientAction::Resume => "epiphany.reorient.resume", + ThreadEpiphanyReorientAction::Regather => "epiphany.reorient.regather", + }; + let scope = match decision.action { + ThreadEpiphanyReorientAction::Resume => "reorient-guided checkpoint resume", + ThreadEpiphanyReorientAction::Regather => "reorient-guided checkpoint regather", + }; + let instruction = build_epiphany_reorient_launch_instruction(decision.action); + 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, + binding_id: EPIPHANY_REORIENT_LAUNCH_BINDING_ID.to_string(), + kind: CoreEpiphanyJobKind::Specialist, + scope: scope.to_string(), + owner_role: EPIPHANY_REORIENT_OWNER_ROLE.to_string(), + authority_scope: authority_scope.to_string(), + linked_subgoal_ids: epiphany_active_subgoal_ids(Some(state)), + linked_graph_node_ids: unique_strings( + epiphany_active_graph_node_ids(Some(state)) + .into_iter() + .chain(decision.active_frontier_node_ids.iter().cloned()), + ), + instruction, + launch_document, + output_contract_id, + max_runtime_seconds, + } +} + +pub(super) fn build_epiphany_reorient_launch_instruction( + action: ThreadEpiphanyReorientAction, +) -> String { + let prompts = &epiphany_specialist_prompt_config().reorientation; + let body = match action { + ThreadEpiphanyReorientAction::Resume => prompts.resume.as_str(), + ThreadEpiphanyReorientAction::Regather => prompts.regather.as_str(), + }; + epiphany_agent_prompt_with_memory(body) +} + +pub(super) fn render_epiphany_coordinator_note( + crrc_action: codex_app_server_protocol::ThreadEpiphanyCrrcAction, + pressure_level: ThreadEpiphanyPressureLevel, + modeling_result_status: codex_app_server_protocol::ThreadEpiphanyRoleResultStatus, + verification_result_status: codex_app_server_protocol::ThreadEpiphanyRoleResultStatus, + reorient_result_status: codex_app_server_protocol::ThreadEpiphanyReorientResultStatus, + coordinator_action: codex_app_server_protocol::ThreadEpiphanyCoordinatorAction, +) -> String { + let template = epiphany_agent_prompt_with_memory( + &epiphany_specialist_prompt_config() + .coordinator + .note_template, + ); + template + .trim() + .replace("{crrc_action}", &format!("{crrc_action:?}")) + .replace("{pressure_level}", &format!("{pressure_level:?}")) + .replace( + "{modeling_result_status}", + &format!("{modeling_result_status:?}"), + ) + .replace( + "{verification_result_status}", + &format!("{verification_result_status:?}"), + ) + .replace( + "{reorient_result_status}", + &format!("{reorient_result_status:?}"), + ) + .replace("{coordinator_action}", &format!("{coordinator_action:?}")) +} + +pub(super) fn unique_strings(values: impl IntoIterator) -> Vec { + let mut unique = Vec::new(); + extend_unique_strings(&mut unique, values); + unique +} + +fn extend_unique_strings(target: &mut Vec, values: impl IntoIterator) { + for value in values { + if !target.iter().any(|existing| existing == &value) { + target.push(value); + } + } +} + +fn epiphany_active_subgoal_ids(state: Option<&EpiphanyThreadState>) -> Vec { + state + .and_then(|state| state.active_subgoal_id.clone()) + .map(|id| vec![id]) + .unwrap_or_default() +} + +fn epiphany_active_graph_node_ids(state: Option<&EpiphanyThreadState>) -> Vec { + state + .and_then(|state| state.graph_frontier.as_ref()) + .map(|frontier| frontier.active_node_ids.clone()) + .unwrap_or_default() +} + +fn path_to_display_string(path: impl AsRef) -> String { + path.as_ref().to_string_lossy().to_string() +} + +fn reorient_action_label(action: ThreadEpiphanyReorientAction) -> &'static str { + match action { + ThreadEpiphanyReorientAction::Resume => "resume", + ThreadEpiphanyReorientAction::Regather => "regather", + } +} + +fn investigation_disposition_label(disposition: EpiphanyInvestigationDisposition) -> &'static str { + match disposition { + EpiphanyInvestigationDisposition::ResumeReady => "resume_ready", + EpiphanyInvestigationDisposition::RegatherRequired => "regather_required", + } +} + +fn reorient_reason_label(reason: ThreadEpiphanyReorientReason) -> &'static str { + match reason { + ThreadEpiphanyReorientReason::MissingState => "missingState", + ThreadEpiphanyReorientReason::MissingCheckpoint => "missingCheckpoint", + ThreadEpiphanyReorientReason::CheckpointRequestedRegather => "checkpointRequestedRegather", + ThreadEpiphanyReorientReason::CheckpointPathsDirty => "checkpointPathsDirty", + ThreadEpiphanyReorientReason::CheckpointPathsChanged => "checkpointPathsChanged", + ThreadEpiphanyReorientReason::FrontierChanged => "frontierChanged", + ThreadEpiphanyReorientReason::UnanchoredCheckpointWhileStateStale => { + "unanchoredCheckpointWhileStateStale" + } + ThreadEpiphanyReorientReason::CheckpointReady => "checkpointReady", + } +} + +pub(super) fn pressure_level_label(level: ThreadEpiphanyPressureLevel) -> &'static str { + match level { + ThreadEpiphanyPressureLevel::Unknown => "unknown", + ThreadEpiphanyPressureLevel::Low => "low", + ThreadEpiphanyPressureLevel::Elevated => "elevated", + ThreadEpiphanyPressureLevel::High => "high", + ThreadEpiphanyPressureLevel::Critical => "critical", + } +} + +fn retrieval_freshness_status_label( + status: ThreadEpiphanyRetrievalFreshnessStatus, +) -> &'static str { + match status { + ThreadEpiphanyRetrievalFreshnessStatus::Missing => "missing", + ThreadEpiphanyRetrievalFreshnessStatus::Ready => "ready", + ThreadEpiphanyRetrievalFreshnessStatus::Stale => "stale", + ThreadEpiphanyRetrievalFreshnessStatus::Indexing => "indexing", + ThreadEpiphanyRetrievalFreshnessStatus::Unavailable => "unavailable", + } +} + +fn graph_freshness_status_label(status: ThreadEpiphanyGraphFreshnessStatus) -> &'static str { + match status { + ThreadEpiphanyGraphFreshnessStatus::Missing => "missing", + ThreadEpiphanyGraphFreshnessStatus::Ready => "ready", + ThreadEpiphanyGraphFreshnessStatus::Stale => "stale", + } +} + +fn invalidation_status_label(status: ThreadEpiphanyInvalidationStatus) -> &'static str { + match status { + ThreadEpiphanyInvalidationStatus::Unavailable => "unavailable", + ThreadEpiphanyInvalidationStatus::Clean => "clean", + ThreadEpiphanyInvalidationStatus::Changed => "changed", + } +} diff --git a/vendor/codex/codex-rs/app-server/src/lib.rs b/vendor/codex/codex-rs/app-server/src/lib.rs index 0e972ffa..75130073 100644 --- a/vendor/codex/codex-rs/app-server/src/lib.rs +++ b/vendor/codex/codex-rs/app-server/src/lib.rs @@ -76,6 +76,7 @@ mod config_manager_service; mod device_key_api; mod dynamic_tools; mod epiphany_invalidation; +mod epiphany_launch; mod error_code; mod external_agent_config_api; mod filters;