Type role state patches
This commit is contained in:
parent
65e5f8829d
commit
5886ff58ca
9 changed files with 197 additions and 115 deletions
|
|
@ -204,6 +204,7 @@ pub use surfaces::EpiphanyRoleFindingInterpretation;
|
|||
pub use surfaces::EpiphanyRoleResultRoleId;
|
||||
pub use surfaces::EpiphanyRoleSelfPersistenceReview;
|
||||
pub use surfaces::EpiphanyRoleSelfPersistenceStatus;
|
||||
pub use surfaces::EpiphanyRoleStatePatchDocument;
|
||||
pub use surfaces::EpiphanyRoleWorkerLaunchDocument;
|
||||
pub use surfaces::EpiphanyScene;
|
||||
pub use surfaces::EpiphanySceneAction;
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ mod reorient;
|
|||
mod role_board;
|
||||
mod role_result;
|
||||
mod scene;
|
||||
mod state_patch;
|
||||
mod worker_launch;
|
||||
|
||||
pub use coordinator::EpiphanyCoordinatorAction;
|
||||
|
|
@ -123,6 +124,7 @@ pub use scene::EpiphanySceneStateStatus;
|
|||
pub use scene::EpiphanySceneStatusCount;
|
||||
pub use scene::EpiphanySceneSubgoal;
|
||||
pub use scene::derive_scene;
|
||||
pub use state_patch::EpiphanyRoleStatePatchDocument;
|
||||
pub use worker_launch::EpiphanyReorientWorkerLaunchDocument;
|
||||
pub use worker_launch::EpiphanyRoleWorkerLaunchDocument;
|
||||
pub use worker_launch::EpiphanyWorkerLaunchDocument;
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ pub struct EpiphanyRoleFindingInterpretation {
|
|||
pub open_questions: Vec<String>,
|
||||
pub evidence_gaps: Vec<String>,
|
||||
pub risks: Vec<String>,
|
||||
pub state_patch: Option<serde_json::Value>,
|
||||
pub state_patch: Option<EpiphanyRoleStatePatchDocument>,
|
||||
pub self_patch: Option<AgentSelfPatch>,
|
||||
pub self_persistence: Option<EpiphanyRoleSelfPersistenceReview>,
|
||||
pub job_error: Option<String>,
|
||||
|
|
@ -109,11 +109,20 @@ pub struct EpiphanyReorientAcceptanceBundle {
|
|||
pub fn interpret_role_finding(
|
||||
role_id: EpiphanyRoleResultRoleId,
|
||||
raw_result: &serde_json::Value,
|
||||
state_patch_parse_error: Option<String>,
|
||||
_legacy_state_patch_parse_error: Option<String>,
|
||||
job_error: Option<String>,
|
||||
item_error: Option<String>,
|
||||
) -> EpiphanyRoleFindingInterpretation {
|
||||
let state_patch = raw_result.get("statePatch").cloned();
|
||||
let state_patch_result = raw_result
|
||||
.get("statePatch")
|
||||
.cloned()
|
||||
.map(serde_json::from_value::<EpiphanyRoleStatePatchDocument>);
|
||||
let state_patch = state_patch_result
|
||||
.as_ref()
|
||||
.and_then(|result| result.as_ref().ok().cloned());
|
||||
let state_patch_parse_error = state_patch_result
|
||||
.as_ref()
|
||||
.and_then(|result| result.as_ref().err().map(ToString::to_string));
|
||||
let (self_patch, self_persistence) = raw_result
|
||||
.get("selfPatch")
|
||||
.map(|patch| decode_role_self_patch(role_id, patch))
|
||||
|
|
@ -437,49 +446,51 @@ fn decode_role_self_patch(
|
|||
(decoded, Some(review))
|
||||
}
|
||||
|
||||
pub fn modeling_role_state_patch_policy_errors(patch: &serde_json::Value) -> Vec<String> {
|
||||
pub fn modeling_role_state_patch_policy_errors(
|
||||
patch: &EpiphanyRoleStatePatchDocument,
|
||||
) -> Vec<String> {
|
||||
let mut errors = Vec::new();
|
||||
if has_field(patch, "objective") {
|
||||
if patch.objective.is_some() {
|
||||
errors
|
||||
.push("objective changes are not allowed through modeling role acceptance".to_string());
|
||||
}
|
||||
if has_field(patch, "activeSubgoalId") || has_field(patch, "subgoals") {
|
||||
if patch.active_subgoal_id.is_some() || patch.subgoals.is_some() {
|
||||
errors.push("subgoal changes are not allowed through modeling role acceptance".to_string());
|
||||
}
|
||||
if has_field(patch, "invariants") {
|
||||
if patch.invariants.is_some() {
|
||||
errors
|
||||
.push("invariant changes are not allowed through modeling role acceptance".to_string());
|
||||
}
|
||||
if has_field(patch, "jobBindings") {
|
||||
if patch.job_bindings.is_some() {
|
||||
errors.push(
|
||||
"job binding changes are not allowed through modeling role acceptance".to_string(),
|
||||
);
|
||||
}
|
||||
if non_empty_array_field(patch, "acceptanceReceipts") {
|
||||
if !patch.acceptance_receipts.is_empty() {
|
||||
errors.push(
|
||||
"acceptance receipt changes are owned by roleAccept, not worker statePatch".to_string(),
|
||||
);
|
||||
}
|
||||
if non_empty_array_field(patch, "runtimeLinks") {
|
||||
if !patch.runtime_links.is_empty() {
|
||||
errors.push(
|
||||
"runtime link changes are owned by launch/read-back surfaces, not worker statePatch"
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
if has_field(patch, "planning") {
|
||||
if patch.planning.is_some() {
|
||||
errors
|
||||
.push("planning changes are not allowed through modeling role acceptance".to_string());
|
||||
}
|
||||
if has_field(patch, "churn") || has_field(patch, "mode") {
|
||||
if patch.churn.is_some() || patch.mode.is_some() {
|
||||
errors.push(
|
||||
"churn or mode changes are not allowed through modeling role acceptance".to_string(),
|
||||
);
|
||||
}
|
||||
if !has_field(patch, "graphs")
|
||||
&& !has_field(patch, "graphFrontier")
|
||||
&& !has_field(patch, "graphCheckpoint")
|
||||
&& !has_field(patch, "scratch")
|
||||
&& !has_field(patch, "investigationCheckpoint")
|
||||
if patch.graphs.is_none()
|
||||
&& patch.graph_frontier.is_none()
|
||||
&& patch.graph_checkpoint.is_none()
|
||||
&& patch.scratch.is_none()
|
||||
&& patch.investigation_checkpoint.is_none()
|
||||
{
|
||||
errors.push(
|
||||
"statePatch must include a modeling field: graphs, graphFrontier, graphCheckpoint, scratch, or investigationCheckpoint"
|
||||
|
|
@ -489,106 +500,89 @@ pub fn modeling_role_state_patch_policy_errors(patch: &serde_json::Value) -> Vec
|
|||
errors
|
||||
}
|
||||
|
||||
pub fn imagination_role_state_patch_policy_errors(patch: &serde_json::Value) -> Vec<String> {
|
||||
pub fn imagination_role_state_patch_policy_errors(
|
||||
patch: &EpiphanyRoleStatePatchDocument,
|
||||
) -> Vec<String> {
|
||||
let mut errors = Vec::new();
|
||||
if has_field(patch, "objective") {
|
||||
if patch.objective.is_some() {
|
||||
errors.push(
|
||||
"objective changes are not allowed through imagination role acceptance".to_string(),
|
||||
);
|
||||
}
|
||||
if has_field(patch, "activeSubgoalId") || has_field(patch, "subgoals") {
|
||||
if patch.active_subgoal_id.is_some() || patch.subgoals.is_some() {
|
||||
errors.push(
|
||||
"subgoal changes are not allowed through imagination role acceptance".to_string(),
|
||||
);
|
||||
}
|
||||
if has_field(patch, "invariants") {
|
||||
if patch.invariants.is_some() {
|
||||
errors.push(
|
||||
"invariant changes are not allowed through imagination role acceptance".to_string(),
|
||||
);
|
||||
}
|
||||
if has_field(patch, "graphs")
|
||||
|| has_field(patch, "graphFrontier")
|
||||
|| has_field(patch, "graphCheckpoint")
|
||||
|| has_field(patch, "investigationCheckpoint")
|
||||
if patch.graphs.is_some()
|
||||
|| patch.graph_frontier.is_some()
|
||||
|| patch.graph_checkpoint.is_some()
|
||||
|| patch.investigation_checkpoint.is_some()
|
||||
{
|
||||
errors.push(
|
||||
"graph or checkpoint changes are not allowed through imagination role acceptance"
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
if has_field(patch, "scratch") {
|
||||
if patch.scratch.is_some() {
|
||||
errors.push(
|
||||
"scratch changes are not allowed through imagination role acceptance".to_string(),
|
||||
);
|
||||
}
|
||||
if has_field(patch, "jobBindings") {
|
||||
if patch.job_bindings.is_some() {
|
||||
errors.push(
|
||||
"job binding changes are not allowed through imagination role acceptance".to_string(),
|
||||
);
|
||||
}
|
||||
if non_empty_array_field(patch, "acceptanceReceipts") {
|
||||
if !patch.acceptance_receipts.is_empty() {
|
||||
errors.push(
|
||||
"acceptance receipt changes are owned by roleAccept, not worker statePatch".to_string(),
|
||||
);
|
||||
}
|
||||
if non_empty_array_field(patch, "runtimeLinks") {
|
||||
if !patch.runtime_links.is_empty() {
|
||||
errors.push(
|
||||
"runtime link changes are owned by launch/read-back surfaces, not worker statePatch"
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
if has_field(patch, "churn") || has_field(patch, "mode") {
|
||||
if patch.churn.is_some() || patch.mode.is_some() {
|
||||
errors.push(
|
||||
"churn or mode changes are not allowed through imagination role acceptance".to_string(),
|
||||
);
|
||||
}
|
||||
let Some(planning) = patch.get("planning") else {
|
||||
let Some(planning) = patch.planning.as_ref() else {
|
||||
errors.push("statePatch must include planning changes".to_string());
|
||||
return errors;
|
||||
};
|
||||
let objective_drafts = get_field(planning, "objectiveDrafts", "objective_drafts")
|
||||
.and_then(serde_json::Value::as_array)
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
if objective_drafts.is_empty() {
|
||||
if planning.objective_drafts.is_empty() {
|
||||
errors.push("planning patch must include at least one objective draft".to_string());
|
||||
}
|
||||
if !objective_drafts.iter().any(|draft| {
|
||||
draft
|
||||
.get("status")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.is_some_and(|status| status.eq_ignore_ascii_case("draft"))
|
||||
}) {
|
||||
if !planning
|
||||
.objective_drafts
|
||||
.iter()
|
||||
.any(|draft| draft.status.eq_ignore_ascii_case("draft"))
|
||||
{
|
||||
errors.push(
|
||||
"planning patch must include at least one objective draft with status draft"
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
for draft in &objective_drafts {
|
||||
let id = draft
|
||||
.get("id")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.unwrap_or("<missing>");
|
||||
if !draft
|
||||
.get("acceptanceCriteria")
|
||||
.or_else(|| draft.get("acceptance_criteria"))
|
||||
.and_then(serde_json::Value::as_array)
|
||||
.is_some_and(|items| !items.is_empty())
|
||||
{
|
||||
for draft in &planning.objective_drafts {
|
||||
if draft.acceptance_criteria.is_empty() {
|
||||
errors.push(format!(
|
||||
"planning objective draft {:?} must include acceptance criteria",
|
||||
id
|
||||
draft.id
|
||||
));
|
||||
}
|
||||
if !draft
|
||||
.get("reviewGates")
|
||||
.or_else(|| draft.get("review_gates"))
|
||||
.and_then(serde_json::Value::as_array)
|
||||
.is_some_and(|items| !items.is_empty())
|
||||
{
|
||||
if draft.review_gates.is_empty() {
|
||||
errors.push(format!(
|
||||
"planning objective draft {:?} must include review gates",
|
||||
id
|
||||
draft.id
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
@ -597,10 +591,10 @@ pub fn imagination_role_state_patch_policy_errors(patch: &serde_json::Value) ->
|
|||
|
||||
fn modeling_role_state_patch_error(
|
||||
raw_result: &serde_json::Value,
|
||||
state_patch: Option<&serde_json::Value>,
|
||||
state_patch: Option<&EpiphanyRoleStatePatchDocument>,
|
||||
parse_error: Option<String>,
|
||||
) -> Option<String> {
|
||||
let Some(value) = raw_result.get("statePatch") else {
|
||||
if raw_result.get("statePatch").is_none() {
|
||||
return Some("modeling result is not reviewable: missing required statePatch".to_string());
|
||||
};
|
||||
if let Some(error) = parse_error {
|
||||
|
|
@ -608,7 +602,10 @@ fn modeling_role_state_patch_error(
|
|||
"modeling result is not reviewable: invalid statePatch ({error})"
|
||||
));
|
||||
}
|
||||
let errors = modeling_role_state_patch_policy_errors(state_patch.unwrap_or(value));
|
||||
let Some(state_patch) = state_patch else {
|
||||
return Some("modeling result is not reviewable: invalid statePatch".to_string());
|
||||
};
|
||||
let errors = modeling_role_state_patch_policy_errors(state_patch);
|
||||
if errors.is_empty() {
|
||||
None
|
||||
} else {
|
||||
|
|
@ -621,10 +618,10 @@ fn modeling_role_state_patch_error(
|
|||
|
||||
fn imagination_role_state_patch_error(
|
||||
raw_result: &serde_json::Value,
|
||||
state_patch: Option<&serde_json::Value>,
|
||||
state_patch: Option<&EpiphanyRoleStatePatchDocument>,
|
||||
parse_error: Option<String>,
|
||||
) -> Option<String> {
|
||||
let Some(value) = raw_result.get("statePatch") else {
|
||||
if raw_result.get("statePatch").is_none() {
|
||||
return Some(
|
||||
"imagination result is not reviewable: missing required statePatch".to_string(),
|
||||
);
|
||||
|
|
@ -634,7 +631,10 @@ fn imagination_role_state_patch_error(
|
|||
"imagination result is not reviewable: invalid statePatch ({error})"
|
||||
));
|
||||
}
|
||||
let errors = imagination_role_state_patch_policy_errors(state_patch.unwrap_or(value));
|
||||
let Some(state_patch) = state_patch else {
|
||||
return Some("imagination result is not reviewable: invalid statePatch".to_string());
|
||||
};
|
||||
let errors = imagination_role_state_patch_policy_errors(state_patch);
|
||||
if errors.is_empty() {
|
||||
None
|
||||
} else {
|
||||
|
|
@ -816,25 +816,6 @@ fn json_string_array_field(value: &serde_json::Value, key: &str) -> Vec<String>
|
|||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn has_field(value: &serde_json::Value, key: &str) -> bool {
|
||||
value.get(key).is_some_and(|field| !field.is_null())
|
||||
}
|
||||
|
||||
fn get_field<'a>(
|
||||
value: &'a serde_json::Value,
|
||||
camel_key: &str,
|
||||
snake_key: &str,
|
||||
) -> Option<&'a serde_json::Value> {
|
||||
value.get(camel_key).or_else(|| value.get(snake_key))
|
||||
}
|
||||
|
||||
fn non_empty_array_field(value: &serde_json::Value, key: &str) -> bool {
|
||||
value
|
||||
.get(key)
|
||||
.and_then(serde_json::Value::as_array)
|
||||
.is_some_and(|items| !items.is_empty())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
@ -1004,11 +985,15 @@ mod tests {
|
|||
&serde_json::json!({
|
||||
"statePatch": {
|
||||
"planning": {
|
||||
"objectiveDrafts": [{
|
||||
"objective_drafts": [{
|
||||
"id": "draft-imagination-test",
|
||||
"title": "Draft from imagination",
|
||||
"summary": "Reviewable objective draft.",
|
||||
"scope": {"includes": ["typed state"], "excludes": []},
|
||||
"status": "draft",
|
||||
"acceptanceCriteria": ["A criterion"],
|
||||
"reviewGates": ["human review"]
|
||||
"lane_plan": {},
|
||||
"acceptance_criteria": ["A criterion"],
|
||||
"review_gates": ["human review"]
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
|
@ -1154,6 +1139,7 @@ mod tests {
|
|||
}));
|
||||
}
|
||||
}
|
||||
use super::state_patch::EpiphanyRoleStatePatchDocument;
|
||||
use crate::agent_memory::AgentSelfPatch;
|
||||
use crate::agent_memory::decode_agent_self_patch;
|
||||
use crate::agent_memory::review_agent_self_patch_contract;
|
||||
|
|
|
|||
56
epiphany-core/src/surfaces/state_patch.rs
Normal file
56
epiphany-core/src/surfaces/state_patch.rs
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
use codex_protocol::protocol::EpiphanyAcceptanceReceipt;
|
||||
use codex_protocol::protocol::EpiphanyChurnState;
|
||||
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::EpiphanyJobBinding;
|
||||
use codex_protocol::protocol::EpiphanyModeState;
|
||||
use codex_protocol::protocol::EpiphanyObservation;
|
||||
use codex_protocol::protocol::EpiphanyPlanningState;
|
||||
use codex_protocol::protocol::EpiphanyRuntimeLink;
|
||||
use codex_protocol::protocol::EpiphanyScratchPad;
|
||||
use codex_protocol::protocol::EpiphanySubgoal;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct EpiphanyRoleStatePatchDocument {
|
||||
#[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, skip_serializing_if = "Option::is_none")]
|
||||
pub subgoals: Option<Vec<EpiphanySubgoal>>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub invariants: Option<Vec<EpiphanyInvariant>>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub graphs: Option<EpiphanyGraphs>,
|
||||
#[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 scratch: Option<EpiphanyScratchPad>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub investigation_checkpoint: Option<EpiphanyInvestigationCheckpoint>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub job_bindings: Option<Vec<EpiphanyJobBinding>>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub acceptance_receipts: Vec<EpiphanyAcceptanceReceipt>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub runtime_links: Vec<EpiphanyRuntimeLink>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub observations: Vec<EpiphanyObservation>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub evidence: Vec<EpiphanyEvidenceRecord>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub churn: Option<EpiphanyChurnState>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub mode: Option<EpiphanyModeState>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub planning: Option<EpiphanyPlanningState>,
|
||||
}
|
||||
|
|
@ -391,14 +391,17 @@ inventory and ledger now exist:
|
|||
`runtime_job_result_to_role_json` or `runtime_job_result_to_reorient_json`.
|
||||
`epiphany-core` owns typed role/reorient runtime-result interpreters and the
|
||||
app-server maps those interpretations directly to protocol projections.
|
||||
- Fourth cut landed: role `statePatch` is no longer raw
|
||||
`serde_json::Value` inside `EpiphanyRoleFindingInterpretation`.
|
||||
`epiphany-core` owns `EpiphanyRoleStatePatchDocument`, policy checks read
|
||||
typed fields directly, and the app-server maps the typed document to the
|
||||
legacy `ThreadEpiphanyUpdatePatch` without JSON round-tripping.
|
||||
|
||||
Continue at the protocol edge. Replace
|
||||
`ThreadEpiphanyJobLaunchParams.input_json` / `output_schema_json` with CultNet
|
||||
typed launch-intent messages, then type role `statePatch` map/planning/graph
|
||||
patch documents instead of carrying `serde_json::Value` through role finding
|
||||
interpretation. Do not resume Rider, Unity, Aquarium, Face, dogfood, planning,
|
||||
app, skill, marketplace, or bridge expansion until this organ is being cut
|
||||
cleanly.
|
||||
typed launch-intent messages, then seal MCP JSON behind a CultNet MCP adapter.
|
||||
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
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ Rule: JSON is allowed at external schema/wire boundaries. Inside Epiphany, live
|
|||
|
||||
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. 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.
|
||||
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. Seal MCP JSON behind a CultNet MCP adapter.
|
||||
6. Audit heartbeat cognition values and either type them or explicitly expire them.
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -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 three 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`. Runtime job results now project through typed role/reorient interpreters instead of JSON round-tripping. Legacy app-server JSON remains only at hostile ingress/protocol quarantine 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 type role `statePatch` map/planning/graph patch documents instead of carrying `serde_json::Value` through role finding interpretation.
|
||||
summary: The Codex starvation pass has four 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 now uses `EpiphanyWorkerLaunchDocument` 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 only at hostile ingress/protocol quarantine 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 seal MCP JSON behind a CultNet MCP adapter instead of letting Codex app/tool plumbing own it.
|
||||
|
|
|
|||
|
|
@ -563,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::EpiphanyRoleStatePatchDocument;
|
||||
use epiphany_core::EpiphanyRoleWorkerLaunchDocument;
|
||||
use epiphany_core::EpiphanyRuntimeJobSnapshot;
|
||||
use epiphany_core::EpiphanyRuntimeJobStatus;
|
||||
|
|
@ -13345,31 +13346,21 @@ fn map_epiphany_role_finding(
|
|||
job_error: Option<String>,
|
||||
item_error: Option<String>,
|
||||
) -> ThreadEpiphanyRoleFinding {
|
||||
let state_patch_result = raw_result
|
||||
.get("statePatch")
|
||||
.cloned()
|
||||
.map(serde_json::from_value::<ThreadEpiphanyUpdatePatch>);
|
||||
let state_patch = state_patch_result
|
||||
.as_ref()
|
||||
.and_then(|result| result.as_ref().ok().cloned());
|
||||
let state_patch_parse_error = state_patch_result
|
||||
.as_ref()
|
||||
.and_then(|result| result.as_ref().err().map(ToString::to_string));
|
||||
let finding = interpret_role_finding(
|
||||
map_core_role_result_role_id(role_id),
|
||||
&raw_result,
|
||||
state_patch_parse_error,
|
||||
None,
|
||||
job_error,
|
||||
item_error,
|
||||
);
|
||||
map_protocol_role_finding(role_id, finding, state_patch)
|
||||
map_protocol_role_finding(role_id, finding)
|
||||
}
|
||||
|
||||
fn map_protocol_role_finding(
|
||||
role_id: ThreadEpiphanyRoleId,
|
||||
finding: EpiphanyRoleFindingInterpretation,
|
||||
state_patch: Option<ThreadEpiphanyUpdatePatch>,
|
||||
) -> ThreadEpiphanyRoleFinding {
|
||||
let state_patch = finding.state_patch.map(map_protocol_update_patch);
|
||||
ThreadEpiphanyRoleFinding {
|
||||
role_id,
|
||||
verdict: finding.verdict,
|
||||
|
|
@ -13399,6 +13390,52 @@ fn map_protocol_role_finding(
|
|||
}
|
||||
}
|
||||
|
||||
fn map_protocol_update_patch(patch: EpiphanyRoleStatePatchDocument) -> ThreadEpiphanyUpdatePatch {
|
||||
ThreadEpiphanyUpdatePatch {
|
||||
objective: patch.objective,
|
||||
active_subgoal_id: patch.active_subgoal_id,
|
||||
subgoals: patch.subgoals,
|
||||
invariants: patch.invariants,
|
||||
graphs: patch.graphs,
|
||||
graph_frontier: patch.graph_frontier,
|
||||
graph_checkpoint: patch.graph_checkpoint,
|
||||
scratch: patch.scratch,
|
||||
investigation_checkpoint: patch.investigation_checkpoint,
|
||||
job_bindings: patch.job_bindings,
|
||||
acceptance_receipts: patch.acceptance_receipts,
|
||||
runtime_links: patch.runtime_links,
|
||||
observations: patch.observations,
|
||||
evidence: patch.evidence,
|
||||
churn: patch.churn,
|
||||
mode: patch.mode,
|
||||
planning: patch.planning,
|
||||
}
|
||||
}
|
||||
|
||||
fn core_state_patch_from_protocol(
|
||||
patch: &ThreadEpiphanyUpdatePatch,
|
||||
) -> EpiphanyRoleStatePatchDocument {
|
||||
EpiphanyRoleStatePatchDocument {
|
||||
objective: patch.objective.clone(),
|
||||
active_subgoal_id: patch.active_subgoal_id.clone(),
|
||||
subgoals: patch.subgoals.clone(),
|
||||
invariants: patch.invariants.clone(),
|
||||
graphs: patch.graphs.clone(),
|
||||
graph_frontier: patch.graph_frontier.clone(),
|
||||
graph_checkpoint: patch.graph_checkpoint.clone(),
|
||||
scratch: patch.scratch.clone(),
|
||||
investigation_checkpoint: patch.investigation_checkpoint.clone(),
|
||||
job_bindings: patch.job_bindings.clone(),
|
||||
acceptance_receipts: patch.acceptance_receipts.clone(),
|
||||
runtime_links: patch.runtime_links.clone(),
|
||||
observations: patch.observations.clone(),
|
||||
evidence: patch.evidence.clone(),
|
||||
churn: patch.churn.clone(),
|
||||
mode: patch.mode.clone(),
|
||||
planning: patch.planning.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn map_core_role_result_role_id(role_id: ThreadEpiphanyRoleId) -> EpiphanyRoleResultRoleId {
|
||||
match role_id {
|
||||
ThreadEpiphanyRoleId::Implementation => EpiphanyRoleResultRoleId::Implementation,
|
||||
|
|
@ -13642,7 +13679,6 @@ fn load_epiphany_role_result_from_runtime_spine_job(
|
|||
map_protocol_role_finding(
|
||||
role_id,
|
||||
interpret_role_runtime_job_result(map_core_role_result_role_id(role_id), result),
|
||||
None,
|
||||
)
|
||||
});
|
||||
let note = render_epiphany_role_result_note(role_id, status, finding.as_ref(), None);
|
||||
|
|
@ -14777,13 +14813,11 @@ fn parse_role_finding_state_patch(
|
|||
}
|
||||
|
||||
fn imagination_role_accept_patch_errors(patch: &ThreadEpiphanyUpdatePatch) -> Vec<String> {
|
||||
let value = serde_json::to_value(patch).unwrap_or_else(|_| serde_json::Value::Null);
|
||||
imagination_role_state_patch_policy_errors(&value)
|
||||
imagination_role_state_patch_policy_errors(&core_state_patch_from_protocol(patch))
|
||||
}
|
||||
|
||||
fn modeling_role_accept_patch_errors(patch: &ThreadEpiphanyUpdatePatch) -> Vec<String> {
|
||||
let value = serde_json::to_value(patch).unwrap_or_else(|_| serde_json::Value::Null);
|
||||
modeling_role_state_patch_policy_errors(&value)
|
||||
modeling_role_state_patch_policy_errors(&core_state_patch_from_protocol(patch))
|
||||
}
|
||||
|
||||
fn role_finding_summary(finding: &ThreadEpiphanyRoleFinding) -> String {
|
||||
|
|
|
|||
Reference in a new issue