Add reversible gestalt presence planning
This commit is contained in:
parent
2b825cacb9
commit
c1f2e0d2a4
8 changed files with 369 additions and 88 deletions
|
|
@ -118,6 +118,28 @@ pub struct GestaltAggregateDelta {
|
|||
pub pressures: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||
pub struct GestaltPromotion {
|
||||
pub gestalt_id: String,
|
||||
pub expected_gestalt_version: u64,
|
||||
pub member_id: String,
|
||||
pub expected_member_version: u64,
|
||||
pub location_id: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||
pub struct GestaltDemotion {
|
||||
pub actor_id: String,
|
||||
#[serde(default)]
|
||||
pub aggregate_delta: GestaltAggregateDelta,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Default)]
|
||||
pub struct GestaltPresencePlan {
|
||||
pub promotions: Vec<GestaltPromotion>,
|
||||
pub demotions: Vec<GestaltDemotion>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Default)]
|
||||
pub struct ActorStateDelta {
|
||||
pub memories_add: Vec<String>,
|
||||
|
|
@ -277,6 +299,11 @@ pub enum WorldCommand {
|
|||
actor_id: String,
|
||||
aggregate_delta: GestaltAggregateDelta,
|
||||
},
|
||||
ReconcileGestaltPresence {
|
||||
expected_revision: u64,
|
||||
reason: String,
|
||||
plan: GestaltPresencePlan,
|
||||
},
|
||||
ResolveReactionWave {
|
||||
expected_revision: u64,
|
||||
event_summary: String,
|
||||
|
|
|
|||
101
crates/ghostlight-dungeon/src/gestalt.rs
Normal file
101
crates/ghostlight-dungeon/src/gestalt.rs
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
use crate::{
|
||||
domain::{Campaign, GestaltPresencePlan},
|
||||
model::{ModelPort, ModelStageReceipt, ModelStageRequest, run_validated_stage},
|
||||
};
|
||||
use anyhow::{Result, anyhow};
|
||||
use schemars::schema_for;
|
||||
use std::{collections::BTreeSet, sync::Arc};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct GestaltPresencePlanner {
|
||||
pub model: Arc<dyn ModelPort>,
|
||||
pub model_name: String,
|
||||
}
|
||||
|
||||
impl GestaltPresencePlanner {
|
||||
pub async fn plan(
|
||||
&self,
|
||||
campaign: &Campaign,
|
||||
event_summary: &str,
|
||||
) -> Result<(GestaltPresencePlan, ModelStageReceipt)> {
|
||||
let player_location = &campaign.actors[&campaign.player_actor_id].location_id;
|
||||
let candidates = serde_json::json!({
|
||||
"player_location_id": player_location,
|
||||
"gestalts": campaign.gestalts,
|
||||
"members": campaign.gestalt_members,
|
||||
"materialized_member_actor_ids": campaign.gestalt_members.values()
|
||||
.filter_map(|member| member.materialized_actor_id.clone()).collect::<Vec<_>>(),
|
||||
});
|
||||
let schema = serde_json::to_value(schema_for!(GestaltPresencePlan))?;
|
||||
let output = run_validated_stage(
|
||||
self.model.as_ref(),
|
||||
&ModelStageRequest {
|
||||
stage: "gestalt_presence_planner".into(),
|
||||
model: self.model_name.clone(),
|
||||
snapshot_binding: format!("campaign:{}:revision:{}", campaign.id, campaign.revision),
|
||||
lived_stream: format!(
|
||||
"Choose reversible Persona population presence after this event. Promote a member only when individualized interaction, perception, custody, promise, injury, possession, or relationship now matters. Demote a materialized member when they are no longer scene-relevant. Use only the supplied IDs and exact versions. Never place a promoted member outside the player location. Aggregate deltas must remain empty; population learning requires separate review. Emit the exact JSON schema.\nSCHEMA:\n{}\nCANDIDATES:\n{}\nEVENT:\n{}",
|
||||
serde_json::to_string_pretty(&schema)?, candidates, event_summary
|
||||
),
|
||||
output_schema: Some(schema),
|
||||
source_receipt_ids: campaign.branch_origin.evidence_receipt_ids.clone(),
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
let plan: GestaltPresencePlan = serde_json::from_value(
|
||||
output
|
||||
.structured
|
||||
.ok_or_else(|| anyhow!("presence planner produced no plan"))?,
|
||||
)?;
|
||||
validate_plan(campaign, &plan, player_location)?;
|
||||
Ok((plan, output.receipt))
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_plan(
|
||||
campaign: &Campaign,
|
||||
plan: &GestaltPresencePlan,
|
||||
player_location: &str,
|
||||
) -> Result<()> {
|
||||
let mut members = BTreeSet::new();
|
||||
for promotion in &plan.promotions {
|
||||
if !members.insert(promotion.member_id.clone()) {
|
||||
return Err(anyhow!("presence plan promotes one member twice"));
|
||||
}
|
||||
let member = campaign
|
||||
.gestalt_members
|
||||
.get(&promotion.member_id)
|
||||
.ok_or_else(|| anyhow!("presence plan invented a member"))?;
|
||||
let gestalt = campaign
|
||||
.gestalts
|
||||
.get(&promotion.gestalt_id)
|
||||
.ok_or_else(|| anyhow!("presence plan invented a gestalt"))?;
|
||||
if member.gestalt_id != promotion.gestalt_id
|
||||
|| member.version != promotion.expected_member_version
|
||||
|| gestalt.version != promotion.expected_gestalt_version
|
||||
|| member.materialized_actor_id.is_some()
|
||||
|| promotion.location_id != player_location
|
||||
{
|
||||
return Err(anyhow!("presence promotion does not match its snapshot"));
|
||||
}
|
||||
}
|
||||
let materialized: BTreeSet<_> = campaign
|
||||
.gestalt_members
|
||||
.values()
|
||||
.filter_map(|member| member.materialized_actor_id.clone())
|
||||
.collect();
|
||||
let mut actors = BTreeSet::new();
|
||||
for demotion in &plan.demotions {
|
||||
if !actors.insert(demotion.actor_id.clone()) || !materialized.contains(&demotion.actor_id) {
|
||||
return Err(anyhow!(
|
||||
"presence plan demotes an unknown or duplicate member"
|
||||
));
|
||||
}
|
||||
if demotion.aggregate_delta != Default::default() {
|
||||
return Err(anyhow!(
|
||||
"automatic presence planning cannot rewrite gestalt knowledge"
|
||||
));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -313,44 +313,16 @@ fn execute(
|
|||
location_id,
|
||||
} => {
|
||||
require_revision(&campaign, expected_revision)?;
|
||||
if !campaign.locations.contains_key(&location_id) {
|
||||
return Err(KernelError::Invalid(
|
||||
"materialization location is unknown".into(),
|
||||
));
|
||||
}
|
||||
let gestalt = campaign
|
||||
.gestalts
|
||||
.get(&gestalt_id)
|
||||
.ok_or_else(|| KernelError::Invalid("gestalt is unknown".into()))?
|
||||
.clone();
|
||||
if gestalt.version != expected_gestalt_version {
|
||||
return Err(KernelError::Invalid("gestalt snapshot is stale".into()));
|
||||
}
|
||||
let member = campaign
|
||||
.gestalt_members
|
||||
.get_mut(&member_id)
|
||||
.ok_or_else(|| KernelError::Invalid("gestalt member is unknown".into()))?;
|
||||
if member.gestalt_id != gestalt_id || member.version != expected_member_version {
|
||||
return Err(KernelError::Invalid(
|
||||
"member snapshot is stale or belongs to another gestalt".into(),
|
||||
));
|
||||
}
|
||||
if member.materialized_actor_id.is_some() {
|
||||
return Err(KernelError::Invalid(
|
||||
"gestalt member is already materialized".into(),
|
||||
));
|
||||
}
|
||||
let actor_id = format!("member:{}", member.id);
|
||||
if campaign.actors.contains_key(&actor_id) {
|
||||
return Err(KernelError::Invalid(
|
||||
"materialized actor id collides".into(),
|
||||
));
|
||||
}
|
||||
let actor = materialize_actor(&gestalt, member, &actor_id, &location_id);
|
||||
member.materialized_actor_id = Some(actor_id.clone());
|
||||
member.last_location_id = Some(location_id);
|
||||
member.version += 1;
|
||||
campaign.actors.insert(actor_id, actor);
|
||||
apply_promotion(
|
||||
&mut campaign,
|
||||
&GestaltPromotion {
|
||||
gestalt_id,
|
||||
expected_gestalt_version,
|
||||
member_id,
|
||||
expected_member_version,
|
||||
location_id,
|
||||
},
|
||||
)?;
|
||||
commit(store, row, campaign, "materialize_gestalt_member", None)
|
||||
}
|
||||
WorldCommand::DematerializeGestaltMember {
|
||||
|
|
@ -359,45 +331,33 @@ fn execute(
|
|||
aggregate_delta,
|
||||
} => {
|
||||
require_revision(&campaign, expected_revision)?;
|
||||
let actor = campaign
|
||||
.actors
|
||||
.get(&actor_id)
|
||||
.ok_or_else(|| KernelError::Invalid("materialized actor is unknown".into()))?
|
||||
.clone();
|
||||
let member_id = campaign
|
||||
.gestalt_members
|
||||
.values()
|
||||
.find(|member| member.materialized_actor_id.as_deref() == Some(actor_id.as_str()))
|
||||
.map(|member| member.id.clone())
|
||||
.ok_or_else(|| {
|
||||
KernelError::Invalid("actor is not a materialized gestalt member".into())
|
||||
})?;
|
||||
let gestalt_id = campaign.gestalt_members[&member_id].gestalt_id.clone();
|
||||
let gestalt = campaign
|
||||
.gestalts
|
||||
.get_mut(&gestalt_id)
|
||||
.ok_or_else(|| KernelError::Invalid("gestalt is missing".into()))?;
|
||||
let member = campaign
|
||||
.gestalt_members
|
||||
.get_mut(&member_id)
|
||||
.expect("member exists");
|
||||
fold_actor_delta(&actor, gestalt, member);
|
||||
member.materialized_actor_id = None;
|
||||
member.version += 1;
|
||||
if !aggregate_delta.knowledge_additions.is_empty()
|
||||
|| !aggregate_delta.resource_additions.is_empty()
|
||||
|| !aggregate_delta.pressures.is_empty()
|
||||
{
|
||||
gestalt
|
||||
.shared_knowledge
|
||||
.extend(aggregate_delta.knowledge_additions);
|
||||
gestalt.resources.extend(aggregate_delta.resource_additions);
|
||||
gestalt.pressures.extend(aggregate_delta.pressures);
|
||||
gestalt.version += 1;
|
||||
}
|
||||
campaign.actors.remove(&actor_id);
|
||||
apply_demotion(
|
||||
&mut campaign,
|
||||
&GestaltDemotion {
|
||||
actor_id,
|
||||
aggregate_delta,
|
||||
},
|
||||
)?;
|
||||
commit(store, row, campaign, "dematerialize_gestalt_member", None)
|
||||
}
|
||||
WorldCommand::ReconcileGestaltPresence {
|
||||
expected_revision,
|
||||
reason,
|
||||
plan,
|
||||
} => {
|
||||
require_revision(&campaign, expected_revision)?;
|
||||
if reason.trim().is_empty() {
|
||||
return Err(KernelError::Invalid("presence plan has no reason".into()));
|
||||
}
|
||||
let mut candidate = campaign.clone();
|
||||
for demotion in &plan.demotions {
|
||||
apply_demotion(&mut candidate, demotion)?;
|
||||
}
|
||||
for promotion in &plan.promotions {
|
||||
apply_promotion(&mut candidate, promotion)?;
|
||||
}
|
||||
commit(store, row, candidate, "reconcile_gestalt_presence", None)
|
||||
}
|
||||
WorldCommand::ResolveReactionWave {
|
||||
expected_revision,
|
||||
event_summary,
|
||||
|
|
@ -469,6 +429,96 @@ fn execute(
|
|||
}
|
||||
}
|
||||
|
||||
fn apply_promotion(
|
||||
campaign: &mut Campaign,
|
||||
promotion: &GestaltPromotion,
|
||||
) -> Result<(), KernelError> {
|
||||
if !campaign.locations.contains_key(&promotion.location_id) {
|
||||
return Err(KernelError::Invalid(
|
||||
"materialization location is unknown".into(),
|
||||
));
|
||||
}
|
||||
let gestalt = campaign
|
||||
.gestalts
|
||||
.get(&promotion.gestalt_id)
|
||||
.ok_or_else(|| KernelError::Invalid("gestalt is unknown".into()))?
|
||||
.clone();
|
||||
if gestalt.version != promotion.expected_gestalt_version {
|
||||
return Err(KernelError::Invalid("gestalt snapshot is stale".into()));
|
||||
}
|
||||
let member = campaign
|
||||
.gestalt_members
|
||||
.get_mut(&promotion.member_id)
|
||||
.ok_or_else(|| KernelError::Invalid("gestalt member is unknown".into()))?;
|
||||
if member.gestalt_id != promotion.gestalt_id
|
||||
|| member.version != promotion.expected_member_version
|
||||
{
|
||||
return Err(KernelError::Invalid(
|
||||
"member snapshot is stale or belongs to another gestalt".into(),
|
||||
));
|
||||
}
|
||||
if member.materialized_actor_id.is_some() {
|
||||
return Err(KernelError::Invalid(
|
||||
"gestalt member is already materialized".into(),
|
||||
));
|
||||
}
|
||||
let actor_id = format!("member:{}", member.id);
|
||||
if campaign.actors.contains_key(&actor_id) {
|
||||
return Err(KernelError::Invalid(
|
||||
"materialized actor id collides".into(),
|
||||
));
|
||||
}
|
||||
let actor = materialize_actor(&gestalt, member, &actor_id, &promotion.location_id);
|
||||
member.materialized_actor_id = Some(actor_id.clone());
|
||||
member.last_location_id = Some(promotion.location_id.clone());
|
||||
member.version += 1;
|
||||
campaign.actors.insert(actor_id, actor);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn apply_demotion(campaign: &mut Campaign, demotion: &GestaltDemotion) -> Result<(), KernelError> {
|
||||
let actor = campaign
|
||||
.actors
|
||||
.get(&demotion.actor_id)
|
||||
.ok_or_else(|| KernelError::Invalid("materialized actor is unknown".into()))?
|
||||
.clone();
|
||||
let member_id = campaign
|
||||
.gestalt_members
|
||||
.values()
|
||||
.find(|member| member.materialized_actor_id.as_deref() == Some(demotion.actor_id.as_str()))
|
||||
.map(|member| member.id.clone())
|
||||
.ok_or_else(|| KernelError::Invalid("actor is not a materialized gestalt member".into()))?;
|
||||
let gestalt_id = campaign.gestalt_members[&member_id].gestalt_id.clone();
|
||||
let gestalt = campaign
|
||||
.gestalts
|
||||
.get_mut(&gestalt_id)
|
||||
.ok_or_else(|| KernelError::Invalid("gestalt is missing".into()))?;
|
||||
let member = campaign
|
||||
.gestalt_members
|
||||
.get_mut(&member_id)
|
||||
.expect("member exists");
|
||||
fold_actor_delta(&actor, gestalt, member);
|
||||
member.materialized_actor_id = None;
|
||||
member.version += 1;
|
||||
if !demotion.aggregate_delta.knowledge_additions.is_empty()
|
||||
|| !demotion.aggregate_delta.resource_additions.is_empty()
|
||||
|| !demotion.aggregate_delta.pressures.is_empty()
|
||||
{
|
||||
gestalt
|
||||
.shared_knowledge
|
||||
.extend(demotion.aggregate_delta.knowledge_additions.clone());
|
||||
gestalt
|
||||
.resources
|
||||
.extend(demotion.aggregate_delta.resource_additions.clone());
|
||||
gestalt
|
||||
.pressures
|
||||
.extend(demotion.aggregate_delta.pressures.clone());
|
||||
gestalt.version += 1;
|
||||
}
|
||||
campaign.actors.remove(&demotion.actor_id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_world_proposal(
|
||||
actor: &ActorState,
|
||||
proposal: &WorldActionProposal,
|
||||
|
|
@ -962,7 +1012,7 @@ mod tests {
|
|||
async fn gestalt_member_dematerializes_to_delta_and_returns_as_same_person() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let store = CampaignStore::open(dir.path().join("campaign.cc")).unwrap();
|
||||
let kernel = WorldKernel::start(store);
|
||||
let kernel = WorldKernel::start(store.clone());
|
||||
let mut seed = campaign();
|
||||
seed.gestalts.insert(
|
||||
"village".into(),
|
||||
|
|
@ -1079,6 +1129,43 @@ mod tests {
|
|||
.shared_knowledge
|
||||
.contains("the player keeps promises")
|
||||
);
|
||||
|
||||
let bad_plan = GestaltPresencePlan {
|
||||
demotions: vec![GestaltDemotion {
|
||||
actor_id: "member:john".into(),
|
||||
aggregate_delta: GestaltAggregateDelta::default(),
|
||||
}],
|
||||
promotions: vec![GestaltPromotion {
|
||||
gestalt_id: "village".into(),
|
||||
expected_gestalt_version: 1,
|
||||
member_id: "invented-person".into(),
|
||||
expected_member_version: 0,
|
||||
location_id: "room".into(),
|
||||
}],
|
||||
};
|
||||
assert!(
|
||||
kernel
|
||||
.command(WorldCommand::ReconcileGestaltPresence {
|
||||
expected_revision: 3,
|
||||
reason: "scene relevance changed".into(),
|
||||
plan: bad_plan,
|
||||
})
|
||||
.await
|
||||
.is_err()
|
||||
);
|
||||
let persisted = store
|
||||
.load::<Campaign>("campaign.v1", &again.id.to_string())
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.1;
|
||||
assert_eq!(persisted.revision, 3);
|
||||
assert!(persisted.actors.contains_key("member:john"));
|
||||
assert_eq!(
|
||||
persisted.gestalt_members["john"]
|
||||
.materialized_actor_id
|
||||
.as_deref(),
|
||||
Some("member:john")
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ pub mod assessor;
|
|||
pub mod compiler;
|
||||
pub mod d20;
|
||||
pub mod domain;
|
||||
pub mod gestalt;
|
||||
pub mod kernel;
|
||||
pub mod model;
|
||||
pub mod persistence;
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ use ghostlight_dungeon::{
|
|||
assessor::ActionAssessor,
|
||||
compiler::{CustomStart, OpeningRequest, OpeningSuggestion, SelectedStart, WorldCompiler},
|
||||
domain::{Campaign, RegionExpansionPreview, WorldCommand, WorldCompilePreview},
|
||||
gestalt::GestaltPresencePlanner,
|
||||
model::{DeepSeekPort, ModelPort, ModelStageRequest, run_validated_stage},
|
||||
persistence::CampaignStore,
|
||||
persona::PersonaProjectionEngine,
|
||||
|
|
@ -537,25 +538,68 @@ async fn command(
|
|||
if let ghostlight_dungeon::kernel::CommandResult::Committed { campaign, .. } =
|
||||
&result
|
||||
{
|
||||
if campaign.actors.len() > 1 {
|
||||
if let Some(model) = &state.model {
|
||||
let summary = campaign
|
||||
.transcript
|
||||
.last()
|
||||
.map(|turn| turn.text.clone())
|
||||
.unwrap_or_else(|| "A consequential event occurred.".into());
|
||||
if let Some(model) = &state.model {
|
||||
let summary = campaign
|
||||
.transcript
|
||||
.last()
|
||||
.map(|turn| turn.text.clone())
|
||||
.unwrap_or_else(|| "A consequential event occurred.".into());
|
||||
let mut reaction_campaign = campaign.clone();
|
||||
let mut presence_result = None;
|
||||
if !campaign.gestalts.is_empty() {
|
||||
let planner = GestaltPresencePlanner {
|
||||
model: model.clone(),
|
||||
model_name: "deepseek-v4-flash".into(),
|
||||
};
|
||||
match planner.plan(campaign, &summary).await {
|
||||
Ok((plan, receipt)) => {
|
||||
let _ = state.store.insert(
|
||||
"persona_stage_receipt.v1",
|
||||
"ghostlight.persona_stage_receipt.v1",
|
||||
&receipt.output_hash,
|
||||
&receipt,
|
||||
);
|
||||
if !plan.promotions.is_empty() || !plan.demotions.is_empty() {
|
||||
match state.kernel.command(WorldCommand::ReconcileGestaltPresence {
|
||||
expected_revision: campaign.revision,
|
||||
reason: summary.clone(),
|
||||
plan,
|
||||
}).await {
|
||||
Ok(committed @ ghostlight_dungeon::kernel::CommandResult::Committed { .. }) => {
|
||||
if let ghostlight_dungeon::kernel::CommandResult::Committed { campaign, .. } = &committed {
|
||||
reaction_campaign = campaign.clone();
|
||||
}
|
||||
presence_result = Some(committed);
|
||||
}
|
||||
Ok(_) => unreachable!(),
|
||||
Err(error) => return (StatusCode::CONFLICT, Json(ErrorBody { error: error.to_string() })).into_response(),
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
return (
|
||||
StatusCode::BAD_GATEWAY,
|
||||
Json(ErrorBody {
|
||||
error: error.to_string(),
|
||||
}),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
}
|
||||
}
|
||||
if reaction_campaign.actors.len() > 1 {
|
||||
let engine = PersonaProjectionEngine {
|
||||
model: model.clone(),
|
||||
permit: Arc::new(SnapshotPermit::new(
|
||||
state.store.clone(),
|
||||
campaign.id,
|
||||
campaign.revision,
|
||||
reaction_campaign.id,
|
||||
reaction_campaign.revision,
|
||||
)),
|
||||
projector_model: "deepseek-v4-flash".into(),
|
||||
persona_model: "deepseek-v4-pro".into(),
|
||||
interpreter_model: "deepseek-v4-flash".into(),
|
||||
};
|
||||
match appraise_present(engine, campaign, &summary).await {
|
||||
match appraise_present(engine, &reaction_campaign, &summary).await {
|
||||
Ok(wave) if !wave.reactions.is_empty() => {
|
||||
for receipt in wave.receipts {
|
||||
let _ = state.store.insert(
|
||||
|
|
@ -565,7 +609,7 @@ async fn command(
|
|||
&receipt,
|
||||
);
|
||||
}
|
||||
match state.kernel.command(WorldCommand::ResolveReactionWave{expected_revision:campaign.revision,event_summary:summary,reactions:wave.reactions}).await{Ok(reaction)=>return Json(serde_json::json!({"primary":result,"reaction_wave":reaction})).into_response(),Err(error)=>return(StatusCode::CONFLICT,Json(ErrorBody{error:error.to_string()})).into_response()}
|
||||
match state.kernel.command(WorldCommand::ResolveReactionWave{expected_revision:reaction_campaign.revision,event_summary:summary,reactions:wave.reactions}).await{Ok(reaction)=>return Json(serde_json::json!({"primary":result,"presence":presence_result,"reaction_wave":reaction})).into_response(),Err(error)=>return(StatusCode::CONFLICT,Json(ErrorBody{error:error.to_string()})).into_response()}
|
||||
}
|
||||
Ok(_) => {}
|
||||
Err(error) => {
|
||||
|
|
@ -579,6 +623,12 @@ async fn command(
|
|||
}
|
||||
}
|
||||
}
|
||||
if presence_result.is_some() {
|
||||
return Json(
|
||||
serde_json::json!({"primary":result,"presence":presence_result}),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -198,6 +198,15 @@ only explicitly reviewed population-level learning may update the gestalt.
|
|||
This allows a corporation or village to take one strategic turn while John the
|
||||
blacksmith remains John when encountered again.
|
||||
|
||||
After each committed player event, a cheap structured relevance stage receives
|
||||
only the current gestalt/member catalog, materialized member IDs, player
|
||||
location, and event summary. It proposes one `GestaltPresencePlan`. The kernel
|
||||
validates exact gestalt/member versions, known IDs, current materialization
|
||||
state, and scene location, then applies the entire demotion/promotion set as one
|
||||
atomic `ReconcileGestaltPresence` command before the participant appraisal
|
||||
wave. Automatic plans cannot write aggregate gestalt learning. A malformed,
|
||||
invented, stale, or partially invalid plan changes nothing.
|
||||
|
||||
### The ownership decision
|
||||
|
||||
Ghostlight owns the reusable Persona projection machinery:
|
||||
|
|
|
|||
|
|
@ -65,6 +65,12 @@ revisioned WorldCommand path. A named member's relationships, memories,
|
|||
possessions, injuries, promises, and identity never become disposable gestalt
|
||||
texture.
|
||||
|
||||
Live turns run a structured gestalt-relevance planner after the player event
|
||||
and before NPC appraisal. Its atomic `ReconcileGestaltPresence` command can
|
||||
materialize newly relevant members and fold irrelevant members back into their
|
||||
persistent deltas. The planner may propose presence only; it cannot promote
|
||||
population-wide learning or mutate state directly.
|
||||
|
||||
Ghostlight owns its own generalized projection machinery:
|
||||
|
||||
1. Projector converts the actor’s permitted typed slice, memories, perceived events, relationships, goals, and retrieved knowledge into a private lived narrative stream.
|
||||
|
|
|
|||
|
|
@ -255,5 +255,5 @@ data_flow:
|
|||
|
||||
current_status:
|
||||
summary: GhostlightDungeon is the active MVP program. The approval-gated world compiler now persists evidence, canon candidates, and bounded on-demand region expansion through the WorldKernel. Fiction-first assessments use model proposals with deterministic DC and state-reference validation. Away-time ticks move institutions and gate news by information access. Present NPCs appraise live events in parallel Persona waves, and the kernel atomically validates their private deltas and world-action proposals. Gestalt populations can materialize durable individuals and fold them back into persistent member deltas. Live DeepSeek plus VoidBot compilation and disposable authenticated HTTP restart smokes have passed. The broader hosted MVP remains incomplete.
|
||||
next_action: Complete initiative selection and world resolution for validated NPC proposals, narrator projection, automatic relevance-driven gestalt presence planning, and the campaign/operator laboratory controls.
|
||||
next_action: Complete initiative selection and world resolution for validated NPC proposals, narrator projection, and the campaign/operator laboratory controls; then acceptance-test automatic relevance-driven gestalt promotion and folding with live model output.
|
||||
gestalt_population: Gestalt Personas own shared population baselines; persistent member deltas own named individuality; WorldKernel materialization commands compose and demote active actors without deleting member identity.
|
||||
|
|
|
|||
Loading…
Reference in a new issue