feat(pncc): coordinate safe organ lifecycles
GuangHu-Human-Responsibility: ICE-GL∞ / 冰朔 GuangHu-Persona-Cognitive-Author: ICE-P-ZY001 / 铸渊 GuangHu-Execution-Runtime: Codex macOS / DEV-20260810-014 GuangHu-Development-ID: DEV-20260810-014 GuangHu-Authorization-Scope: GH-PNCC local runtime development and registered REPO-014 publication GuangHu-Source-Language-Anchor: continue PNCC persona runtime; UI and execution limb remain deferred
This commit is contained in:
parent
18944f5362
commit
8f35834a7a
11 changed files with 402 additions and 39 deletions
|
|
@ -528,6 +528,7 @@ macro_rules! app_invoke_handler {
|
|||
persona_code_channel::query_persona_code_channel_runtime,
|
||||
persona_code_channel::run_persona_code_channel_fact_task,
|
||||
persona_code_channel::run_persona_code_channel_memory_metabolism,
|
||||
persona_code_channel::run_persona_code_channel_lifecycle,
|
||||
persona_code_channel::inspect_persona_code_channel_session,
|
||||
persona_code_channel::recover_persona_code_channel_session,
|
||||
guanghu_router::guanghu_router_connect,
|
||||
|
|
|
|||
|
|
@ -282,6 +282,44 @@ pub struct PersonaMemoryMetabolismReceipt {
|
|||
pub attribution: PersonaAttribution,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(tag = "kind", rename_all = "SCREAMING_SNAKE_CASE")]
|
||||
pub enum PersonaLifecycleOperationInput {
|
||||
FactSense {
|
||||
question: String,
|
||||
provider: crate::ai_models::AiModelProvider,
|
||||
api_key_override: Option<String>,
|
||||
},
|
||||
MemoryMetabolism {
|
||||
source_session_id: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PersonaLifecycleRunInput {
|
||||
pub wake: PersonaWakeInput,
|
||||
pub operation: PersonaLifecycleOperationInput,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(tag = "kind", content = "receipt", rename_all = "SCREAMING_SNAKE_CASE")]
|
||||
pub enum PersonaLifecycleCompletionReceipt {
|
||||
FactSense(PersonaFactTaskReceipt),
|
||||
MemoryMetabolism(PersonaMemoryMetabolismReceipt),
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PersonaLifecycleRunReceipt {
|
||||
pub schema: &'static str,
|
||||
pub session_id: String,
|
||||
pub persona_id: String,
|
||||
pub repository_path: String,
|
||||
pub wake_receipt: PersonaWakeReceipt,
|
||||
pub completion: PersonaLifecycleCompletionReceipt,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PersonaSessionControlInput {
|
||||
|
|
@ -2330,6 +2368,138 @@ fn run_memory_metabolism_at(
|
|||
})
|
||||
}
|
||||
|
||||
fn validate_lifecycle_operation_before_wake(
|
||||
input: &PersonaLifecycleRunInput,
|
||||
) -> Result<(), String> {
|
||||
let inspection = inspect_manifest_at(PersonaManifestInspectionInput {
|
||||
repository_path: input.wake.repository_path.clone(),
|
||||
expected_persona_id: input.wake.expected_persona_id.clone(),
|
||||
expected_head: input.wake.expected_head.clone(),
|
||||
})?;
|
||||
let contract = inspection
|
||||
.organ_contracts
|
||||
.iter()
|
||||
.find(|contract| contract.organ_id == input.wake.organ_id)
|
||||
.ok_or_else(|| "ORGAN_NOT_DECLARED".to_string())?;
|
||||
let expected_kind = match &input.operation {
|
||||
PersonaLifecycleOperationInput::FactSense { provider, .. } => {
|
||||
let repository = Path::new(&input.wake.repository_path)
|
||||
.canonicalize()
|
||||
.map_err(|error| format!("PERSONA_REPOSITORY_UNAVAILABLE: {error}"))?;
|
||||
let manifest = load_manifest(&repository)?;
|
||||
let provider_base_url = provider.base_url.as_deref().unwrap_or("").trim();
|
||||
if provider.id != input.wake.model_provider_id
|
||||
|| manifest.model_binding.base_url != provider_base_url
|
||||
|| !provider
|
||||
.models
|
||||
.iter()
|
||||
.any(|model| model.id == input.wake.model_id)
|
||||
{
|
||||
return Err("PERSONA_MODEL_BINDING_MISMATCH".into());
|
||||
}
|
||||
PersonaOrganKind::FactSense
|
||||
}
|
||||
PersonaLifecycleOperationInput::MemoryMetabolism { .. } => {
|
||||
PersonaOrganKind::MemoryMetabolism
|
||||
}
|
||||
};
|
||||
if contract.kind != expected_kind {
|
||||
return Err("PERSONA_LIFECYCLE_OPERATION_ORGAN_MISMATCH".into());
|
||||
}
|
||||
if !contract.activatable || contract.kind == PersonaOrganKind::ExecutionLimb {
|
||||
return Err("ORGAN_NOT_ACTIVATABLE".into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_fact_task_with_bound_provider_at(
|
||||
runtime_root: &Path,
|
||||
input: PersonaFactTaskInput,
|
||||
timestamp: &str,
|
||||
) -> Result<PersonaFactTaskReceipt, String> {
|
||||
let provider = input.provider.clone();
|
||||
let model_id = input.model_id.clone();
|
||||
let api_key_override = input.api_key_override.clone();
|
||||
run_fact_task_at(
|
||||
runtime_root,
|
||||
input,
|
||||
timestamp,
|
||||
move |system_prompt, message| {
|
||||
let mut text = String::new();
|
||||
crate::ai_models::run_ai_model_stream(
|
||||
crate::ai_models::AiModelStreamRequest {
|
||||
provider,
|
||||
model_id,
|
||||
message,
|
||||
system_prompt: Some(system_prompt),
|
||||
vault_path: None,
|
||||
vault_paths: Vec::new(),
|
||||
api_key_override,
|
||||
event_name: None,
|
||||
},
|
||||
|event| {
|
||||
if let crate::ai_agents::AiAgentStreamEvent::TextDelta { text: delta } = event {
|
||||
text.push_str(&delta);
|
||||
}
|
||||
},
|
||||
)?;
|
||||
Ok(text)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn run_lifecycle_at<F>(
|
||||
runtime_root: &Path,
|
||||
input: PersonaLifecycleRunInput,
|
||||
session_id: &str,
|
||||
wake_timestamp: &str,
|
||||
operation_timestamp: &str,
|
||||
run_fact: F,
|
||||
) -> Result<PersonaLifecycleRunReceipt, String>
|
||||
where
|
||||
F: FnOnce(&Path, PersonaFactTaskInput, &str) -> Result<PersonaFactTaskReceipt, String>,
|
||||
{
|
||||
validate_lifecycle_operation_before_wake(&input)?;
|
||||
let model_id = input.wake.model_id.clone();
|
||||
let wake = input.wake;
|
||||
let wake_receipt = prepare_wake_at(runtime_root, wake, session_id, wake_timestamp)?;
|
||||
let completion = match input.operation {
|
||||
PersonaLifecycleOperationInput::FactSense {
|
||||
question,
|
||||
provider,
|
||||
api_key_override,
|
||||
} => PersonaLifecycleCompletionReceipt::FactSense(run_fact(
|
||||
runtime_root,
|
||||
PersonaFactTaskInput {
|
||||
session_id: wake_receipt.session_id.clone(),
|
||||
question,
|
||||
model_id,
|
||||
provider,
|
||||
api_key_override,
|
||||
},
|
||||
operation_timestamp,
|
||||
)?),
|
||||
PersonaLifecycleOperationInput::MemoryMetabolism { source_session_id } => {
|
||||
PersonaLifecycleCompletionReceipt::MemoryMetabolism(run_memory_metabolism_at(
|
||||
runtime_root,
|
||||
PersonaMemoryMetabolismInput {
|
||||
session_id: wake_receipt.session_id.clone(),
|
||||
source_session_id,
|
||||
},
|
||||
operation_timestamp,
|
||||
)?)
|
||||
}
|
||||
};
|
||||
Ok(PersonaLifecycleRunReceipt {
|
||||
schema: "hololake.pncc-lifecycle-run-receipt/v1",
|
||||
session_id: wake_receipt.session_id.clone(),
|
||||
persona_id: wake_receipt.persona_id.clone(),
|
||||
repository_path: wake_receipt.repository_path.clone(),
|
||||
wake_receipt,
|
||||
completion,
|
||||
})
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn prepare_persona_code_channel_wake(
|
||||
input: PersonaWakeInput,
|
||||
|
|
@ -2362,37 +2532,7 @@ pub async fn run_persona_code_channel_fact_task(
|
|||
let runtime_root = crate::app_config::preferred_app_config_path("pncc-runtime")?;
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let timestamp = Utc::now().to_rfc3339_opts(SecondsFormat::Millis, true);
|
||||
let provider = input.provider.clone();
|
||||
let model_id = input.model_id.clone();
|
||||
let api_key_override = input.api_key_override.clone();
|
||||
run_fact_task_at(
|
||||
&runtime_root,
|
||||
input,
|
||||
×tamp,
|
||||
move |system_prompt, message| {
|
||||
let mut text = String::new();
|
||||
crate::ai_models::run_ai_model_stream(
|
||||
crate::ai_models::AiModelStreamRequest {
|
||||
provider,
|
||||
model_id,
|
||||
message,
|
||||
system_prompt: Some(system_prompt),
|
||||
vault_path: None,
|
||||
vault_paths: Vec::new(),
|
||||
api_key_override,
|
||||
event_name: None,
|
||||
},
|
||||
|event| {
|
||||
if let crate::ai_agents::AiAgentStreamEvent::TextDelta { text: delta } =
|
||||
event
|
||||
{
|
||||
text.push_str(&delta);
|
||||
}
|
||||
},
|
||||
)?;
|
||||
Ok(text)
|
||||
},
|
||||
)
|
||||
run_fact_task_with_bound_provider_at(&runtime_root, input, ×tamp)
|
||||
})
|
||||
.await
|
||||
.map_err(|error| format!("PERSONA_FACT_TASK_JOIN_FAILED: {error}"))?
|
||||
|
|
@ -2411,6 +2551,28 @@ pub async fn run_persona_code_channel_memory_metabolism(
|
|||
.map_err(|error| format!("PERSONA_MEMORY_METABOLISM_JOIN_FAILED: {error}"))?
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn run_persona_code_channel_lifecycle(
|
||||
input: PersonaLifecycleRunInput,
|
||||
) -> Result<PersonaLifecycleRunReceipt, String> {
|
||||
let runtime_root = crate::app_config::preferred_app_config_path("pncc-runtime")?;
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let session_id = format!("PNCC-{}", Uuid::new_v4());
|
||||
let wake_timestamp = Utc::now().to_rfc3339_opts(SecondsFormat::Millis, true);
|
||||
let operation_timestamp = Utc::now().to_rfc3339_opts(SecondsFormat::Millis, true);
|
||||
run_lifecycle_at(
|
||||
&runtime_root,
|
||||
input,
|
||||
&session_id,
|
||||
&wake_timestamp,
|
||||
&operation_timestamp,
|
||||
run_fact_task_with_bound_provider_at,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.map_err(|error| format!("PERSONA_LIFECYCLE_JOIN_FAILED: {error}"))?
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn inspect_persona_code_channel_session(
|
||||
input: PersonaSessionControlInput,
|
||||
|
|
@ -2581,6 +2743,18 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
fn lifecycle_fact_input(repo: &Path) -> PersonaLifecycleRunInput {
|
||||
let fact = fact_task_input("unused-until-coordinator-creates-session");
|
||||
PersonaLifecycleRunInput {
|
||||
wake: wake_input(repo),
|
||||
operation: PersonaLifecycleOperationInput::FactSense {
|
||||
question: fact.question,
|
||||
provider: fact.provider,
|
||||
api_key_override: fact.api_key_override,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn session_control(repo: &Path, session_id: &str) -> PersonaSessionControlInput {
|
||||
PersonaSessionControlInput {
|
||||
session_id: session_id.into(),
|
||||
|
|
@ -2803,6 +2977,126 @@ mod tests {
|
|||
require_clean_repository(repo.path()).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn coordinates_one_registered_fact_lifecycle_without_ui_or_execution_limb() {
|
||||
let repo = persona_repo();
|
||||
let runtime = tempfile::TempDir::new().unwrap();
|
||||
let receipt = run_lifecycle_at(
|
||||
runtime.path(),
|
||||
lifecycle_fact_input(repo.path()),
|
||||
"PNCC-COORDINATED-FACT",
|
||||
"2026-08-11T00:00:00.000Z",
|
||||
"2026-08-11T00:00:01.000Z",
|
||||
|runtime_root, input, timestamp| {
|
||||
run_fact_task_at(runtime_root, input, timestamp, |_, _| {
|
||||
Ok(r#"{"summary":"Coordinated verified fact.","facts":[{"statement":"The brain exists.","evidencePaths":["brain/CORE.hdlp"]}],"limitations":[]}"#.into())
|
||||
})
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(receipt.schema, "hololake.pncc-lifecycle-run-receipt/v1");
|
||||
assert_eq!(receipt.session_id, "PNCC-COORDINATED-FACT");
|
||||
assert_eq!(receipt.wake_receipt.events[0].kind, "WAKING");
|
||||
assert_eq!(receipt.wake_receipt.events[2].kind, "ORGAN_ACTIVE");
|
||||
match receipt.completion {
|
||||
PersonaLifecycleCompletionReceipt::FactSense(completion) => {
|
||||
assert_eq!(completion.runtime_state, "DORMANT");
|
||||
assert_eq!(completion.events.last().unwrap().kind, "DORMANT");
|
||||
assert!(completion.model_inference_completed);
|
||||
}
|
||||
PersonaLifecycleCompletionReceipt::MemoryMetabolism(_) => {
|
||||
panic!("coordinator returned the wrong completion kind")
|
||||
}
|
||||
}
|
||||
assert!(!runtime.path().join("leases/ICE-P-ZY001.json").exists());
|
||||
require_clean_repository(repo.path()).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_a_lifecycle_operation_organ_mismatch_before_wake_or_lease() {
|
||||
let repo = persona_repo();
|
||||
let runtime = tempfile::TempDir::new().unwrap();
|
||||
let input = PersonaLifecycleRunInput {
|
||||
wake: wake_input(repo.path()),
|
||||
operation: PersonaLifecycleOperationInput::MemoryMetabolism {
|
||||
source_session_id: "PNCC-SOURCE-NOT-USED".into(),
|
||||
},
|
||||
};
|
||||
let error = run_lifecycle_at(
|
||||
runtime.path(),
|
||||
input,
|
||||
"PNCC-COORDINATOR-MISMATCH",
|
||||
"2026-08-11T00:00:00.000Z",
|
||||
"2026-08-11T00:00:01.000Z",
|
||||
|_, _, _| panic!("mismatched lifecycle must not run an organ"),
|
||||
)
|
||||
.unwrap_err();
|
||||
|
||||
assert!(error.contains("PERSONA_LIFECYCLE_OPERATION_ORGAN_MISMATCH"));
|
||||
assert!(!runtime.path().join("leases/ICE-P-ZY001.json").exists());
|
||||
assert!(!runtime
|
||||
.path()
|
||||
.join("sessions/PNCC-COORDINATOR-MISMATCH")
|
||||
.exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn coordinates_registered_memory_metabolism_after_a_verified_fact_session() {
|
||||
let repo = persona_repo();
|
||||
declare_memory_organ(repo.path());
|
||||
let runtime = tempfile::TempDir::new().unwrap();
|
||||
prepare_wake_at(
|
||||
runtime.path(),
|
||||
wake_input(repo.path()),
|
||||
"PNCC-COORDINATOR-SOURCE",
|
||||
"2026-08-11T00:00:00.000Z",
|
||||
)
|
||||
.unwrap();
|
||||
run_fact_task_at(
|
||||
runtime.path(),
|
||||
fact_task_input("PNCC-COORDINATOR-SOURCE"),
|
||||
"2026-08-11T00:00:01.000Z",
|
||||
|_, _| {
|
||||
Ok(r#"{"summary":"Verified memory candidate.","facts":[{"statement":"The brain exists.","evidencePaths":["brain/CORE.hdlp"]}],"limitations":[]}"#.into())
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let mut wake = wake_input(repo.path());
|
||||
wake.expected_head = head(repo.path());
|
||||
wake.organ_id = "memory-metabolism.checkpoint".into();
|
||||
let receipt = run_lifecycle_at(
|
||||
runtime.path(),
|
||||
PersonaLifecycleRunInput {
|
||||
wake,
|
||||
operation: PersonaLifecycleOperationInput::MemoryMetabolism {
|
||||
source_session_id: "PNCC-COORDINATOR-SOURCE".into(),
|
||||
},
|
||||
},
|
||||
"PNCC-COORDINATED-MEMORY",
|
||||
"2026-08-11T00:00:02.000Z",
|
||||
"2026-08-11T00:00:03.000Z",
|
||||
|_, _, _| panic!("memory lifecycle must not invoke a model"),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
match receipt.completion {
|
||||
PersonaLifecycleCompletionReceipt::MemoryMetabolism(completion) => {
|
||||
assert_eq!(completion.source_session_id, "PNCC-COORDINATOR-SOURCE");
|
||||
assert!(!completion.model_inference_started);
|
||||
assert_eq!(completion.runtime_state, "DORMANT");
|
||||
assert_eq!(completion.events[3].kind, "MEMORY_CANDIDATE_VERIFIED");
|
||||
assert_eq!(completion.events.last().unwrap().kind, "DORMANT");
|
||||
}
|
||||
PersonaLifecycleCompletionReceipt::FactSense(_) => {
|
||||
panic!("coordinator returned the wrong completion kind")
|
||||
}
|
||||
}
|
||||
assert!(!runtime.path().join("leases/ICE-P-ZY001.json").exists());
|
||||
require_clean_repository(repo.path()).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn independently_promotes_only_the_current_verified_structured_checkpoint() {
|
||||
let repo = persona_repo();
|
||||
|
|
|
|||
Loading…
Reference in a new issue