Project Face as Persona state

This commit is contained in:
Meta Vens 2026-05-29 02:19:53 +01:00
commit 18751933e0
8 changed files with 250 additions and 4 deletions

View file

@ -1,6 +1,8 @@
use anyhow::Context;
use anyhow::Result;
use anyhow::anyhow;
use chrono::SecondsFormat;
use chrono::Utc;
use cultcache_rs::CultCache;
use cultcache_rs::DatabaseEntry;
use cultcache_rs::SingleFileMessagePackBackingStore;
@ -334,6 +336,8 @@ pub struct AgentCanonicalTraitSeed {
pub source: Option<String>,
}
pub const PERSONA_STATE_SCHEMA_VERSION: &str = "gamecult.persona_state.v0";
pub fn migrate_agent_memory_json_dir_to_cultcache(
agent_dir: impl AsRef<Path>,
store_path: impl AsRef<Path>,
@ -713,6 +717,223 @@ pub fn agent_memory_status(store_path: impl AsRef<Path>) -> Result<Value> {
}))
}
pub fn project_persona_state_for_role(
store_path: impl AsRef<Path>,
role_id: &str,
) -> Result<Value> {
let store_path = store_path.as_ref();
let profile = organ_state_profile_for_role(role_id);
if profile.profile_kind != EpiphanyOrganStateProfileKind::Persona {
return Err(anyhow!(
"{role_id:?} is {:?}, not persona; use epiphany.work_organ_state.v0 for work organs",
profile.profile_kind
));
}
let entry = load_agent_memory_entry_for_role(store_path, role_id)?
.ok_or_else(|| anyhow!("CultCache has no role memory entry for {role_id:?}"))?;
Ok(project_persona_state_from_entry(&entry, store_path))
}
pub fn project_persona_state_from_entry(
entry: &EpiphanyAgentMemoryEntry,
store_path: &Path,
) -> Value {
let exported_at = now_rfc3339();
let persona_id = entry.agent.agent_id.clone();
let repo_name = "EpiphanyAgent";
serde_json::json!({
"schemaVersion": PERSONA_STATE_SCHEMA_VERSION,
"provenance": {
"sourceSystem": "epiphany",
"sourceDocumentId": format!("{}#{}", store_path.display(), entry.role_id),
"sourceUpdatedAt": exported_at,
"exportedAt": exported_at,
"authority": "projection",
},
"personaId": persona_id,
"publicName": &entry.agent.identity.name,
"publicDescription": &entry.agent.identity.public_description,
"presentation": {
"voiceSummary": &entry.agent.identity.public_description,
"defaultRenderer": "chat",
"homeContext": {
"kind": "repo",
"id": repo_name,
"label": repo_name,
},
"jurisdiction": entry
.agent
.identity
.roles
.iter()
.map(String::as_str)
.collect::<Vec<_>>()
.join(", "),
"publicHandles": [],
},
"privateNotes": &entry.agent.identity.private_notes,
"values": entry.agent.canonical_state.values.iter().map(persona_value).collect::<Vec<_>>(),
"activationProfile": {
"underlyingOrganization": persona_trait_map(&entry.agent.canonical_state.underlying_organization),
"stableDispositions": persona_trait_map(&entry.agent.canonical_state.stable_dispositions),
"behavioralDimensions": persona_trait_map(&entry.agent.canonical_state.behavioral_dimensions),
"presentationStrategy": persona_trait_map(&entry.agent.canonical_state.presentation_strategy),
"voiceStyle": persona_trait_map(&entry.agent.canonical_state.voice_style),
"situationalState": persona_trait_map(&entry.agent.canonical_state.situational_state),
},
"thoughtMemory": {
"shortTerm": [],
"memories": entry
.agent
.memories
.semantic
.iter()
.chain(entry.agent.memories.episodic.iter())
.chain(entry.agent.memories.relationship_summaries.iter())
.map(|memory| persona_memory_thought(memory, &persona_id, &exported_at))
.collect::<Vec<_>>(),
"incubation": [],
},
"agencyPressure": {
"pressures": entry.agent.goals.iter().map(|goal| persona_goal_thought(goal, &persona_id, &exported_at)).collect::<Vec<_>>(),
},
"candidateActions": {
"actions": [],
},
"voidbotProjection": {
"candidateInterventions": [],
},
"affect": {
"needs": [],
"socialBonds": [],
"statusReads": entry.agent.perceived_state_overlays.iter().map(|overlay| persona_status_read(overlay, &persona_id, &exported_at)).collect::<Vec<_>>(),
"moodDimensions": [],
"socialBiases": [],
"doctrineStances": entry.agent.canonical_state.values.iter().map(|value| persona_doctrine_stance(value, &persona_id, &exported_at)).collect::<Vec<_>>(),
},
"updatedAt": exported_at,
})
}
fn persona_trait_map(vectors: &BTreeMap<String, GhostlightTraitVector>) -> Value {
let mut map = serde_json::Map::new();
for (name, vector) in vectors {
map.insert(
name.clone(),
serde_json::json!({
"mean": vector.mean,
"plasticity": vector.plasticity,
"currentActivation": vector.current_activation,
}),
);
}
Value::Object(map)
}
fn persona_value(value: &GhostlightValue) -> Value {
serde_json::json!({
"id": value.value_id,
"label": value.label,
"priority": value.priority,
"summary": if value.unforgivable_if_betrayed {
"Protected value; betrayal is marked as unforgivable in local Face state."
} else {
"Value projected from local Face organ memory."
},
})
}
fn persona_target(kind: &str, id: &str, label: &str) -> Value {
serde_json::json!({
"kind": kind,
"id": id,
"label": label,
})
}
fn persona_memory_thought(memory: &GhostlightMemory, persona_id: &str, timestamp: &str) -> Value {
serde_json::json!({
"id": memory.memory_id,
"status": "crystallized",
"target": persona_target("self", persona_id, persona_id),
"summary": memory.summary,
"claim": memory.summary,
"tension": "Projected from local Face memory; consumers should preserve provenance and avoid treating this as omniscient project truth.",
"actionImplication": "Use as Persona memory pressure, not as direct action authority.",
"intensity": memory.salience,
"valence": 0,
"createdAt": timestamp,
"updatedAt": timestamp,
"tags": ["epiphany", "face-memory"],
})
}
fn persona_goal_thought(goal: &GhostlightGoal, persona_id: &str, timestamp: &str) -> Value {
serde_json::json!({
"id": goal.goal_id,
"status": persona_goal_status(&goal.status),
"target": persona_target("self", persona_id, persona_id),
"summary": goal.description,
"claim": goal.description,
"tension": goal.emotional_stake,
"actionImplication": "This goal may create Face agency pressure, but action still requires the caller's review path.",
"intensity": goal.priority,
"createdAt": timestamp,
"updatedAt": timestamp,
"tags": ["epiphany", "face-goal", &goal.scope],
})
}
fn persona_status_read(
overlay: &GhostlightPerceivedStateOverlay,
persona_id: &str,
timestamp: &str,
) -> Value {
serde_json::json!({
"id": overlay.overlay_id,
"status": "active",
"target": persona_target("self", persona_id, &overlay.label),
"statusKind": "uncertainty",
"summary": overlay.summary,
"confidence": overlay.confidence,
"intensity": overlay.salience,
"valence": 0,
"updatedAt": timestamp,
"extensions": {
"source": overlay.source,
"linkedMemoryIds": overlay.linked_memory_ids,
},
})
}
fn persona_doctrine_stance(value: &GhostlightValue, persona_id: &str, timestamp: &str) -> Value {
serde_json::json!({
"id": format!("stance-{}", value.value_id),
"status": "active",
"target": persona_target("self", persona_id, persona_id),
"stanceKind": "aligned",
"principle": value.label,
"summary": value.label,
"actionImplication": "Let this value bend Face speech and interpretation without granting automatic action authority.",
"intensity": value.priority,
"updatedAt": timestamp,
})
}
fn persona_goal_status(status: &str) -> &'static str {
match status {
"active" => "active",
"blocked" | "dormant" => "cooling",
"resolved" => "resolved",
"abandoned" => "retired",
_ => "draft",
}
}
fn now_rfc3339() -> String {
Utc::now().to_rfc3339_opts(SecondsFormat::Secs, true)
}
pub fn organ_state_profile_for_role(role_id: &str) -> EpiphanyOrganStateProfile {
match role_id {
"face" => EpiphanyOrganStateProfile {

View file

@ -5,6 +5,7 @@ use epiphany_core::agent_memory_status;
use epiphany_core::apply_agent_self_patch_document;
use epiphany_core::migrate_agent_memory_json_dir_to_cultcache;
use epiphany_core::project_agent_memory_to_json_dir;
use epiphany_core::project_persona_state_for_role;
use epiphany_core::review_agent_self_patch_document;
use epiphany_core::validate_agent_memory_store;
use std::env;
@ -32,6 +33,11 @@ fn main() -> Result<()> {
let output_dir = require_path_arg(&mut args, "--output-dir")?;
print_json(&project_agent_memory_to_json_dir(store, output_dir)?)?;
}
"project-persona" => {
let store = require_path_arg(&mut args, "--store")?;
let role_id = require_string_arg(&mut args, "--role-id")?;
print_json(&project_persona_state_for_role(store, &role_id)?)?;
}
"status" => {
let store = require_path_arg(&mut args, "--store")?;
print_json(&agent_memory_status(store)?)?;
@ -130,7 +136,7 @@ fn print_json<T: serde::Serialize>(value: &T) -> Result<()> {
fn print_usage() {
eprintln!(
"usage: epiphany-agent-memory-store <migrate-json-dir|project-json-dir|status|validate|review-patch|apply-patch|smoke> ..."
"usage: epiphany-agent-memory-store <migrate-json-dir|project-json-dir|project-persona|status|validate|review-patch|apply-patch|smoke> ..."
);
}
@ -185,7 +191,9 @@ fn run_smoke(store: &Path) -> Result<serde_json::Value> {
&& wrong_role.status == "rejected"
&& forbidden.status == "rejected"
&& applied.status == "accepted"
&& temp_validation_errors.is_empty();
&& temp_validation_errors.is_empty()
&& project_persona_state_for_role(store, "face")?["schemaVersion"]
== epiphany_core::PERSONA_STATE_SCHEMA_VERSION;
Ok(serde_json::json!({
"ok": ok,
"accepted": accepted,

View file

@ -64,6 +64,7 @@ pub use agent_memory::GhostlightPerceivedStateOverlay;
pub use agent_memory::GhostlightRelationship;
pub use agent_memory::GhostlightScene;
pub use agent_memory::GhostlightTraitVector;
pub use agent_memory::PERSONA_STATE_SCHEMA_VERSION;
pub use agent_memory::agent_memory_role_ids;
pub use agent_memory::agent_memory_status;
pub use agent_memory::apply_agent_canonical_trait_seeds;
@ -73,6 +74,8 @@ pub use agent_memory::load_agent_memory_entry_for_role;
pub use agent_memory::migrate_agent_memory_json_dir_to_cultcache;
pub use agent_memory::organ_state_profile_for_role;
pub use agent_memory::project_agent_memory_to_json_dir;
pub use agent_memory::project_persona_state_for_role;
pub use agent_memory::project_persona_state_from_entry;
pub use agent_memory::review_agent_self_patch;
pub use agent_memory::review_agent_self_patch_document;
pub use agent_memory::validate_agent_memory_store;

View file

@ -140,7 +140,7 @@ remembers doctrine; the branch remembers the blade.
- The native runtime-spine hello now publishes the interactive surface catalog, not just the deep runtime bones. In addition to runtime/session/job/memory/heartbeat/ledger documents, Aquarium-discoverable contracts now advertise scene, pressure, reorient, CRRC, jobs, roles, role-result, reorient-result, planning, coordinator, Face, Void memory, repo initialization/birth runner, Rider bridge, and Unity bridge surfaces, with explicit read-only versus coordinator-owned intent/receipt posture. The point is to let Aquarium discover operator affordances from CultNet instead of hard-coding a secret menu of verbs.
- Epiphany now publishes those CultNet receipts locally under `schemas/cultnet/`, and `epiphany-runtime-spine schema-catalog --include-schema-json true` emits a merged builtin-plus-local schema catalog Aquarium can consume directly. The local catalog covers runtime-spine documents, durable agent/heartbeat/ledger state, operator-safe `epiphany.surface.*` projections, control intents, and receipt/artifact payloads. The vendored `cultnet-rs` wire contract was aligned with the C# canon at the same time: raw document replication now keys on `schemaId` and `recordKey`, snapshot filters use `schemaIds` / `recordKeys`, and hello supports typed mutation-contract advertisement instead of the old half-remembered payload-schema-version folklore.
- Epiphany now owns the canonical schema paperwork locally under `schemas/`. The key receipts are `schemas/cultnet/gamecult.persona_state.v0.schema.json`, `schemas/cultnet/epiphany.work_organ_state.v0.schema.json`, `schemas/canonical-agent-state-schema.md`, `schemas/agent-state-variable-glossary.md`, `schemas/organ-state-profiles.md`, `schemas/repo-personality-birth-projection.md`, and `schemas/heartbeat-state-schema.md`. If a standing trait name like `routing_discipline` changes, or if organ-state, Persona, heartbeat, or birth-projection semantics move, update those docs in the same pass instead of relying on old Ghostlight notes or live store archaeology.
- The schema doctrine now makes the profile split explicit: resident Epiphany organs are `work_organ` state with lean role memory and growth via rumination/distillation, while Face is the Epiphany organ that may use portable `persona` state shared with VoidBot repo Faces and Ghostlight characters. Work organs do not inherit Persona affect, social bonds, or status-read machinery. PersonaState now has explicit provenance and required presentation surfaces, `date-time` timestamps, typed `candidateActions` with action type/target/readiness/risk/urgency/confidence/evidence/expiry, `voidbotProjection.candidateInterventions` only as a VoidBot compatibility projection, strict anchored thoughts, extension data marked non-authoritative, custom enum companion labels, and typed affect records for bonds/status reads/doctrine stances. `privateNotes` remain a deliberate v0 simplification, not an authority surface. `epiphany-agent-memory-store status` and `epiphany-character-loop` now surface profile classification on the wire as `organStateProfile`.
- The schema doctrine now makes the profile split explicit: resident Epiphany organs are `work_organ` state with lean role memory and growth via rumination/distillation, while Face is the Epiphany organ that may use portable `persona` state shared with VoidBot repo Faces and Ghostlight characters. Work organs do not inherit Persona affect, social bonds, or status-read machinery. PersonaState now has explicit provenance and required presentation surfaces, `date-time` timestamps, typed `candidateActions` with action type/target/readiness/risk/urgency/confidence/evidence/expiry, `voidbotProjection.candidateInterventions` only as a VoidBot compatibility projection, strict anchored thoughts, extension data marked non-authoritative, custom enum companion labels, and typed affect records for bonds/status reads/doctrine stances. `privateNotes` remain a deliberate v0 simplification, not an authority surface. `epiphany-agent-memory-store status` and `epiphany-character-loop` now surface profile classification on the wire as `organStateProfile`; `epiphany-agent-memory-store project-persona --store .\state\agents.msgpack --role-id face` projects the local Face record into `gamecult.persona_state.v0` for interop.
- Face packets now carry deterministic Ghostlight-style `projectionSeed`, `appraisalSeed`, and `reactionSeed` surfaces rebuilt from local organ traits, relationship memories, perceived overlays, and visible stimulus so the public mouth can use actual projection/appraisal machinery instead of decorative prompt text. Also record this now before some future fool hard-codes singularity: multiple Persona actors are part of the plan, even though the current MVP still routes through one `face` role.
- Repo personality initialization now has a terrain-reduced plan and native birth surface. `epiphany-repo-personality scout/project/agent-packet/status` scouts local git repos, scores body taxonomy/history temperament/memory doctrine, writes typed CultCache terrain/profile/role-projection MessagePack stores, emits JSON/Markdown inspection exports, and renders a birth-only Repo Personality Distiller specialist prompt packet. `epiphany-repo-personality startup` now checks for accepted personality/memory initialization records, launches packet generation only when absent, and `accept-init` can route reviewed role `selfPatch` candidates into initial role memory, stamp the newborn Ghostlight trait lattice from deterministic role projections, and seed heartbeat physiology from the same typed role personality projections. The birth specialists are startup-only initialization actuators, not heartbeat lanes; heartbeat receives only the accepted physiology seed. `epiphany-repo-birth-runner` owns the startup-only execution path through `epiphany-model-runtime` typed model-request documents plus runtime-spine receipts and reviewable `result.json`; the old Codex CLI executor path is cut so birth specialists cannot receive Codex prompt wrappers. The remaining seam is Aquarium review/action surfacing. After accepted birth, personality drift belongs to heartbeat, mood, rumination, sleep consolidation, lived evidence, and reviewed `selfPatch`, not repeated startup distillation.
- Repo trajectory initialization is now a separate birth-only organ. `epiphany-repo-personality` derives a typed `repo_trajectory_report` from early-history, recent-history, doctrine/content excerpts, and deterministic theme scoring, and `trajectory-packet` renders a startup-only Repo Trajectory Distiller prompt/packet so Self can review historical direction, self-image, implicit goals, and anti-goals before the newborn wakes.

View file

@ -19,6 +19,12 @@ organ. The portable light organ contract is
[`epiphany.work_organ_state.v0`](./cultnet/epiphany.work_organ_state.v0.schema.json).
The portable person-state contract is
[`gamecult.persona_state.v0`](./cultnet/gamecult.persona_state.v0.schema.json).
Epiphany Face projects its local organ-state record into the portable Persona
contract with:
```powershell
cargo run --manifest-path .\epiphany-core\Cargo.toml --bin epiphany-agent-memory-store -- project-persona --store .\state\agents.msgpack --role-id face
```
## Objective

View file

@ -143,8 +143,16 @@ The distinction is visible to tools through `organStateProfile`:
Current typed surfaces expose profile classification through:
- `epiphany-agent-memory-store status`
- `epiphany-agent-memory-store project-persona --role-id face`
- `epiphany-character-loop` packets
- `epiphany.agent_utterance_state.v0`
If a surface consumes local organ state without surfacing the profile kind, it
is already drifting back toward folklore.
`project-persona` is the current Epiphany Face bridge into
`gamecult.persona_state.v0`: it reads the local Face organ-state record from
`state/agents.msgpack` and emits the portable Persona document with provenance,
presentation, activation profile, memories, agency pressure, typed candidate
actions, typed affect projections, and the VoidBot projection slot. The local
store remains Epiphany-owned; the Persona document is the interchange nerve.

Binary file not shown.

View file

@ -168,7 +168,7 @@ accepted_design:
- Epiphany's public pitch is project-native agency, not just better prompting. Aquarium is the clearest client, but Discord roles, voice/WebRTC rooms, stream overlays, native CLIs, and other CultNet tools should all speak to the same typed project/Faces substrate. Faces advocate for repo needs, answer from jurisdiction, and can propose work; the scheduler and coordinator decide what thinks next, while review gates decide what may mutate canonical state.
- Epiphany publishes its Aquarium-facing CultNet receipts locally under `schemas/cultnet/`, and `epiphany-runtime-spine schema-catalog --include-schema-json true` emits a merged builtin-plus-local schema catalog for consumers. The local receipts cover runtime-spine documents, durable agent/heartbeat/ledger state, `epiphany.surface.*` operator projections, control intents, and receipt/artifact payloads. Vendored `cultnet-rs` is aligned to the canonical C# CultNet raw record shapes: replication uses `schemaId` plus `recordKey`, snapshot filters use `schemaIds` / `recordKeys`, and hello advertises typed mutation contracts.
- Epiphany now owns the canonical schema paperwork in `schemas/`: a local mirror of `ghostlight.agent-state.schema.json`, plus human-facing receipts for canonical agent-state shape, standing role-trait glossary, heartbeat store shape, Persona state, WorkOrgan state, and repo-personality birth projection. Trait names like `routing_discipline` are now documented at the source instead of existing only in live organ-state records, startup templates, or stale Ghostlight notes.
- Organ-state doctrine now distinguishes `work_organ` from `persona`. Most Epiphany sub-agents want lean identity, memory, goals, values, private-note growth, and heartbeat activation because effectiveness matters more than theatrical density. Epiphany Face, VoidBot repo Faces, and Ghostlight characters want portable Persona state: denser personality, provenance, public presentation, generic candidate actions, affect, relationship pressure, perceived overlays, and appraisal-shaped fallibility. `anchoredThought.extensions` is preservation-only sludge quarantine, not portable authority. The classification is now queryable through native agent-memory status, character-loop packets, and `epiphany.agent_utterance_state.v0` as `organStateProfile`.
- Organ-state doctrine now distinguishes `work_organ` from `persona`. Most Epiphany sub-agents want lean identity, memory, goals, values, private-note growth, and heartbeat activation because effectiveness matters more than theatrical density. Epiphany Face, VoidBot repo Faces, and Ghostlight characters want portable Persona state: denser personality, provenance, public presentation, generic candidate actions, affect, relationship pressure, perceived overlays, and appraisal-shaped fallibility. `anchoredThought.extensions` is preservation-only sludge quarantine, not portable authority. The classification is now queryable through native agent-memory status, character-loop packets, and `epiphany.agent_utterance_state.v0` as `organStateProfile`; `epiphany-agent-memory-store project-persona --role-id face` projects the local Face record into `gamecult.persona_state.v0`.
- Face packets now expose deterministic Ghostlight-style projection, appraisal, and reaction seeds built from local canonical traits, relationship pressure, perceived overlays, and visible stimulus. They also carry `epiphany.agent_utterance_state.v0`: a memory-free speech-conditioning document derived from the organ-state record plus heartbeat physiology, including a 32-axis current emotional-state projection and the fixed 64-float `weksa.utterance_embedding_handoff.v0.1` character-state vector for AquaSynth/Weksa utterance embedding. This makes embodied response machinery available to the public mouth without granting omniscience, leaking memories into speech embeddings, or silently mutating state. Multiple Face actors remain part of the long-plan; the current singular `face` role is an MVP simplification, not the intended endpoint.
- Repo personality initialization is now specified as a map-reduce projection, not a hand-written vibe pass: read-only scouts score body taxonomy, history temperament, and memory doctrine across nearby repos, reduce them into normalized variance criteria, and emit reviewable `repoTerrainReport`, `repoPersonalityProfile`, and `rolePersonalityProjection` candidates before touching agent memory.
- Native `epiphany-repo-personality` is the first startup-personality scout and birth-only distiller packet renderer: it deterministically inventories local git repos, scores repo terrain/history/state surfaces, writes typed CultCache terrain/profile/role-projection MessagePack stores, emits JSON/Markdown inspection glass, and renders a Repo Personality Distiller specialist prompt packet for Self-reviewed first role-memory petitions. The distiller must run only when a repo/swarm has no accepted personality initialization; after that, personality drift belongs to heartbeat, mood, rumination, sleep consolidation, lived evidence, and reviewed `selfPatch`. Startup routing now checks accepted initialization records, generates missing startup-only packets only once, applies reviewed birth memory and heartbeat physiology seeds, and leaves later drift alone. Birth specialists are not heartbeat lanes; heartbeat receives only accepted seed pressure.