2325 lines
89 KiB
Rust
2325 lines
89 KiB
Rust
use crate::EpiphanyWorkerLaunchDocument;
|
|
use crate::agent_memory::AGENT_MEMORY_TYPE;
|
|
use crate::heartbeat_state::HEARTBEAT_STATE_SCHEMA_VERSION;
|
|
use crate::heartbeat_state::HEARTBEAT_STATE_TYPE;
|
|
use crate::memory_graph::MEMORY_GRAPH_SCHEMA_VERSION;
|
|
use crate::memory_graph::MEMORY_GRAPH_TYPE;
|
|
use crate::state_ledger::STATE_LEDGER_SCHEMA_VERSION;
|
|
use crate::state_ledger::STATE_LEDGER_STORE_TYPE;
|
|
use crate::thread_state_store::THREAD_STATE_SCHEMA_VERSION;
|
|
use crate::thread_state_store::THREAD_STATE_TYPE;
|
|
use anyhow::Context;
|
|
use anyhow::Result;
|
|
use anyhow::anyhow;
|
|
use cultcache_rs::CultCache;
|
|
use cultcache_rs::DatabaseEntry;
|
|
use cultcache_rs::SingleFileMessagePackBackingStore;
|
|
use cultnet_rs::CultNetDocumentMutationContract;
|
|
use cultnet_rs::CultNetDocumentOperation;
|
|
use cultnet_rs::CultNetMessage;
|
|
use cultnet_rs::CultNetMutationAuthority;
|
|
use cultnet_rs::CultNetSchemaKind;
|
|
use cultnet_rs::CultNetSchemaRegistration;
|
|
use cultnet_rs::CultNetSchemaRegistry;
|
|
use cultnet_rs::CultNetWireContract;
|
|
use cultnet_rs::builtin_schema_registry;
|
|
use cultnet_rs::encode_cultnet_message_to_vec;
|
|
use cultnet_rs::encode_frame;
|
|
use epiphany_openai_adapter::EpiphanyOpenAiAdapterStatus;
|
|
use epiphany_openai_adapter::EpiphanyOpenAiModelReceipt;
|
|
use epiphany_openai_adapter::EpiphanyOpenAiModelRequest;
|
|
use epiphany_openai_adapter::EpiphanyOpenAiStreamEvent;
|
|
use epiphany_state_model::EpiphanyJobBinding;
|
|
use epiphany_state_model::EpiphanyJobKind;
|
|
use epiphany_state_model::EpiphanyRuntimeLink;
|
|
use epiphany_state_model::EpiphanyThreadState;
|
|
use serde::Deserialize;
|
|
use serde::Serialize;
|
|
use std::collections::BTreeMap;
|
|
use std::fs;
|
|
use std::path::Path;
|
|
use std::path::PathBuf;
|
|
|
|
pub const RUNTIME_IDENTITY_TYPE: &str = "epiphany.runtime.identity";
|
|
pub const RUNTIME_SESSION_TYPE: &str = "epiphany.runtime.session";
|
|
pub const RUNTIME_JOB_TYPE: &str = "epiphany.runtime.job";
|
|
pub const RUNTIME_WORKER_LAUNCH_REQUEST_TYPE: &str = "epiphany.runtime.worker_launch_request";
|
|
pub const RUNTIME_ROLE_WORKER_RESULT_TYPE: &str = "epiphany.runtime.role_worker_result";
|
|
pub const RUNTIME_REORIENT_WORKER_RESULT_TYPE: &str = "epiphany.runtime.reorient_worker_result";
|
|
pub const RUNTIME_JOB_RESULT_TYPE: &str = "epiphany.runtime.job_result";
|
|
pub const RUNTIME_EVENT_TYPE: &str = "epiphany.runtime.event";
|
|
pub const OPENAI_ADAPTER_STATUS_TYPE: &str = "epiphany.openai_adapter_status.v0";
|
|
pub const OPENAI_MODEL_REQUEST_TYPE: &str = "epiphany.openai_model_request.v0";
|
|
pub const OPENAI_MODEL_STREAM_EVENT_TYPE: &str = "epiphany.openai_model_stream_event.v0";
|
|
pub const OPENAI_MODEL_RECEIPT_TYPE: &str = "epiphany.openai_model_receipt.v0";
|
|
pub const SURFACE_SCENE_TYPE: &str = "epiphany.surface.scene";
|
|
pub const SURFACE_FRESHNESS_TYPE: &str = "epiphany.surface.freshness";
|
|
pub const SURFACE_CONTEXT_TYPE: &str = "epiphany.surface.context";
|
|
pub const SURFACE_GRAPH_QUERY_TYPE: &str = "epiphany.surface.graph_query";
|
|
pub const SURFACE_PRESSURE_TYPE: &str = "epiphany.surface.pressure";
|
|
pub const SURFACE_REORIENT_TYPE: &str = "epiphany.surface.reorient";
|
|
pub const SURFACE_CRRC_TYPE: &str = "epiphany.surface.crrc";
|
|
pub const SURFACE_JOBS_TYPE: &str = "epiphany.surface.jobs";
|
|
pub const SURFACE_ROLES_TYPE: &str = "epiphany.surface.roles";
|
|
pub const SURFACE_ROLE_RESULT_TYPE: &str = "epiphany.surface.role_result";
|
|
pub const SURFACE_REORIENT_RESULT_TYPE: &str = "epiphany.surface.reorient_result";
|
|
pub const SURFACE_PLANNING_TYPE: &str = "epiphany.surface.planning";
|
|
pub const SURFACE_COORDINATOR_TYPE: &str = "epiphany.surface.coordinator";
|
|
pub const SURFACE_FACE_TYPE: &str = "epiphany.surface.face";
|
|
pub const SURFACE_VOID_MEMORY_TYPE: &str = "epiphany.surface.void_memory";
|
|
pub const SURFACE_REPO_INITIALIZATION_TYPE: &str = "epiphany.surface.repo_initialization";
|
|
pub const SURFACE_REPO_BIRTH_RUNNER_TYPE: &str = "epiphany.surface.repo_birth_runner";
|
|
pub const SURFACE_RIDER_BRIDGE_TYPE: &str = "epiphany.surface.rider_bridge";
|
|
pub const SURFACE_UNITY_BRIDGE_TYPE: &str = "epiphany.surface.unity_bridge";
|
|
pub const RUNTIME_IDENTITY_KEY: &str = "self";
|
|
pub const RUNTIME_SPINE_SCHEMA_VERSION: &str = "epiphany.runtime_spine.v0";
|
|
pub const RUNTIME_WORKER_LAUNCH_REQUEST_SCHEMA_VERSION: &str =
|
|
"epiphany.runtime.worker_launch_request.v0";
|
|
pub const RUNTIME_ROLE_WORKER_RESULT_SCHEMA_VERSION: &str =
|
|
"epiphany.runtime.role_worker_result.v0";
|
|
pub const RUNTIME_REORIENT_WORKER_RESULT_SCHEMA_VERSION: &str =
|
|
"epiphany.runtime.reorient_worker_result.v0";
|
|
pub const OPENAI_ADAPTER_STATUS_SCHEMA_VERSION: &str = "epiphany.openai_adapter_status.v0";
|
|
pub const OPENAI_MODEL_REQUEST_SCHEMA_VERSION: &str = "epiphany.openai_model_request.v0";
|
|
pub const OPENAI_MODEL_STREAM_EVENT_SCHEMA_VERSION: &str = "epiphany.openai_model_stream_event.v0";
|
|
pub const OPENAI_MODEL_RECEIPT_SCHEMA_VERSION: &str = "epiphany.openai_model_receipt.v0";
|
|
pub const SCENE_SURFACE_SCHEMA_VERSION: &str = "epiphany.scene_surface.v0";
|
|
pub const FRESHNESS_SURFACE_SCHEMA_VERSION: &str = "epiphany.freshness_surface.v0";
|
|
pub const CONTEXT_SURFACE_SCHEMA_VERSION: &str = "epiphany.context_surface.v0";
|
|
pub const GRAPH_QUERY_SURFACE_SCHEMA_VERSION: &str = "epiphany.graph_query_surface.v0";
|
|
pub const PRESSURE_SURFACE_SCHEMA_VERSION: &str = "epiphany.pressure_surface.v0";
|
|
pub const REORIENT_SURFACE_SCHEMA_VERSION: &str = "epiphany.reorient_surface.v0";
|
|
pub const CRRC_SURFACE_SCHEMA_VERSION: &str = "epiphany.crrc_surface.v0";
|
|
pub const JOBS_SURFACE_SCHEMA_VERSION: &str = "epiphany.jobs_surface.v0";
|
|
pub const ROLES_SURFACE_SCHEMA_VERSION: &str = "epiphany.roles_surface.v0";
|
|
pub const ROLE_RESULT_SURFACE_SCHEMA_VERSION: &str = "epiphany.role_result_surface.v0";
|
|
pub const REORIENT_RESULT_SURFACE_SCHEMA_VERSION: &str = "epiphany.reorient_result_surface.v0";
|
|
pub const PLANNING_SURFACE_SCHEMA_VERSION: &str = "epiphany.planning_surface.v0";
|
|
pub const COORDINATOR_SURFACE_SCHEMA_VERSION: &str = "epiphany.coordinator_surface.v0";
|
|
pub const FACE_SURFACE_SCHEMA_VERSION: &str = "epiphany.face_surface.v0";
|
|
pub const VOID_MEMORY_SURFACE_SCHEMA_VERSION: &str = "epiphany.void_memory_surface.v0";
|
|
pub const REPO_INITIALIZATION_SURFACE_SCHEMA_VERSION: &str =
|
|
"epiphany.repo_initialization_surface.v0";
|
|
pub const REPO_BIRTH_RUNNER_SURFACE_SCHEMA_VERSION: &str = "epiphany.repo_birth_runner_surface.v0";
|
|
pub const RIDER_BRIDGE_SURFACE_SCHEMA_VERSION: &str = "epiphany.rider_bridge_surface.v0";
|
|
pub const UNITY_BRIDGE_SURFACE_SCHEMA_VERSION: &str = "epiphany.unity_bridge_surface.v0";
|
|
pub const AGENT_MEMORY_PAYLOAD_SCHEMA_VERSION: &str = "epiphany.agent_memory.v0";
|
|
pub const CULTNET_SCHEMA_INDEX_RELATIVE: &str = "schemas/cultnet/index.json";
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
struct EpiphanyCultNetSchemaIndex {
|
|
schema_version: String,
|
|
schemas: Vec<EpiphanyCultNetSchemaIndexEntry>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
struct EpiphanyCultNetSchemaIndexEntry {
|
|
schema_id: String,
|
|
kind: CultNetSchemaKind,
|
|
wire_contracts: Vec<CultNetWireContract>,
|
|
#[serde(default)]
|
|
schema_version: Option<String>,
|
|
#[serde(default)]
|
|
document_type: Option<String>,
|
|
#[serde(default)]
|
|
title: Option<String>,
|
|
path: String,
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, DatabaseEntry)]
|
|
#[cultcache(type = "epiphany.runtime.identity", schema = "EpiphanyRuntimeIdentity")]
|
|
pub struct EpiphanyRuntimeIdentity {
|
|
#[cultcache(key = 0)]
|
|
pub schema_version: String,
|
|
#[cultcache(key = 1)]
|
|
pub runtime_id: String,
|
|
#[cultcache(key = 2)]
|
|
pub display_name: String,
|
|
#[cultcache(key = 3)]
|
|
pub runtime_kind: String,
|
|
#[cultcache(key = 4)]
|
|
pub created_at: String,
|
|
#[cultcache(key = 5)]
|
|
pub updated_at: String,
|
|
#[cultcache(key = 6)]
|
|
pub supported_document_types: Vec<String>,
|
|
#[cultcache(key = 7, default)]
|
|
pub metadata: BTreeMap<String, String>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, DatabaseEntry)]
|
|
#[cultcache(type = "epiphany.runtime.session", schema = "EpiphanyRuntimeSession")]
|
|
pub struct EpiphanyRuntimeSession {
|
|
#[cultcache(key = 0)]
|
|
pub schema_version: String,
|
|
#[cultcache(key = 1)]
|
|
pub session_id: String,
|
|
#[cultcache(key = 2)]
|
|
pub objective: String,
|
|
#[cultcache(key = 3)]
|
|
pub status: EpiphanyRuntimeSessionStatus,
|
|
#[cultcache(key = 4)]
|
|
pub created_at: String,
|
|
#[cultcache(key = 5)]
|
|
pub updated_at: String,
|
|
#[cultcache(key = 6, default)]
|
|
pub coordinator_note: String,
|
|
#[cultcache(key = 7, default)]
|
|
pub metadata: BTreeMap<String, String>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, DatabaseEntry)]
|
|
#[cultcache(type = "epiphany.runtime.job", schema = "EpiphanyRuntimeJob")]
|
|
pub struct EpiphanyRuntimeJob {
|
|
#[cultcache(key = 0)]
|
|
pub schema_version: String,
|
|
#[cultcache(key = 1)]
|
|
pub job_id: String,
|
|
#[cultcache(key = 2)]
|
|
pub session_id: String,
|
|
#[cultcache(key = 3)]
|
|
pub role: String,
|
|
#[cultcache(key = 4)]
|
|
pub status: EpiphanyRuntimeJobStatus,
|
|
#[cultcache(key = 5)]
|
|
pub created_at: String,
|
|
#[cultcache(key = 6)]
|
|
pub updated_at: String,
|
|
#[cultcache(key = 7, default)]
|
|
pub summary: String,
|
|
#[cultcache(key = 8, default)]
|
|
pub artifact_refs: Vec<String>,
|
|
#[cultcache(key = 9, default)]
|
|
pub metadata: BTreeMap<String, String>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, DatabaseEntry)]
|
|
#[cultcache(
|
|
type = "epiphany.runtime.worker_launch_request",
|
|
schema = "EpiphanyRuntimeWorkerLaunchRequest"
|
|
)]
|
|
pub struct EpiphanyRuntimeWorkerLaunchRequest {
|
|
#[cultcache(key = 0)]
|
|
pub schema_version: String,
|
|
#[cultcache(key = 1)]
|
|
pub job_id: String,
|
|
#[cultcache(key = 2)]
|
|
pub binding_id: String,
|
|
#[cultcache(key = 3)]
|
|
pub role: String,
|
|
#[cultcache(key = 4)]
|
|
pub authority_scope: String,
|
|
#[cultcache(key = 5)]
|
|
pub instruction: String,
|
|
#[cultcache(key = 6)]
|
|
pub output_contract_id: String,
|
|
#[cultcache(key = 7)]
|
|
pub document_kind: String,
|
|
#[cultcache(key = 8)]
|
|
pub launch_document_msgpack: Vec<u8>,
|
|
#[cultcache(key = 9, default)]
|
|
pub metadata: BTreeMap<String, String>,
|
|
}
|
|
|
|
impl EpiphanyRuntimeWorkerLaunchRequest {
|
|
pub fn launch_document(&self) -> Result<EpiphanyWorkerLaunchDocument> {
|
|
let document: EpiphanyWorkerLaunchDocument =
|
|
rmp_serde::from_slice(&self.launch_document_msgpack)
|
|
.context("failed to decode worker launch document MessagePack")?;
|
|
let actual_kind = worker_launch_document_kind(&document);
|
|
if actual_kind != self.document_kind {
|
|
return Err(anyhow!(
|
|
"worker launch document kind mismatch: indexed {:?}, payload {:?}",
|
|
self.document_kind,
|
|
actual_kind
|
|
));
|
|
}
|
|
Ok(document)
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, DatabaseEntry)]
|
|
#[cultcache(
|
|
type = "epiphany.runtime.role_worker_result",
|
|
schema = "EpiphanyRuntimeRoleWorkerResult"
|
|
)]
|
|
pub struct EpiphanyRuntimeRoleWorkerResult {
|
|
#[cultcache(key = 0)]
|
|
pub schema_version: String,
|
|
#[cultcache(key = 1)]
|
|
pub result_id: String,
|
|
#[cultcache(key = 2)]
|
|
pub job_id: String,
|
|
#[cultcache(key = 3)]
|
|
pub role_id: String,
|
|
#[cultcache(key = 4)]
|
|
pub verdict: String,
|
|
#[cultcache(key = 5)]
|
|
pub summary: String,
|
|
#[cultcache(key = 6)]
|
|
pub next_safe_move: String,
|
|
#[cultcache(key = 7, default)]
|
|
pub checkpoint_summary: Option<String>,
|
|
#[cultcache(key = 8, default)]
|
|
pub scratch_summary: Option<String>,
|
|
#[cultcache(key = 9, default)]
|
|
pub files_inspected: Vec<String>,
|
|
#[cultcache(key = 10, default)]
|
|
pub frontier_node_ids: Vec<String>,
|
|
#[cultcache(key = 11, default)]
|
|
pub evidence_ids: Vec<String>,
|
|
#[cultcache(key = 12, default)]
|
|
pub artifact_refs: Vec<String>,
|
|
#[cultcache(key = 13, default)]
|
|
pub open_questions: Vec<String>,
|
|
#[cultcache(key = 14, default)]
|
|
pub evidence_gaps: Vec<String>,
|
|
#[cultcache(key = 15, default)]
|
|
pub risks: Vec<String>,
|
|
#[cultcache(key = 16, default)]
|
|
pub state_patch_msgpack: Option<Vec<u8>>,
|
|
#[cultcache(key = 17, default)]
|
|
pub self_patch_msgpack: Option<Vec<u8>>,
|
|
#[cultcache(key = 18, default)]
|
|
pub item_error: Option<String>,
|
|
#[cultcache(key = 19, default)]
|
|
pub metadata: BTreeMap<String, String>,
|
|
}
|
|
|
|
impl EpiphanyRuntimeRoleWorkerResult {
|
|
pub fn state_patch(&self) -> Result<Option<crate::EpiphanyRoleStatePatchDocument>> {
|
|
decode_optional_msgpack(
|
|
self.state_patch_msgpack.as_deref(),
|
|
"role worker statePatch",
|
|
)
|
|
}
|
|
|
|
pub fn self_patch(&self) -> Result<Option<crate::AgentSelfPatch>> {
|
|
decode_optional_msgpack(self.self_patch_msgpack.as_deref(), "role worker selfPatch")
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, DatabaseEntry)]
|
|
#[cultcache(
|
|
type = "epiphany.runtime.reorient_worker_result",
|
|
schema = "EpiphanyRuntimeReorientWorkerResult"
|
|
)]
|
|
pub struct EpiphanyRuntimeReorientWorkerResult {
|
|
#[cultcache(key = 0)]
|
|
pub schema_version: String,
|
|
#[cultcache(key = 1)]
|
|
pub result_id: String,
|
|
#[cultcache(key = 2)]
|
|
pub job_id: String,
|
|
#[cultcache(key = 3)]
|
|
pub mode: String,
|
|
#[cultcache(key = 4)]
|
|
pub summary: String,
|
|
#[cultcache(key = 5)]
|
|
pub next_safe_move: String,
|
|
#[cultcache(key = 6, default)]
|
|
pub checkpoint_still_valid: Option<bool>,
|
|
#[cultcache(key = 7, default)]
|
|
pub files_inspected: Vec<String>,
|
|
#[cultcache(key = 8, default)]
|
|
pub frontier_node_ids: Vec<String>,
|
|
#[cultcache(key = 9, default)]
|
|
pub evidence_ids: Vec<String>,
|
|
#[cultcache(key = 10, default)]
|
|
pub artifact_refs: Vec<String>,
|
|
#[cultcache(key = 11, default)]
|
|
pub open_questions: Vec<String>,
|
|
#[cultcache(key = 12, default)]
|
|
pub continuity_risks: Vec<String>,
|
|
#[cultcache(key = 13, default)]
|
|
pub item_error: Option<String>,
|
|
#[cultcache(key = 14, default)]
|
|
pub metadata: BTreeMap<String, String>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, DatabaseEntry)]
|
|
#[cultcache(
|
|
type = "epiphany.runtime.job_result",
|
|
schema = "EpiphanyRuntimeJobResult"
|
|
)]
|
|
pub struct EpiphanyRuntimeJobResult {
|
|
#[cultcache(key = 0)]
|
|
pub schema_version: String,
|
|
#[cultcache(key = 1)]
|
|
pub result_id: String,
|
|
#[cultcache(key = 2)]
|
|
pub job_id: String,
|
|
#[cultcache(key = 3)]
|
|
pub session_id: String,
|
|
#[cultcache(key = 4)]
|
|
pub role: String,
|
|
#[cultcache(key = 5)]
|
|
pub verdict: String,
|
|
#[cultcache(key = 6)]
|
|
pub summary: String,
|
|
#[cultcache(key = 7)]
|
|
pub completed_at: String,
|
|
#[cultcache(key = 8, default)]
|
|
pub next_safe_move: String,
|
|
#[cultcache(key = 9, default)]
|
|
pub evidence_refs: Vec<String>,
|
|
#[cultcache(key = 10, default)]
|
|
pub artifact_refs: Vec<String>,
|
|
#[cultcache(key = 11, default)]
|
|
pub metadata: BTreeMap<String, String>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, DatabaseEntry)]
|
|
#[cultcache(type = "epiphany.runtime.event", schema = "EpiphanyRuntimeEvent")]
|
|
pub struct EpiphanyRuntimeEvent {
|
|
#[cultcache(key = 0)]
|
|
pub schema_version: String,
|
|
#[cultcache(key = 1)]
|
|
pub event_id: String,
|
|
#[cultcache(key = 2)]
|
|
pub occurred_at: String,
|
|
#[cultcache(key = 3)]
|
|
pub event_type: String,
|
|
#[cultcache(key = 4)]
|
|
pub source: String,
|
|
#[cultcache(key = 5, default)]
|
|
pub session_id: Option<String>,
|
|
#[cultcache(key = 6, default)]
|
|
pub job_id: Option<String>,
|
|
#[cultcache(key = 7, default)]
|
|
pub summary: String,
|
|
#[cultcache(key = 8, default)]
|
|
pub metadata: BTreeMap<String, String>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub enum EpiphanyRuntimeSessionStatus {
|
|
#[default]
|
|
Proposed,
|
|
Active,
|
|
WaitingForReview,
|
|
Sleeping,
|
|
Completed,
|
|
Archived,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub enum EpiphanyRuntimeJobStatus {
|
|
#[default]
|
|
Queued,
|
|
Running,
|
|
WaitingForReview,
|
|
Completed,
|
|
Failed,
|
|
Cancelled,
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct EpiphanyRuntimeSpineStatus {
|
|
pub store: String,
|
|
pub present: bool,
|
|
pub runtime_id: Option<String>,
|
|
pub display_name: Option<String>,
|
|
pub sessions: usize,
|
|
pub active_sessions: usize,
|
|
pub jobs: usize,
|
|
pub open_jobs: usize,
|
|
pub job_results: usize,
|
|
pub events: usize,
|
|
pub supported_document_types: Vec<String>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
pub struct RuntimeSpineInitOptions {
|
|
pub runtime_id: String,
|
|
pub display_name: String,
|
|
pub created_at: String,
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
pub struct RuntimeSpineSessionOptions {
|
|
pub session_id: String,
|
|
pub objective: String,
|
|
pub created_at: String,
|
|
pub coordinator_note: String,
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
pub struct RuntimeSpineEventOptions {
|
|
pub event_id: String,
|
|
pub occurred_at: String,
|
|
pub event_type: String,
|
|
pub source: String,
|
|
pub session_id: Option<String>,
|
|
pub job_id: Option<String>,
|
|
pub summary: String,
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
pub struct RuntimeSpineJobOptions {
|
|
pub job_id: String,
|
|
pub session_id: String,
|
|
pub role: String,
|
|
pub created_at: String,
|
|
pub summary: String,
|
|
pub artifact_refs: Vec<String>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
pub struct RuntimeSpineJobResultOptions {
|
|
pub result_id: String,
|
|
pub job_id: String,
|
|
pub completed_at: String,
|
|
pub verdict: String,
|
|
pub summary: String,
|
|
pub next_safe_move: String,
|
|
pub evidence_refs: Vec<String>,
|
|
pub artifact_refs: Vec<String>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq)]
|
|
pub struct RuntimeSpineHeartbeatJobOptions {
|
|
pub runtime_id: String,
|
|
pub display_name: String,
|
|
pub session_id: String,
|
|
pub objective: String,
|
|
pub coordinator_note: String,
|
|
pub job_id: String,
|
|
pub role: String,
|
|
pub binding_id: String,
|
|
pub authority_scope: String,
|
|
pub instruction: String,
|
|
pub launch_document: EpiphanyWorkerLaunchDocument,
|
|
pub output_contract_id: String,
|
|
pub created_at: String,
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq)]
|
|
pub struct RuntimeSpineHeartbeatLaunchPlanOptions {
|
|
pub binding_id: String,
|
|
pub kind: EpiphanyJobKind,
|
|
pub scope: String,
|
|
pub owner_role: String,
|
|
pub authority_scope: String,
|
|
pub linked_subgoal_ids: Vec<String>,
|
|
pub linked_graph_node_ids: Vec<String>,
|
|
pub instruction: String,
|
|
pub launch_document: EpiphanyWorkerLaunchDocument,
|
|
pub output_contract_id: String,
|
|
pub max_runtime_seconds: Option<u64>,
|
|
pub runtime_job_id: String,
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq)]
|
|
pub struct RuntimeSpineHeartbeatLaunchPlan {
|
|
pub binding: EpiphanyJobBinding,
|
|
pub runtime_link: EpiphanyRuntimeLink,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq)]
|
|
pub struct EpiphanyJobLaunchRequest {
|
|
pub expected_revision: Option<u64>,
|
|
pub binding_id: String,
|
|
pub kind: EpiphanyJobKind,
|
|
pub scope: String,
|
|
pub owner_role: String,
|
|
pub authority_scope: String,
|
|
pub linked_subgoal_ids: Vec<String>,
|
|
pub linked_graph_node_ids: Vec<String>,
|
|
pub instruction: String,
|
|
pub launch_document: EpiphanyWorkerLaunchDocument,
|
|
pub output_contract_id: String,
|
|
pub max_runtime_seconds: Option<u64>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq)]
|
|
pub struct EpiphanyJobLaunchResult {
|
|
pub epiphany_state: EpiphanyThreadState,
|
|
pub binding_id: String,
|
|
pub launcher_job_id: String,
|
|
pub backend_job_id: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub struct EpiphanyJobInterruptRequest {
|
|
pub expected_revision: Option<u64>,
|
|
pub binding_id: String,
|
|
pub reason: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq)]
|
|
pub struct EpiphanyJobInterruptResult {
|
|
pub epiphany_state: EpiphanyThreadState,
|
|
pub binding_id: String,
|
|
pub cancel_requested: bool,
|
|
pub interrupted_thread_ids: Vec<String>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq)]
|
|
pub struct EpiphanyRuntimeJobSnapshot {
|
|
pub job: EpiphanyRuntimeJob,
|
|
pub result: Option<EpiphanyRuntimeJobResult>,
|
|
}
|
|
|
|
pub fn runtime_spine_cache(store_path: impl AsRef<Path>) -> Result<CultCache> {
|
|
let store_path = store_path.as_ref();
|
|
let mut cache = CultCache::new();
|
|
cache.register_entry_type::<EpiphanyRuntimeIdentity>()?;
|
|
cache.register_entry_type::<EpiphanyRuntimeSession>()?;
|
|
cache.register_entry_type::<EpiphanyRuntimeJob>()?;
|
|
cache.register_entry_type::<EpiphanyRuntimeWorkerLaunchRequest>()?;
|
|
cache.register_entry_type::<EpiphanyRuntimeRoleWorkerResult>()?;
|
|
cache.register_entry_type::<EpiphanyRuntimeReorientWorkerResult>()?;
|
|
cache.register_entry_type::<EpiphanyRuntimeJobResult>()?;
|
|
cache.register_entry_type::<EpiphanyRuntimeEvent>()?;
|
|
cache.register_entry_type::<EpiphanyOpenAiAdapterStatus>()?;
|
|
cache.register_entry_type::<EpiphanyOpenAiModelRequest>()?;
|
|
cache.register_entry_type::<EpiphanyOpenAiStreamEvent>()?;
|
|
cache.register_entry_type::<EpiphanyOpenAiModelReceipt>()?;
|
|
cache.add_generic_backing_store(SingleFileMessagePackBackingStore::new(
|
|
store_path.to_path_buf(),
|
|
));
|
|
Ok(cache)
|
|
}
|
|
|
|
pub fn initialize_runtime_spine(
|
|
store_path: impl AsRef<Path>,
|
|
options: RuntimeSpineInitOptions,
|
|
) -> Result<EpiphanyRuntimeIdentity> {
|
|
validate_non_empty(&options.runtime_id, "runtime id")?;
|
|
validate_non_empty(&options.display_name, "display name")?;
|
|
validate_non_empty(&options.created_at, "created at")?;
|
|
let store_path = store_path.as_ref();
|
|
let mut cache = runtime_spine_cache(store_path)?;
|
|
cache.pull_all_backing_stores()?;
|
|
let existing = cache.get::<EpiphanyRuntimeIdentity>(RUNTIME_IDENTITY_KEY)?;
|
|
let created_at = existing
|
|
.as_ref()
|
|
.map(|identity| identity.created_at.clone())
|
|
.unwrap_or_else(|| options.created_at.clone());
|
|
let identity = EpiphanyRuntimeIdentity {
|
|
schema_version: RUNTIME_SPINE_SCHEMA_VERSION.to_string(),
|
|
runtime_id: options.runtime_id,
|
|
display_name: options.display_name,
|
|
runtime_kind: "epiphany.native".to_string(),
|
|
created_at,
|
|
updated_at: options.created_at,
|
|
supported_document_types: supported_runtime_document_types(),
|
|
metadata: BTreeMap::from([("codexEvacuationBridge".to_string(), "temporary".to_string())]),
|
|
};
|
|
cache.put(RUNTIME_IDENTITY_KEY, &identity)?;
|
|
Ok(identity)
|
|
}
|
|
|
|
pub fn create_runtime_session(
|
|
store_path: impl AsRef<Path>,
|
|
options: RuntimeSpineSessionOptions,
|
|
) -> Result<EpiphanyRuntimeSession> {
|
|
validate_non_empty(&options.session_id, "session id")?;
|
|
validate_non_empty(&options.objective, "objective")?;
|
|
validate_non_empty(&options.created_at, "created at")?;
|
|
let mut cache = runtime_spine_cache(store_path.as_ref())?;
|
|
cache.pull_all_backing_stores()?;
|
|
require_identity(&cache)?;
|
|
if cache
|
|
.get::<EpiphanyRuntimeSession>(&options.session_id)?
|
|
.is_some()
|
|
{
|
|
return Err(anyhow!(
|
|
"runtime session {:?} already exists",
|
|
options.session_id
|
|
));
|
|
}
|
|
let session = EpiphanyRuntimeSession {
|
|
schema_version: RUNTIME_SPINE_SCHEMA_VERSION.to_string(),
|
|
session_id: options.session_id.clone(),
|
|
objective: options.objective,
|
|
status: EpiphanyRuntimeSessionStatus::Active,
|
|
created_at: options.created_at.clone(),
|
|
updated_at: options.created_at,
|
|
coordinator_note: options.coordinator_note,
|
|
metadata: BTreeMap::new(),
|
|
};
|
|
cache.put(&options.session_id, &session)?;
|
|
Ok(session)
|
|
}
|
|
|
|
pub fn ensure_runtime_session(
|
|
store_path: impl AsRef<Path>,
|
|
options: RuntimeSpineSessionOptions,
|
|
) -> Result<EpiphanyRuntimeSession> {
|
|
validate_non_empty(&options.session_id, "session id")?;
|
|
validate_non_empty(&options.objective, "objective")?;
|
|
validate_non_empty(&options.created_at, "created at")?;
|
|
let mut cache = runtime_spine_cache(store_path.as_ref())?;
|
|
cache.pull_all_backing_stores()?;
|
|
require_identity(&cache)?;
|
|
if let Some(existing) = cache.get::<EpiphanyRuntimeSession>(&options.session_id)? {
|
|
if matches!(
|
|
existing.status,
|
|
EpiphanyRuntimeSessionStatus::Completed | EpiphanyRuntimeSessionStatus::Archived
|
|
) {
|
|
return Err(anyhow!(
|
|
"runtime session {:?} is terminal and cannot accept jobs",
|
|
options.session_id
|
|
));
|
|
}
|
|
return Ok(existing);
|
|
}
|
|
let session = EpiphanyRuntimeSession {
|
|
schema_version: RUNTIME_SPINE_SCHEMA_VERSION.to_string(),
|
|
session_id: options.session_id.clone(),
|
|
objective: options.objective,
|
|
status: EpiphanyRuntimeSessionStatus::Active,
|
|
created_at: options.created_at.clone(),
|
|
updated_at: options.created_at,
|
|
coordinator_note: options.coordinator_note,
|
|
metadata: BTreeMap::new(),
|
|
};
|
|
cache.put(&options.session_id, &session)?;
|
|
Ok(session)
|
|
}
|
|
|
|
pub fn create_runtime_job(
|
|
store_path: impl AsRef<Path>,
|
|
options: RuntimeSpineJobOptions,
|
|
) -> Result<EpiphanyRuntimeJob> {
|
|
validate_non_empty(&options.job_id, "job id")?;
|
|
validate_non_empty(&options.session_id, "session id")?;
|
|
validate_non_empty(&options.role, "role")?;
|
|
validate_non_empty(&options.created_at, "created at")?;
|
|
let mut cache = runtime_spine_cache(store_path.as_ref())?;
|
|
cache.pull_all_backing_stores()?;
|
|
require_identity(&cache)?;
|
|
let session = cache
|
|
.get::<EpiphanyRuntimeSession>(&options.session_id)?
|
|
.ok_or_else(|| anyhow!("runtime session {:?} does not exist", options.session_id))?;
|
|
if matches!(
|
|
session.status,
|
|
EpiphanyRuntimeSessionStatus::Completed | EpiphanyRuntimeSessionStatus::Archived
|
|
) {
|
|
return Err(anyhow!(
|
|
"runtime session {:?} is not open for jobs",
|
|
options.session_id
|
|
));
|
|
}
|
|
if cache.get::<EpiphanyRuntimeJob>(&options.job_id)?.is_some() {
|
|
return Err(anyhow!("runtime job {:?} already exists", options.job_id));
|
|
}
|
|
let job = EpiphanyRuntimeJob {
|
|
schema_version: RUNTIME_SPINE_SCHEMA_VERSION.to_string(),
|
|
job_id: options.job_id.clone(),
|
|
session_id: options.session_id.clone(),
|
|
role: options.role,
|
|
status: EpiphanyRuntimeJobStatus::Queued,
|
|
created_at: options.created_at.clone(),
|
|
updated_at: options.created_at.clone(),
|
|
summary: options.summary,
|
|
artifact_refs: options.artifact_refs,
|
|
metadata: BTreeMap::new(),
|
|
};
|
|
cache.put(&options.job_id, &job)?;
|
|
let event = EpiphanyRuntimeEvent {
|
|
schema_version: RUNTIME_SPINE_SCHEMA_VERSION.to_string(),
|
|
event_id: format!("event-job-opened-{}", options.job_id),
|
|
occurred_at: options.created_at,
|
|
event_type: "job.opened".to_string(),
|
|
source: "runtime-spine".to_string(),
|
|
session_id: Some(options.session_id),
|
|
job_id: Some(options.job_id),
|
|
summary: "Native runtime job opened.".to_string(),
|
|
metadata: BTreeMap::new(),
|
|
};
|
|
cache.put(&event.event_id, &event)?;
|
|
Ok(job)
|
|
}
|
|
|
|
pub fn plan_runtime_spine_heartbeat_launch(
|
|
state: &EpiphanyThreadState,
|
|
options: RuntimeSpineHeartbeatLaunchPlanOptions,
|
|
) -> Result<RuntimeSpineHeartbeatLaunchPlan> {
|
|
validate_heartbeat_launch_options(state, &options)?;
|
|
Ok(RuntimeSpineHeartbeatLaunchPlan {
|
|
binding: EpiphanyJobBinding {
|
|
id: options.binding_id.clone(),
|
|
kind: options.kind,
|
|
scope: options.scope.clone(),
|
|
owner_role: options.owner_role.clone(),
|
|
authority_scope: Some(options.authority_scope.clone()),
|
|
linked_subgoal_ids: options.linked_subgoal_ids.clone(),
|
|
linked_graph_node_ids: options.linked_graph_node_ids.clone(),
|
|
blocking_reason: None,
|
|
},
|
|
runtime_link: EpiphanyRuntimeLink {
|
|
id: format!(
|
|
"runtime-link-{}-{}",
|
|
options.binding_id, options.runtime_job_id
|
|
),
|
|
binding_id: options.binding_id,
|
|
surface: "jobLaunch".to_string(),
|
|
role_id: options.owner_role,
|
|
authority_scope: options.authority_scope,
|
|
runtime_job_id: options.runtime_job_id,
|
|
runtime_result_id: None,
|
|
linked_subgoal_ids: options.linked_subgoal_ids,
|
|
linked_graph_node_ids: options.linked_graph_node_ids,
|
|
},
|
|
})
|
|
}
|
|
|
|
pub fn replace_or_append_epiphany_job_binding(
|
|
mut bindings: Vec<EpiphanyJobBinding>,
|
|
replacement: EpiphanyJobBinding,
|
|
) -> Vec<EpiphanyJobBinding> {
|
|
if let Some(existing) = bindings
|
|
.iter_mut()
|
|
.find(|binding| binding.id == replacement.id)
|
|
{
|
|
*existing = replacement;
|
|
return bindings;
|
|
}
|
|
bindings.push(replacement);
|
|
bindings
|
|
}
|
|
|
|
pub fn clear_epiphany_job_binding_backend(
|
|
mut bindings: Vec<EpiphanyJobBinding>,
|
|
binding_index: usize,
|
|
blocking_reason: &str,
|
|
) -> Vec<EpiphanyJobBinding> {
|
|
let binding = &mut bindings[binding_index];
|
|
binding.blocking_reason = Some(blocking_reason.to_string());
|
|
bindings
|
|
}
|
|
|
|
pub fn open_runtime_spine_heartbeat_job(
|
|
store_path: impl AsRef<Path>,
|
|
options: RuntimeSpineHeartbeatJobOptions,
|
|
) -> Result<EpiphanyRuntimeJob> {
|
|
validate_non_empty(&options.runtime_id, "runtime id")?;
|
|
validate_non_empty(&options.display_name, "display name")?;
|
|
validate_non_empty(&options.session_id, "session id")?;
|
|
validate_non_empty(&options.objective, "objective")?;
|
|
validate_non_empty(&options.job_id, "job id")?;
|
|
validate_non_empty(&options.role, "role")?;
|
|
validate_non_empty(&options.binding_id, "binding id")?;
|
|
validate_non_empty(&options.authority_scope, "authority scope")?;
|
|
validate_non_empty(&options.instruction, "instruction")?;
|
|
validate_non_empty(
|
|
options.launch_document.thread_id(),
|
|
"worker launch document thread id",
|
|
)?;
|
|
validate_non_empty(&options.output_contract_id, "output contract id")?;
|
|
if options.output_contract_id != options.launch_document.output_contract_id() {
|
|
return Err(anyhow!(
|
|
"worker launch output_contract_id must match the typed launch document"
|
|
));
|
|
}
|
|
validate_non_empty(&options.created_at, "created at")?;
|
|
let store_path = store_path.as_ref();
|
|
let job_id = options.job_id.clone();
|
|
let binding_id = options.binding_id.clone();
|
|
let role = options.role.clone();
|
|
let authority_scope = options.authority_scope.clone();
|
|
let instruction = options.instruction.clone();
|
|
let output_contract_id = options.output_contract_id.clone();
|
|
let launch_document = options.launch_document.clone();
|
|
initialize_runtime_spine(
|
|
store_path,
|
|
RuntimeSpineInitOptions {
|
|
runtime_id: options.runtime_id,
|
|
display_name: options.display_name,
|
|
created_at: options.created_at.clone(),
|
|
},
|
|
)?;
|
|
ensure_runtime_session(
|
|
store_path,
|
|
RuntimeSpineSessionOptions {
|
|
session_id: options.session_id.clone(),
|
|
objective: options.objective,
|
|
created_at: options.created_at.clone(),
|
|
coordinator_note: options.coordinator_note,
|
|
},
|
|
)?;
|
|
let job = create_runtime_job(
|
|
store_path,
|
|
RuntimeSpineJobOptions {
|
|
job_id: options.job_id,
|
|
session_id: options.session_id,
|
|
role: options.role,
|
|
created_at: options.created_at,
|
|
summary: format!(
|
|
"Heartbeat activation queued for binding {} with authority {}.",
|
|
options.binding_id, options.authority_scope
|
|
),
|
|
artifact_refs: Vec::new(),
|
|
},
|
|
)?;
|
|
let mut cache = runtime_spine_cache(store_path)?;
|
|
cache.pull_all_backing_stores()?;
|
|
if cache
|
|
.get::<EpiphanyRuntimeWorkerLaunchRequest>(&job_id)?
|
|
.is_some()
|
|
{
|
|
return Err(anyhow!(
|
|
"runtime worker launch request {:?} already exists",
|
|
job_id
|
|
));
|
|
}
|
|
let request = EpiphanyRuntimeWorkerLaunchRequest {
|
|
schema_version: RUNTIME_WORKER_LAUNCH_REQUEST_SCHEMA_VERSION.to_string(),
|
|
job_id: job_id.clone(),
|
|
binding_id,
|
|
role,
|
|
authority_scope,
|
|
instruction,
|
|
output_contract_id,
|
|
document_kind: worker_launch_document_kind(&launch_document).to_string(),
|
|
launch_document_msgpack: encode_worker_launch_document(&launch_document)?,
|
|
metadata: BTreeMap::new(),
|
|
};
|
|
cache.put(&job_id, &request)?;
|
|
Ok(job)
|
|
}
|
|
|
|
pub fn runtime_job_snapshot(
|
|
store_path: impl AsRef<Path>,
|
|
job_id: &str,
|
|
) -> Result<Option<EpiphanyRuntimeJobSnapshot>> {
|
|
validate_non_empty(job_id, "job id")?;
|
|
let store_path = store_path.as_ref();
|
|
if !store_path.exists() {
|
|
return Ok(None);
|
|
}
|
|
let mut cache = runtime_spine_cache(store_path)?;
|
|
cache.pull_all_backing_stores()?;
|
|
let Some(job) = cache.get::<EpiphanyRuntimeJob>(job_id)? else {
|
|
return Ok(None);
|
|
};
|
|
let result = cache
|
|
.get_all::<EpiphanyRuntimeJobResult>()?
|
|
.into_iter()
|
|
.filter(|result| result.job_id == job_id)
|
|
.max_by(|left, right| {
|
|
left.completed_at
|
|
.cmp(&right.completed_at)
|
|
.then_with(|| left.result_id.cmp(&right.result_id))
|
|
});
|
|
Ok(Some(EpiphanyRuntimeJobSnapshot { job, result }))
|
|
}
|
|
|
|
pub fn put_runtime_role_worker_result(
|
|
store_path: impl AsRef<Path>,
|
|
result: &EpiphanyRuntimeRoleWorkerResult,
|
|
) -> Result<()> {
|
|
validate_non_empty(&result.job_id, "role worker result job id")?;
|
|
validate_non_empty(&result.result_id, "role worker result id")?;
|
|
validate_non_empty(&result.role_id, "role worker result role id")?;
|
|
let mut cache = runtime_spine_cache(store_path)?;
|
|
cache.pull_all_backing_stores()?;
|
|
cache.put(&result.job_id, result)?;
|
|
Ok(())
|
|
}
|
|
|
|
pub fn runtime_role_worker_result(
|
|
store_path: impl AsRef<Path>,
|
|
job_id: &str,
|
|
) -> Result<Option<EpiphanyRuntimeRoleWorkerResult>> {
|
|
validate_non_empty(job_id, "role worker result job id")?;
|
|
let mut cache = runtime_spine_cache(store_path)?;
|
|
cache.pull_all_backing_stores()?;
|
|
cache.get::<EpiphanyRuntimeRoleWorkerResult>(job_id)
|
|
}
|
|
|
|
pub fn put_runtime_reorient_worker_result(
|
|
store_path: impl AsRef<Path>,
|
|
result: &EpiphanyRuntimeReorientWorkerResult,
|
|
) -> Result<()> {
|
|
validate_non_empty(&result.job_id, "reorient worker result job id")?;
|
|
validate_non_empty(&result.result_id, "reorient worker result id")?;
|
|
validate_non_empty(&result.mode, "reorient worker result mode")?;
|
|
let mut cache = runtime_spine_cache(store_path)?;
|
|
cache.pull_all_backing_stores()?;
|
|
cache.put(&result.job_id, result)?;
|
|
Ok(())
|
|
}
|
|
|
|
pub fn runtime_reorient_worker_result(
|
|
store_path: impl AsRef<Path>,
|
|
job_id: &str,
|
|
) -> Result<Option<EpiphanyRuntimeReorientWorkerResult>> {
|
|
validate_non_empty(job_id, "reorient worker result job id")?;
|
|
let mut cache = runtime_spine_cache(store_path)?;
|
|
cache.pull_all_backing_stores()?;
|
|
cache.get::<EpiphanyRuntimeReorientWorkerResult>(job_id)
|
|
}
|
|
|
|
pub fn complete_runtime_job(
|
|
store_path: impl AsRef<Path>,
|
|
options: RuntimeSpineJobResultOptions,
|
|
) -> Result<EpiphanyRuntimeJobResult> {
|
|
validate_non_empty(&options.result_id, "result id")?;
|
|
validate_non_empty(&options.job_id, "job id")?;
|
|
validate_non_empty(&options.completed_at, "completed at")?;
|
|
validate_non_empty(&options.verdict, "verdict")?;
|
|
validate_non_empty(&options.summary, "summary")?;
|
|
let mut cache = runtime_spine_cache(store_path.as_ref())?;
|
|
cache.pull_all_backing_stores()?;
|
|
require_identity(&cache)?;
|
|
let mut job = cache
|
|
.get::<EpiphanyRuntimeJob>(&options.job_id)?
|
|
.ok_or_else(|| anyhow!("runtime job {:?} does not exist", options.job_id))?;
|
|
if matches!(
|
|
job.status,
|
|
EpiphanyRuntimeJobStatus::Completed
|
|
| EpiphanyRuntimeJobStatus::Failed
|
|
| EpiphanyRuntimeJobStatus::Cancelled
|
|
) {
|
|
return Err(anyhow!(
|
|
"runtime job {:?} is already terminal",
|
|
options.job_id
|
|
));
|
|
}
|
|
if cache
|
|
.get::<EpiphanyRuntimeJobResult>(&options.result_id)?
|
|
.is_some()
|
|
{
|
|
return Err(anyhow!(
|
|
"runtime job result {:?} already exists",
|
|
options.result_id
|
|
));
|
|
}
|
|
let terminal_status = terminal_status_for_verdict(&options.verdict);
|
|
job.status = terminal_status;
|
|
job.updated_at = options.completed_at.clone();
|
|
job.summary = options.summary.clone();
|
|
job.artifact_refs = merge_refs(&job.artifact_refs, &options.artifact_refs);
|
|
let result = EpiphanyRuntimeJobResult {
|
|
schema_version: RUNTIME_SPINE_SCHEMA_VERSION.to_string(),
|
|
result_id: options.result_id.clone(),
|
|
job_id: options.job_id.clone(),
|
|
session_id: job.session_id.clone(),
|
|
role: job.role.clone(),
|
|
verdict: options.verdict,
|
|
summary: options.summary,
|
|
completed_at: options.completed_at.clone(),
|
|
next_safe_move: options.next_safe_move,
|
|
evidence_refs: options.evidence_refs,
|
|
artifact_refs: options.artifact_refs,
|
|
metadata: BTreeMap::new(),
|
|
};
|
|
cache.put(&job.job_id, &job)?;
|
|
cache.put(&result.result_id, &result)?;
|
|
let event = EpiphanyRuntimeEvent {
|
|
schema_version: RUNTIME_SPINE_SCHEMA_VERSION.to_string(),
|
|
event_id: format!("event-job-completed-{}", options.job_id),
|
|
occurred_at: options.completed_at,
|
|
event_type: "job.completed".to_string(),
|
|
source: "runtime-spine".to_string(),
|
|
session_id: Some(result.session_id.clone()),
|
|
job_id: Some(options.job_id),
|
|
summary: format!(
|
|
"Native runtime job completed with verdict {}.",
|
|
result.verdict
|
|
),
|
|
metadata: BTreeMap::from([("resultId".to_string(), result.result_id.clone())]),
|
|
};
|
|
cache.put(&event.event_id, &event)?;
|
|
Ok(result)
|
|
}
|
|
|
|
pub fn append_runtime_event(
|
|
store_path: impl AsRef<Path>,
|
|
options: RuntimeSpineEventOptions,
|
|
) -> Result<EpiphanyRuntimeEvent> {
|
|
validate_non_empty(&options.event_id, "event id")?;
|
|
validate_non_empty(&options.occurred_at, "occurred at")?;
|
|
validate_non_empty(&options.event_type, "event type")?;
|
|
validate_non_empty(&options.source, "source")?;
|
|
let mut cache = runtime_spine_cache(store_path.as_ref())?;
|
|
cache.pull_all_backing_stores()?;
|
|
require_identity(&cache)?;
|
|
if cache
|
|
.get::<EpiphanyRuntimeEvent>(&options.event_id)?
|
|
.is_some()
|
|
{
|
|
return Err(anyhow!(
|
|
"runtime event {:?} already exists",
|
|
options.event_id
|
|
));
|
|
}
|
|
let event = EpiphanyRuntimeEvent {
|
|
schema_version: RUNTIME_SPINE_SCHEMA_VERSION.to_string(),
|
|
event_id: options.event_id.clone(),
|
|
occurred_at: options.occurred_at,
|
|
event_type: options.event_type,
|
|
source: options.source,
|
|
session_id: options.session_id,
|
|
job_id: options.job_id,
|
|
summary: options.summary,
|
|
metadata: BTreeMap::new(),
|
|
};
|
|
cache.put(&options.event_id, &event)?;
|
|
Ok(event)
|
|
}
|
|
|
|
pub fn runtime_spine_status(store_path: impl AsRef<Path>) -> Result<EpiphanyRuntimeSpineStatus> {
|
|
let store_path = store_path.as_ref();
|
|
if !store_path.exists() {
|
|
return Ok(EpiphanyRuntimeSpineStatus {
|
|
store: store_path.display().to_string(),
|
|
present: false,
|
|
runtime_id: None,
|
|
display_name: None,
|
|
sessions: 0,
|
|
active_sessions: 0,
|
|
jobs: 0,
|
|
open_jobs: 0,
|
|
job_results: 0,
|
|
events: 0,
|
|
supported_document_types: Vec::new(),
|
|
});
|
|
}
|
|
let mut cache = runtime_spine_cache(store_path)?;
|
|
cache
|
|
.pull_all_backing_stores()
|
|
.with_context(|| format!("failed to read runtime spine {}", store_path.display()))?;
|
|
let identity = cache.get::<EpiphanyRuntimeIdentity>(RUNTIME_IDENTITY_KEY)?;
|
|
let sessions = cache.get_all::<EpiphanyRuntimeSession>()?;
|
|
let jobs = cache.get_all::<EpiphanyRuntimeJob>()?;
|
|
let job_results = cache.get_all::<EpiphanyRuntimeJobResult>()?;
|
|
let events = cache.get_all::<EpiphanyRuntimeEvent>()?;
|
|
let active_sessions = sessions
|
|
.iter()
|
|
.filter(|session| {
|
|
matches!(
|
|
session.status,
|
|
EpiphanyRuntimeSessionStatus::Active
|
|
| EpiphanyRuntimeSessionStatus::WaitingForReview
|
|
)
|
|
})
|
|
.count();
|
|
let open_jobs = jobs
|
|
.iter()
|
|
.filter(|job| {
|
|
matches!(
|
|
job.status,
|
|
EpiphanyRuntimeJobStatus::Queued
|
|
| EpiphanyRuntimeJobStatus::Running
|
|
| EpiphanyRuntimeJobStatus::WaitingForReview
|
|
)
|
|
})
|
|
.count();
|
|
Ok(EpiphanyRuntimeSpineStatus {
|
|
store: store_path.display().to_string(),
|
|
present: identity.is_some(),
|
|
runtime_id: identity.as_ref().map(|item| item.runtime_id.clone()),
|
|
display_name: identity.as_ref().map(|item| item.display_name.clone()),
|
|
sessions: sessions.len(),
|
|
active_sessions,
|
|
jobs: jobs.len(),
|
|
open_jobs,
|
|
job_results: job_results.len(),
|
|
events: events.len(),
|
|
supported_document_types: identity
|
|
.map(|item| item.supported_document_types)
|
|
.unwrap_or_default(),
|
|
})
|
|
}
|
|
|
|
pub fn runtime_hello_frame(store_path: impl AsRef<Path>) -> Result<Vec<u8>> {
|
|
let mut cache = runtime_spine_cache(store_path.as_ref())?;
|
|
cache.pull_all_backing_stores()?;
|
|
let identity = require_identity(&cache)?;
|
|
let message = CultNetMessage::Hello {
|
|
runtime_id: identity.runtime_id,
|
|
runtime_kind: identity.runtime_kind,
|
|
agent_id: Some("self".to_string()),
|
|
role: Some("coordinator".to_string()),
|
|
display_name: Some(identity.display_name),
|
|
supported_document_types: Some(identity.supported_document_types),
|
|
supported_mutation_contracts: Some(epiphany_mutation_contracts()),
|
|
supported_message_versions: Some(vec![
|
|
"cultnet.hello.v0".to_string(),
|
|
"cultnet.document_put.v0".to_string(),
|
|
"cultnet.snapshot_request.v0".to_string(),
|
|
"cultnet.snapshot_response.v0".to_string(),
|
|
"cultnet.schema_catalog_request.v0".to_string(),
|
|
"cultnet.schema_catalog_response.v0".to_string(),
|
|
]),
|
|
supports_schema_catalog: Some(true),
|
|
};
|
|
let payload = encode_cultnet_message_to_vec(&message, CultNetWireContract::CultNetSchemaV0)?;
|
|
encode_frame(&payload)
|
|
}
|
|
|
|
pub fn write_runtime_hello_frame(
|
|
store_path: impl AsRef<Path>,
|
|
output_path: impl AsRef<Path>,
|
|
) -> Result<usize> {
|
|
let output_path = output_path.as_ref();
|
|
if let Some(parent) = output_path.parent() {
|
|
fs::create_dir_all(parent)
|
|
.with_context(|| format!("failed to create {}", parent.display()))?;
|
|
}
|
|
let frame = runtime_hello_frame(store_path)?;
|
|
fs::write(output_path, &frame)
|
|
.with_context(|| format!("failed to write {}", output_path.display()))?;
|
|
Ok(frame.len())
|
|
}
|
|
|
|
pub fn epiphany_schema_registry() -> Result<CultNetSchemaRegistry> {
|
|
let mut registry = builtin_schema_registry()?;
|
|
let schema_root = epiphany_schema_root();
|
|
let index_path = schema_root.join("index.json");
|
|
let raw_index = fs::read_to_string(&index_path)
|
|
.with_context(|| format!("failed to read {}", index_path.display()))?;
|
|
let index: EpiphanyCultNetSchemaIndex = serde_json::from_str(&raw_index)
|
|
.with_context(|| format!("failed to parse {}", index_path.display()))?;
|
|
if index.schema_version.trim().is_empty() {
|
|
return Err(anyhow!(
|
|
"CultNet schema index at {} is missing schemaVersion",
|
|
index_path.display()
|
|
));
|
|
}
|
|
|
|
for entry in index.schemas {
|
|
let schema_path = schema_root.join(&entry.path);
|
|
let schema_json = fs::read_to_string(&schema_path)
|
|
.with_context(|| format!("failed to read {}", schema_path.display()))?;
|
|
registry.register(CultNetSchemaRegistration {
|
|
schema_id: entry.schema_id,
|
|
kind: entry.kind,
|
|
wire_contracts: entry.wire_contracts,
|
|
schema_version: entry.schema_version,
|
|
document_type: entry.document_type,
|
|
title: entry.title,
|
|
schema_json: Some(schema_json),
|
|
})?;
|
|
}
|
|
|
|
Ok(registry)
|
|
}
|
|
|
|
pub fn runtime_schema_catalog_response(
|
|
message_id: impl Into<String>,
|
|
include_schema_json: bool,
|
|
schema_ids: Option<Vec<String>>,
|
|
kinds: Option<Vec<CultNetSchemaKind>>,
|
|
) -> Result<CultNetMessage> {
|
|
let registry = epiphany_schema_registry()?;
|
|
registry.create_catalog_response(&CultNetMessage::SchemaCatalogRequest {
|
|
message_id: message_id.into(),
|
|
include_schema_json: Some(include_schema_json),
|
|
schema_ids,
|
|
kinds,
|
|
})
|
|
}
|
|
|
|
pub fn write_runtime_schema_catalog_json(
|
|
output_path: impl AsRef<Path>,
|
|
include_schema_json: bool,
|
|
) -> Result<usize> {
|
|
let output_path = output_path.as_ref();
|
|
if let Some(parent) = output_path.parent() {
|
|
fs::create_dir_all(parent)
|
|
.with_context(|| format!("failed to create {}", parent.display()))?;
|
|
}
|
|
let response = runtime_schema_catalog_response(
|
|
"runtime-spine-schema-catalog".to_string(),
|
|
include_schema_json,
|
|
None,
|
|
None,
|
|
)?;
|
|
let body = serde_json::to_vec_pretty(&response)?;
|
|
fs::write(output_path, &body)
|
|
.with_context(|| format!("failed to write {}", output_path.display()))?;
|
|
Ok(body.len())
|
|
}
|
|
|
|
fn epiphany_schema_root() -> PathBuf {
|
|
Path::new(env!("CARGO_MANIFEST_DIR"))
|
|
.parent()
|
|
.expect("epiphany-core has no parent repo root")
|
|
.join(CULTNET_SCHEMA_INDEX_RELATIVE)
|
|
.parent()
|
|
.expect("cultnet schema index has no parent directory")
|
|
.to_path_buf()
|
|
}
|
|
|
|
fn require_identity(cache: &CultCache) -> Result<EpiphanyRuntimeIdentity> {
|
|
cache
|
|
.get::<EpiphanyRuntimeIdentity>(RUNTIME_IDENTITY_KEY)?
|
|
.ok_or_else(|| anyhow!("runtime spine is missing identity; run init first"))
|
|
}
|
|
|
|
fn supported_runtime_document_types() -> Vec<String> {
|
|
let mut document_types = Vec::new();
|
|
for contract in epiphany_mutation_contracts() {
|
|
if !document_types.contains(&contract.document_type) {
|
|
document_types.push(contract.document_type);
|
|
}
|
|
}
|
|
document_types
|
|
}
|
|
|
|
fn mutation_contract(
|
|
document_type: impl Into<String>,
|
|
payload_schema_version: impl Into<String>,
|
|
operations: Vec<CultNetDocumentOperation>,
|
|
authority: CultNetMutationAuthority,
|
|
intent_document_types: Vec<&str>,
|
|
receipt_document_types: Vec<&str>,
|
|
notes: Vec<&str>,
|
|
) -> CultNetDocumentMutationContract {
|
|
CultNetDocumentMutationContract {
|
|
document_type: document_type.into(),
|
|
payload_schema_version: Some(payload_schema_version.into()),
|
|
operations,
|
|
authority,
|
|
intent_document_types: (!intent_document_types.is_empty()).then(|| {
|
|
intent_document_types
|
|
.into_iter()
|
|
.map(str::to_string)
|
|
.collect()
|
|
}),
|
|
receipt_document_types: (!receipt_document_types.is_empty()).then(|| {
|
|
receipt_document_types
|
|
.into_iter()
|
|
.map(str::to_string)
|
|
.collect()
|
|
}),
|
|
notes: (!notes.is_empty()).then(|| notes.into_iter().map(str::to_string).collect()),
|
|
}
|
|
}
|
|
|
|
fn read_only_surface_contract(
|
|
document_type: impl Into<String>,
|
|
payload_schema_version: impl Into<String>,
|
|
notes: Vec<&str>,
|
|
) -> CultNetDocumentMutationContract {
|
|
mutation_contract(
|
|
document_type,
|
|
payload_schema_version,
|
|
vec![CultNetDocumentOperation::Snapshot],
|
|
CultNetMutationAuthority::ReadOnly,
|
|
vec![],
|
|
vec![],
|
|
notes,
|
|
)
|
|
}
|
|
|
|
fn coordinator_surface_contract(
|
|
document_type: impl Into<String>,
|
|
payload_schema_version: impl Into<String>,
|
|
intent_document_types: Vec<&str>,
|
|
receipt_document_types: Vec<&str>,
|
|
notes: Vec<&str>,
|
|
) -> CultNetDocumentMutationContract {
|
|
mutation_contract(
|
|
document_type,
|
|
payload_schema_version,
|
|
vec![
|
|
CultNetDocumentOperation::Snapshot,
|
|
CultNetDocumentOperation::IntentSubmit,
|
|
CultNetDocumentOperation::ReceiptWatch,
|
|
],
|
|
CultNetMutationAuthority::Coordinator,
|
|
intent_document_types,
|
|
receipt_document_types,
|
|
notes,
|
|
)
|
|
}
|
|
|
|
fn epiphany_mutation_contracts() -> Vec<CultNetDocumentMutationContract> {
|
|
vec![
|
|
mutation_contract(
|
|
RUNTIME_IDENTITY_TYPE,
|
|
RUNTIME_SPINE_SCHEMA_VERSION,
|
|
vec![CultNetDocumentOperation::Snapshot],
|
|
CultNetMutationAuthority::ReadOnly,
|
|
vec![],
|
|
vec![],
|
|
vec!["Runtime identity is advertised by the coordinator, not remotely mutated."],
|
|
),
|
|
mutation_contract(
|
|
RUNTIME_SESSION_TYPE,
|
|
RUNTIME_SPINE_SCHEMA_VERSION,
|
|
vec![
|
|
CultNetDocumentOperation::Snapshot,
|
|
CultNetDocumentOperation::IntentSubmit,
|
|
CultNetDocumentOperation::ReceiptWatch,
|
|
],
|
|
CultNetMutationAuthority::Coordinator,
|
|
vec!["epiphany.runtime.session_intent.v0"],
|
|
vec!["epiphany.swarm_control_receipt.v0"],
|
|
vec!["Sessions change through coordinator-reviewed typed intents."],
|
|
),
|
|
mutation_contract(
|
|
RUNTIME_JOB_TYPE,
|
|
RUNTIME_SPINE_SCHEMA_VERSION,
|
|
vec![
|
|
CultNetDocumentOperation::Snapshot,
|
|
CultNetDocumentOperation::IntentSubmit,
|
|
CultNetDocumentOperation::ReceiptWatch,
|
|
],
|
|
CultNetMutationAuthority::Coordinator,
|
|
vec!["epiphany.heartbeat_pump_intent.v0"],
|
|
vec!["epiphany.swarm_control_receipt.v0"],
|
|
vec![
|
|
"Heartbeat activation owns agent work; external callers submit intents and watch receipts.",
|
|
],
|
|
),
|
|
mutation_contract(
|
|
RUNTIME_WORKER_LAUNCH_REQUEST_TYPE,
|
|
RUNTIME_WORKER_LAUNCH_REQUEST_SCHEMA_VERSION,
|
|
vec![
|
|
CultNetDocumentOperation::Snapshot,
|
|
CultNetDocumentOperation::IntentSubmit,
|
|
CultNetDocumentOperation::ReceiptWatch,
|
|
],
|
|
CultNetMutationAuthority::Coordinator,
|
|
vec![RUNTIME_WORKER_LAUNCH_REQUEST_TYPE],
|
|
vec![RUNTIME_JOB_TYPE],
|
|
vec![
|
|
"Worker launch requests are typed task-intent documents; runtime jobs are lifecycle receipts, not the source of work intent.",
|
|
"Core/coordinator policy owns the launch yes/no; transport bridges translate, gather host facts, execute side effects, and return receipts.",
|
|
],
|
|
),
|
|
mutation_contract(
|
|
RUNTIME_ROLE_WORKER_RESULT_TYPE,
|
|
RUNTIME_ROLE_WORKER_RESULT_SCHEMA_VERSION,
|
|
vec![CultNetDocumentOperation::Snapshot],
|
|
CultNetMutationAuthority::ReadOnly,
|
|
vec![],
|
|
vec![],
|
|
vec![
|
|
"Role worker results preserve the typed finding payload; generic runtime job results are lifecycle receipts.",
|
|
],
|
|
),
|
|
mutation_contract(
|
|
RUNTIME_REORIENT_WORKER_RESULT_TYPE,
|
|
RUNTIME_REORIENT_WORKER_RESULT_SCHEMA_VERSION,
|
|
vec![CultNetDocumentOperation::Snapshot],
|
|
CultNetMutationAuthority::ReadOnly,
|
|
vec![],
|
|
vec![],
|
|
vec![
|
|
"Reorient worker results preserve continuity findings separately from generic runtime lifecycle receipts.",
|
|
],
|
|
),
|
|
mutation_contract(
|
|
RUNTIME_JOB_RESULT_TYPE,
|
|
RUNTIME_SPINE_SCHEMA_VERSION,
|
|
vec![CultNetDocumentOperation::Snapshot],
|
|
CultNetMutationAuthority::ReadOnly,
|
|
vec![],
|
|
vec![],
|
|
vec![
|
|
"Job results are evidence records; review and acceptance are separate typed flows.",
|
|
],
|
|
),
|
|
mutation_contract(
|
|
RUNTIME_EVENT_TYPE,
|
|
RUNTIME_SPINE_SCHEMA_VERSION,
|
|
vec![CultNetDocumentOperation::Snapshot],
|
|
CultNetMutationAuthority::ReadOnly,
|
|
vec![],
|
|
vec![],
|
|
vec!["Runtime events are append-only projections for inspection."],
|
|
),
|
|
mutation_contract(
|
|
OPENAI_ADAPTER_STATUS_TYPE,
|
|
OPENAI_ADAPTER_STATUS_SCHEMA_VERSION,
|
|
vec![CultNetDocumentOperation::Snapshot],
|
|
CultNetMutationAuthority::ReadOnly,
|
|
vec![],
|
|
vec![],
|
|
vec![
|
|
"OpenAI adapter status is a typed document; it is not a Codex app-server status blob.",
|
|
],
|
|
),
|
|
mutation_contract(
|
|
OPENAI_MODEL_REQUEST_TYPE,
|
|
OPENAI_MODEL_REQUEST_SCHEMA_VERSION,
|
|
vec![
|
|
CultNetDocumentOperation::IntentSubmit,
|
|
CultNetDocumentOperation::ReceiptWatch,
|
|
],
|
|
CultNetMutationAuthority::Coordinator,
|
|
vec![OPENAI_MODEL_REQUEST_TYPE],
|
|
vec![OPENAI_MODEL_STREAM_EVENT_TYPE, OPENAI_MODEL_RECEIPT_TYPE],
|
|
vec![
|
|
"Model turns enter through typed Epiphany request documents and return typed stream events/receipts.",
|
|
"The Codex spine may authenticate and transport; it must not own Epiphany state or scheduling.",
|
|
],
|
|
),
|
|
mutation_contract(
|
|
OPENAI_MODEL_STREAM_EVENT_TYPE,
|
|
OPENAI_MODEL_STREAM_EVENT_SCHEMA_VERSION,
|
|
vec![CultNetDocumentOperation::ReceiptWatch],
|
|
CultNetMutationAuthority::ReadOnly,
|
|
vec![],
|
|
vec![],
|
|
vec!["OpenAI stream events are receipts from a typed model request."],
|
|
),
|
|
mutation_contract(
|
|
OPENAI_MODEL_RECEIPT_TYPE,
|
|
OPENAI_MODEL_RECEIPT_SCHEMA_VERSION,
|
|
vec![CultNetDocumentOperation::ReceiptWatch],
|
|
CultNetMutationAuthority::ReadOnly,
|
|
vec![],
|
|
vec![],
|
|
vec!["Terminal model receipts carry response id, usage, and transport evidence."],
|
|
),
|
|
mutation_contract(
|
|
AGENT_MEMORY_TYPE,
|
|
AGENT_MEMORY_PAYLOAD_SCHEMA_VERSION,
|
|
vec![
|
|
CultNetDocumentOperation::Snapshot,
|
|
CultNetDocumentOperation::IntentSubmit,
|
|
CultNetDocumentOperation::ReceiptWatch,
|
|
],
|
|
CultNetMutationAuthority::Coordinator,
|
|
vec!["epiphany.agent_memory_intent.v0"],
|
|
vec!["epiphany.swarm_control_receipt.v0"],
|
|
vec![
|
|
"Sub-agents request memory mutations; the coordinator accepts, rejects, or explains the refusal.",
|
|
],
|
|
),
|
|
mutation_contract(
|
|
HEARTBEAT_STATE_TYPE,
|
|
HEARTBEAT_STATE_SCHEMA_VERSION,
|
|
vec![
|
|
CultNetDocumentOperation::Snapshot,
|
|
CultNetDocumentOperation::IntentSubmit,
|
|
CultNetDocumentOperation::ReceiptWatch,
|
|
],
|
|
CultNetMutationAuthority::Coordinator,
|
|
vec![
|
|
"epiphany.heartbeat_pump_intent.v0",
|
|
"epiphany.heartbeat_heat_intent.v0",
|
|
"epiphany.circadian_rhythm_intent.v0",
|
|
],
|
|
vec!["epiphany.swarm_control_receipt.v0"],
|
|
vec![
|
|
"Aquarium controls heartbeat and circadian rhythm through typed intents, not blind state replacement.",
|
|
"Initiative heat is heartbeat policy: global, group, role, and agent tempo changes enter through the heartbeat heat intent.",
|
|
],
|
|
),
|
|
mutation_contract(
|
|
STATE_LEDGER_STORE_TYPE,
|
|
STATE_LEDGER_SCHEMA_VERSION,
|
|
vec![CultNetDocumentOperation::Snapshot],
|
|
CultNetMutationAuthority::ReadOnly,
|
|
vec![],
|
|
vec![],
|
|
vec![
|
|
"The ledger is inspected as durable memory; writes are mediated by role-specific state flows.",
|
|
],
|
|
),
|
|
mutation_contract(
|
|
THREAD_STATE_TYPE,
|
|
THREAD_STATE_SCHEMA_VERSION,
|
|
vec![CultNetDocumentOperation::Snapshot],
|
|
CultNetMutationAuthority::ReadOnly,
|
|
vec![],
|
|
vec![],
|
|
vec![
|
|
"The mirrored thread state is the typed repo/control-plane state source; Codex rollout is a compatibility source, not the network contract.",
|
|
],
|
|
),
|
|
mutation_contract(
|
|
MEMORY_GRAPH_TYPE,
|
|
MEMORY_GRAPH_SCHEMA_VERSION,
|
|
vec![CultNetDocumentOperation::Snapshot],
|
|
CultNetMutationAuthority::ReadOnly,
|
|
vec![],
|
|
vec![],
|
|
vec![
|
|
"The unified memory graph is typed durable state; Qdrant embeddings are rebuildable cache, not canonical memory.",
|
|
],
|
|
),
|
|
read_only_surface_contract(
|
|
SURFACE_SCENE_TYPE,
|
|
SCENE_SURFACE_SCHEMA_VERSION,
|
|
vec![
|
|
"Operator-safe scene reflection over typed Epiphany state.",
|
|
"Aquarium should read this before offering live coordination actions.",
|
|
],
|
|
),
|
|
read_only_surface_contract(
|
|
SURFACE_FRESHNESS_TYPE,
|
|
FRESHNESS_SURFACE_SCHEMA_VERSION,
|
|
vec![
|
|
"Freshness reflection over retrieval, watcher, and graph staleness signals.",
|
|
"Use this to visualize retrieval and graph staleness without inventing a hidden refresh daemon.",
|
|
],
|
|
),
|
|
read_only_surface_contract(
|
|
SURFACE_CONTEXT_TYPE,
|
|
CONTEXT_SURFACE_SCHEMA_VERSION,
|
|
vec![
|
|
"Targeted graph, frontier, checkpoint, observation, and evidence context shard over typed Epiphany state.",
|
|
"Aquarium should inspect bounded state shards here instead of scraping state blobs by superstition.",
|
|
],
|
|
),
|
|
read_only_surface_contract(
|
|
SURFACE_GRAPH_QUERY_TYPE,
|
|
GRAPH_QUERY_SURFACE_SCHEMA_VERSION,
|
|
vec![
|
|
"Bounded graph traversal over typed architecture/dataflow graph state.",
|
|
"Use this for architecture/dataflow inspection and frontier neighborhoods without mutating state.",
|
|
],
|
|
),
|
|
read_only_surface_contract(
|
|
SURFACE_PRESSURE_TYPE,
|
|
PRESSURE_SURFACE_SCHEMA_VERSION,
|
|
vec![
|
|
"Current context pressure and compaction posture derived from typed pressure inputs.",
|
|
"This is a read-only warning surface, not a backdoor to force state mutation.",
|
|
],
|
|
),
|
|
coordinator_surface_contract(
|
|
SURFACE_REORIENT_TYPE,
|
|
REORIENT_SURFACE_SCHEMA_VERSION,
|
|
vec![
|
|
"epiphany.reorient_launch_intent.v0",
|
|
"epiphany.reorient_accept_intent.v0",
|
|
],
|
|
vec![
|
|
"epiphany.swarm_control_receipt.v0",
|
|
"epiphany.reorient_result_surface.v0",
|
|
],
|
|
vec![
|
|
"Reorientation policy is the typed resume/regather verdict surface.",
|
|
"Launch and acceptance stay review-gated through explicit typed intents.",
|
|
"The bridge may carry a reorient launch intent and report receipts, but the resume/regather verdict belongs to core.",
|
|
],
|
|
),
|
|
read_only_surface_contract(
|
|
SURFACE_CRRC_TYPE,
|
|
CRRC_SURFACE_SCHEMA_VERSION,
|
|
vec![
|
|
"CRRC recommendation surface over continuity, pressure, and reorientation signals.",
|
|
"Use this to understand continuity pressure without letting CRRC seize authority.",
|
|
],
|
|
),
|
|
coordinator_surface_contract(
|
|
SURFACE_JOBS_TYPE,
|
|
JOBS_SURFACE_SCHEMA_VERSION,
|
|
vec![
|
|
"epiphany.job_launch_intent.v0",
|
|
"epiphany.job_interrupt_intent.v0",
|
|
],
|
|
vec!["epiphany.swarm_control_receipt.v0"],
|
|
vec![
|
|
"Job reflection over typed job bindings and runtime-spine lifecycle receipts.",
|
|
"Heartbeat/runtime-spine owns activation; callers submit typed intents and watch receipts.",
|
|
"Launch receipts must name decisionOwner and bridgeRole so transport never grows an unreadable second opinion.",
|
|
],
|
|
),
|
|
coordinator_surface_contract(
|
|
SURFACE_ROLES_TYPE,
|
|
ROLES_SURFACE_SCHEMA_VERSION,
|
|
vec!["epiphany.role_launch_intent.v0"],
|
|
vec!["epiphany.swarm_control_receipt.v0"],
|
|
vec![
|
|
"Role ownership surface for fixed Epiphany lanes and launch affordances.",
|
|
"Treat this as the discoverable lane catalog for Aquarium.",
|
|
],
|
|
),
|
|
coordinator_surface_contract(
|
|
SURFACE_ROLE_RESULT_TYPE,
|
|
ROLE_RESULT_SURFACE_SCHEMA_VERSION,
|
|
vec!["epiphany.role_accept_intent.v0"],
|
|
vec!["epiphany.swarm_control_receipt.v0"],
|
|
vec![
|
|
"Role findings are typed review surfaces accepted through explicit role acceptance intents.",
|
|
"Semantic findings remain explicitly review-gated.",
|
|
],
|
|
),
|
|
coordinator_surface_contract(
|
|
SURFACE_REORIENT_RESULT_TYPE,
|
|
REORIENT_RESULT_SURFACE_SCHEMA_VERSION,
|
|
vec!["epiphany.reorient_accept_intent.v0"],
|
|
vec!["epiphany.swarm_control_receipt.v0"],
|
|
vec![
|
|
"Completed reorientation findings are typed review surfaces accepted through explicit reorientation acceptance intents.",
|
|
],
|
|
),
|
|
coordinator_surface_contract(
|
|
SURFACE_PLANNING_TYPE,
|
|
PLANNING_SURFACE_SCHEMA_VERSION,
|
|
vec![
|
|
"epiphany.planning_update_intent.v0",
|
|
"epiphany.objective_adoption_intent.v0",
|
|
],
|
|
vec!["epiphany.swarm_control_receipt.v0"],
|
|
vec![
|
|
"Planning projection over captures, backlog, roadmap streams, and Objective Drafts.",
|
|
"Backlog, captures, and Objective Drafts are planning state until explicit adoption.",
|
|
],
|
|
),
|
|
read_only_surface_contract(
|
|
SURFACE_COORDINATOR_TYPE,
|
|
COORDINATOR_SURFACE_SCHEMA_VERSION,
|
|
vec![
|
|
"Fixed-lane recommendation surface derived from typed role, pressure, reorientation, and result signals.",
|
|
"Aquarium should treat this as the primary action oracle, not invent its own scheduler.",
|
|
],
|
|
),
|
|
coordinator_surface_contract(
|
|
SURFACE_FACE_TYPE,
|
|
FACE_SURFACE_SCHEMA_VERSION,
|
|
vec![
|
|
"epiphany.face_bubble_intent.v0",
|
|
"epiphany.character_turn_intent.v0",
|
|
"epiphany.discord_persona_post_intent.v0",
|
|
],
|
|
vec![
|
|
"epiphany.face_bubble.v0",
|
|
"epiphany.face_chat.v0",
|
|
"epiphany.character_turn_packet.v0",
|
|
],
|
|
vec![
|
|
"Face bubble, draft, and Discord persona affordances are projected from typed Face and character-loop artifacts.",
|
|
"Humans talk to Face; sealed inner thoughts stay behind the projection boundary.",
|
|
],
|
|
),
|
|
read_only_surface_contract(
|
|
SURFACE_VOID_MEMORY_TYPE,
|
|
VOID_MEMORY_SURFACE_SCHEMA_VERSION,
|
|
vec![
|
|
"Void-derived memory status/search/context availability is projected from the typed Void memory bridge.",
|
|
"This is an inspection surface for the memory organs, not a license to bypass typed Epiphany state.",
|
|
],
|
|
),
|
|
coordinator_surface_contract(
|
|
SURFACE_REPO_INITIALIZATION_TYPE,
|
|
REPO_INITIALIZATION_SURFACE_SCHEMA_VERSION,
|
|
vec![
|
|
"epiphany.repo_startup_intent.v0",
|
|
"epiphany.repo_initialization_accept_intent.v0",
|
|
],
|
|
vec![
|
|
"epiphany.repo_personality_artifacts.v0",
|
|
"epiphany.repo_initialization_record.v0",
|
|
],
|
|
vec![
|
|
"Repo birth/startup status is projected from typed repo-personality startup and accept-init receipts.",
|
|
"Birth specialists are startup-only and remain outside the heartbeat lane system.",
|
|
],
|
|
),
|
|
coordinator_surface_contract(
|
|
SURFACE_REPO_BIRTH_RUNNER_TYPE,
|
|
REPO_BIRTH_RUNNER_SURFACE_SCHEMA_VERSION,
|
|
vec!["epiphany.repo_birth_run_intent.v0"],
|
|
vec![
|
|
"epiphany.repo_birth_runner.v0",
|
|
"epiphany.repo_initialization_record.v0",
|
|
],
|
|
vec![
|
|
"Startup-only birth runner plan/run affordances are projected from typed birth-runner receipts.",
|
|
"Aquarium should review birth artifacts and accept them explicitly instead of growing a hidden wizard.",
|
|
],
|
|
),
|
|
coordinator_surface_contract(
|
|
SURFACE_RIDER_BRIDGE_TYPE,
|
|
RIDER_BRIDGE_SURFACE_SCHEMA_VERSION,
|
|
vec![
|
|
"epiphany.rider_status_intent.v0",
|
|
"epiphany.rider_context_intent.v0",
|
|
"epiphany.rider_open_ref_intent.v0",
|
|
],
|
|
vec!["epiphany.swarm_control_receipt.v0"],
|
|
vec![
|
|
"Rider bridge affordances are projected from typed Rider bridge artifacts.",
|
|
"Use this surface for source/IDE inspection instead of scraping local artifacts by convention.",
|
|
],
|
|
),
|
|
coordinator_surface_contract(
|
|
SURFACE_UNITY_BRIDGE_TYPE,
|
|
UNITY_BRIDGE_SURFACE_SCHEMA_VERSION,
|
|
vec![
|
|
"epiphany.unity_inspect_intent.v0",
|
|
"epiphany.unity_probe_intent.v0",
|
|
"epiphany.unity_check_compilation_intent.v0",
|
|
"epiphany.unity_run_tests_intent.v0",
|
|
],
|
|
vec!["epiphany.swarm_control_receipt.v0"],
|
|
vec![
|
|
"Unity bridge affordances are projected from typed Unity bridge artifacts.",
|
|
"Runtime/editor truth must come through the pinned bridge and its receipts, not wishful source reading.",
|
|
],
|
|
),
|
|
]
|
|
}
|
|
|
|
fn validate_non_empty(value: &str, field: &str) -> Result<()> {
|
|
if value.trim().is_empty() {
|
|
return Err(anyhow!("{field} must be non-empty"));
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn worker_launch_document_kind(document: &EpiphanyWorkerLaunchDocument) -> &'static str {
|
|
match document {
|
|
EpiphanyWorkerLaunchDocument::Role(_) => "role",
|
|
EpiphanyWorkerLaunchDocument::Reorient(_) => "reorient",
|
|
}
|
|
}
|
|
|
|
fn encode_worker_launch_document(document: &EpiphanyWorkerLaunchDocument) -> Result<Vec<u8>> {
|
|
rmp_serde::to_vec_named(document).context("failed to encode worker launch document MessagePack")
|
|
}
|
|
|
|
fn decode_optional_msgpack<T>(payload: Option<&[u8]>, label: &str) -> Result<Option<T>>
|
|
where
|
|
T: for<'de> Deserialize<'de>,
|
|
{
|
|
payload
|
|
.map(|payload| {
|
|
rmp_serde::from_slice(payload).with_context(|| format!("failed to decode {label}"))
|
|
})
|
|
.transpose()
|
|
}
|
|
|
|
fn validate_heartbeat_launch_options(
|
|
state: &EpiphanyThreadState,
|
|
options: &RuntimeSpineHeartbeatLaunchPlanOptions,
|
|
) -> Result<()> {
|
|
validate_non_empty(&options.binding_id, "epiphany job launch binding_id")?;
|
|
if matches!(
|
|
options.binding_id.as_str(),
|
|
"retrieval-index" | "graph-remap" | "verification"
|
|
) {
|
|
return Err(anyhow!(
|
|
"epiphany job launch binding_id {:?} is reserved for a derived built-in slot",
|
|
options.binding_id
|
|
));
|
|
}
|
|
if options.kind != EpiphanyJobKind::Specialist {
|
|
return Err(anyhow!(
|
|
"epiphany job launch currently supports only specialist heartbeat turns"
|
|
));
|
|
}
|
|
validate_non_empty(&options.scope, "epiphany job launch scope")?;
|
|
validate_non_empty(&options.owner_role, "epiphany job launch owner_role")?;
|
|
validate_non_empty(
|
|
&options.authority_scope,
|
|
"epiphany job launch authority_scope",
|
|
)?;
|
|
validate_non_empty(&options.instruction, "epiphany job launch instruction")?;
|
|
validate_non_empty(
|
|
options.launch_document.thread_id(),
|
|
"epiphany job launch document thread id",
|
|
)?;
|
|
validate_non_empty(
|
|
&options.output_contract_id,
|
|
"epiphany job launch output_contract_id",
|
|
)?;
|
|
if options.output_contract_id != options.launch_document.output_contract_id() {
|
|
return Err(anyhow!(
|
|
"epiphany job launch output_contract_id must match the typed launch document"
|
|
));
|
|
}
|
|
if let Some(max_runtime_seconds) = options.max_runtime_seconds
|
|
&& max_runtime_seconds == 0
|
|
{
|
|
return Err(anyhow!(
|
|
"epiphany job launch max_runtime_seconds must be >= 1"
|
|
));
|
|
}
|
|
let existing_binding = state
|
|
.job_bindings
|
|
.iter()
|
|
.find(|binding| binding.id == options.binding_id);
|
|
let existing_runtime_link = state.runtime_links.iter().find(|link| {
|
|
link.binding_id == options.binding_id && !link.runtime_job_id.trim().is_empty()
|
|
});
|
|
if existing_runtime_link.is_some()
|
|
&& existing_binding.is_none_or(|binding| binding.blocking_reason.is_none())
|
|
{
|
|
return Err(anyhow!(
|
|
"epiphany job binding {:?} is already bound to an active heartbeat turn; interrupt it before launching a replacement",
|
|
options.binding_id
|
|
));
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn terminal_status_for_verdict(verdict: &str) -> EpiphanyRuntimeJobStatus {
|
|
if matches!(
|
|
verdict,
|
|
"failed" | "fail" | "error" | "blocked" | "cancelled" | "canceled"
|
|
) {
|
|
EpiphanyRuntimeJobStatus::Failed
|
|
} else {
|
|
EpiphanyRuntimeJobStatus::Completed
|
|
}
|
|
}
|
|
|
|
fn merge_refs(existing: &[String], incoming: &[String]) -> Vec<String> {
|
|
let mut merged = existing.to_vec();
|
|
for item in incoming {
|
|
if !merged.contains(item) {
|
|
merged.push(item.clone());
|
|
}
|
|
}
|
|
merged
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use cultnet_rs::CultNetWireContract;
|
|
use cultnet_rs::decode_cultnet_message_from_slice;
|
|
use tempfile::tempdir;
|
|
|
|
#[test]
|
|
fn runtime_spine_initializes_sessions_events_and_status() -> Result<()> {
|
|
let temp = tempdir()?;
|
|
let store = temp.path().join("runtime.msgpack");
|
|
initialize_runtime_spine(
|
|
&store,
|
|
RuntimeSpineInitOptions {
|
|
runtime_id: "epiphany-test".to_string(),
|
|
display_name: "Epiphany Test".to_string(),
|
|
created_at: "2026-05-06T00:00:00Z".to_string(),
|
|
},
|
|
)?;
|
|
create_runtime_session(
|
|
&store,
|
|
RuntimeSpineSessionOptions {
|
|
session_id: "session-1".to_string(),
|
|
objective: "Build the spine.".to_string(),
|
|
created_at: "2026-05-06T00:01:00Z".to_string(),
|
|
coordinator_note: "Native first.".to_string(),
|
|
},
|
|
)?;
|
|
append_runtime_event(
|
|
&store,
|
|
RuntimeSpineEventOptions {
|
|
event_id: "event-1".to_string(),
|
|
occurred_at: "2026-05-06T00:02:00Z".to_string(),
|
|
event_type: "session.started".to_string(),
|
|
source: "test".to_string(),
|
|
session_id: Some("session-1".to_string()),
|
|
job_id: None,
|
|
summary: "Session started.".to_string(),
|
|
},
|
|
)?;
|
|
|
|
let status = runtime_spine_status(&store)?;
|
|
assert!(status.present);
|
|
assert_eq!(status.runtime_id.as_deref(), Some("epiphany-test"));
|
|
assert_eq!(status.sessions, 1);
|
|
assert_eq!(status.active_sessions, 1);
|
|
assert_eq!(status.events, 1);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn runtime_spine_opens_and_completes_native_jobs() -> Result<()> {
|
|
let temp = tempdir()?;
|
|
let store = temp.path().join("runtime.msgpack");
|
|
initialize_runtime_spine(
|
|
&store,
|
|
RuntimeSpineInitOptions {
|
|
runtime_id: "epiphany-test".to_string(),
|
|
display_name: "Epiphany Test".to_string(),
|
|
created_at: "2026-05-06T00:00:00Z".to_string(),
|
|
},
|
|
)?;
|
|
create_runtime_session(
|
|
&store,
|
|
RuntimeSpineSessionOptions {
|
|
session_id: "session-1".to_string(),
|
|
objective: "Build the job artery.".to_string(),
|
|
created_at: "2026-05-06T00:01:00Z".to_string(),
|
|
coordinator_note: "Native jobs.".to_string(),
|
|
},
|
|
)?;
|
|
let job = create_runtime_job(
|
|
&store,
|
|
RuntimeSpineJobOptions {
|
|
job_id: "job-1".to_string(),
|
|
session_id: "session-1".to_string(),
|
|
role: "modeling".to_string(),
|
|
created_at: "2026-05-06T00:02:00Z".to_string(),
|
|
summary: "Model the target.".to_string(),
|
|
artifact_refs: vec!["artifact:modeling-plan".to_string()],
|
|
},
|
|
)?;
|
|
assert_eq!(job.status, EpiphanyRuntimeJobStatus::Queued);
|
|
let result = complete_runtime_job(
|
|
&store,
|
|
RuntimeSpineJobResultOptions {
|
|
result_id: "result-1".to_string(),
|
|
job_id: "job-1".to_string(),
|
|
completed_at: "2026-05-06T00:03:00Z".to_string(),
|
|
verdict: "pass".to_string(),
|
|
summary: "Model is ready.".to_string(),
|
|
next_safe_move: "Launch verification.".to_string(),
|
|
evidence_refs: vec!["evidence:model".to_string()],
|
|
artifact_refs: vec!["artifact:model".to_string()],
|
|
},
|
|
)?;
|
|
assert_eq!(result.role, "modeling");
|
|
let status = runtime_spine_status(&store)?;
|
|
assert_eq!(status.jobs, 1);
|
|
assert_eq!(status.open_jobs, 0);
|
|
assert_eq!(status.job_results, 1);
|
|
assert_eq!(status.events, 2);
|
|
let snapshot =
|
|
runtime_job_snapshot(&store, "job-1")?.expect("completed job snapshot should exist");
|
|
assert_eq!(snapshot.job.status, EpiphanyRuntimeJobStatus::Completed);
|
|
assert_eq!(
|
|
snapshot
|
|
.result
|
|
.as_ref()
|
|
.map(|result| result.result_id.as_str()),
|
|
Some("result-1")
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn runtime_spine_opens_heartbeat_job_from_single_typed_call() -> Result<()> {
|
|
let temp = tempdir()?;
|
|
let store = temp.path().join("runtime.msgpack");
|
|
|
|
let job = open_runtime_spine_heartbeat_job(
|
|
&store,
|
|
RuntimeSpineHeartbeatJobOptions {
|
|
runtime_id: "epiphany-test".to_string(),
|
|
display_name: "Epiphany Test".to_string(),
|
|
session_id: "epiphany-main".to_string(),
|
|
objective: "Run heartbeat worker.".to_string(),
|
|
coordinator_note: "Test launch.".to_string(),
|
|
job_id: "heartbeat-job-1".to_string(),
|
|
role: "modeling".to_string(),
|
|
binding_id: "modeling-checkpoint-worker".to_string(),
|
|
authority_scope: "modeling".to_string(),
|
|
instruction: "Inspect the checkpoint and return typed role findings.".to_string(),
|
|
launch_document: EpiphanyWorkerLaunchDocument::Role(
|
|
crate::EpiphanyRoleWorkerLaunchDocument {
|
|
thread_id: "thread-1".to_string(),
|
|
role_id: "modeling".to_string(),
|
|
state_revision: 7,
|
|
objective: Some("Run heartbeat worker.".to_string()),
|
|
active_subgoal_id: None,
|
|
active_subgoals: Vec::new(),
|
|
active_graph_node_ids: vec!["node-model".to_string()],
|
|
investigation_checkpoint: None,
|
|
scratch: None,
|
|
invariants: Vec::new(),
|
|
graphs: None,
|
|
recent_evidence: Vec::new(),
|
|
recent_observations: Vec::new(),
|
|
graph_frontier: None,
|
|
graph_checkpoint: None,
|
|
planning: None,
|
|
churn: None,
|
|
},
|
|
),
|
|
output_contract_id: crate::ROLE_WORKER_OUTPUT_CONTRACT_ID.to_string(),
|
|
created_at: "2026-05-06T00:02:00Z".to_string(),
|
|
},
|
|
)?;
|
|
|
|
assert_eq!(job.job_id, "heartbeat-job-1");
|
|
assert_eq!(job.session_id, "epiphany-main");
|
|
assert_eq!(job.role, "modeling");
|
|
let status = runtime_spine_status(&store)?;
|
|
assert_eq!(status.sessions, 1);
|
|
assert_eq!(status.jobs, 1);
|
|
assert_eq!(status.open_jobs, 1);
|
|
assert_eq!(status.events, 1);
|
|
let mut cache = runtime_spine_cache(&store)?;
|
|
cache.pull_all_backing_stores()?;
|
|
let launch_request = cache
|
|
.get::<EpiphanyRuntimeWorkerLaunchRequest>("heartbeat-job-1")?
|
|
.expect("typed worker launch request should be durable");
|
|
assert_eq!(launch_request.job_id, "heartbeat-job-1");
|
|
assert_eq!(launch_request.binding_id, "modeling-checkpoint-worker");
|
|
assert_eq!(launch_request.role, "modeling");
|
|
assert_eq!(
|
|
launch_request.output_contract_id,
|
|
crate::ROLE_WORKER_OUTPUT_CONTRACT_ID
|
|
);
|
|
assert_eq!(launch_request.document_kind, "role");
|
|
assert!(!launch_request.launch_document_msgpack.is_empty());
|
|
assert_eq!(launch_request.launch_document()?.thread_id(), "thread-1");
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn heartbeat_launch_plan_leaves_lifecycle_to_runtime_links() {
|
|
let launch_plan = plan_runtime_spine_heartbeat_launch(
|
|
&EpiphanyThreadState::default(),
|
|
RuntimeSpineHeartbeatLaunchPlanOptions {
|
|
binding_id: "modeling-checkpoint-worker".to_string(),
|
|
kind: EpiphanyJobKind::Specialist,
|
|
scope: "role-scoped modeling/checkpoint maintenance".to_string(),
|
|
owner_role: "epiphany-modeler".to_string(),
|
|
authority_scope: "epiphany.role.modeling".to_string(),
|
|
linked_subgoal_ids: vec!["phase-6".to_string()],
|
|
linked_graph_node_ids: vec!["runtime-spine".to_string()],
|
|
instruction: "Model the target before implementation.".to_string(),
|
|
launch_document: EpiphanyWorkerLaunchDocument::Role(
|
|
crate::EpiphanyRoleWorkerLaunchDocument {
|
|
thread_id: "thread-1".to_string(),
|
|
role_id: "modeling".to_string(),
|
|
state_revision: 7,
|
|
objective: Some("keep state typed".to_string()),
|
|
active_subgoal_id: None,
|
|
active_subgoals: Vec::new(),
|
|
active_graph_node_ids: vec!["runtime-spine".to_string()],
|
|
investigation_checkpoint: None,
|
|
scratch: None,
|
|
invariants: Vec::new(),
|
|
graphs: None,
|
|
recent_evidence: Vec::new(),
|
|
recent_observations: Vec::new(),
|
|
graph_frontier: None,
|
|
graph_checkpoint: None,
|
|
planning: None,
|
|
churn: None,
|
|
},
|
|
),
|
|
output_contract_id: "epiphany.worker.role_result.v0".to_string(),
|
|
max_runtime_seconds: Some(60),
|
|
runtime_job_id: "turn-1".to_string(),
|
|
},
|
|
)
|
|
.expect("launch planning should build binding and runtime link");
|
|
|
|
assert_eq!(
|
|
launch_plan.binding.authority_scope.as_deref(),
|
|
Some("epiphany.role.modeling")
|
|
);
|
|
assert_eq!(
|
|
launch_plan.runtime_link.id,
|
|
"runtime-link-modeling-checkpoint-worker-turn-1"
|
|
);
|
|
assert_eq!(launch_plan.runtime_link.runtime_job_id, "turn-1");
|
|
assert_eq!(launch_plan.runtime_link.runtime_result_id, None);
|
|
assert_eq!(launch_plan.runtime_link.role_id, "epiphany-modeler");
|
|
}
|
|
|
|
#[test]
|
|
fn runtime_spine_emits_cultnet_hello_frame() -> Result<()> {
|
|
let temp = tempdir()?;
|
|
let store = temp.path().join("runtime.msgpack");
|
|
initialize_runtime_spine(
|
|
&store,
|
|
RuntimeSpineInitOptions {
|
|
runtime_id: "epiphany-test".to_string(),
|
|
display_name: "Epiphany Test".to_string(),
|
|
created_at: "2026-05-06T00:00:00Z".to_string(),
|
|
},
|
|
)?;
|
|
let frame = runtime_hello_frame(&store)?;
|
|
let payload_len = u32::from_be_bytes([frame[0], frame[1], frame[2], frame[3]]) as usize;
|
|
let message =
|
|
decode_cultnet_message_from_slice(&frame[4..], CultNetWireContract::CultNetSchemaV0)?;
|
|
assert_eq!(payload_len, frame.len() - 4);
|
|
match message {
|
|
CultNetMessage::Hello {
|
|
runtime_id,
|
|
runtime_kind,
|
|
supported_document_types,
|
|
supported_mutation_contracts,
|
|
..
|
|
} => {
|
|
assert_eq!(runtime_id, "epiphany-test");
|
|
assert_eq!(runtime_kind, "epiphany.native");
|
|
assert!(
|
|
supported_document_types
|
|
.unwrap()
|
|
.contains(&RUNTIME_JOB_RESULT_TYPE.to_string())
|
|
);
|
|
let contracts = supported_mutation_contracts.unwrap();
|
|
let heartbeat_contract = contracts
|
|
.iter()
|
|
.find(|contract| contract.document_type == HEARTBEAT_STATE_TYPE)
|
|
.expect("heartbeat state should advertise mutation contract");
|
|
assert_eq!(
|
|
heartbeat_contract.authority,
|
|
CultNetMutationAuthority::Coordinator
|
|
);
|
|
assert!(
|
|
heartbeat_contract
|
|
.operations
|
|
.contains(&CultNetDocumentOperation::IntentSubmit)
|
|
);
|
|
assert!(
|
|
heartbeat_contract
|
|
.intent_document_types
|
|
.as_ref()
|
|
.is_some_and(|items| items
|
|
.iter()
|
|
.any(|item| item == "epiphany.heartbeat_heat_intent.v0"))
|
|
);
|
|
let coordinator_contract = contracts
|
|
.iter()
|
|
.find(|contract| contract.document_type == SURFACE_COORDINATOR_TYPE)
|
|
.expect("coordinator surface should advertise a read-only contract");
|
|
assert_eq!(
|
|
coordinator_contract.authority,
|
|
CultNetMutationAuthority::ReadOnly
|
|
);
|
|
let face_contract = contracts
|
|
.iter()
|
|
.find(|contract| contract.document_type == SURFACE_FACE_TYPE)
|
|
.expect("face surface should advertise an interactive contract");
|
|
assert_eq!(
|
|
face_contract.authority,
|
|
CultNetMutationAuthority::Coordinator
|
|
);
|
|
assert!(
|
|
face_contract
|
|
.receipt_document_types
|
|
.as_ref()
|
|
.is_some_and(|items| items
|
|
.iter()
|
|
.any(|item| item == "epiphany.face_bubble.v0"))
|
|
);
|
|
let unity_contract = contracts
|
|
.iter()
|
|
.find(|contract| contract.document_type == SURFACE_UNITY_BRIDGE_TYPE)
|
|
.expect("unity bridge surface should advertise an interactive contract");
|
|
assert!(
|
|
unity_contract
|
|
.intent_document_types
|
|
.as_ref()
|
|
.is_some_and(|items| items
|
|
.iter()
|
|
.any(|item| item == "epiphany.unity_probe_intent.v0"))
|
|
);
|
|
let birth_contract = contracts
|
|
.iter()
|
|
.find(|contract| contract.document_type == SURFACE_REPO_BIRTH_RUNNER_TYPE)
|
|
.expect("repo birth runner surface should advertise startup actions");
|
|
assert!(
|
|
birth_contract
|
|
.receipt_document_types
|
|
.as_ref()
|
|
.is_some_and(|items| items
|
|
.iter()
|
|
.any(|item| item == "epiphany.repo_birth_runner.v0"))
|
|
);
|
|
let openai_contract = contracts
|
|
.iter()
|
|
.find(|contract| contract.document_type == OPENAI_MODEL_REQUEST_TYPE)
|
|
.expect("OpenAI adapter should advertise typed model request contract");
|
|
assert_eq!(
|
|
openai_contract.authority,
|
|
CultNetMutationAuthority::Coordinator
|
|
);
|
|
assert!(
|
|
openai_contract
|
|
.receipt_document_types
|
|
.as_ref()
|
|
.is_some_and(|items| items
|
|
.iter()
|
|
.any(|item| item == OPENAI_MODEL_RECEIPT_TYPE))
|
|
);
|
|
let memory_graph_contract = contracts
|
|
.iter()
|
|
.find(|contract| contract.document_type == MEMORY_GRAPH_TYPE)
|
|
.expect("memory graph should advertise a read-only contract");
|
|
assert_eq!(
|
|
memory_graph_contract.authority,
|
|
CultNetMutationAuthority::ReadOnly
|
|
);
|
|
let thread_state_contract = contracts
|
|
.iter()
|
|
.find(|contract| contract.document_type == THREAD_STATE_TYPE)
|
|
.expect("thread state should advertise a read-only contract");
|
|
assert_eq!(
|
|
thread_state_contract.authority,
|
|
CultNetMutationAuthority::ReadOnly
|
|
);
|
|
}
|
|
other => panic!("expected hello, got {other:?}"),
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn runtime_spine_schema_catalog_includes_surface_and_control_receipts() -> Result<()> {
|
|
let registry = epiphany_schema_registry()?;
|
|
let schemas = registry.list(&cultnet_rs::CultNetSchemaCatalogOptions {
|
|
include_schema_json: false,
|
|
schema_ids: None,
|
|
kinds: None,
|
|
});
|
|
assert!(schemas.iter().any(|schema| {
|
|
schema.document_type.as_deref() == Some(SURFACE_SCENE_TYPE)
|
|
&& schema.schema_version.as_deref() == Some(SCENE_SURFACE_SCHEMA_VERSION)
|
|
}));
|
|
assert!(schemas.iter().any(|schema| {
|
|
schema.document_type.as_deref() == Some(SURFACE_COORDINATOR_TYPE)
|
|
&& schema.schema_version.as_deref() == Some(COORDINATOR_SURFACE_SCHEMA_VERSION)
|
|
}));
|
|
assert!(schemas.iter().any(|schema| {
|
|
schema.document_type.as_deref() == Some("epiphany.role_launch_intent.v0")
|
|
&& schema.schema_version.as_deref() == Some("epiphany.role_launch_intent.v0")
|
|
}));
|
|
assert!(schemas.iter().any(|schema| {
|
|
schema.document_type.as_deref() == Some("epiphany.heartbeat_initiative_heat.v0")
|
|
&& schema.schema_version.as_deref() == Some("epiphany.heartbeat_initiative_heat.v0")
|
|
}));
|
|
assert!(schemas.iter().any(|schema| {
|
|
schema.document_type.as_deref() == Some("epiphany.heartbeat_heat_intent.v0")
|
|
&& schema.schema_version.as_deref() == Some("epiphany.heartbeat_heat_intent.v0")
|
|
}));
|
|
assert!(schemas.iter().any(|schema| {
|
|
schema.document_type.as_deref() == Some("epiphany.swarm_control_receipt.v0")
|
|
&& schema.schema_version.as_deref() == Some("epiphany.swarm_control_receipt.v0")
|
|
}));
|
|
let receipt_schema_path =
|
|
epiphany_schema_root().join("epiphany.swarm-control-receipt.schema.json");
|
|
let receipt_schema: serde_json::Value =
|
|
serde_json::from_str(&fs::read_to_string(receipt_schema_path)?)?;
|
|
let required = receipt_schema["required"]
|
|
.as_array()
|
|
.expect("receipt schema should list required fields");
|
|
assert!(required.iter().any(|field| field == "decisionOwner"));
|
|
assert!(required.iter().any(|field| field == "bridgeRole"));
|
|
assert!(
|
|
receipt_schema["properties"]["decisionOwner"]["description"]
|
|
.as_str()
|
|
.is_some_and(|description| description.contains("epiphany-core"))
|
|
);
|
|
assert!(
|
|
receipt_schema["properties"]["bridgeRole"]["description"]
|
|
.as_str()
|
|
.is_some_and(|description| description.contains("transport"))
|
|
);
|
|
assert!(schemas.iter().any(|schema| {
|
|
schema.document_type.as_deref() == Some(OPENAI_MODEL_REQUEST_TYPE)
|
|
&& schema.schema_version.as_deref() == Some(OPENAI_MODEL_REQUEST_SCHEMA_VERSION)
|
|
}));
|
|
assert!(schemas.iter().any(|schema| {
|
|
schema.document_type.as_deref() == Some(OPENAI_MODEL_RECEIPT_TYPE)
|
|
&& schema.schema_version.as_deref() == Some(OPENAI_MODEL_RECEIPT_SCHEMA_VERSION)
|
|
}));
|
|
assert!(schemas.iter().any(|schema| {
|
|
schema.document_type.as_deref() == Some(MEMORY_GRAPH_TYPE)
|
|
&& schema.schema_version.as_deref() == Some(MEMORY_GRAPH_SCHEMA_VERSION)
|
|
}));
|
|
assert!(schemas.iter().any(|schema| {
|
|
schema.document_type.as_deref() == Some(THREAD_STATE_TYPE)
|
|
&& schema.schema_version.as_deref() == Some(THREAD_STATE_SCHEMA_VERSION)
|
|
}));
|
|
Ok(())
|
|
}
|
|
}
|