stream: soul pass s1 fixes (stale repo news, userinfo bypass, reads, prompt version, poll cost)
- Repo activity is news only for events after her newest thought, and the notice is stamped with the event time; a capped repo counts only beside such an event. - OllamaStream::open parses the URL as the HTTP client does and refuses userinfo, a path or a query. - A thought's reads cite what the capped context sections kept. - Every thought records prompt_version: Ghostlight revision, word budget, guidance and the continuer frame (thought.v2, not yet deployed). - tick decides due-ness from responsibilities and news before building the seed or reading the persona. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
4bf29ca16a
commit
70de8cfec2
8 changed files with 266 additions and 50 deletions
|
|
@ -30,7 +30,7 @@ sample.memory yggdrasil:memory:2026-10-31t041200-t0-m0 epiphany.pipeline.memory.
|
|||
sample.research eureka-state:research:p1.scope epiphany.pipeline.research.v2 EpiphanyPipelineResearchDocument 9189a863616d706169676eac657572656b612d7374617465a470617373a27031a56c6162656ca573636f7065a5636c61696dd93654686520656d626564206c696d6974206973203430393620746f6b656e7320776974682073696c656e74207472756e636174696f6e2ea664657461696cd934526561642066726f6d20746865206461656d6f6e277320696e646578206d6f64756c6520616e642070726f626564206c6976652ea8696e666572726564c2a7736f75726365739481a45265706f82a47265706fb147616d6543756c742f4570697068616e79a2617484a470617468d9256372617465732f687567696e6e2d6461656d6f6e2f7372632f696e6465782f6d6f642e7273aa73746172745f6c696e6532a8656e645f6c696e6538a6636f6d6d6974a8356639383232386481a357656283a375726cd92268747470733a2f2f7a657474656c6b617374656e2e64652f61746f6d69636974792fa4776f726bb65a657474656c6b617374656e2061746f6d6963697479ac7265747269657665645f6f6eaa323032362d30392d313581a84f6273657276656481a865766964656e636583a46b696e64a454657374a76c6f6361746f72aa636172676f2074657374a6726573756c74a26f6b81a8446f63756d656e7481a97265666572656e636582a46b696e64a652756c696e67a26964b6657572656b612d73746174653a72756c696e673a5238a67365727665739182a46b696e64a85175657374696f6ea26964b8657572656b612d73746174653a7175657374696f6e3a5131ab6f627365727665645f6f6eaa323032362d30392d3135
|
||||
sample.idea eureka-state:idea:p1.i1 epiphany.pipeline.idea.v2 EpiphanyPipelineIdeaDocument 9189a863616d706169676eac657572656b612d7374617465a470617373a27031a56c6162656ca26931a57469746c65b24f6e65206e6f74652070657220636c61696da469646561d9204b65657020612070617373206173206d616e7920736d616c6c206e6f7465732ea974726164656f666673b74d6f726520646f63756d656e747320746f206c6973742ea872657374735f6f6e9182a46b696e64a85265736561726368a26964be657572656b612d73746174653a72657365617263683a70312e73636f7065a6746f776172649182a46b696e64a85175657374696f6ea26964b8657572656b612d73746174653a7175657374696f6e3a5131ab70726f706f7365645f6f6eaa323032362d30392d3135
|
||||
sample.reply yggdrasil:reply:mind-2026-10-09-1-r1 epiphany.pipeline.reply.v2 EpiphanyPipelineReplyDocument 9185a8696e7374616e6365a979676764726173696ca56c6162656cb46d696e642d323032362d31302d30392d312d7231a372756e82a46b696e64a352756ea26964bf79676764726173696c3a72756e3a6d696e642d323032362d31302d30392d31a7616e73776572739182a46b696e64a8466f6c6c6f775570a26964d923657572656b612d73746174653a666f6c6c6f775f75703a61736b2d7468652d73656c66a474657874d93054686520736861646572207061737320697320646f6e653b2074686520736f756e647320617265204c6962627927732e
|
||||
sample.thought yggdrasil:thought:t1793000000 epiphany.pipeline.thought.v2 EpiphanyPipelineThoughtDocument 91de0013a8696e7374616e6365a979676764726173696ca56c6162656cab7431373933303030303030a474657874d93d54686520736f756e647320617265204c6962627927733b2074686520736861646572732077657265206e6576657220746865206861726420706172742ea572656164739182a46b696e64a64d656d6f7279a26964d92879676764726173696c3a6d656d6f72793a323032362d31302d3331743034313230302d74302d6d30af706572736f6e615f72656365697074b06d696e642d636f6d6d69742d30613162a86964656e74697479be457572656b612c207368652f6865722c20706c61696e2d73706f6b656e2ea7636f6e74657874d92056616c7565733a20636f686572656e6365206f7665722076656c6f636974792ea87374696d756c7573bd4c696262792061736b65642061626f75742074686520736f756e64732eaf70726f6a6563746f725f6d6f64656cb070726f6a6563746f723a6c6174657374aa70726f6a656374696f6ed92949206b656570207475726e696e67206f7665722077686f20686f6c64732074686520736f756e64732eaa626173655f6d6f64656cab626173653a6c6174657374b474656d70657261747572655f7065726d696c6c65cd03e8ae6d696e5f705f7065726d696c6c6532aa6d61785f746f6b656e73cca0aa63616e6469646174657392d93d54686520736f756e647320617265204c6962627927733b2074686520736861646572732077657265206e6576657220746865206861726420706172742eaf41207365636f6e642064726966742eb06e6561726573745f7065726d696c6c6592cd0136cd0366a663686f73656e00ac70726f6a6563746f725f6d73cd1068ac636f6e74696e7565725f6d73cd238c
|
||||
sample.thought yggdrasil:thought:t1793000000 epiphany.pipeline.thought.v2 EpiphanyPipelineThoughtDocument 91de0014a8696e7374616e6365a979676764726173696ca56c6162656cab7431373933303030303030a474657874d93d54686520736f756e647320617265204c6962627927733b2074686520736861646572732077657265206e6576657220746865206861726420706172742ea572656164739182a46b696e64a64d656d6f7279a26964d92879676764726173696c3a6d656d6f72793a323032362d31302d3331743034313230302d74302d6d30af706572736f6e615f72656365697074b06d696e642d636f6d6d69742d30613162a86964656e74697479be457572656b612c207368652f6865722c20706c61696e2d73706f6b656e2ea7636f6e74657874d92056616c7565733a20636f686572656e6365206f7665722076656c6f636974792ea87374696d756c7573bd4c696262792061736b65642061626f75742074686520736f756e64732eaf70726f6a6563746f725f6d6f64656cb070726f6a6563746f723a6c6174657374aa70726f6a656374696f6ed92949206b656570207475726e696e67206f7665722077686f20686f6c64732074686520736f756e64732eaa626173655f6d6f64656cab626173653a6c6174657374b474656d70657261747572655f7065726d696c6c65cd03e8ae6d696e5f705f7065726d696c6c6532aa6d61785f746f6b656e73cca0aa63616e6469646174657392d93d54686520736f756e647320617265204c6962627927733b2074686520736861646572732077657265206e6576657220746865206861726420706172742eaf41207365636f6e642064726966742eb06e6561726573745f7065726d696c6c6592cd0136cd0366a663686f73656e00ac70726f6a6563746f725f6d73cd1068ac636f6e74696e7565725f6d73cd238cae70726f6d70745f76657273696f6ebd67686f73746c696768743d633433656235343b20776f7264733d313830
|
||||
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
|
||||
|
|
|
|||
|
|
@ -634,6 +634,9 @@ fn thought_is_well_formed(thought: &PipelineThought, at: &str) -> Result<(), Pip
|
|||
if thought.text.0.trim().is_empty() {
|
||||
return Err(format_error(&format!("{at}.text"), "a thought with no words"));
|
||||
}
|
||||
if thought.prompt_version.0.trim().is_empty() {
|
||||
return Err(format_error(&format!("{at}.prompt_version"), "a thought with no prompt version"));
|
||||
}
|
||||
if thought.candidates.is_empty() || thought.candidates.len() > THOUGHT_CANDIDATES_MAX {
|
||||
return Err(format_error(&format!("{at}.candidates"), &format!("{} candidates", thought.candidates.len())));
|
||||
}
|
||||
|
|
@ -878,12 +881,16 @@ value_types! {
|
|||
/// through something she wrote. Instance-rooted: the key is
|
||||
/// `<instance>:thought:<label>`, the label `t<unix seconds>` of the tick.
|
||||
/// Semantically indexed on `text` only. `nearest_permille` is 1000 where she
|
||||
/// had no earlier thought.
|
||||
/// had no earlier thought. `prompt_version` names the fixed parts of both
|
||||
/// prompts (the Projector's builder revision, guidance and word budget, and
|
||||
/// the continuer's frame), so the exact prompts of an old thought can be
|
||||
/// rebuilt from the trace.
|
||||
pub struct PipelineThought {
|
||||
instance: Slug, label: Label, text: Para, reads: Vec<PipelineRef>[64], persona_receipt: Short,
|
||||
identity: Line, context: Para, stimulus: Para, projector_model: Short, projection: Para, base_model: Short,
|
||||
temperature_permille: u16, min_p_permille: u16, max_tokens: u16,
|
||||
candidates: Vec<Line>[4], nearest_permille: Vec<u16>[4], chosen: u8, projector_ms: u32, continuer_ms: u32,
|
||||
prompt_version: Line,
|
||||
} => thought_is_well_formed
|
||||
/// One thing a Mind keeps for herself, in her own mind: what she was asked,
|
||||
/// decided, committed to, learned or did, an idea, or the rapport between
|
||||
|
|
@ -1715,6 +1722,7 @@ mod tests {
|
|||
temperature_permille: 1000, min_p_permille: 50, max_tokens: 160,
|
||||
candidates: vec!["The sounds are Libby's; the shaders were never the hard part.".into(), "A second drift.".into()],
|
||||
nearest_permille: vec![310, 870], chosen: 0, projector_ms: 4200, continuer_ms: 9100,
|
||||
prompt_version: "ghostlight=c43eb54; words=180".into(),
|
||||
}), format!("{INSTANCE}:thought:t1793000000")),
|
||||
]
|
||||
}
|
||||
|
|
@ -2073,6 +2081,7 @@ mod tests {
|
|||
(&|thought: &mut PipelineThought| { thought.nearest_permille.pop(); }, "thought.nearest_permille"),
|
||||
(&|thought: &mut PipelineThought| thought.nearest_permille[1] = 1001, "thought.nearest_permille[1]"),
|
||||
(&|thought: &mut PipelineThought| thought.chosen = 2, "thought.chosen"),
|
||||
(&|thought: &mut PipelineThought| thought.prompt_version = " ".into(), "thought.prompt_version"),
|
||||
(&|thought: &mut PipelineThought| { thought.candidates.clear(); thought.nearest_permille.clear(); }, "thought.candidates"),
|
||||
] {
|
||||
assert!(matches!(validated(change), Err(PipelineRefusal::InvalidFormat { field: f, .. }) if f == field), "{field}");
|
||||
|
|
|
|||
|
|
@ -376,6 +376,7 @@ pub(crate) fn thought(label: &str, text: &str, reads: &[PipelineRef]) -> Pipelin
|
|||
chosen: 0,
|
||||
projector_ms: 1,
|
||||
continuer_ms: 1,
|
||||
prompt_version: "test".into(),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -50,6 +50,11 @@ const MIN_P_PERMILLE: u16 = 50;
|
|||
const MAX_TOKENS: u16 = 160;
|
||||
const NUM_THREAD: u32 = 4;
|
||||
|
||||
/// The Ghostlight revision `build_projector_prompt` comes from; a test holds it to the manifest's pin.
|
||||
const GHOSTLIGHT_REV: &str = "c43eb54962ed4a09cc737ecac73a16b0d7ce62b4";
|
||||
/// How the continuer frame writes the date.
|
||||
const FRAME_DATE_FORMAT: &str = "%-d %B %Y";
|
||||
|
||||
/// The setting the Projector renders for: where she is between conversations.
|
||||
const STREAM_GUIDANCE: &str = "This person is a Mind of the GameCult swarm in the quiet between conversations. What she holds is her repos, her fellow Minds and the people she works with. Render what is on her mind in this quiet.";
|
||||
|
||||
|
|
@ -105,15 +110,16 @@ pub struct OllamaStream {
|
|||
impl OllamaStream {
|
||||
/// `endpoint` is `http://<loopback host>:<port>`; any other address is refused here.
|
||||
pub fn open(endpoint: &str, projector_model: &str, base_model: &str) -> Result<Self> {
|
||||
let rest = endpoint.strip_prefix("http://").ok_or_else(|| anyhow!("the stream endpoint must be http://<loopback>:<port>"))?;
|
||||
let authority = rest.trim_end_matches('/');
|
||||
if authority.contains('/') {
|
||||
// Parsed as the HTTP client parses it, so the host checked is the host connected to.
|
||||
let uri: ureq::http::Uri = endpoint.parse().map_err(|_| anyhow!("the stream endpoint is not a URL"))?;
|
||||
let authority = uri.authority().filter(|_| uri.scheme_str() == Some("http")).ok_or_else(|| anyhow!("the stream endpoint must be http://<loopback>:<port>"))?;
|
||||
if authority.as_str().contains('@') {
|
||||
bail!("the stream endpoint carries userinfo");
|
||||
}
|
||||
if uri.path() != "/" || uri.query().is_some() {
|
||||
bail!("the stream endpoint is an origin, not a path");
|
||||
}
|
||||
let host = match authority.strip_prefix('[') {
|
||||
Some(inner) => inner.split(']').next().unwrap_or_default(),
|
||||
None => authority.rsplit_once(':').map_or(authority, |(host, _)| host),
|
||||
};
|
||||
let host = authority.host().trim_start_matches('[').trim_end_matches(']');
|
||||
let loopback = host.eq_ignore_ascii_case("localhost") || host.parse::<IpAddr>().is_ok_and(|ip| ip.is_loopback());
|
||||
if !loopback {
|
||||
bail!("the stream endpoint is not a loopback address");
|
||||
|
|
@ -349,11 +355,20 @@ fn news(mind: &dyn MindPort, instance: &Slug, since: Option<DateTime<Utc>>, resp
|
|||
found.sort_by_key(|(ordinal, ..)| std::cmp::Reverse(*ordinal));
|
||||
let mut out: Vec<(String, Option<PipelineRef>)> = found.into_iter().map(|(_, line, id)| (line, Some(id))).collect();
|
||||
// Her stewarded repos' activity by others is the forge's word, not a document; a daemon with no forge adds nothing.
|
||||
// Only what happened after her newest thought is news, and the notice is stamped with when it happened.
|
||||
if !responsibilities.repos.is_empty()
|
||||
&& let Ok(activity) = mind.stewarded_activity(&responsibilities.repos, None)
|
||||
&& let Some(line) = repo_notice_line(&activity, u64::try_from(now.timestamp_millis()).unwrap_or(0))
|
||||
&& let Ok(mut activity) = mind.stewarded_activity(&responsibilities.repos, None)
|
||||
{
|
||||
out.push((format!("- {line}"), None));
|
||||
let since_ms = since.map_or(0, |since| u64::try_from(since.timestamp_millis()).unwrap_or(0));
|
||||
activity.events.retain(|event| since.is_none() || event.at > since_ms);
|
||||
// A capped repo has no time of its own: it is news only beside an event that is.
|
||||
if activity.events.is_empty() && since.is_some() {
|
||||
activity.capped.clear();
|
||||
}
|
||||
let happened = activity.events.iter().map(|event| event.at).max().unwrap_or_else(|| u64::try_from(now.timestamp_millis()).unwrap_or(0));
|
||||
if let Some(line) = repo_notice_line(&activity, happened) {
|
||||
out.push((format!("- {line}"), None));
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
|
@ -378,8 +393,8 @@ fn unbidden(mind: &dyn MindPort, instance: &Slug, now: DateTime<Utc>, pick: u64)
|
|||
}))
|
||||
}
|
||||
|
||||
/// The seed, rebuilt from her mind and nothing else. `since` is her newest thought's admission.
|
||||
fn build_seed(mind: &dyn MindPort, persona: &PersonaRead, since: Option<DateTime<Utc>>, now: DateTime<Utc>) -> Result<Seed> {
|
||||
/// The seed, rebuilt from her mind and nothing else. `since` is her newest thought's admission and `fresh` what `news` found.
|
||||
fn build_seed(mind: &dyn MindPort, persona: &PersonaRead, since: Option<DateTime<Utc>>, now: DateTime<Utc>, responsibilities: &Responsibilities, fresh: Vec<(String, Option<PipelineRef>)>) -> Result<Seed> {
|
||||
let instance = mind.instance().clone();
|
||||
let state = &persona.state;
|
||||
let mut reads: Vec<PipelineRef> = Vec::new();
|
||||
|
|
@ -389,17 +404,12 @@ fn build_seed(mind: &dyn MindPort, persona: &PersonaRead, since: Option<DateTime
|
|||
}
|
||||
};
|
||||
|
||||
let responsibilities = render_responsibilities(mind, &instance, &BTreeMap::new())?;
|
||||
let runs = recent_runs(mind, &instance)?;
|
||||
let people = bonded_people(state);
|
||||
let cue = Cue { senders: people.iter().map(String::as_str).collect(), texts: Vec::new(), names: BTreeMap::new(), listed: &responsibilities.listed, now };
|
||||
let recalled = recall::select(mind, &cue)?;
|
||||
for entry in recalled.entries() {
|
||||
cite(PipelineRef { kind: PipelineKind::Memory, id: Short(entry.id.clone()) });
|
||||
}
|
||||
let heard = replies(mind, &instance)?;
|
||||
let heard_section = section("What your Self said lately:", heard.iter().map(|(_, text)| format!("- {}", quoted(text))).collect());
|
||||
heard.iter().for_each(|(id, _)| cite(id.clone()));
|
||||
|
||||
let sections = [
|
||||
values_section(state),
|
||||
|
|
@ -410,9 +420,15 @@ fn build_seed(mind: &dyn MindPort, persona: &PersonaRead, since: Option<DateTime
|
|||
recall::render(&recalled),
|
||||
heard_section,
|
||||
];
|
||||
let context = sections.iter().zip(BUDGETS).map(|(text, (_, budget))| cap(text, budget)).filter(|text| !text.trim().is_empty()).collect::<Vec<_>>().join("\n");
|
||||
let kept: Vec<String> = sections.iter().zip(BUDGETS).map(|(text, (_, budget))| cap(text, budget)).collect();
|
||||
// The reads are what the Projector was shown after the bounds: the recall lines and replies the capped sections kept.
|
||||
for entry in recalled.entries().filter(|entry| kept[5].contains(entry.line.trim_end())) {
|
||||
cite(PipelineRef { kind: PipelineKind::Memory, id: Short(entry.id.clone()) });
|
||||
}
|
||||
let replies_shown = kept[6].lines().count().saturating_sub(1);
|
||||
heard.iter().take(replies_shown).for_each(|(id, _)| cite(id.clone()));
|
||||
let context = kept.iter().filter(|text| !text.trim().is_empty()).cloned().collect::<Vec<_>>().join("\n");
|
||||
|
||||
let fresh = news(mind, &instance, since, &responsibilities, now)?;
|
||||
let has_news = !fresh.is_empty();
|
||||
let stimulus = if has_news {
|
||||
let body: Vec<&str> = fresh.iter().map(|(line, _)| line.as_str()).collect();
|
||||
|
|
@ -437,8 +453,16 @@ fn build_seed(mind: &dyn MindPort, persona: &PersonaRead, since: Option<DateTime
|
|||
|
||||
/// The frame the base model continues: her notebook heading, the date, the projection verbatim
|
||||
/// and an open paragraph. It instructs nothing.
|
||||
fn continuer_frame(name: &str, now: DateTime<Utc>, projection: &str) -> String {
|
||||
format!("{name}'s notebook\n\n{}\n\n{projection}\n\n", now.format("%-d %B %Y"))
|
||||
fn continuer_frame(name: &str, date: &str, projection: &str) -> String {
|
||||
format!("{name}'s notebook\n\n{date}\n\n{projection}\n\n")
|
||||
}
|
||||
|
||||
/// The fixed parts of both prompts, recorded in every thought: the frame is the function's own output over placeholders.
|
||||
fn prompt_version() -> String {
|
||||
format!(
|
||||
"ghostlight={GHOSTLIGHT_REV}; words={PROJECTOR_WORDS}; guidance={STREAM_GUIDANCE:?}; frame={:?}",
|
||||
continuer_frame("<name>", &format!("<date {FRAME_DATE_FORMAT}>"), "<projection>")
|
||||
)
|
||||
}
|
||||
|
||||
/// Each candidate's nearness to her earlier thoughts, in thousandths of the daemon's top score;
|
||||
|
|
@ -524,11 +548,14 @@ pub fn tick(organs: &Organs, brake: &dyn Fn() -> Effective, mode: Mode, now: Dat
|
|||
if mode == Mode::Live && since.is_some_and(|at| (now - at).num_seconds() < STREAM_MIN_GAP_S) {
|
||||
return Ok(Outcome::NotDue);
|
||||
}
|
||||
let persona = organs.persona.persona()?;
|
||||
let seed = build_seed(mind, &persona, since, now)?;
|
||||
if mode == Mode::Live && !due(since, now, seed.news) {
|
||||
// Due-ness is decided on the cheap reads; the seed (recall, runs, replies) is built only for a tick that may run.
|
||||
let responsibilities = render_responsibilities(mind, mind.instance(), &BTreeMap::new())?;
|
||||
let fresh = news(mind, mind.instance(), since, &responsibilities, now)?;
|
||||
if mode == Mode::Live && !due(since, now, !fresh.is_empty()) {
|
||||
return Ok(Outcome::NotDue);
|
||||
}
|
||||
let persona = organs.persona.persona()?;
|
||||
let seed = build_seed(mind, &persona, since, now, &responsibilities, fresh)?;
|
||||
|
||||
let began = std::time::Instant::now();
|
||||
let projection = bounded(&organs.model.project(&seed.projector_prompt())?, PARA_MAX_BYTES);
|
||||
|
|
@ -536,7 +563,7 @@ pub fn tick(organs: &Organs, brake: &dyn Fn() -> Effective, mode: Mode, now: Dat
|
|||
if projection.is_empty() {
|
||||
return Ok(Outcome::Blank);
|
||||
}
|
||||
let frame = continuer_frame(text_of(&persona.state, &["publicName"]), now, &projection);
|
||||
let frame = continuer_frame(text_of(&persona.state, &["publicName"]), &now.format(FRAME_DATE_FORMAT).to_string(), &projection);
|
||||
let began = std::time::Instant::now();
|
||||
let candidates: Vec<String> = organs
|
||||
.model
|
||||
|
|
@ -572,6 +599,7 @@ pub fn tick(organs: &Organs, brake: &dyn Fn() -> Effective, mode: Mode, now: Dat
|
|||
chosen: u8::try_from(chosen).unwrap_or(0),
|
||||
projector_ms,
|
||||
continuer_ms,
|
||||
prompt_version: Line::from(prompt_version().as_str()),
|
||||
};
|
||||
if mode == Mode::DryRun {
|
||||
return Ok(Outcome::Thought { thought: Box::new(thought), admitted: false });
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ use anyhow::{Result, anyhow};
|
|||
use chrono::{DateTime, Utc};
|
||||
use cultnet_rs::Selection;
|
||||
use eureka_pipeline::{MemorySort, PipelineDocument, PipelineKind, PipelineReply, PipelineThought, RunOperator, RunTurn, Slug};
|
||||
use huginn_mind::{PipelineAdmissionOutcome, PipelineSelectionPage};
|
||||
use huginn_mind::{PipelineAdmissionOutcome, PipelineSelectionPage, RepoActivity, RepoChange, RepoEvent};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use super::*;
|
||||
|
|
@ -85,11 +85,18 @@ struct Wrap<'a> {
|
|||
scores: RefCell<VecDeque<f32>>,
|
||||
selections: RefCell<Vec<Selection>>,
|
||||
admitted: RefCell<Vec<Vec<PipelineDocument>>>,
|
||||
forge: Option<RepoActivity>,
|
||||
}
|
||||
|
||||
impl<'a> Wrap<'a> {
|
||||
fn over(inner: &'a TestMind, scores: &[f32]) -> Self {
|
||||
Self { inner, scores: RefCell::new(scores.iter().copied().collect()), selections: RefCell::default(), admitted: RefCell::default() }
|
||||
Self { inner, scores: RefCell::new(scores.iter().copied().collect()), selections: RefCell::default(), admitted: RefCell::default(), forge: None }
|
||||
}
|
||||
|
||||
/// The same mind with a forge that answers `events`, whatever the asker's cursor.
|
||||
fn with_forge(mut self, events: Vec<RepoEvent>) -> Self {
|
||||
self.forge = Some(RepoActivity { head: 1, events, capped: vec![], open_prs: vec![], unseen: vec![], moved: vec![] });
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -114,12 +121,41 @@ impl MindPort for Wrap<'_> {
|
|||
self.inner.roots_touched()
|
||||
}
|
||||
|
||||
fn stewarded_activity(&self, repos: &[huginn_mind::eureka_pipeline::OrgRepo], after: Option<u64>) -> Result<RepoActivity> {
|
||||
match &self.forge {
|
||||
Some(activity) => Ok(activity.clone()),
|
||||
None => self.inner.stewarded_activity(repos, after),
|
||||
}
|
||||
}
|
||||
|
||||
fn admit(&self, agent: &str, session: &str, documents: Vec<PipelineDocument>) -> Result<PipelineAdmissionOutcome> {
|
||||
self.admitted.borrow_mut().push(documents.clone());
|
||||
self.inner.admit(agent, session, documents)
|
||||
}
|
||||
}
|
||||
|
||||
/// The seed as the tick builds it: the cheap reads first, then the rest.
|
||||
fn seed_of(mind: &dyn MindPort, read: &PersonaRead, since: Option<DateTime<Utc>>, now: DateTime<Utc>) -> Result<Seed> {
|
||||
let responsibilities = render_responsibilities(mind, mind.instance(), &BTreeMap::new())?;
|
||||
let fresh = news(mind, mind.instance(), since, &responsibilities, now)?;
|
||||
build_seed(mind, read, since, now, &responsibilities, fresh)
|
||||
}
|
||||
|
||||
/// A push by someone else to her repo at `secs`.
|
||||
fn push(id: u64, secs: i64) -> RepoEvent {
|
||||
RepoEvent { id, at: u64::try_from(t(secs).timestamp_millis()).unwrap(), repo: "GameCult/Huginn".into(), actor: "libby".into(), change: RepoChange::Pushed { commits: 1 } }
|
||||
}
|
||||
|
||||
/// Counts how often her persona is read, which only a tick that may run does.
|
||||
struct CountedPersona(Persona, std::cell::Cell<usize>);
|
||||
|
||||
impl PersonaSource for CountedPersona {
|
||||
fn persona(&self) -> Result<PersonaRead> {
|
||||
self.1.set(self.1.get() + 1);
|
||||
self.0.persona()
|
||||
}
|
||||
}
|
||||
|
||||
fn thought_doc(label: &str, text: &str) -> PipelineDocument {
|
||||
PipelineDocument::Thought(PipelineThought {
|
||||
instance: Slug(INSTANCE.into()),
|
||||
|
|
@ -141,6 +177,7 @@ fn thought_doc(label: &str, text: &str) -> PipelineDocument {
|
|||
chosen: 0,
|
||||
projector_ms: 0,
|
||||
continuer_ms: 0,
|
||||
prompt_version: "test".into(),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -177,8 +214,8 @@ fn seed_reads_no_thought() {
|
|||
with.committed(vec![thought_doc("t1", "A drift that must stay out of the seed")]);
|
||||
let (plain, thoughtful) = (Wrap::over(&without, &[]), Wrap::over(&with, &[]));
|
||||
let read = PersonaRead { state: state(), receipt_id: "r".into() };
|
||||
let one = build_seed(&plain, &read, None, t(100_000)).unwrap();
|
||||
let two = build_seed(&thoughtful, &read, None, t(100_000)).unwrap();
|
||||
let one = seed_of(&plain, &read, None, t(100_000)).unwrap();
|
||||
let two = seed_of(&thoughtful, &read, None, t(100_000)).unwrap();
|
||||
assert_eq!(one.projector_prompt(), two.projector_prompt(), "a mind holding thoughts yields the prompt of the same mind without them");
|
||||
assert_eq!(one.reads, two.reads);
|
||||
assert!(!two.projector_prompt().contains("must stay out"));
|
||||
|
|
@ -203,7 +240,7 @@ fn seed_carries_relationships() {
|
|||
answers: vec![],
|
||||
text: "The shader pass is done; the sounds are Libby's.".into(),
|
||||
})]);
|
||||
let seed = build_seed(&Wrap::over(&mind, &[]), &PersonaRead { state: state(), receipt_id: "r".into() }, None, t(100_000)).unwrap();
|
||||
let seed = seed_of(&Wrap::over(&mind, &[]), &PersonaRead { state: state(), receipt_id: "r".into() }, None, t(100_000)).unwrap();
|
||||
for wanted in ["Libby holds the sounds", "collaborator", "Libby wants the sounds before the shaders", "Noor distrusts the new scheduler", "The shader pass is done", "Coherence over velocity", "The shaders are late"] {
|
||||
assert!(seed.context.contains(wanted), "{wanted:?} missing from {}", seed.context);
|
||||
}
|
||||
|
|
@ -220,7 +257,7 @@ fn seed_sections_keep_budgets() {
|
|||
crowded["agencyPressure"]["pressures"] = json!((0..40).map(|_| json!({ "summary": huge })).collect::<Vec<_>>());
|
||||
let mind = TestMind::seeded();
|
||||
mind.committed(vec![memory("m1", MemorySort::Learned, "Libby wants the sounds first", &["@libby:chat"])]);
|
||||
let seed = build_seed(&Wrap::over(&mind, &[]), &PersonaRead { state: crowded, receipt_id: "r".into() }, None, t(100_000)).unwrap();
|
||||
let seed = seed_of(&Wrap::over(&mind, &[]), &PersonaRead { state: crowded, receipt_id: "r".into() }, None, t(100_000)).unwrap();
|
||||
assert!(seed.context.len() <= SECTION_MAX_BYTES, "{}", seed.context.len());
|
||||
assert!(seed.context.contains("Libby holds the sounds"), "oversized values and pressures crowded out the bonds");
|
||||
assert!(seed.context.contains("Libby wants the sounds first"), "oversized values and pressures crowded out the recall");
|
||||
|
|
@ -241,7 +278,7 @@ fn stimulus_is_what_is_new() {
|
|||
mind.committed(vec![crate::testkit::close(run_ref("w9"), crate::testkit::recorded())]);
|
||||
let wrap = Wrap::over(&mind, &[]);
|
||||
let read = PersonaRead { state: state(), receipt_id: "r".into() };
|
||||
let seed = build_seed(&wrap, &read, Some(t(200)), t(400)).unwrap();
|
||||
let seed = seed_of(&wrap, &read, Some(t(200)), t(400)).unwrap();
|
||||
assert!(seed.news);
|
||||
assert!(seed.stimulus.contains("After the thought, the window"), "{}", seed.stimulus);
|
||||
assert!(seed.stimulus.contains("w9 closed"), "a run closure is news: {}", seed.stimulus);
|
||||
|
|
@ -262,7 +299,7 @@ fn stimulus_without_news_is_an_unbidden_memory_chosen_by_the_tick_label_else_the
|
|||
let read = PersonaRead { state: state(), receipt_id: "r".into() };
|
||||
let picks: Vec<String> = (0..4)
|
||||
.map(|offset| {
|
||||
let seed = build_seed(&wrap, &read, Some(t(long_after - 10)), t(long_after + offset)).unwrap();
|
||||
let seed = seed_of(&wrap, &read, Some(t(long_after - 10)), t(long_after + offset)).unwrap();
|
||||
assert!(!seed.news);
|
||||
seed.stimulus
|
||||
})
|
||||
|
|
@ -273,7 +310,7 @@ fn stimulus_without_news_is_an_unbidden_memory_chosen_by_the_tick_label_else_the
|
|||
assert_eq!(picks[0], picks[2], "the same label seed picks the same memory");
|
||||
|
||||
let bare = TestMind::seeded();
|
||||
let seed = build_seed(&Wrap::over(&bare, &[]), &read, Some(t(1000)), t(5000)).unwrap();
|
||||
let seed = seed_of(&Wrap::over(&bare, &[]), &read, Some(t(1000)), t(5000)).unwrap();
|
||||
assert!(!seed.news);
|
||||
assert!(seed.stimulus.starts_with("Nothing has happened since"), "{}", seed.stimulus);
|
||||
}
|
||||
|
|
@ -355,7 +392,7 @@ fn no_earlier_thought_is_nearness_1000() {
|
|||
|
||||
#[test]
|
||||
fn frame_holds_projection_verbatim() {
|
||||
assert_eq!(continuer_frame("Yggdrasil", t(0), "I turn it over."), "Yggdrasil's notebook\n\n9 October 2026\n\nI turn it over.\n\n");
|
||||
assert_eq!(continuer_frame("Yggdrasil", "9 October 2026", "I turn it over."), "Yggdrasil's notebook\n\n9 October 2026\n\nI turn it over.\n\n");
|
||||
let mind = mind_with_thought(1000);
|
||||
let model = Fake::saying("I keep turning over the sounds.", &["a", "b", "c", "d", "e"]);
|
||||
let persona = persona();
|
||||
|
|
@ -363,7 +400,7 @@ fn frame_holds_projection_verbatim() {
|
|||
tick(&organs(&wrap, &persona, &model), &|| released(), Mode::Live, t(1000 + STREAM_IDLE_GAP_S)).unwrap();
|
||||
let continued = model.continued.borrow();
|
||||
let (prompt, sampling, n) = &continued[0];
|
||||
assert_eq!(prompt, &continuer_frame("Yggdrasil", t(1000 + STREAM_IDLE_GAP_S), "I keep turning over the sounds."));
|
||||
assert_eq!(prompt, &continuer_frame("Yggdrasil", &t(1000 + STREAM_IDLE_GAP_S).format(FRAME_DATE_FORMAT).to_string(), "I keep turning over the sounds."));
|
||||
assert_eq!((*n, *sampling), (4, Sampling::STREAM));
|
||||
for foreign in ["Libby", "Coherence", "Plain.", "she/her", "private Projector"] {
|
||||
assert!(!prompt.contains(foreign), "{foreign} reached the continuer");
|
||||
|
|
@ -500,10 +537,27 @@ fn a_model_host_that_answers_badly_is_a_fault() {
|
|||
|
||||
#[test]
|
||||
fn non_loopback_endpoint_refused() {
|
||||
for refused in [
|
||||
"http://localhost:11434@example.com",
|
||||
"http://localhost@example.com",
|
||||
"http://127.0.0.1:11434@example.com:80",
|
||||
"http://user:secret@127.0.0.1:11434",
|
||||
"http://127.0.0.1:11434@localhost",
|
||||
"http://[::1]:11434@example.com",
|
||||
"http://localhost:11434\\@example.com",
|
||||
"http://example.com#@127.0.0.1",
|
||||
"http://example.com?@localhost",
|
||||
"http://127.0.0.1:11434?x=1",
|
||||
"http://localhost.example.com:11434",
|
||||
"http://",
|
||||
"",
|
||||
] {
|
||||
assert!(OllamaStream::open(refused, "p", "b").is_err(), "{refused} must be refused");
|
||||
}
|
||||
for refused in ["http://10.77.0.1:11434", "http://example.com:11434", "http://0.0.0.0:11434", "https://127.0.0.1:11434", "127.0.0.1:11434", "http://127.0.0.1:11434/api", "http://[2001:db8::1]:11434"] {
|
||||
assert!(OllamaStream::open(refused, "p", "b").is_err(), "{refused} must be refused");
|
||||
}
|
||||
for opened in ["http://127.0.0.1:11434", "http://localhost:11434", "http://[::1]:11434", "http://127.0.0.2:1/"] {
|
||||
for opened in ["http://127.0.0.1:11434", "http://localhost:11434", "http://[::1]:11434", "http://127.0.0.2:1/", "http://LOCALHOST:11434/"] {
|
||||
assert!(OllamaStream::open(opened, "p", "b").is_ok(), "{opened} is loopback");
|
||||
}
|
||||
}
|
||||
|
|
@ -544,9 +598,9 @@ fn news_is_strictly_after_her_newest_thought() {
|
|||
mind.admit_at(t(500));
|
||||
mind.committed(vec![memory("m1", MemorySort::Learned, "Admitted at the very instant of the thought", &[])]);
|
||||
let read = PersonaRead { state: state(), receipt_id: "r".into() };
|
||||
let seed = build_seed(&Wrap::over(&mind, &[]), &read, Some(t(500)), t(600)).unwrap();
|
||||
let seed = seed_of(&Wrap::over(&mind, &[]), &read, Some(t(500)), t(600)).unwrap();
|
||||
assert!(!seed.news, "{}", seed.stimulus);
|
||||
let seed = build_seed(&Wrap::over(&mind, &[]), &read, Some(t(499)), t(600)).unwrap();
|
||||
let seed = seed_of(&Wrap::over(&mind, &[]), &read, Some(t(499)), t(600)).unwrap();
|
||||
assert!(seed.news);
|
||||
}
|
||||
|
||||
|
|
@ -557,9 +611,9 @@ fn an_unbidden_memory_is_older_than_seven_days_not_exactly_seven() {
|
|||
mind.committed(vec![memory("m1", MemorySort::Learned, "The week-old thing", &[])]);
|
||||
let read = PersonaRead { state: state(), receipt_id: "r".into() };
|
||||
let week = 7 * 86_400;
|
||||
let exactly = build_seed(&Wrap::over(&mind, &[]), &read, Some(t(1000)), t(1000 + week)).unwrap();
|
||||
let exactly = seed_of(&Wrap::over(&mind, &[]), &read, Some(t(1000)), t(1000 + week)).unwrap();
|
||||
assert!(exactly.stimulus.starts_with("Nothing has happened"), "{}", exactly.stimulus);
|
||||
let over = build_seed(&Wrap::over(&mind, &[]), &read, Some(t(1000)), t(1001 + week)).unwrap();
|
||||
let over = seed_of(&Wrap::over(&mind, &[]), &read, Some(t(1000)), t(1001 + week)).unwrap();
|
||||
assert!(over.stimulus.contains("The week-old thing"), "{}", over.stimulus);
|
||||
}
|
||||
|
||||
|
|
@ -570,7 +624,7 @@ fn a_flood_of_news_fills_the_stimulus_with_whole_lines_newest_first() {
|
|||
mind.admit_at(t(100));
|
||||
mind.committed(flood);
|
||||
let read = PersonaRead { state: state(), receipt_id: "r".into() };
|
||||
let seed = build_seed(&Wrap::over(&mind, &[]), &read, Some(t(50)), t(200)).unwrap();
|
||||
let seed = seed_of(&Wrap::over(&mind, &[]), &read, Some(t(50)), t(200)).unwrap();
|
||||
let lines: Vec<&str> = seed.stimulus.lines().skip(1).collect();
|
||||
assert!(seed.stimulus.len() <= SECTION_MAX_BYTES && seed.stimulus.len() > SECTION_MAX_BYTES - 250, "{}", seed.stimulus.len());
|
||||
assert!(lines.iter().all(|line| line.starts_with("- You noted") && line.ends_with('"')), "a line was cut: {lines:?}");
|
||||
|
|
@ -606,3 +660,115 @@ fn news_at_exactly_the_minimum_gap_is_due() {
|
|||
let wrap = Wrap::over(&mind, &[0.1]);
|
||||
let Outcome::Thought { admitted: true, .. } = tick(&organs(&wrap, &persona, &model), &|| released(), Mode::Live, t(1000 + STREAM_MIN_GAP_S)).unwrap() else { panic!("due at the minimum gap") };
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repo_activity_is_news_only_after_her_newest_thought_and_is_stamped_when_it_happened() {
|
||||
let mind = mind_with_thought(1000);
|
||||
let read = PersonaRead { state: state(), receipt_id: "r".into() };
|
||||
let old = Wrap::over(&mind, &[]).with_forge(vec![push(1, 500)]);
|
||||
let seed = seed_of(&old, &read, Some(t(1000)), t(1901)).unwrap();
|
||||
assert!(!seed.news && !seed.stimulus.contains("Changes in your repos"), "an event older than her newest thought is not news: {}", seed.stimulus);
|
||||
let edge = Wrap::over(&mind, &[]).with_forge(vec![push(1, 1000)]);
|
||||
assert!(!seed_of(&edge, &read, Some(t(1000)), t(1901)).unwrap().news, "an event at her newest thought's time is not after it");
|
||||
let mixed = Wrap::over(&mind, &[]).with_forge(vec![push(2, 1500), push(1, 500)]);
|
||||
let seed = seed_of(&mixed, &read, Some(t(1000)), t(1901)).unwrap();
|
||||
assert!(seed.news && seed.stimulus.contains("Changes in your repos: GameCult/Huginn: 1 commit pushed by libby"), "{}", seed.stimulus);
|
||||
let stamp = t(1500).to_rfc3339_opts(chrono::SecondsFormat::Secs, true);
|
||||
assert!(seed.stimulus.contains(&format!("at={stamp} ")), "the notice carries the event's time, not the tick's: {}", seed.stimulus);
|
||||
let first = Wrap::over(&mind, &[]).with_forge(vec![push(1, 500)]);
|
||||
assert!(seed_of(&first, &read, None, t(1901)).unwrap().news, "with no thought yet, what the forge holds is news");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_capped_repo_is_news_only_beside_an_event_after_her_newest_thought() {
|
||||
let mind = mind_with_thought(1000);
|
||||
let read = PersonaRead { state: state(), receipt_id: "r".into() };
|
||||
let mut quiet = Wrap::over(&mind, &[]).with_forge(vec![push(1, 500)]);
|
||||
quiet.forge.as_mut().unwrap().capped.push("GameCult/Huginn".into());
|
||||
assert!(!seed_of(&quiet, &read, Some(t(1000)), t(1901)).unwrap().news);
|
||||
let mut busy = Wrap::over(&mind, &[]).with_forge(vec![push(2, 1500)]);
|
||||
busy.forge.as_mut().unwrap().capped.push("GameCult/Huginn".into());
|
||||
let seed = seed_of(&busy, &read, Some(t(1000)), t(1901)).unwrap();
|
||||
assert!(seed.news && seed.stimulus.to_lowercase().contains("page limit"), "{}", seed.stimulus);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn old_repo_activity_does_not_make_a_tick_due() {
|
||||
let mind = mind_with_thought(1000);
|
||||
let model = Fake::saying("I turn it over.", &["A new drift"]);
|
||||
let persona = CountedPersona(persona(), std::cell::Cell::new(0));
|
||||
let wrap = Wrap::over(&mind, &[0.1]).with_forge(vec![push(1, 500)]);
|
||||
let organs = Organs { mind: &wrap, persona: &persona, model: &model, projector_model: "projector:test", base_model: "base:test" };
|
||||
assert_eq!(tick(&organs, &|| released(), Mode::Live, t(1901)).unwrap(), Outcome::NotDue);
|
||||
assert_eq!((model.calls(), persona.1.get(), thoughts(&mind).len()), (0, 0, 1), "no model call, no seed, no thought");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_repo_activity_makes_a_tick_due_and_the_hourly_memory_comes_for_an_active_steward() {
|
||||
let week = 8 * 86_400;
|
||||
let mind = TestMind::seeded();
|
||||
mind.admit_at(t(100));
|
||||
mind.committed(vec![memory("m1", MemorySort::Learned, "An old thing she holds", &[])]);
|
||||
mind.admit_at(t(week));
|
||||
mind.committed(vec![thought_doc("t1", "An earlier drift")]);
|
||||
let model = Fake::saying("I turn it over.", &["A new drift", "Another"]);
|
||||
let persona = persona();
|
||||
let fresh = Wrap::over(&mind, &[0.1, 0.2]).with_forge(vec![push(2, week + 1000)]);
|
||||
let Outcome::Thought { thought, admitted: true } = tick(&organs(&fresh, &persona, &model), &|| released(), Mode::Live, t(week + 1000 + STREAM_MIN_GAP_S)).unwrap() else { panic!("new activity past the minimum gap") };
|
||||
assert!(thought.stimulus.0.contains("Changes in your repos"), "{}", thought.stimulus.0);
|
||||
|
||||
let model = Fake::saying("I turn it over.", &["Yet another"]);
|
||||
let quiet = Wrap::over(&mind, &[0.3]).with_forge(vec![push(1, week - 5000)]);
|
||||
let at = week + 1000 + STREAM_MIN_GAP_S + STREAM_IDLE_GAP_S + 10;
|
||||
let Outcome::Thought { thought, admitted: true } = tick(&organs(&quiet, &persona, &model), &|| released(), Mode::Live, t(at)).unwrap() else { panic!("the idle gap") };
|
||||
assert!(thought.stimulus.0.starts_with("Out of nowhere you recall"), "old activity is not news, so the hourly memory comes: {}", thought.stimulus.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reads_cite_only_what_the_context_showed() {
|
||||
let mind = TestMind::seeded();
|
||||
let memories: Vec<PipelineDocument> = (0..12).map(|at| memory(&format!("m{at:02}"), MemorySort::Learned, &format!("zz{at:02} {}", "x".repeat(290)), &["@libby:chat"])).collect();
|
||||
mind.admit_at(t(100));
|
||||
mind.committed(memories);
|
||||
mind.admit_at(t(200));
|
||||
mind.committed(vec![run("w1", RunTurn::SelfRun, RunOperator::Mind, &[])]);
|
||||
mind.committed(
|
||||
(1..=3)
|
||||
.map(|at| PipelineDocument::Reply(PipelineReply { instance: Slug(INSTANCE.into()), label: format!("w1-r{at}").as_str().into(), run: run_ref("w1"), answers: vec![], text: format!("rp{at} {}", "y".repeat(400)).as_str().into() }))
|
||||
.collect(),
|
||||
);
|
||||
let read = PersonaRead { state: state(), receipt_id: "r".into() };
|
||||
let seed = seed_of(&Wrap::over(&mind, &[]), &read, Some(t(300)), t(400)).unwrap();
|
||||
assert!(!seed.news);
|
||||
let cited: Vec<String> = seed.reads.iter().filter(|read| read.kind == PipelineKind::Memory).map(|read| read.id.0.rsplit(':').next().unwrap().to_owned()).collect();
|
||||
assert!(!cited.is_empty() && cited.len() < 12, "{cited:?}");
|
||||
for label in &cited {
|
||||
assert!(seed.context.contains(&format!("zz{}", &label[1..])), "{label} is cited but not shown");
|
||||
}
|
||||
let shown = (0..12).filter(|at| seed.context.contains(&format!("zz{at:02}"))).count();
|
||||
assert_eq!(shown, cited.len(), "every memory the context shows is cited");
|
||||
let replies_cited = seed.reads.iter().filter(|read| read.kind == PipelineKind::Reply).count();
|
||||
let replies_shown = (1..=3).filter(|at| seed.context.contains(&format!("rp{at} "))).count();
|
||||
assert!(replies_shown >= 1 && replies_shown < 3, "the replies section is bounded: {replies_shown}");
|
||||
assert_eq!(replies_cited, replies_shown, "replies cited are the replies shown");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_thought_records_the_fixed_parts_of_its_prompts() {
|
||||
let mind = mind_with_thought(1000);
|
||||
let model = Fake::saying("I turn it over.", &["A new drift"]);
|
||||
let persona = persona();
|
||||
let wrap = Wrap::over(&mind, &[0.1]);
|
||||
let Outcome::Thought { thought, .. } = tick(&organs(&wrap, &persona, &model), &|| released(), Mode::Live, t(1000 + STREAM_IDLE_GAP_S)).unwrap() else { panic!("idle gap") };
|
||||
let version = &thought.prompt_version.0;
|
||||
assert_eq!(version, &prompt_version());
|
||||
let guidance = format!("{STREAM_GUIDANCE:?}");
|
||||
for part in [guidance.as_str(), GHOSTLIGHT_REV, "words=180", "<name>'s notebook", "<projection>", "<date %-d %B %Y>"] {
|
||||
assert!(version.contains(part), "{part} missing from {version}");
|
||||
}
|
||||
assert!(version.len() <= LINE_MAX_BYTES, "{}", version.len());
|
||||
assert!(include_str!("../../Cargo.toml").contains(GHOSTLIGHT_REV), "the recorded revision is the manifest's pin");
|
||||
let rebuilt = build_projector_prompt(&ProjectorPrompt { identity: &thought.identity.0, typed_context: &thought.context.0, visible_stimulus: &thought.stimulus.0, domain_guidance: STREAM_GUIDANCE, word_budget: PROJECTOR_WORDS });
|
||||
assert_eq!(model.projected.borrow()[0], rebuilt, "the trace rebuilds the Projector's exact prompt");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@
|
|||
},
|
||||
"$id": "https://gamecult.dev/epiphany/cultnet/epiphany.pipeline.thought.v2.schema.json",
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"description": "One tick of her stream of thought, offline: the words a base model\ncontinued from a projection of her own state, with the whole trace that\nmade them (the seed, the projection, every candidate and how near each\nwas to her earlier thoughts). `text` is the chosen candidate. `reads`\nare the documents the seed was drawn from. A thought is divergence for\ndelivery to weigh, never a memory: its words reach a later seed only\nthrough something she wrote. Instance-rooted: the key is\n`<instance>:thought:<label>`, the label `t<unix seconds>` of the tick.\nSemantically indexed on `text` only. `nearest_permille` is 1000 where she\nhad no earlier thought.",
|
||||
"description": "One tick of her stream of thought, offline: the words a base model\ncontinued from a projection of her own state, with the whole trace that\nmade them (the seed, the projection, every candidate and how near each\nwas to her earlier thoughts). `text` is the chosen candidate. `reads`\nare the documents the seed was drawn from. A thought is divergence for\ndelivery to weigh, never a memory: its words reach a later seed only\nthrough something she wrote. Instance-rooted: the key is\n`<instance>:thought:<label>`, the label `t<unix seconds>` of the tick.\nSemantically indexed on `text` only. `nearest_permille` is 1000 where she\nhad no earlier thought. `prompt_version` names the fixed parts of both\nprompts (the Projector's builder revision, guidance and word budget, and\nthe continuer's frame), so the exact prompts of an old thought can be\nrebuilt from the trace.",
|
||||
"properties": {
|
||||
"base_model": {
|
||||
"$ref": "#/$defs/Short"
|
||||
|
|
@ -140,6 +140,9 @@
|
|||
"minimum": 0,
|
||||
"type": "integer"
|
||||
},
|
||||
"prompt_version": {
|
||||
"$ref": "#/$defs/Line"
|
||||
},
|
||||
"reads": {
|
||||
"items": {
|
||||
"$ref": "#/$defs/PipelineRef"
|
||||
|
|
@ -179,7 +182,8 @@
|
|||
"nearest_permille",
|
||||
"chosen",
|
||||
"projector_ms",
|
||||
"continuer_ms"
|
||||
"continuer_ms",
|
||||
"prompt_version"
|
||||
],
|
||||
"title": "PipelineThought",
|
||||
"type": "object"
|
||||
|
|
|
|||
|
|
@ -2115,7 +2115,7 @@
|
|||
]
|
||||
},
|
||||
"PipelineThought": {
|
||||
"description": "One tick of her stream of thought, offline: the words a base model\ncontinued from a projection of her own state, with the whole trace that\nmade them (the seed, the projection, every candidate and how near each\nwas to her earlier thoughts). `text` is the chosen candidate. `reads`\nare the documents the seed was drawn from. A thought is divergence for\ndelivery to weigh, never a memory: its words reach a later seed only\nthrough something she wrote. Instance-rooted: the key is\n`<instance>:thought:<label>`, the label `t<unix seconds>` of the tick.\nSemantically indexed on `text` only. `nearest_permille` is 1000 where she\nhad no earlier thought.",
|
||||
"description": "One tick of her stream of thought, offline: the words a base model\ncontinued from a projection of her own state, with the whole trace that\nmade them (the seed, the projection, every candidate and how near each\nwas to her earlier thoughts). `text` is the chosen candidate. `reads`\nare the documents the seed was drawn from. A thought is divergence for\ndelivery to weigh, never a memory: its words reach a later seed only\nthrough something she wrote. Instance-rooted: the key is\n`<instance>:thought:<label>`, the label `t<unix seconds>` of the tick.\nSemantically indexed on `text` only. `nearest_permille` is 1000 where she\nhad no earlier thought. `prompt_version` names the fixed parts of both\nprompts (the Projector's builder revision, guidance and word budget, and\nthe continuer's frame), so the exact prompts of an old thought can be\nrebuilt from the trace.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"base_model": {
|
||||
|
|
@ -2187,6 +2187,9 @@
|
|||
"format": "uint32",
|
||||
"minimum": 0
|
||||
},
|
||||
"prompt_version": {
|
||||
"$ref": "#/$defs/Line"
|
||||
},
|
||||
"reads": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
|
|
@ -2226,7 +2229,8 @@
|
|||
"nearest_permille",
|
||||
"chosen",
|
||||
"projector_ms",
|
||||
"continuer_ms"
|
||||
"continuer_ms",
|
||||
"prompt_version"
|
||||
]
|
||||
},
|
||||
"PipelineVerdict": {
|
||||
|
|
|
|||
|
|
@ -4618,7 +4618,7 @@
|
|||
]
|
||||
},
|
||||
"PipelineThought": {
|
||||
"description": "One tick of her stream of thought, offline: the words a base model\ncontinued from a projection of her own state, with the whole trace that\nmade them (the seed, the projection, every candidate and how near each\nwas to her earlier thoughts). `text` is the chosen candidate. `reads`\nare the documents the seed was drawn from. A thought is divergence for\ndelivery to weigh, never a memory: its words reach a later seed only\nthrough something she wrote. Instance-rooted: the key is\n`<instance>:thought:<label>`, the label `t<unix seconds>` of the tick.\nSemantically indexed on `text` only. `nearest_permille` is 1000 where she\nhad no earlier thought.",
|
||||
"description": "One tick of her stream of thought, offline: the words a base model\ncontinued from a projection of her own state, with the whole trace that\nmade them (the seed, the projection, every candidate and how near each\nwas to her earlier thoughts). `text` is the chosen candidate. `reads`\nare the documents the seed was drawn from. A thought is divergence for\ndelivery to weigh, never a memory: its words reach a later seed only\nthrough something she wrote. Instance-rooted: the key is\n`<instance>:thought:<label>`, the label `t<unix seconds>` of the tick.\nSemantically indexed on `text` only. `nearest_permille` is 1000 where she\nhad no earlier thought. `prompt_version` names the fixed parts of both\nprompts (the Projector's builder revision, guidance and word budget, and\nthe continuer's frame), so the exact prompts of an old thought can be\nrebuilt from the trace.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"base_model": {
|
||||
|
|
@ -4690,6 +4690,9 @@
|
|||
"format": "uint32",
|
||||
"minimum": 0
|
||||
},
|
||||
"prompt_version": {
|
||||
"$ref": "#/$defs/Line"
|
||||
},
|
||||
"reads": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
|
|
@ -4729,7 +4732,8 @@
|
|||
"nearest_permille",
|
||||
"chosen",
|
||||
"projector_ms",
|
||||
"continuer_ms"
|
||||
"continuer_ms",
|
||||
"prompt_version"
|
||||
]
|
||||
},
|
||||
"PipelineVerdict": {
|
||||
|
|
|
|||
Loading…
Reference in a new issue