Make agent admission refusal replayable

This commit is contained in:
Meta Vens 2026-08-22 20:56:09 +02:00
commit e046a4d190
10 changed files with 819 additions and 94 deletions

View file

@ -71,10 +71,19 @@ fn repo_frontier_item_output_schema() -> serde_json::Value {
"migration_body": {"type": "string", "minLength": 1},
"question": {"type": "string", "minLength": 1},
"gap": {"type": "string", "minLength": 1},
"target_claim_ids": {"type": "array", "minItems": 1, "items": {"type": "string", "minLength": 1}},
"target_claim_ids": {
"type": "array",
"minItems": 1,
"description": "RepoModel node identities whose claims this frontier item changes or resolves.",
"items": {"type": "string", "minLength": 1}
},
"source_scope": {"type": "array", "items": {"type": "string"}},
"recommended_next_organ": {"type": "string", "minLength": 1},
"dependency_item_ids": {"type": "array", "items": {"type": "string"}},
"dependency_item_ids": {
"type": "array",
"description": "RepoModel frontier-item identities that must resolve first. Never put node or claim identities here.",
"items": {"type": "string", "minLength": 1}
},
"status": {"type": "string", "enum": ["proposed", "active", "blocked", "resolved", "retired", "superseded"]},
"evidence_refs": {"type": "array", "items": {"type": "string"}}
}

View file

