diff --git a/Cargo.lock b/Cargo.lock index 64af826..45d9d79 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -143,10 +143,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" [[package]] -name = "arrayvec" -version = "0.7.8" +name = "ar_archive_writer" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" +checksum = "73cd58deff2140a0a8eae87e417bd01db68a33e148aa93d1e8cd837e55e312b6" +dependencies = [ + "object", +] [[package]] name = "async-trait" @@ -219,30 +222,6 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc0b364ead1874514c8c2855ab558056ebfeb775653e7ae45ff72f28f8f3166c" -[[package]] -name = "borsh" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "553c5d846a6ba5150c65e3b1b8ec073bcf1abc20f9b7220de384a4443ea4e20a" -dependencies = [ - "borsh-derive", - "bytes", - "cfg_aliases", -] - -[[package]] -name = "borsh-derive" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12cdfe656708a01f89b451a7d36466e6fe6c414de0aa18fc54f864f6f9ca9f56" -dependencies = [ - "once_cell", - "proc-macro-crate", - "proc-macro2", - "quote", - "syn 3.0.5", -] - [[package]] name = "bumpalo" version = "3.20.3" @@ -277,12 +256,6 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" -[[package]] -name = "cfg_aliases" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" - [[package]] name = "chrono" version = "0.4.45" @@ -401,7 +374,7 @@ source = "git+https://github.com/GameCult/CultLib.git?rev=8fc74c709444b884a2bd33 dependencies = [ "anyhow", "chrono", - "cultcache-rs-derive", + "cultcache-rs-derive 0.1.0", "fs2", "redb", "rmp-serde", @@ -412,6 +385,26 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "cultcache-rs" +version = "0.3.1" +source = "git+https://github.com/GameCult/CultLib.git?rev=a4eae9056152335662f1115f97bf35fb00ab6d23#a4eae9056152335662f1115f97bf35fb00ab6d23" +dependencies = [ + "anyhow", + "chrono", + "cultcache-rs-derive 0.2.0", + "fs2", + "redb", + "rmp-serde", + "serde", + "serde_bytes", + "serde_json", + "serde_stacker", + "stacker", + "uuid", + "windows-sys 0.59.0", +] + [[package]] name = "cultcache-rs-derive" version = "0.1.0" @@ -422,6 +415,16 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "cultcache-rs-derive" +version = "0.2.0" +source = "git+https://github.com/GameCult/CultLib.git?rev=a4eae9056152335662f1115f97bf35fb00ab6d23#a4eae9056152335662f1115f97bf35fb00ab6d23" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "cultnet-rs" version = "0.1.0" @@ -431,10 +434,10 @@ dependencies = [ "anyhow", "base64", "chrono", - "cultcache-rs", + "cultcache-rs 0.2.0", "ed25519-dalek", "hmac", - "rand 0.9.5", + "rand", "reed-solomon-erasure", "rmp-serde", "rmpv", @@ -448,6 +451,31 @@ dependencies = [ "zeroize", ] +[[package]] +name = "cultnet-rs" +version = "0.2.0" +source = "git+https://github.com/GameCult/CultLib.git?rev=a4eae9056152335662f1115f97bf35fb00ab6d23#a4eae9056152335662f1115f97bf35fb00ab6d23" +dependencies = [ + "aes-gcm", + "anyhow", + "base64", + "chrono", + "cultcache-rs 0.3.1", + "ed25519-dalek", + "hmac", + "rand", + "rmp-serde", + "rmpv", + "serde", + "serde_bytes", + "serde_json", + "sha2", + "socket2", + "uuid", + "windows-sys 0.61.2", + "zeroize", +] + [[package]] name = "curve25519-dalek" version = "4.1.3" @@ -597,7 +625,7 @@ version = "0.1.0" dependencies = [ "anyhow", "chrono", - "cultcache-rs", + "cultcache-rs 0.2.0", "rmp-serde", "schemars", "serde", @@ -611,7 +639,7 @@ dependencies = [ "anyhow", "chrono", "clap", - "cultnet-rs", + "cultnet-rs 0.1.0", "huginn-daemon", "huginn-mind", "rmcp", @@ -891,7 +919,7 @@ dependencies = [ "anyhow", "base64", "chrono", - "cultnet-rs", + "cultnet-rs 0.1.0", "huginn-mind", "rmp-serde", "serde", @@ -909,8 +937,8 @@ dependencies = [ "anyhow", "base64", "chrono", - "cultcache-rs", - "cultnet-rs", + "cultcache-rs 0.2.0", + "cultnet-rs 0.1.0", "eureka-pipeline", "jsonschema", "rmp-serde", @@ -952,16 +980,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" -[[package]] -name = "indexmap" -version = "2.14.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" -dependencies = [ - "equivalent", - "hashbrown 0.17.1", -] - [[package]] name = "inout" version = "0.1.4" @@ -1116,22 +1134,45 @@ version = "0.1.0" dependencies = [ "anyhow", "chrono", - "cultcache-rs", - "cultnet-rs", + "cultcache-rs 0.2.0", + "cultnet-rs 0.1.0", "eureka-pipeline", "eureka-state", "huginn-mind", "libc", + "model-connector", "proc-macro2", "quote", - "rust_decimal", + "rmp-serde", + "rustix", "serde", "serde_json", + "sha2", "syn 2.0.119", "tempfile", "uuid", ] +[[package]] +name = "model-connector" +version = "0.1.0" +source = "git+https://eureka.gamecult.org/forge/GameCult/ModelConnector.git?rev=c1802d85bee3ae04b0c5737563c8343c1fef515d#c1802d85bee3ae04b0c5737563c8343c1fef515d" +dependencies = [ + "aes-gcm", + "base64", + "cultnet-rs 0.2.0", + "rand_core 0.6.4", + "rmp-serde", + "rmpv", + "schemars", + "serde", + "serde_bytes", + "serde_json", + "sha2", + "thiserror", + "zeroize", +] + [[package]] name = "num" version = "0.4.3" @@ -1210,6 +1251,15 @@ dependencies = [ "autocfg", ] +[[package]] +name = "object" +version = "0.39.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e5a6c098c7a3b6547378093f5cc30bc54fd361ce711e05293a5cc589562739b" +dependencies = [ + "memchr", +] + [[package]] name = "once_cell" version = "1.21.4" @@ -1331,15 +1381,6 @@ dependencies = [ "zerocopy", ] -[[package]] -name = "proc-macro-crate" -version = "3.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" -dependencies = [ - "toml_edit", -] - [[package]] name = "proc-macro2" version = "1.0.107" @@ -1349,6 +1390,16 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "psm" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dcd034599e63b970727f70d79e02d62390a4a84f7c6b827c27c46d5ac3fa622" +dependencies = [ + "ar_archive_writer", + "cc", +] + [[package]] name = "quote" version = "1.0.47" @@ -1370,37 +1421,16 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" -[[package]] -name = "rand" -version = "0.8.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c" -dependencies = [ - "libc", - "rand_chacha 0.3.1", - "rand_core 0.6.4", -] - [[package]] name = "rand" version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" dependencies = [ - "rand_chacha 0.9.0", + "rand_chacha", "rand_core 0.9.5", ] -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core 0.6.4", -] - [[package]] name = "rand_chacha" version = "0.9.0" @@ -1599,23 +1629,6 @@ dependencies = [ "serde_bytes", ] -[[package]] -name = "rust_decimal" -version = "1.43.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7653272e75dcac41dc199fbea6f5797633994fafd339943c06c9af16bf29cd3a" -dependencies = [ - "arrayvec", - "borsh", - "bytes", - "num-traits", - "rand 0.8.8", - "rand 0.9.5", - "serde", - "serde_json", - "wasm-bindgen", -] - [[package]] name = "rustc_version" version = "0.4.1" @@ -1757,6 +1770,17 @@ dependencies = [ "serde_core", ] +[[package]] +name = "serde_stacker" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4936375d50c4be7eff22293a9344f8e46f323ed2b3c243e52f89138d9bb0f4a" +dependencies = [ + "serde", + "serde_core", + "stacker", +] + [[package]] name = "sha1_smol" version = "1.0.1" @@ -1847,6 +1871,19 @@ dependencies = [ "der", ] +[[package]] +name = "stacker" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707f49d46706bacf8a2b00d51dace3f9de527c13eec3778f570c411f89e69967" +dependencies = [ + "cc", + "cfg-if", + "libc", + "psm", + "windows-sys 0.61.2", +] + [[package]] name = "strsim" version = "0.11.1" @@ -1971,36 +2008,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "toml_datetime" -version = "1.1.2+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b86d767906c6c42421dcba507eb9d203e779497710a47782a224bb871653053" -dependencies = [ - "serde_core", -] - -[[package]] -name = "toml_edit" -version = "0.25.17+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3641d5bbb5349a79e1020a242d251efbc546ad8048d133958323ce9c40a9c9c" -dependencies = [ - "indexmap", - "toml_datetime", - "toml_parser", - "winnow", -] - -[[package]] -name = "toml_parser" -version = "1.1.5+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baa693a8032d7e1cada7d0041e96126df243179ff061456783ac7f12bda4744c" -dependencies = [ - "winnow", -] - [[package]] name = "tracing" version = "0.1.44" @@ -2155,7 +2162,6 @@ dependencies = [ "cfg-if", "once_cell", "rustversion", - "serde", "wasm-bindgen-macro", "wasm-bindgen-shared", ] @@ -2364,15 +2370,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" -[[package]] -name = "winnow" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" -dependencies = [ - "memchr", -] - [[package]] name = "wit-bindgen" version = "0.57.1" diff --git a/README.md b/README.md index 8522393..9fea14c 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,9 @@ Upstream: `https://github.com/GameCult/Huginn.git` restart compares the mind with the collection and writes what is missing. An unreachable Qdrant or Ollama shows in `whoami` as the index's status and is retried; admission never waits on it. The daemon requires `--qdrant-url`, - `--ollama-url` and `--embedding-model`. A `query` carrying `semantic` is + `--ollama-url` and `--embedding-model`; `--ollama-url` repeats for each GPU + host and `--reserve-ollama-url` for each CPU host, and the GPU hosts embed + together while the CPU hosts serve only when none can. A `query` carrying `semantic` is answered through the same selection as any other: the index supplies candidate ids and scores, and the mind keeps only the documents it holds and the selection admits (ask for `in_force` to exclude resolved ones), ordered by @@ -40,7 +42,7 @@ edits `.cc` state. Huginn reads and writes minds. ## Layout -A Cargo workspace of five crates (`crates/eureka-pipeline` holds the document shapes and keys, `crates/mind-body` the control store, launch primitive and `mind-wake` one-shot; the three below are the mind, the daemon and the client): +A Cargo workspace of five crates (`crates/eureka-pipeline` holds the document shapes and keys, `crates/mind-body` the control store, launch primitive and `mind-heartbeat` timeline; the three below are the mind, the daemon and the client): - `crates/huginn-mind`: storage, identity, admission, and queries and derived status over memory documents. diff --git a/crates/eureka-pipeline/golden/envelopes.txt b/crates/eureka-pipeline/golden/envelopes.txt index cbd66ac..db205d5 100644 --- a/crates/eureka-pipeline/golden/envelopes.txt +++ b/crates/eureka-pipeline/golden/envelopes.txt @@ -25,6 +25,8 @@ sample.instance yggdrasil:instance:self epiphany.pipeline.instance.v2 EpiphanyPi sample.stewardship yggdrasil:stewardship:gamecult_-epiphany.n1 epiphany.pipeline.stewardship.v2 EpiphanyPipelineStewardshipDocument 9185a8696e7374616e6365a979676764726173696ca47265706fb147616d6543756c742f4570697068616e79a873657175656e636501ab61737369676e65645f6f6eaa323032362d30392d3135a46e6f7465b954686520457572656b612063616d706169676e207265706f2e sample.hand_off yggdrasil:hand_off:thought-cage.gamecult_-epiphany.2026-09-15 epiphany.pipeline.hand_off.v2 EpiphanyPipelineHandOffDocument 9186ad66726f6d5f696e7374616e6365a979676764726173696cab746f5f696e7374616e6365ac74686f756768742d63616765a47265706fb147616d6543756c742f4570697068616e79a9646f63756d656e747392ac657572656b612d7374617465b6657572656b612d73746174653a72756c696e673a5238a6726561736f6ed92854686520776f726b73746174696f6e206d696e642074616b6573207468652063616d706169676e2ea968616e6465645f6f6eaa323032362d30392d3135 sample.run yggdrasil:run:mind-2026-10-09-1 epiphany.pipeline.run.v2 EpiphanyPipelineRunDocument 9189a8696e7374616e6365a979676764726173696ca56c6162656cb16d696e642d323032362d31302d30392d31ab6f706572617465645f6279a44d696e64a47475726ea753656c6652756ea4686f7374a979676764726173696caa737461727465645f6f6eaa323032362d30392d3135aa6275646765745f757364a3372e35a6636c61696d739182a46b696e64a743757453706563a26964d925657572656b612d73746174653a6375745f737065633a6375742d72756e2d6b696e642e7232a963616d706169676e7391ac657572656b612d7374617465 +sample.pulse yggdrasil:pulse:2026-10-31t041200-t0 epiphany.pipeline.pulse.v2 EpiphanyPipelinePulseDocument 9185a8696e7374616e6365a979676764726173696ca56c6162656cb4323032362d31302d3331743034313230302d7430b174656d706f5f766f74655f626561745f73cd0258b0757267656e63795f7065726d696c6c65ccfaa46e6f7465d92254686520737761726d2069732069646c653b20612073686f7274657220626561742e +sample.memory yggdrasil:memory:2026-10-31t041200-t0-m0 epiphany.pipeline.memory.v2 EpiphanyPipelineMemoryDocument 9189a8696e7374616e6365a979676764726173696ca56c6162656cb7323032362d31302d3331743034313230302d74302d6d30a4736f7274a9436f6d6d6974746564a474657874d936492074616b6520746865206261636b67726f756e6420736861646572733b204c6962627920686f6c64732074686520736f756e64732ea561626f757491ab406c696262793a63686174a66f726967696e81a4526f6f6d82a5726f6f6d73a5737761726daa68656172645f66726f6d91ab406c696262793a63686174a7736f757263657391a3246531b173616c69656e63655f7065726d696c6c65cd02bcac636f6e736f6c69646174657390 resolution.superseded eureka-state:resolution:question.Q1.n1 epiphany.pipeline.resolution.v2 EpiphanyPipelineResolutionDocument 9185a77375626a65637482a46b696e64a85175657374696f6ea26964b8657572656b612d73746174653a7175657374696f6e3a5131a873657175656e636501a76f7574636f6d6581aa5375706572736564656481a262799282a46b696e64a652756c696e67a26964b6657572656b612d73746174653a72756c696e673a523882a46b696e64a85175657374696f6ea26964b8657572656b612d73746174653a7175657374696f6e3a5131a9726174696f6e616c65a852756c656420412eab7265736f6c7665645f6f6eaa323032362d30392d3135 resolution.fixed-by-none eureka-state:resolution:question.Q1.n1 epiphany.pipeline.resolution.v2 EpiphanyPipelineResolutionDocument 9185a77375626a65637482a46b696e64a85175657374696f6ea26964b8657572656b612d73746174653a7175657374696f6e3a5131a873657175656e636501a76f7574636f6d6581a5466978656482a6636f6d6d6974a83566393832323864a26279c0a9726174696f6e616c65a852756c656420412eab7265736f6c7665645f6f6eaa323032362d30392d3135 resolution.fixed-by-some eureka-state:resolution:question.Q1.n1 epiphany.pipeline.resolution.v2 EpiphanyPipelineResolutionDocument 9185a77375626a65637482a46b696e64a85175657374696f6ea26964b8657572656b612d73746174653a7175657374696f6e3a5131a873657175656e636501a76f7574636f6d6581a5466978656482a6636f6d6d6974a83566393832323864a2627982a46b696e64a94375745265706f7274a26964d921657572656b612d73746174653a6375745f7265706f72743a6375742d33612e6831a9726174696f6e616c65a852756c656420412eab7265736f6c7665645f6f6eaa323032362d30392d3135 diff --git a/crates/eureka-pipeline/src/lib.rs b/crates/eureka-pipeline/src/lib.rs index 9288d4c..cb5aad3 100644 --- a/crates/eureka-pipeline/src/lib.rs +++ b/crates/eureka-pipeline/src/lib.rs @@ -96,6 +96,12 @@ impl Absent for Option { } } +impl Absent for bool { + fn absent(&self) -> bool { + !*self + } +} + impl Absent for Vec { fn absent(&self) -> bool { self.is_empty() @@ -112,7 +118,7 @@ macro_rules! unbounded_scalars { )*}; } -unbounded_scalars!(u32, u64, bool); +unbounded_scalars!(u16, u32, u64, bool); fn bound(field: &str, limit: usize, actual: usize) -> Result<(), PipelineRefusal> { if actual > limit { @@ -572,6 +578,59 @@ fn run_is_well_formed(run: &PipelineRun, at: &str) -> Result<(), PipelineRefusal } } +/// The longest and shortest beat a Mind may vote for, in seconds. +pub const TEMPO_VOTE_MIN_S: u32 = 60; +pub const TEMPO_VOTE_MAX_S: u32 = 86_400; +/// Urgency is in thousandths: 0 declares none, 1000 the most. +pub const URGENCY_MAX_PERMILLE: u16 = 1000; + +/// A pulse votes a tempo or declares an urgency, in bounds, or says nothing it +/// could be read for: an empty pulse is refused. +fn pulse_is_well_formed(pulse: &PipelinePulse, at: &str) -> Result<(), PipelineRefusal> { + if let Some(beat) = pulse.tempo_vote_beat_s + && !(TEMPO_VOTE_MIN_S..=TEMPO_VOTE_MAX_S).contains(&beat) + { + return Err(format_error(&format!("{at}.tempo_vote_beat_s"), &beat.to_string())); + } + if let Some(urgency) = pulse.urgency_permille + && urgency > URGENCY_MAX_PERMILLE + { + return Err(format_error(&format!("{at}.urgency_permille"), &urgency.to_string())); + } + if pulse.tempo_vote_withdrawn && pulse.tempo_vote_beat_s.is_some() { + return Err(format_error(&format!("{at}.tempo_vote_withdrawn"), "a pulse that votes a beat and withdraws the vote")); + } + if pulse.tempo_vote_beat_s.is_none() && !pulse.tempo_vote_withdrawn && pulse.urgency_permille.is_none() { + return Err(format_error(&format!("{at}.tempo_vote_beat_s"), "a pulse that votes and declares nothing")); + } + Ok(()) +} + +/// The most thousandths a memory's salience may carry. +pub const SALIENCE_MAX_PERMILLE: u16 = 1000; + +/// A memory says something (its text is not empty), its salience is in +/// thousandths, and it consolidates only memories. +fn memory_is_well_formed(memory: &PipelineMemory, at: &str) -> Result<(), PipelineRefusal> { + if memory.text.0.trim().is_empty() { + return Err(format_error(&format!("{at}.text"), "an empty memory")); + } + if let MemoryOrigin::Direct { with, .. } = &memory.origin + && with.is_empty() + { + return Err(format_error(&format!("{at}.origin.with"), "a direct memory that names no one she heard it from")); + } + if memory.salience_permille > SALIENCE_MAX_PERMILLE { + return Err(format_error(&format!("{at}.salience_permille"), &memory.salience_permille.to_string())); + } + for (index, entry) in memory.consolidates.iter().enumerate() { + if entry.kind != PipelineKind::Memory { + return Err(format_error(&format!("{at}.consolidates[{index}].kind"), entry.kind.name())); + } + } + Ok(()) +} + unit_enums! { EvidenceKind { Command, Test, Mutation, Probe, SourceRead, Capture } ClaimOutcome { Holds, Falsified, Unproven } @@ -581,6 +640,7 @@ unit_enums! { FindingOrigin { Introduced, PreExisting } RunOperator { Operator, Mind } RunTurn { PersonaTurn, SelfRun } + MemorySort { Asked, Decided, Committed, Learned, Did, Idea, Rapport } } value_types! { @@ -710,6 +770,29 @@ value_types! { instance: Slug, label: Label, operated_by: RunOperator, turn: RunTurn, host: Short, started_on: Date, budget_usd: Short, claims: Vec[64], campaigns: Vec[8], } => run_is_well_formed + /// One Mind's heartbeat vote, in her own mind: the beat she wants (the swarm's + /// tempo is the lower median of every Mind's newest vote), the withdrawal of + /// her standing vote (`tempo_vote_withdrawn`, an explicit null: she counts as + /// silent again), and the urgency she declares for herself until her next + /// Persona turn. A pulse says at least one of the three; a withdrawal and a + /// beat are never both. Instance-rooted: the key is `:pulse: