Type durable agent memory documents
This commit is contained in:
parent
824c005524
commit
05e0554ae4
7 changed files with 231 additions and 63 deletions
|
|
@ -41,13 +41,11 @@ pub struct EpiphanyAgentMemoryEntry {
|
|||
#[cultcache(key = 3)]
|
||||
pub agent: GhostlightAgent,
|
||||
#[cultcache(key = 4, default)]
|
||||
pub relationships: Vec<Value>,
|
||||
pub relationships: Vec<GhostlightRelationship>,
|
||||
#[cultcache(key = 5, default)]
|
||||
pub events: Vec<Value>,
|
||||
pub events: Vec<GhostlightEvent>,
|
||||
#[cultcache(key = 6, default)]
|
||||
pub scenes: Vec<Value>,
|
||||
#[cultcache(key = 7, default)]
|
||||
pub extra: BTreeMap<String, Value>,
|
||||
pub scenes: Vec<GhostlightScene>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
|
||||
|
|
@ -56,15 +54,11 @@ pub struct GhostlightWorld {
|
|||
pub setting: String,
|
||||
pub time: GhostlightTime,
|
||||
pub canon_context: Vec<String>,
|
||||
#[serde(flatten)]
|
||||
pub extra: BTreeMap<String, Value>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
|
||||
pub struct GhostlightTime {
|
||||
pub label: String,
|
||||
#[serde(flatten)]
|
||||
pub extra: BTreeMap<String, Value>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
|
||||
|
|
@ -74,9 +68,82 @@ pub struct GhostlightAgent {
|
|||
pub canonical_state: GhostlightCanonicalState,
|
||||
pub goals: Vec<GhostlightGoal>,
|
||||
pub memories: GhostlightMemories,
|
||||
pub perceived_state_overlays: Vec<Value>,
|
||||
#[serde(flatten)]
|
||||
pub extra: BTreeMap<String, Value>,
|
||||
#[serde(default)]
|
||||
pub perceived_state_overlays: Vec<GhostlightPerceivedStateOverlay>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
|
||||
pub struct GhostlightRelationship {
|
||||
#[serde(default)]
|
||||
pub relationship_id: String,
|
||||
#[serde(default)]
|
||||
pub participant_ids: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub summary: String,
|
||||
#[serde(default)]
|
||||
pub stance: String,
|
||||
#[serde(default)]
|
||||
pub salience: f64,
|
||||
#[serde(default)]
|
||||
pub confidence: f64,
|
||||
#[serde(default)]
|
||||
pub linked_memory_ids: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
|
||||
pub struct GhostlightEvent {
|
||||
#[serde(default)]
|
||||
pub event_id: String,
|
||||
#[serde(default)]
|
||||
pub kind: String,
|
||||
#[serde(default)]
|
||||
pub summary: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub timestamp_label: Option<String>,
|
||||
#[serde(default)]
|
||||
pub participant_ids: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub linked_memory_ids: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub salience: f64,
|
||||
#[serde(default)]
|
||||
pub confidence: f64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
|
||||
pub struct GhostlightScene {
|
||||
#[serde(default)]
|
||||
pub scene_id: String,
|
||||
#[serde(default)]
|
||||
pub label: String,
|
||||
#[serde(default)]
|
||||
pub summary: String,
|
||||
#[serde(default)]
|
||||
pub participant_ids: Vec<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub anchor: Option<String>,
|
||||
#[serde(default)]
|
||||
pub salience: f64,
|
||||
#[serde(default)]
|
||||
pub status: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
|
||||
pub struct GhostlightPerceivedStateOverlay {
|
||||
#[serde(default)]
|
||||
pub overlay_id: String,
|
||||
#[serde(default)]
|
||||
pub label: String,
|
||||
#[serde(default)]
|
||||
pub summary: String,
|
||||
#[serde(default)]
|
||||
pub source: String,
|
||||
#[serde(default)]
|
||||
pub salience: f64,
|
||||
#[serde(default)]
|
||||
pub confidence: f64,
|
||||
#[serde(default)]
|
||||
pub linked_memory_ids: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
|
||||
|
|
@ -87,8 +154,6 @@ pub struct GhostlightIdentity {
|
|||
pub public_description: String,
|
||||
#[serde(default)]
|
||||
pub private_notes: Vec<String>,
|
||||
#[serde(flatten)]
|
||||
pub extra: BTreeMap<String, Value>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
|
||||
|
|
@ -100,8 +165,6 @@ pub struct GhostlightCanonicalState {
|
|||
pub voice_style: BTreeMap<String, GhostlightTraitVector>,
|
||||
pub situational_state: BTreeMap<String, GhostlightTraitVector>,
|
||||
pub values: Vec<GhostlightValue>,
|
||||
#[serde(flatten)]
|
||||
pub extra: BTreeMap<String, Value>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
|
||||
|
|
@ -109,8 +172,6 @@ pub struct GhostlightTraitVector {
|
|||
pub mean: f64,
|
||||
pub plasticity: f64,
|
||||
pub current_activation: f64,
|
||||
#[serde(flatten)]
|
||||
pub extra: BTreeMap<String, Value>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
|
||||
|
|
@ -119,8 +180,6 @@ pub struct GhostlightValue {
|
|||
pub label: String,
|
||||
pub priority: f64,
|
||||
pub unforgivable_if_betrayed: bool,
|
||||
#[serde(flatten)]
|
||||
pub extra: BTreeMap<String, Value>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
|
||||
|
|
@ -133,8 +192,6 @@ pub struct GhostlightGoal {
|
|||
#[serde(default)]
|
||||
pub blockers: Vec<String>,
|
||||
pub status: String,
|
||||
#[serde(flatten)]
|
||||
pub extra: BTreeMap<String, Value>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
|
||||
|
|
@ -142,8 +199,6 @@ pub struct GhostlightMemories {
|
|||
pub episodic: Vec<GhostlightMemory>,
|
||||
pub semantic: Vec<GhostlightMemory>,
|
||||
pub relationship_summaries: Vec<GhostlightMemory>,
|
||||
#[serde(flatten)]
|
||||
pub extra: BTreeMap<String, Value>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
|
||||
|
|
@ -156,8 +211,6 @@ pub struct GhostlightMemory {
|
|||
pub linked_event_ids: Option<Vec<String>>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub linked_relationship_id: Option<String>,
|
||||
#[serde(flatten)]
|
||||
pub extra: BTreeMap<String, Value>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
|
|
@ -166,13 +219,11 @@ struct AgentMemoryProjection {
|
|||
pub world: GhostlightWorld,
|
||||
pub agents: Vec<GhostlightAgent>,
|
||||
#[serde(default)]
|
||||
pub relationships: Vec<Value>,
|
||||
pub relationships: Vec<GhostlightRelationship>,
|
||||
#[serde(default)]
|
||||
pub events: Vec<Value>,
|
||||
pub events: Vec<GhostlightEvent>,
|
||||
#[serde(default)]
|
||||
pub scenes: Vec<Value>,
|
||||
#[serde(flatten)]
|
||||
pub extra: BTreeMap<String, Value>,
|
||||
pub scenes: Vec<GhostlightScene>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
|
||||
|
|
@ -582,7 +633,6 @@ pub fn apply_agent_canonical_trait_seeds(
|
|||
mean: clamp_unit(seed.mean),
|
||||
plasticity: clamp_unit(seed.plasticity),
|
||||
current_activation: clamp_unit(seed.current_activation),
|
||||
extra: BTreeMap::new(),
|
||||
};
|
||||
let mut entry = cache
|
||||
.get::<EpiphanyAgentMemoryEntry>(&seed.role_id)?
|
||||
|
|
@ -718,7 +768,6 @@ pub fn project_agent_memory_to_json_dir(
|
|||
relationships: entry.relationships,
|
||||
events: entry.events,
|
||||
scenes: entry.scenes,
|
||||
extra: entry.extra,
|
||||
};
|
||||
let path = output_dir.join(filename);
|
||||
fs::write(
|
||||
|
|
@ -793,7 +842,6 @@ fn entry_from_projection(
|
|||
relationships: projection.relationships,
|
||||
events: projection.events,
|
||||
scenes: projection.scenes,
|
||||
extra: projection.extra,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -855,6 +903,26 @@ fn validate_agent_entry(entry: &EpiphanyAgentMemoryEntry, expected_agent_id: &st
|
|||
validate_memory(memory, &format!("memories.{bundle}[{index}]"), &mut errors);
|
||||
}
|
||||
}
|
||||
for (index, overlay) in entry.agent.perceived_state_overlays.iter().enumerate() {
|
||||
validate_overlay(
|
||||
overlay,
|
||||
&format!("perceived_state_overlays[{index}]"),
|
||||
&mut errors,
|
||||
);
|
||||
}
|
||||
for (index, relationship) in entry.relationships.iter().enumerate() {
|
||||
validate_relationship(
|
||||
relationship,
|
||||
&format!("relationships[{index}]"),
|
||||
&mut errors,
|
||||
);
|
||||
}
|
||||
for (index, event) in entry.events.iter().enumerate() {
|
||||
validate_event(event, &format!("events[{index}]"), &mut errors);
|
||||
}
|
||||
for (index, scene) in entry.scenes.iter().enumerate() {
|
||||
validate_scene(scene, &format!("scenes[{index}]"), &mut errors);
|
||||
}
|
||||
for (index, goal) in entry.agent.goals.iter().enumerate() {
|
||||
validate_goal(goal, &format!("goals[{index}]"), &mut errors);
|
||||
}
|
||||
|
|
@ -905,6 +973,69 @@ fn validate_memory(memory: &GhostlightMemory, path: &str, errors: &mut Vec<Strin
|
|||
check_unit(memory.confidence, &format!("{path}.confidence"), errors);
|
||||
}
|
||||
|
||||
fn validate_overlay(
|
||||
overlay: &GhostlightPerceivedStateOverlay,
|
||||
path: &str,
|
||||
errors: &mut Vec<String>,
|
||||
) {
|
||||
check_optional_identifier(&overlay.overlay_id, &format!("{path}.overlay_id"), errors);
|
||||
check_string(&overlay.summary, &format!("{path}.summary"), errors, 800);
|
||||
check_optional_text(&overlay.label, &format!("{path}.label"), errors, 240);
|
||||
check_optional_text(&overlay.source, &format!("{path}.source"), errors, 240);
|
||||
check_unit(overlay.salience, &format!("{path}.salience"), errors);
|
||||
check_unit(overlay.confidence, &format!("{path}.confidence"), errors);
|
||||
}
|
||||
|
||||
fn validate_relationship(
|
||||
relationship: &GhostlightRelationship,
|
||||
path: &str,
|
||||
errors: &mut Vec<String>,
|
||||
) {
|
||||
check_optional_identifier(
|
||||
&relationship.relationship_id,
|
||||
&format!("{path}.relationship_id"),
|
||||
errors,
|
||||
);
|
||||
check_string(
|
||||
&relationship.summary,
|
||||
&format!("{path}.summary"),
|
||||
errors,
|
||||
800,
|
||||
);
|
||||
check_optional_text(&relationship.stance, &format!("{path}.stance"), errors, 240);
|
||||
if relationship.participant_ids.is_empty() {
|
||||
errors.push(format!("{path}.participant_ids must not be empty"));
|
||||
}
|
||||
check_unit(relationship.salience, &format!("{path}.salience"), errors);
|
||||
check_unit(
|
||||
relationship.confidence,
|
||||
&format!("{path}.confidence"),
|
||||
errors,
|
||||
);
|
||||
}
|
||||
|
||||
fn validate_event(event: &GhostlightEvent, path: &str, errors: &mut Vec<String>) {
|
||||
check_optional_identifier(&event.event_id, &format!("{path}.event_id"), errors);
|
||||
check_string(&event.kind, &format!("{path}.kind"), errors, 120);
|
||||
check_string(&event.summary, &format!("{path}.summary"), errors, 800);
|
||||
if event.participant_ids.is_empty() {
|
||||
errors.push(format!("{path}.participant_ids must not be empty"));
|
||||
}
|
||||
check_unit(event.salience, &format!("{path}.salience"), errors);
|
||||
check_unit(event.confidence, &format!("{path}.confidence"), errors);
|
||||
}
|
||||
|
||||
fn validate_scene(scene: &GhostlightScene, path: &str, errors: &mut Vec<String>) {
|
||||
check_optional_identifier(&scene.scene_id, &format!("{path}.scene_id"), errors);
|
||||
check_string(&scene.label, &format!("{path}.label"), errors, 240);
|
||||
check_string(&scene.summary, &format!("{path}.summary"), errors, 800);
|
||||
if scene.participant_ids.is_empty() {
|
||||
errors.push(format!("{path}.participant_ids must not be empty"));
|
||||
}
|
||||
check_unit(scene.salience, &format!("{path}.salience"), errors);
|
||||
check_optional_text(&scene.status, &format!("{path}.status"), errors, 120);
|
||||
}
|
||||
|
||||
fn validate_goal(goal: &GhostlightGoal, path: &str, errors: &mut Vec<String>) {
|
||||
check_string(&goal.goal_id, &format!("{path}.goal_id"), errors, 120);
|
||||
check_string(
|
||||
|
|
@ -958,6 +1089,29 @@ fn check_string(value: &str, path: &str, errors: &mut Vec<String>, max_len: usiz
|
|||
}
|
||||
}
|
||||
|
||||
fn check_optional_text(value: &str, path: &str, errors: &mut Vec<String>, max_len: usize) {
|
||||
if !value.is_empty() && (value.trim().is_empty() || value.len() > max_len) {
|
||||
errors.push(format!(
|
||||
"{path} must be empty or text under {max_len} characters"
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
fn check_optional_identifier(value: &str, path: &str, errors: &mut Vec<String>) {
|
||||
if value.is_empty() {
|
||||
return;
|
||||
}
|
||||
if value.len() > 120
|
||||
|| !value.chars().all(|ch| {
|
||||
ch.is_ascii() && (ch.is_ascii_alphanumeric() || matches!(ch, '-' | '_' | '.'))
|
||||
})
|
||||
{
|
||||
errors.push(format!(
|
||||
"{path} must be empty or an ASCII identifier without whitespace"
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
fn check_unit(value: f64, path: &str, errors: &mut Vec<String>) {
|
||||
if !value.is_finite() || !(0.0..=1.0).contains(&value) {
|
||||
errors.push(format!("{path} must be between 0 and 1"));
|
||||
|
|
@ -1218,7 +1372,6 @@ fn upsert_memories(records: &mut Vec<GhostlightMemory>, incoming: Vec<SelfPatchM
|
|||
confidence: item.confidence,
|
||||
linked_event_ids: item.linked_event_ids,
|
||||
linked_relationship_id: item.linked_relationship_id,
|
||||
extra: BTreeMap::new(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
@ -1242,7 +1395,6 @@ fn upsert_goals(records: &mut Vec<GhostlightGoal>, incoming: Vec<SelfPatchGoal>)
|
|||
emotional_stake: item.emotional_stake,
|
||||
blockers: item.blockers,
|
||||
status: item.status,
|
||||
extra: BTreeMap::new(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
@ -1263,7 +1415,6 @@ fn upsert_values(records: &mut Vec<GhostlightValue>, incoming: Vec<SelfPatchValu
|
|||
label: item.label,
|
||||
priority: item.priority,
|
||||
unforgivable_if_betrayed: item.unforgivable_if_betrayed,
|
||||
extra: BTreeMap::new(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ use anyhow::anyhow;
|
|||
use chrono::SecondsFormat;
|
||||
use epiphany_core::EpiphanyAgentMemoryEntry;
|
||||
use epiphany_core::GhostlightMemory;
|
||||
use epiphany_core::GhostlightPerceivedStateOverlay;
|
||||
use epiphany_core::GhostlightTraitVector;
|
||||
use epiphany_core::dossier_profile_for_role;
|
||||
use epiphany_core::load_agent_memory_entry_for_role;
|
||||
|
|
@ -175,7 +176,7 @@ fn character_turn_packet(
|
|||
"semanticMemories": entry.agent.memories.semantic.iter().map(memory_projection).collect::<Vec<_>>(),
|
||||
"episodicMemories": entry.agent.memories.episodic.iter().map(memory_projection).collect::<Vec<_>>(),
|
||||
"relationshipMemories": entry.agent.memories.relationship_summaries.iter().map(memory_projection).collect::<Vec<_>>(),
|
||||
"perceivedStateOverlays": entry.agent.perceived_state_overlays.iter().take(6).cloned().collect::<Vec<_>>(),
|
||||
"perceivedStateOverlays": entry.agent.perceived_state_overlays.iter().take(6).map(perceived_overlay_projection).collect::<Vec<_>>(),
|
||||
"privateNoteCount": entry.agent.identity.private_notes.len(),
|
||||
},
|
||||
"projectionSeed": projection_seed,
|
||||
|
|
@ -556,15 +557,10 @@ fn strongest_overlay_pressure(
|
|||
.perceived_state_overlays
|
||||
.iter()
|
||||
.map(|overlay| {
|
||||
let mut tokens = BTreeSet::new();
|
||||
collect_json_tokens(overlay, &mut tokens);
|
||||
let tokens = overlay_tokens(overlay);
|
||||
let overlap = token_overlap(&tokens, thought_tokens);
|
||||
let summary = overlay
|
||||
.get("summary")
|
||||
.and_then(Value::as_str)
|
||||
.or_else(|| overlay.get("label").and_then(Value::as_str))
|
||||
.unwrap_or("Perceived overlay relevance");
|
||||
(overlap, summary.to_string())
|
||||
let summary = overlay_summary(overlay);
|
||||
(overlap, summary)
|
||||
})
|
||||
.max_by(|left, right| left.0.total_cmp(&right.0))
|
||||
.map(|(strength, summary)| (round3(strength.clamp(0.0, 1.0)), summary))
|
||||
|
|
@ -590,21 +586,37 @@ fn stimulus_token_set(
|
|||
tokens
|
||||
}
|
||||
|
||||
fn collect_json_tokens(value: &Value, tokens: &mut BTreeSet<String>) {
|
||||
match value {
|
||||
Value::String(text) => tokens.extend(summary_tokens(text)),
|
||||
Value::Array(items) => {
|
||||
for item in items {
|
||||
collect_json_tokens(item, tokens);
|
||||
}
|
||||
}
|
||||
Value::Object(object) => {
|
||||
for (key, value) in object {
|
||||
tokens.extend(summary_tokens(key));
|
||||
collect_json_tokens(value, tokens);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
fn perceived_overlay_projection(overlay: &GhostlightPerceivedStateOverlay) -> Value {
|
||||
serde_json::json!({
|
||||
"overlayId": overlay.overlay_id,
|
||||
"label": overlay.label,
|
||||
"summary": overlay.summary,
|
||||
"source": overlay.source,
|
||||
"salience": overlay.salience,
|
||||
"confidence": overlay.confidence,
|
||||
"linkedMemoryIds": overlay.linked_memory_ids,
|
||||
})
|
||||
}
|
||||
|
||||
fn overlay_tokens(overlay: &GhostlightPerceivedStateOverlay) -> BTreeSet<String> {
|
||||
let mut tokens = BTreeSet::new();
|
||||
tokens.extend(summary_tokens(&overlay.overlay_id));
|
||||
tokens.extend(summary_tokens(&overlay.label));
|
||||
tokens.extend(summary_tokens(&overlay.summary));
|
||||
tokens.extend(summary_tokens(&overlay.source));
|
||||
for memory_id in &overlay.linked_memory_ids {
|
||||
tokens.extend(summary_tokens(memory_id));
|
||||
}
|
||||
tokens
|
||||
}
|
||||
|
||||
fn overlay_summary(overlay: &GhostlightPerceivedStateOverlay) -> String {
|
||||
if !overlay.summary.trim().is_empty() {
|
||||
overlay.summary.clone()
|
||||
} else if !overlay.label.trim().is_empty() {
|
||||
overlay.label.clone()
|
||||
} else {
|
||||
"Perceived overlay relevance".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -17,7 +17,11 @@ pub use agent_memory::AgentSelfPatch;
|
|||
pub use agent_memory::EpiphanyAgentMemoryEntry;
|
||||
pub use agent_memory::EpiphanyDossierProfile;
|
||||
pub use agent_memory::EpiphanyDossierProfileKind;
|
||||
pub use agent_memory::GhostlightEvent;
|
||||
pub use agent_memory::GhostlightMemory;
|
||||
pub use agent_memory::GhostlightPerceivedStateOverlay;
|
||||
pub use agent_memory::GhostlightRelationship;
|
||||
pub use agent_memory::GhostlightScene;
|
||||
pub use agent_memory::GhostlightTraitVector;
|
||||
pub use agent_memory::agent_memory_role_ids;
|
||||
pub use agent_memory::agent_memory_status;
|
||||
|
|
|
|||
|
|
@ -251,7 +251,6 @@ mod tests {
|
|||
relationships: Vec::new(),
|
||||
events: Vec::new(),
|
||||
scenes: Vec::new(),
|
||||
extra: Default::default(),
|
||||
};
|
||||
|
||||
let snapshot = memory_graph_from_agent_memories("agent-profile", &[entry]);
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ remembers doctrine; the branch remembers the blade.
|
|||
- Edge JSON is allowed for schema description, hostile ingress before immediate typed parsing, sealed forensic artifacts, or named quarantine experiments. When both subsystems are ours, runtime data must remain typed CultCache documents and move over CultNet typed contracts. Generic `serde_json::Value` in worker launch/result/selfPatch/runtime flow is contamination until classified and replaced.
|
||||
- `epiphany-openai-runtime` worker result ingress now parses assistant output directly into typed role/reorient ingress structs selected from the durable launch document, stores nested `statePatch` and `selfPatch` as typed MessagePack in `EpiphanyRuntimeRoleWorkerResult`, and no longer passes worker result data through generic `serde_json::Value`. JSON survives there only as the OpenAI text-output edge before typed deserialization.
|
||||
- Agent self-memory review/application now has typed `AgentSelfPatch` core APIs. JSON-value selfPatch functions are compatibility wrappers for CLI/artifact ingress; native callers should parse once at the edge and then call `review_agent_self_patch_document` / `apply_agent_self_patch_document`. Role-result self-persistence no longer exports a JSON-value reviewer; the live API is `review_role_self_patch_document`.
|
||||
- Durable Ghostlight role dossiers now store typed relationship, event, scene, and perceived-overlay documents instead of `Vec<serde_json::Value>`, and the durable dossier structs no longer preserve flattened unknown `extra` maps. `AgentSelfPatch.extra` remains deliberately, but only as an ingress rejection ledger for unexpected patch fields.
|
||||
- `epiphany-openai-codex-spine` no longer uses Codex's Responses client, model-provider layer, `codex-api`, `codex-model-provider`, `codex-model-provider-info`, or `codex-protocol` for outbound model requests. It reaches `codex-login` only through `epiphany-openai-auth-spine` so credential loading/refresh stays Codex-compatible while request/status/event/runtime surfaces stay typed Epiphany documents.
|
||||
- `notes/epiphany-memory-graph-unified-plan.md` is the current memory architecture direction drawn from `Fractal Domains And The Cache That Bites` plus the VoidBot memory scar tissue. The repo semantic architecture/dataflow graph and agent memory are one `EpiphanyMemoryGraph` substrate with repo, role, short-term, incubation, agency, candidate-intervention, and evidence profiles. The first native skeleton is landed: `epiphany-state-model` owns shared graph document types, `epiphany-core::memory_graph` owns stable ids, validation, freshness, context cuts, and CultCache persistence, and `epiphany-memory-graph` can status/validate/context/smoke the store without Qdrant. Qdrant is a contribution cache for typed graph documents, not truth. `notes/repo-fractal-dataflow-cache-plan.md` and `notes/agent-memory-fractal-cache-plan.md` are profile plans, not rival organs.
|
||||
- Durable Epiphany state types now live in root crate `epiphany-state-model`. `codex-protocol` re-exports them only so old rollout/API callers keep compiling; do not add new durable `Epiphany*` state structs under `vendor/codex`. The deterministic `<epiphany_state>` prompt renderer also lives beside that typed model now, with prompt text under `epiphany-state-model/src/prompts/`; `epiphany-core` only re-exports it for compatibility. `epiphany-core` imports the native model directly and no longer depends on `codex-protocol`. Pressure derives from native `EpiphanyTokenUsageSnapshot`; Codex token telemetry is mapped at the bridge. Codex rollout/event reconstruction now lives in `codex-core::epiphany_rollout`, where the Codex `RolloutItem` / `EventMsg` types belong. Exact retrieval path matching is native to `epiphany-core`, so `epiphany-core` has no direct vendored Codex crate dependency.
|
||||
|
|
@ -92,7 +93,7 @@ remembers doctrine; the branch remembers the blade.
|
|||
- The prompt doctrine pass is landed. Shared Epiphany prompts now carry distilled memory/evidence discipline. Rendered state intro/doctrine text lives in `epiphany-core/src/prompts/`, and lane/control prompt text lives in `vendor/codex/codex-rs/app-server/src/prompts/epiphany_specialists.toml`: modeling is the Body, implementation is the Hands and GUI-launched main coding lane, verification is the Soul, reorientation is Life, coordinator remains the read-only Self, and CRRC owns the pre-compaction intervention template.
|
||||
- The machine-priest voice now says the quiet part plainly: crusade/heresy/purity language is aimed at technical rot, hidden state, duplicate truth, drift, and lying structures, not at humans. Future Epiphany swarms should inherit severity toward systems and curiosity toward people.
|
||||
- The Ghostlight memory pass is landed. `epiphany_specialists.toml` now has a shared persistent-memory projection prepended by the harness to fixed role specialists, reorientation workers, coordinator notes, and CRRC checkpoint interventions. The rendered base doctrine also states the Perfect Machine rule directly: prompt is projection, durable typed state is the mind, every lane must improve its own memory/model/prompt/evidence habit or name the repair, and each lane phrases that duty in its own organ language so the salience sticks.
|
||||
- The role self-memory persistence pass is native now. Each lane has a Ghostlight-shaped typed dossier in `state/agents.msgpack`, and specialists may return optional `selfPatch` requests beside their normal role result. `roleResult`/`roleAccept` project coordinator review as `selfPersistence`: accepted requests are role-matched, bounded lane memory/goal/value/private-note mutations; refused requests explain wrong role, project-truth smuggling, authority grabs, bloat, missing reason, or malformed records. GUI/coordinator accept paths apply accepted `selfPatch` requests through the native `epiphany-agent-memory-store` binary; project truth still belongs only in `EpiphanyThreadState`.
|
||||
- The role self-memory persistence pass is native now. Each lane has a Ghostlight-shaped typed dossier in `state/agents.msgpack`, and specialists may return optional `selfPatch` requests beside their normal role result. Relationship, event, scene, and perceived-overlay arrays are typed dossier documents, not JSON cargo, and the durable structs do not keep unknown `extra` maps. `roleResult`/`roleAccept` project coordinator review as `selfPersistence`: accepted requests are role-matched, bounded lane memory/goal/value/private-note mutations; refused requests explain wrong role, project-truth smuggling, authority grabs, bloat, missing reason, or malformed records. GUI/coordinator accept paths apply accepted `selfPatch` requests through the native `epiphany-agent-memory-store` binary; project truth still belongs only in `EpiphanyThreadState`.
|
||||
- The heartbeat initiative pass is landed as a bounded tool seam. `state/agent-heartbeats.msgpack` tracks Self, Face, Imagination, Eyes, Body, Hands, Soul, and Life as Ghostlight-style initiative participants with arena, participant kind, speed, next-ready time, reaction bias, interrupt threshold, load, status, constraints, personality cooldown, mood cooldown, effective cooldown, adaptive pacing, history, and pending turns through `epiphany-core::EpiphanyHeartbeatStateEntry` and the native `epiphany-heartbeat-store` binary. Role dossiers set baseline timing; appraisal mood/anxiety bends it through urgency, arousal, thought pressure, guardedness, and reaction intensity, so Hands-like work pressure and high-need anxious lanes recover sooner without being heartbeaten while a prior turn is running. `epiphany-heartbeat-store pump` computes pressure from external urgency, mood/anxiety, reaction intensity, thought pressure, and current pending load, then chooses tempo plus target concurrency: calm can launch zero and sleep slow; alarm can fill most active lanes. Idle turns are for rumination: light thought shuffling, role-quality attention, and candidate selfPatch pressure. Sleep/dream cycle passes are the intended distillation window for durable self-memory and doctrine. JSON heartbeat state and Python wrapper state are gone; general CultCache schema sync, polyglot domain loading, and debug display tools belong in CultLib. This is a callable scheduler seam, not yet an always-on daemon; a heart valve, not a whole circulatory god.
|
||||
|
||||
- Heartbeat initiative heat is now part of that seam. `EpiphanyHeartbeatStateEntry` owns `initiative_heat`, pacing applies active global/all/agent/role/arena/participant-kind/group/constraint multipliers into per-participant `initiativeHeatMultiplier`, and recovery divides by that multiplier. The CLI exposes `epiphany-heartbeat-store heat`, status/schedule projections show heat, and the CultNet catalog advertises both `epiphany.heartbeat_initiative_heat.v0` and coordinator intent `epiphany.heartbeat_heat_intent.v0`; runtime-spine hello lists the heat intent on the heartbeat mutation contract. Smoke proves a heated implementation lane reaches the live action catalog with `initiative_heat_multiplier: 4.0`.
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -99,6 +99,7 @@ accepted_design:
|
|||
- `notes/codex-auth-spine-inventory.md` is the current source-grounded inventory of the Codex organ that may survive. Corrected compliance invariant: Codex-compatible auth identity should remain anchored in retained vendored Codex-derived auth machinery, while Epiphany owns typed request/status/event/runtime surfaces around it. Codex app-server, apps, skills, marketplace, plugin UX, MCP OAuth handlers, and collaboration-mode surfaces are not part of the keeper spine.
|
||||
- `epiphany-openai-runtime` worker result ingress now parses assistant output directly into launch-kind-specific typed documents and stores typed nested `statePatch` / `selfPatch` payloads as MessagePack inside `EpiphanyRuntimeRoleWorkerResult`; generic `serde_json::Value` no longer mediates role/reorient worker results in that crate. JSON still exists at the OpenAI text edge because the model's output is text, but the runtime boundary after parsing is typed.
|
||||
- Agent self-memory review/application now has typed `AgentSelfPatch` core APIs. The old JSON-value selfPatch functions remain only as edge compatibility wrappers for CLI/artifact ingress; `epiphany-agent-memory-store`, heartbeat rumination application, and repo initialization selfPatch processing parse once at the edge and then operate on typed documents. Role-result self-persistence no longer exports a JSON-value reviewer; native review uses `review_role_self_patch_document`.
|
||||
- Durable Ghostlight role dossiers are typed through their relationship/event/scene/perceived-overlay surfaces now. `EpiphanyAgentMemoryEntry` no longer stores those arrays as `serde_json::Value`, and durable Ghostlight structs no longer carry flattened unknown `extra` bags. `AgentSelfPatch.extra` remains only as an ingress guard so unknown patch keys can be rejected with reasons.
|
||||
- Heartbeat durable document/schema types live in `epiphany-core/src/heartbeat_state/heartbeat_documents.rs`; role/default construction lives in `epiphany-core/src/heartbeat_state/heartbeat_roles.rs`; heartbeat persistence/validation lives in `epiphany-core/src/heartbeat_state/heartbeat_store.rs`; heartbeat status/artifact JSON projection lives in `epiphany-core/src/heartbeat_state/heartbeat_projection.rs`; the root heartbeat module remains the scheduling/cognition orchestration surface until those responsibilities are split into their own tested seams.
|
||||
- Heartbeat role/default and store seams are pinned by module-local unit tests for fixed lane catalog, Ghostlight scene role projection, and typed state/cognition CultCache round-trip.
|
||||
- Heartbeat adaptive pacing lives in `epiphany-core/src/heartbeat_state/heartbeat_pacing.rs`; it owns pressure/concurrency derivation, running-turn count, initiative heat projection, and effective cooldown multipliers for scheduler/projection callers.
|
||||
|
|
|
|||
Reference in a new issue