@ -817,14 +817,14 @@ fn run_coordinator(args: &Args) -> Result<Value> {
steps.push(step);
break;
}
let accepted = epiphany_core::accept_frontier_verification_result(
let admission = epiphany_core::accept_frontier_verification_result(
&runtime_store,
&job_id,
&now(),
)?;
push_event(
&mut step,
json!({"type": "frontierVerificationAccept", "roleId": role_id, "commit": accepted}),
json!({"type": "frontierVerificationAdmission", "roleId": role_id, "outcome": admission}),
);
final_status = collect_coordinator_status(&runtime_store, &thread_id)?;
append_operator_step_jsonl(&steps_path, &step)?;
@ -854,14 +854,14 @@ fn run_coordinator(args: &Args) -> Result<Value> {
steps.push(step);
break;
}
let accepted = epiphany_core::accept_proposal_modeling_result(
let admission = epiphany_core::accept_proposal_modeling_result(
&runtime_store,
&job_id,
&now(),
)?;
push_event(
&mut step,
json!({"type": "proposalModelingAccept", "roleId": role_id, "commit": accepted}),
json!({"type": "proposalModelingAdmission", "roleId": role_id, "outcome": admission}),
);
final_status = collect_coordinator_status(&runtime_store, &thread_id)?;
append_operator_step_jsonl(&steps_path, &step)?;
@ -891,14 +891,14 @@ fn run_coordinator(args: &Args) -> Result<Value> {
steps.push(step);
break;
}
let accepted = epiphany_core::accept_body_modeling_result(
let admission = epiphany_core::accept_body_modeling_result(
&runtime_store,
&job_id,
&now(),
)?;
push_event(
&mut step,
json!({"type": "bodyModelingAccept", "roleId": role_id, "commit": accepted}),
json!({"type": "bodyModelingAdmission", "roleId": role_id, "outcome": admission}),
);
final_status = collect_coordinator_status(&runtime_store, &thread_id)?;
append_operator_step_jsonl(&steps_path, &step)?;
@ -932,14 +932,14 @@ fn run_coordinator(args: &Args) -> Result<Value> {
steps.push(step);
break;
}
let accepted = epiphany_core::accept_frontier_verdict_modeling_result(
let admission = epiphany_core::accept_frontier_verdict_modeling_result(
&runtime_store,
&job_id,
&now(),
)?;
push_event(
&mut step,
json!({"type": "frontierVerdictModelingAccept", "roleId": role_id, "commit": accepted}),
json!({"type": "frontierVerdictModelingAdmission", "roleId": role_id, "outcome": admission}),
);
final_status = collect_coordinator_status(&runtime_store, &thread_id)?;
append_operator_step_jsonl(&steps_path, &step)?;

View file

@ -2,9 +2,11 @@ use anyhow::Context;
use anyhow::Result;
use anyhow::anyhow;
use chrono::SecondsFormat;
use epiphany_core::EpiphanyCoordinatorRunReceipt;
use epiphany_core::EpiphanyRuntimeEvent;
use epiphany_core::EpiphanyRuntimeJob;
use epiphany_core::EpiphanyRuntimeJobResult;
use epiphany_core::EpiphanyRuntimeRoleWorkerResult;
use epiphany_core::EpiphanyRuntimeSession;
use epiphany_core::RuntimeSpineEventOptions;
use epiphany_core::RuntimeSpineInitOptions;
@ -184,6 +186,52 @@ fn main() -> Result<()> {
);
}
}
Command::ListCoordinatorRuns => {
let mut cache = runtime_spine_cache(&args.store)?;
cache.pull_all_backing_stores()?;
let mut receipts = cache.get_all::<EpiphanyCoordinatorRunReceipt>()?;
receipts.sort_by(|left, right| {
left.created_at
.cmp(&right.created_at)
.then_with(|| left.receipt_id.cmp(&right.receipt_id))
});
println!("coordinator runs");
for receipt in receipts {
println!(
"{}\t{}\t{}\t{}\t{}\t{}",
receipt.created_at,
receipt.receipt_id,
receipt.thread_id,
receipt.status,
receipt.final_action,
receipt
.final_reason
.as_deref()
.unwrap_or("no-reason")
.replace(['\r', '\n', '\t'], " ")
);
}
}
Command::ListRepoModelProposals => {
let mut cache = runtime_spine_cache(&args.store)?;
cache.pull_all_backing_stores()?;
let mut results = cache.get_all::<EpiphanyRuntimeRoleWorkerResult>()?;
results.sort_by(|left, right| left.job_id.cmp(&right.job_id));
println!("RepoModel mutation proposals");
for result in results {
let Some(proposal) = result.repo_model_mutation_proposal()? else {
continue;
};
let operations = proposal.operations()?;
println!(
"{}\t{}\t{}\t{}",
result.job_id,
proposal.proposal_id,
result.decision_context_id,
serde_json::to_string(&operations)?
);
}
}
Command::OpenSession {
session_id,
objective,
@ -334,6 +382,8 @@ enum Command {
ListSessions,
ListModelRequests,
ListEvents,
ListCoordinatorRuns,
ListRepoModelProposals,
OpenSession {
session_id: String,
objective: String,
@ -425,6 +475,8 @@ fn parse_command(mut args: Vec<String>) -> Result<Command> {
"list-sessions" => Ok(Command::ListSessions),
"list-model-requests" => Ok(Command::ListModelRequests),
"list-events" => Ok(Command::ListEvents),
"list-coordinator-runs" => Ok(Command::ListCoordinatorRuns),
"list-repo-model-proposals" => Ok(Command::ListRepoModelProposals),
"open-session" => {
let mut session_id = format!("session-{}", Uuid::new_v4());
let mut objective = String::new();
@ -709,7 +761,7 @@ fn now() -> String {
}
fn usage() -> &'static str {
"usage: epiphany-runtime-spine [--store path] <init|status|list-jobs|list-sessions|list-model-requests|list-events|open-session|close-session|repair-root-session|open-job|complete-job|record-event|hello-frame|schema-catalog>"
"usage: epiphany-runtime-spine [--store path] <init|status|list-jobs|list-sessions|list-model-requests|list-events|list-coordinator-runs|list-repo-model-proposals|open-session|close-session|repair-root-session|open-job|complete-job|record-event|hello-frame|schema-catalog>"
}
fn model_input_item_chars(item: &EpiphanyModelInputItem) -> usize {

File diff suppressed because it is too large Load diff

View file

@ -13,7 +13,7 @@ use sha2::{Digest, Sha256};
use crate::{EpiphanyMindDocumentVersion, runtime_spine_cache};
pub const MIND_SCHEMA_EPOCH: &str = "epiphany.mind.epoch.v2";
pub const MIND_SCHEMA_EPOCH: &str = "epiphany.mind.epoch.v3";
pub const MIND_OBJECTIVE_KEY: &str = "objective";
pub const MIND_FOCUS_KEY: &str = "focus";
pub const MIND_MODE_KEY: &str = "mode";
@ -354,6 +354,7 @@ pub struct EpiphanyMindView {
pub verification_audits: Vec<EpiphanyMindVerificationAuditDocument>,
pub reorientation_decisions: Vec<crate::EpiphanyMindReorientationDecisionDocument>,
pub reorientation_failures: Vec<crate::EpiphanyMindReorientationPassFailureDocument>,
pub agent_pass_admission_refusals: Vec<crate::EpiphanyAgentPassAdmissionRefusal>,
pub investigation_checkpoint: Option<EpiphanyInvestigationCheckpoint>,
pub mode: Option<EpiphanyModeState>,
pub planning: EpiphanyPlanningState,
@ -450,6 +451,11 @@ pub(crate) fn validate_mind_write_envelope(envelope: &CultCacheEnvelope) -> Resu
)?;
value.validate(&work)?;
value.work_id
} else if envelope.r#type == crate::EpiphanyAgentPassAdmissionRefusal::TYPE {
let value: crate::EpiphanyAgentPassAdmissionRefusal =
rmp_serde::from_slice(&envelope.payload)?;
value.validate()?;
value.refusal_id
} else if envelope.r#type == EpiphanyMindIdentity::TYPE {
let value: EpiphanyMindIdentity = rmp_serde::from_slice(&envelope.payload)?;
if value.schema_epoch != MIND_SCHEMA_EPOCH || value.runtime_id.trim().is_empty() {
@ -587,6 +593,11 @@ pub fn assemble_mind_view(store_path: impl AsRef<Path>) -> Result<EpiphanyMindVi
cache.get_all::<crate::EpiphanyMindReorientationDecisionDocument>()?;
let mut reorientation_failures =
cache.get_all::<crate::EpiphanyMindReorientationPassFailureDocument>()?;
let mut agent_pass_admission_refusals =
cache.get_all::<crate::EpiphanyAgentPassAdmissionRefusal>()?;
for refusal in &agent_pass_admission_refusals {
refusal.validate()?;
}
let checkpoints =
values::<EpiphanyMindInvestigationCheckpointDocument, _>(&cache, |value| value.value)?;
let mut captures =
@ -630,6 +641,7 @@ pub fn assemble_mind_view(store_path: impl AsRef<Path>) -> Result<EpiphanyMindVi
verification_audits.sort_by(|left, right| left.audit_id.cmp(&right.audit_id));
reorientation_decisions.sort_by(|left, right| left.decision_id.cmp(&right.decision_id));
reorientation_failures.sort_by(|left, right| left.failure_id.cmp(&right.failure_id));
agent_pass_admission_refusals.sort_by(|left, right| left.refusal_id.cmp(&right.refusal_id));
captures.sort_by(|left, right| left.id.cmp(&right.id));
backlog_items.sort_by(|left, right| left.id.cmp(&right.id));
roadmap_streams.sort_by(|left, right| left.id.cmp(&right.id));
@ -674,6 +686,7 @@ pub fn assemble_mind_view(store_path: impl AsRef<Path>) -> Result<EpiphanyMindVi
verification_audits,
reorientation_decisions,
reorientation_failures,
agent_pass_admission_refusals,
investigation_checkpoint,
mode,
planning: EpiphanyPlanningState {
@ -714,6 +727,7 @@ fn canonical_mind_versions(
| EpiphanyMindVerificationAuditDocument::TYPE
| crate::EpiphanyMindReorientationDecisionDocument::TYPE
| crate::EpiphanyMindReorientationPassFailureDocument::TYPE
| crate::EpiphanyAgentPassAdmissionRefusal::TYPE
| EpiphanyMindInvestigationCheckpointDocument::TYPE
| EpiphanyMindPlanningCaptureDocument::TYPE
| EpiphanyMindBacklogItemDocument::TYPE

View file

@ -119,6 +119,7 @@ pub struct EpiphanyMindPromptProjection {
pub verification_audits: Vec<crate::EpiphanyMindVerificationAuditDocument>,
pub reorientation_decisions: Vec<crate::EpiphanyMindReorientationDecisionDocument>,
pub reorientation_failures: Vec<crate::EpiphanyMindReorientationPassFailureDocument>,
pub agent_pass_admission_refusals: Vec<crate::EpiphanyAgentPassAdmissionRefusal>,
pub investigation_checkpoint: Option<epiphany_state_model::EpiphanyInvestigationCheckpoint>,
pub mode: Option<epiphany_state_model::EpiphanyModeState>,
pub planning: epiphany_state_model::EpiphanyPlanningState,
@ -141,6 +142,7 @@ impl From<crate::EpiphanyMindView> for EpiphanyMindPromptProjection {
verification_audits: value.verification_audits,
reorientation_decisions: value.reorientation_decisions,
reorientation_failures: value.reorientation_failures,
agent_pass_admission_refusals: value.agent_pass_admission_refusals,
investigation_checkpoint: value.investigation_checkpoint,
mode: value.mode,
planning: value.planning,
@ -2023,6 +2025,7 @@ mod tests {
verification_audits: Vec::new(),
reorientation_decisions: Vec::new(),
reorientation_failures: Vec::new(),
agent_pass_admission_refusals: Vec::new(),
investigation_checkpoint: None,
mode: None,
planning: Default::default(),

View file

@ -170,7 +170,7 @@ pub const TOOL_INVOCATION_RECEIPT_TYPE: &str = "epiphany.tool_invocation_receipt
pub const RUNTIME_IDENTITY_KEY: &str = "self";
pub const RUNTIME_SWARM_BINDING_KEY: &str = "runtime-swarm-binding";
pub const RUNTIME_SWARM_BINDING_SCHEMA_VERSION: &str = "epiphany.runtime.swarm_binding.v0";
pub const RUNTIME_SPINE_SCHEMA_VERSION: &str = "epiphany.runtime_spine.v3";
pub const RUNTIME_SPINE_SCHEMA_VERSION: &str = "epiphany.runtime_spine.v4";
pub const EPIPHANY_RUNTIME_ROOT_SESSION_ID: &str = "epiphany-main";
pub const RUNTIME_MODEL_EXECUTION_BINDING_SCHEMA_VERSION: &str =
"epiphany.runtime.model_execution_binding.v0";
@ -5870,6 +5870,13 @@ pub(crate) fn validate_proposal_modeling_worker_fulfillment(
&proposal.evidence_refs,
&proposal.public_source_refs,
)?;
let attempt_ordinal =
crate::current_work::proposal_modeling_attempt_ordinal(request_id, &result.job_id)?;
let prior_admission_refusals = crate::current_work::proposal_modeling_prior_admission_refusals(
cache,
request_id,
attempt_ordinal,
)?;
let projection_matches_authenticated_request = projection.is_some_and(|projection| {
let expected = crate::RepoFrontierProposalModelingContextProjection {
schema_version: crate::REPO_FRONTIER_PROPOSAL_MODELING_CONTEXT_SCHEMA_VERSION.into(),
@ -5894,6 +5901,7 @@ pub(crate) fn validate_proposal_modeling_worker_fulfillment(
private_state_included: proposal.private_state_included,
model_projection_digest: projection.model_projection_digest.clone(),
model_source_documents: projection.model_source_documents.clone(),
prior_admission_refusals: prior_admission_refusals.clone(),
};
projection == &expected
});
@ -6046,11 +6054,28 @@ pub fn runtime_typed_request_fulfillment(
validate_non_empty(request_id, "typed fulfillment request id")?;
let mut cache = runtime_spine_cache(store_path)?;
cache.pull_all_backing_stores()?;
let admission_refusals = cache
.get_all::<crate::EpiphanyAgentPassAdmissionRefusal>()?
.into_iter()
.map(|refusal| {
refusal.validate()?;
Ok(refusal)
})
.collect::<Result<Vec<_>>>()?;
let archived_matches = cache
.get_all::<EpiphanyArchivedRuntimeWorkerAttempt>()?
.into_iter()
.filter(|attempt| {
attempt.request_id == request_id && attempt.fulfilled_result_id().is_some()
attempt.request_id == request_id
&& attempt.fulfilled_result_id().is_some()
&& !admission_refusals.iter().any(|refusal| {
refusal.pass_family.request_kind() == request.kind()
&& refusal.request_id == request_id
&& refusal.job_id == attempt.job_id
&& attempt.fulfilled_result_id() == Some(refusal.result_id.as_str())
&& attempt.decision_context_id()
== Some(refusal.decision_context_id.as_str())
})
})
.collect::<Vec<_>>();
if archived_matches.len() > 1 {
@ -6107,7 +6132,16 @@ pub fn runtime_typed_request_fulfillment(
let matches = cache
.get_all::<EpiphanyRuntimeRoleWorkerResult>()?
.into_iter()
.filter(|result| request.matches_result(result))
.filter(|result| {
request.matches_result(result)
&& !admission_refusals.iter().any(|refusal| {
refusal.pass_family.request_kind() == request.kind()
&& refusal.request_id == request_id
&& refusal.job_id == result.job_id
&& refusal.result_id == result.result_id
&& refusal.decision_context_id == result.decision_context_id
})
})
.collect::<Vec<_>>();
if matches.is_empty() {
return Ok(None);

View file

@ -133,6 +133,7 @@ mod tests {
verification_audits: Vec::new(),
reorientation_decisions: Vec::new(),
reorientation_failures: Vec::new(),
agent_pass_admission_refusals: Vec::new(),
investigation_checkpoint: None,
mode: None,
planning: EpiphanyPlanningState {

View file

@ -429,6 +429,7 @@ mod tests {
verification_audits: Vec::new(),
reorientation_decisions: Vec::new(),
reorientation_failures: Vec::new(),
agent_pass_admission_refusals: Vec::new(),
investigation_checkpoint: None,
mode: None,
planning: EpiphanyPlanningState::default(),
@ -449,10 +450,8 @@ mod tests {
Some(true)
);
assert_eq!(scene.invariant_status_counts[0].count, 2);
assert!(
scene
.available_actions
.contains(&EpiphanySceneAction::ReorientResult)
);
assert!(scene
.available_actions
.contains(&EpiphanySceneAction::ReorientResult));
}
}

View file

@ -15,9 +15,9 @@ use serde::Serialize;
pub const ROLE_WORKER_OUTPUT_CONTRACT_ID: &str = "epiphany.worker.role_result.v4";
pub const REORIENT_WORKER_OUTPUT_CONTRACT_ID: &str = "epiphany.worker.reorient_result.v0";
pub const REPO_FRONTIER_PROPOSAL_MODELING_CONTEXT_SCHEMA_VERSION: &str =
"epiphany.worker.repo_frontier_proposal_modeling_context.v1";
"epiphany.worker.repo_frontier_proposal_modeling_context.v2";
pub const REPO_FRONTIER_PROPOSAL_MODELING_CONTEXT_CONTRACT: &str =
"epiphany.repo_frontier_proposal_modeling_context.v1";
"epiphany.repo_frontier_proposal_modeling_context.v2";
pub const REPO_FRONTIER_PLANNING_CONTEXT_SCHEMA_VERSION: &str =
"epiphany.worker.repo_frontier_planning_context.v2";
pub const REPO_FRONTIER_PLANNING_CONTEXT_CONTRACT: &str =
@ -209,6 +209,7 @@ pub struct RepoFrontierProposalModelingContextProjection {
pub private_state_included: bool,
pub model_projection_digest: String,
pub model_source_documents: Vec<crate::EpiphanyMindDocumentVersion>,
pub prior_admission_refusals: Vec<crate::EpiphanyAgentPassAdmissionRefusal>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]