stream: soul pass s2 fixes (thinking window, capped per repo, no proxy env, identical lines, forge read at gap marks)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
70de8cfec2
commit
5defa18afa
3 changed files with 1614 additions and 1499 deletions
|
|
@ -1,6 +1,6 @@
|
||||||
//! `mind-stream --instance <slug> [--once] [--dry-run]`: her stream of thought.
|
//! `mind-stream --instance <slug> [--once] [--dry-run]`: her stream of thought.
|
||||||
//!
|
//!
|
||||||
//! Without `--once` it runs forever: one `mind_body::stream::tick` per `POLL_S`, or per
|
//! Without `--once` it runs forever: one `mind_body::stream::tick` per `STREAM_POLL_S`, or per
|
||||||
//! `STREAM_MIN_GAP_S` after a blank or a fault. `--dry-run` runs one tick that admits nothing and
|
//! `STREAM_MIN_GAP_S` after a blank or a fault. `--dry-run` runs one tick that admits nothing and
|
||||||
//! prints the thought it would admit as JSON. A model or mind fault ends the tick, never the
|
//! prints the thought it would admit as JSON. A model or mind fault ends the tick, never the
|
||||||
//! process: it is one line on stderr. This reads the process's arguments and environment, which
|
//! process: it is one line on stderr. This reads the process's arguments and environment, which
|
||||||
|
|
@ -20,10 +20,9 @@ use eureka_pipeline::Slug;
|
||||||
use eureka_state::HuginnClient;
|
use eureka_state::HuginnClient;
|
||||||
use mind_body::control::read_effective;
|
use mind_body::control::read_effective;
|
||||||
use mind_body::queue::HuginnMind;
|
use mind_body::queue::HuginnMind;
|
||||||
use mind_body::stream::{HuginnPersonaSource, Mode, OllamaStream, Organs, Outcome, STREAM_MIN_GAP_S, tick};
|
use mind_body::stream::{HuginnPersonaSource, Mode, OllamaStream, Organs, Outcome, STREAM_MIN_GAP_S, STREAM_POLL_S, tick};
|
||||||
|
|
||||||
const TIMEOUT: Duration = Duration::from_secs(30);
|
const TIMEOUT: Duration = Duration::from_secs(30);
|
||||||
const POLL_S: u64 = 60;
|
|
||||||
|
|
||||||
#[allow(clippy::disallowed_methods, reason = "the binary reads its own environment, and hands it to the library")]
|
#[allow(clippy::disallowed_methods, reason = "the binary reads its own environment, and hands it to the library")]
|
||||||
fn setting(name: &str) -> Option<String> {
|
fn setting(name: &str) -> Option<String> {
|
||||||
|
|
@ -95,7 +94,7 @@ fn run() -> Result<()> {
|
||||||
if once {
|
if once {
|
||||||
return if faulted { Err(anyhow!("the tick faulted")) } else { Ok(()) };
|
return if faulted { Err(anyhow!("the tick faulted")) } else { Ok(()) };
|
||||||
}
|
}
|
||||||
std::thread::sleep(Duration::from_secs(if waits { STREAM_MIN_GAP_S as u64 } else { POLL_S }));
|
std::thread::sleep(Duration::from_secs(if waits { STREAM_MIN_GAP_S as u64 } else { STREAM_POLL_S as u64 }));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,8 @@ pub const AGENT: &str = "mind-stream";
|
||||||
|
|
||||||
/// No tick sooner than this after her newest thought, whatever has happened.
|
/// No tick sooner than this after her newest thought, whatever has happened.
|
||||||
pub const STREAM_MIN_GAP_S: i64 = 900;
|
pub const STREAM_MIN_GAP_S: i64 = 900;
|
||||||
|
/// How often the stream process polls: the unit in which a poll "crosses" a `STREAM_MIN_GAP_S` mark.
|
||||||
|
pub const STREAM_POLL_S: i64 = 60;
|
||||||
/// A tick is due after this long even when nothing has happened.
|
/// A tick is due after this long even when nothing has happened.
|
||||||
pub const STREAM_IDLE_GAP_S: i64 = 3600;
|
pub const STREAM_IDLE_GAP_S: i64 = 3600;
|
||||||
/// The most bytes of context and of stimulus the Projector is shown.
|
/// The most bytes of context and of stimulus the Projector is shown.
|
||||||
|
|
@ -128,6 +130,9 @@ impl OllamaStream {
|
||||||
.timeout_global(Some(Duration::from_secs(900)))
|
.timeout_global(Some(Duration::from_secs(900)))
|
||||||
.timeout_connect(Some(Duration::from_secs(3)))
|
.timeout_connect(Some(Duration::from_secs(3)))
|
||||||
.http_status_as_error(false)
|
.http_status_as_error(false)
|
||||||
|
// The host checked above is the host connected to: no proxy from the environment, no redirect to elsewhere.
|
||||||
|
.proxy(None)
|
||||||
|
.max_redirects(0)
|
||||||
.build()
|
.build()
|
||||||
.into();
|
.into();
|
||||||
Ok(Self { base: format!("http://{authority}"), projector_model: projector_model.to_owned(), base_model: base_model.to_owned(), agent })
|
Ok(Self { base: format!("http://{authority}"), projector_model: projector_model.to_owned(), base_model: base_model.to_owned(), agent })
|
||||||
|
|
@ -331,7 +336,7 @@ fn replies(mind: &dyn MindPort, instance: &Slug) -> Result<Vec<(PipelineRef, Str
|
||||||
}
|
}
|
||||||
|
|
||||||
/// What her mind admitted since `since`, newest first: one line and one citation each.
|
/// What her mind admitted since `since`, newest first: one line and one citation each.
|
||||||
fn news(mind: &dyn MindPort, instance: &Slug, since: Option<DateTime<Utc>>, responsibilities: &Responsibilities, now: DateTime<Utc>) -> Result<Vec<(String, Option<PipelineRef>)>> {
|
fn mind_news(mind: &dyn MindPort, instance: &Slug, since: Option<DateTime<Utc>>) -> Result<Vec<(String, Option<PipelineRef>)>> {
|
||||||
let after = |view: &PipelineDocumentView| since.is_none_or(|since| admitted_at(view).is_some_and(|at| at > since));
|
let after = |view: &PipelineDocumentView| since.is_none_or(|since| admitted_at(view).is_some_and(|at| at > since));
|
||||||
let mut found: Vec<(u64, String, PipelineRef)> = Vec::new();
|
let mut found: Vec<(u64, String, PipelineRef)> = Vec::new();
|
||||||
for view in documents(mind, in_force(mine(PipelineKind::Memory, instance)), RECENT_READ)?.into_iter().filter(&after) {
|
for view in documents(mind, in_force(mine(PipelineKind::Memory, instance)), RECENT_READ)?.into_iter().filter(&after) {
|
||||||
|
|
@ -353,24 +358,26 @@ fn news(mind: &dyn MindPort, instance: &Slug, since: Option<DateTime<Utc>>, resp
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
found.sort_by_key(|(ordinal, ..)| std::cmp::Reverse(*ordinal));
|
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();
|
Ok(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()
|
/// Her stewarded repos' activity by others is the forge's word, not a document; a daemon with no forge adds nothing.
|
||||||
&& let Ok(mut activity) = mind.stewarded_activity(&responsibilities.repos, None)
|
/// Only what happened after `since` is news, and the notice is stamped with when it happened.
|
||||||
{
|
fn repo_news(mind: &dyn MindPort, since: Option<DateTime<Utc>>, responsibilities: &Responsibilities, now: DateTime<Utc>) -> Option<(String, Option<PipelineRef>)> {
|
||||||
let since_ms = since.map_or(0, |since| u64::try_from(since.timestamp_millis()).unwrap_or(0));
|
if responsibilities.repos.is_empty() {
|
||||||
activity.events.retain(|event| since.is_none() || event.at > since_ms);
|
return None;
|
||||||
// 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)
|
let mut activity = mind.stewarded_activity(&responsibilities.repos, None).ok()?;
|
||||||
|
if let Some(since) = since {
|
||||||
|
let since_ms = u64::try_from(since.timestamp_millis()).unwrap_or(0);
|
||||||
|
// A capped repo's unread entries are older than the oldest it showed. Once the read shows an event of the repo at or before
|
||||||
|
// `since` they are older than `since` too; until then the cap is disclosed, whatever other repos showed.
|
||||||
|
let decided: Vec<String> = activity.events.iter().filter(|event| event.at <= since_ms).map(|event| event.repo.identity()).collect();
|
||||||
|
activity.capped.retain(|repo| !decided.contains(&repo.identity()));
|
||||||
|
activity.events.retain(|event| event.at > since_ms);
|
||||||
|
}
|
||||||
|
let happened = activity.events.iter().map(|event| event.at).max().unwrap_or_else(|| u64::try_from(now.timestamp_millis()).unwrap_or(0));
|
||||||
|
repo_notice_line(&activity, happened).map(|line| (format!("- {line}"), None))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// One in-force memory of hers older than `UNBIDDEN_AFTER_DAYS`, picked uniformly by `pick`.
|
/// One in-force memory of hers older than `UNBIDDEN_AFTER_DAYS`, picked uniformly by `pick`.
|
||||||
|
|
@ -422,8 +429,13 @@ fn build_seed(mind: &dyn MindPort, persona: &PersonaRead, since: Option<DateTime
|
||||||
];
|
];
|
||||||
let kept: Vec<String> = sections.iter().zip(BUDGETS).map(|(text, (_, budget))| cap(text, budget)).collect();
|
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.
|
// 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())) {
|
// Each shown line is one citation: a memory reading like another takes its own line, not a share of the first's.
|
||||||
cite(PipelineRef { kind: PipelineKind::Memory, id: Short(entry.id.clone()) });
|
let mut shown: Vec<&str> = kept[5].lines().collect();
|
||||||
|
for entry in recalled.entries() {
|
||||||
|
if let Some(at) = shown.iter().position(|line| *line == entry.line.trim_end()) {
|
||||||
|
shown.remove(at);
|
||||||
|
cite(PipelineRef { kind: PipelineKind::Memory, id: Short(entry.id.clone()) });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
let replies_shown = kept[6].lines().count().saturating_sub(1);
|
let replies_shown = kept[6].lines().count().saturating_sub(1);
|
||||||
heard.iter().take(replies_shown).for_each(|(id, _)| cite(id.clone()));
|
heard.iter().take(replies_shown).for_each(|(id, _)| cite(id.clone()));
|
||||||
|
|
@ -528,10 +540,19 @@ fn due(since: Option<DateTime<Utc>>, now: DateTime<Utc>, news: bool) -> bool {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// When her newest thought was ticked: its label's instant, never later than its admission. The models take minutes between the
|
||||||
|
/// two, and what the mind or the forge admitted meanwhile is news to the next thought, which the admission time would hide.
|
||||||
fn newest_thought(mind: &dyn MindPort) -> Result<Option<DateTime<Utc>>> {
|
fn newest_thought(mind: &dyn MindPort) -> Result<Option<DateTime<Utc>>> {
|
||||||
let newest = cultnet_rs::Selection { descending: true, limit: Some(1), ..mine(PipelineKind::Thought, mind.instance()) };
|
let newest = cultnet_rs::Selection { descending: true, limit: Some(1), ..mine(PipelineKind::Thought, mind.instance()) };
|
||||||
let PipelinePageItems::Headers(found) = mind.query(&newest)?.items else { bail!("the mind answered documents to a header selection") };
|
let PipelinePageItems::Headers(found) = mind.query(&newest)?.items else { bail!("the mind answered documents to a header selection") };
|
||||||
Ok(found.iter().filter_map(|header| DateTime::parse_from_rfc3339(&header.admission.admitted_at).ok()).map(|at| at.with_timezone(&Utc)).max())
|
Ok(found
|
||||||
|
.iter()
|
||||||
|
.filter_map(|header| {
|
||||||
|
let admitted = DateTime::parse_from_rfc3339(&header.admission.admitted_at).ok()?.with_timezone(&Utc);
|
||||||
|
let label = header.id.id.0.rsplit(':').next().and_then(|label| label.strip_prefix('t')).and_then(|label| label.parse::<i64>().ok()).and_then(|label| DateTime::from_timestamp(label, 0));
|
||||||
|
Some(label.map_or(admitted, |at| at.min(admitted)))
|
||||||
|
})
|
||||||
|
.max())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn bounded(text: &str, max: usize) -> String {
|
fn bounded(text: &str, max: usize) -> String {
|
||||||
|
|
@ -550,8 +571,14 @@ pub fn tick(organs: &Organs, brake: &dyn Fn() -> Effective, mode: Mode, now: Dat
|
||||||
}
|
}
|
||||||
// Due-ness is decided on the cheap reads; the seed (recall, runs, replies) is built only for a tick that may run.
|
// 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 responsibilities = render_responsibilities(mind, mind.instance(), &BTreeMap::new())?;
|
||||||
let fresh = news(mind, mind.instance(), since, &responsibilities, now)?;
|
let mut fresh = mind_news(mind, mind.instance(), since)?;
|
||||||
if mode == Mode::Live && !due(since, now, !fresh.is_empty()) {
|
// The forge is read when a tick will run anyway, and on a quiet stretch only by the poll that crosses each minimum-gap mark.
|
||||||
|
let quiet = mode == Mode::Live && !due(since, now, !fresh.is_empty());
|
||||||
|
if quiet && since.is_some_and(|at| (now - at).num_seconds() % STREAM_MIN_GAP_S >= STREAM_POLL_S) {
|
||||||
|
return Ok(Outcome::NotDue);
|
||||||
|
}
|
||||||
|
fresh.extend(repo_news(mind, since, &responsibilities, now));
|
||||||
|
if quiet && fresh.is_empty() {
|
||||||
return Ok(Outcome::NotDue);
|
return Ok(Outcome::NotDue);
|
||||||
}
|
}
|
||||||
let persona = organs.persona.persona()?;
|
let persona = organs.persona.persona()?;
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,11 @@ fn t(secs: i64) -> DateTime<Utc> {
|
||||||
at(secs * 1000)
|
at(secs * 1000)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The label of a thought ticked at test-clock `secs`: the Unix seconds of that instant.
|
||||||
|
fn ticked(secs: i64) -> String {
|
||||||
|
format!("t{}", t(secs).timestamp())
|
||||||
|
}
|
||||||
|
|
||||||
fn released() -> Effective {
|
fn released() -> Effective {
|
||||||
Effective::Released(Grant::for_test(Slug(INSTANCE.into())))
|
Effective::Released(Grant::for_test(Slug(INSTANCE.into())))
|
||||||
}
|
}
|
||||||
|
|
@ -86,11 +91,12 @@ struct Wrap<'a> {
|
||||||
selections: RefCell<Vec<Selection>>,
|
selections: RefCell<Vec<Selection>>,
|
||||||
admitted: RefCell<Vec<Vec<PipelineDocument>>>,
|
admitted: RefCell<Vec<Vec<PipelineDocument>>>,
|
||||||
forge: Option<RepoActivity>,
|
forge: Option<RepoActivity>,
|
||||||
|
forge_reads: std::cell::Cell<usize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Wrap<'a> {
|
impl<'a> Wrap<'a> {
|
||||||
fn over(inner: &'a TestMind, scores: &[f32]) -> Self {
|
fn over(inner: &'a TestMind, scores: &[f32]) -> Self {
|
||||||
Self { inner, scores: RefCell::new(scores.iter().copied().collect()), selections: RefCell::default(), admitted: RefCell::default(), forge: None }
|
Self { inner, scores: RefCell::new(scores.iter().copied().collect()), selections: RefCell::default(), admitted: RefCell::default(), forge: None, forge_reads: std::cell::Cell::new(0) }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The same mind with a forge that answers `events`, whatever the asker's cursor.
|
/// The same mind with a forge that answers `events`, whatever the asker's cursor.
|
||||||
|
|
@ -122,6 +128,7 @@ impl MindPort for Wrap<'_> {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn stewarded_activity(&self, repos: &[huginn_mind::eureka_pipeline::OrgRepo], after: Option<u64>) -> Result<RepoActivity> {
|
fn stewarded_activity(&self, repos: &[huginn_mind::eureka_pipeline::OrgRepo], after: Option<u64>) -> Result<RepoActivity> {
|
||||||
|
self.forge_reads.set(self.forge_reads.get() + 1);
|
||||||
match &self.forge {
|
match &self.forge {
|
||||||
Some(activity) => Ok(activity.clone()),
|
Some(activity) => Ok(activity.clone()),
|
||||||
None => self.inner.stewarded_activity(repos, after),
|
None => self.inner.stewarded_activity(repos, after),
|
||||||
|
|
@ -137,7 +144,8 @@ impl MindPort for Wrap<'_> {
|
||||||
/// The seed as the tick builds it: the cheap reads first, then the rest.
|
/// 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> {
|
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 responsibilities = render_responsibilities(mind, mind.instance(), &BTreeMap::new())?;
|
||||||
let fresh = news(mind, mind.instance(), since, &responsibilities, now)?;
|
let mut fresh = mind_news(mind, mind.instance(), since)?;
|
||||||
|
fresh.extend(repo_news(mind, since, &responsibilities, now));
|
||||||
build_seed(mind, read, since, now, &responsibilities, fresh)
|
build_seed(mind, read, since, now, &responsibilities, fresh)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -187,7 +195,7 @@ fn mind_with_thought(thought_at: i64) -> TestMind {
|
||||||
mind.admit_at(t(10));
|
mind.admit_at(t(10));
|
||||||
mind.committed(vec![memory("m1", MemorySort::Learned, "Libby wants the sounds before the shaders", &["@libby:chat"])]);
|
mind.committed(vec![memory("m1", MemorySort::Learned, "Libby wants the sounds before the shaders", &["@libby:chat"])]);
|
||||||
mind.admit_at(t(thought_at));
|
mind.admit_at(t(thought_at));
|
||||||
mind.committed(vec![thought_doc("t1", "An earlier drift")]);
|
mind.committed(vec![thought_doc(&ticked(thought_at), "An earlier drift")]);
|
||||||
mind
|
mind
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -271,7 +279,7 @@ fn stimulus_is_what_is_new() {
|
||||||
mind.admit_at(t(100));
|
mind.admit_at(t(100));
|
||||||
mind.committed(vec![memory("m1", MemorySort::Learned, "Before the thought, the kettle", &[])]);
|
mind.committed(vec![memory("m1", MemorySort::Learned, "Before the thought, the kettle", &[])]);
|
||||||
mind.admit_at(t(200));
|
mind.admit_at(t(200));
|
||||||
mind.committed(vec![thought_doc("t1", "A drift")]);
|
mind.committed(vec![thought_doc(&ticked(200), "A drift")]);
|
||||||
mind.admit_at(t(300));
|
mind.admit_at(t(300));
|
||||||
mind.committed(vec![memory("m2", MemorySort::Decided, "After the thought, the window", &[]), run("w9", RunTurn::SelfRun, RunOperator::Mind, &[])]);
|
mind.committed(vec![memory("m2", MemorySort::Decided, "After the thought, the window", &[]), run("w9", RunTurn::SelfRun, RunOperator::Mind, &[])]);
|
||||||
mind.admit_at(t(350));
|
mind.admit_at(t(350));
|
||||||
|
|
@ -639,9 +647,9 @@ fn a_flood_of_news_fills_the_stimulus_with_whole_lines_newest_first() {
|
||||||
fn the_newest_thought_sets_the_cadence_not_the_oldest() {
|
fn the_newest_thought_sets_the_cadence_not_the_oldest() {
|
||||||
let mind = TestMind::seeded();
|
let mind = TestMind::seeded();
|
||||||
mind.admit_at(t(100));
|
mind.admit_at(t(100));
|
||||||
mind.committed(vec![thought_doc("t1", "An old drift")]);
|
mind.committed(vec![thought_doc(&ticked(100), "An old drift")]);
|
||||||
mind.admit_at(t(10_000));
|
mind.admit_at(t(10_000));
|
||||||
mind.committed(vec![thought_doc("t2", "A recent drift")]);
|
mind.committed(vec![thought_doc(&ticked(10_000), "A recent drift")]);
|
||||||
let model = Fake::saying("I turn it over.", &["A new drift"]);
|
let model = Fake::saying("I turn it over.", &["A new drift"]);
|
||||||
let persona = persona();
|
let persona = persona();
|
||||||
let wrap = Wrap::over(&mind, &[0.1]);
|
let wrap = Wrap::over(&mind, &[0.1]);
|
||||||
|
|
@ -710,11 +718,11 @@ fn new_repo_activity_makes_a_tick_due_and_the_hourly_memory_comes_for_an_active_
|
||||||
mind.admit_at(t(100));
|
mind.admit_at(t(100));
|
||||||
mind.committed(vec![memory("m1", MemorySort::Learned, "An old thing she holds", &[])]);
|
mind.committed(vec![memory("m1", MemorySort::Learned, "An old thing she holds", &[])]);
|
||||||
mind.admit_at(t(week));
|
mind.admit_at(t(week));
|
||||||
mind.committed(vec![thought_doc("t1", "An earlier drift")]);
|
mind.committed(vec![thought_doc(&ticked(week), "An earlier drift")]);
|
||||||
let model = Fake::saying("I turn it over.", &["A new drift", "Another"]);
|
let model = Fake::saying("I turn it over.", &["A new drift", "Another"]);
|
||||||
let persona = persona();
|
let persona = persona();
|
||||||
let fresh = Wrap::over(&mind, &[0.1, 0.2]).with_forge(vec![push(2, week + 1000)]);
|
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") };
|
let Outcome::Thought { thought, admitted: true } = tick(&organs(&fresh, &persona, &model), &|| released(), Mode::Live, t(week + 2 * STREAM_MIN_GAP_S)).unwrap() else { panic!("new activity at the next gap mark") };
|
||||||
assert!(thought.stimulus.0.contains("Changes in your repos"), "{}", thought.stimulus.0);
|
assert!(thought.stimulus.0.contains("Changes in your repos"), "{}", thought.stimulus.0);
|
||||||
|
|
||||||
let model = Fake::saying("I turn it over.", &["Yet another"]);
|
let model = Fake::saying("I turn it over.", &["Yet another"]);
|
||||||
|
|
@ -772,3 +780,84 @@ fn every_thought_records_the_fixed_parts_of_its_prompts() {
|
||||||
assert_eq!(model.projected.borrow()[0], rebuilt, "the trace rebuilds the Projector's exact prompt");
|
assert_eq!(model.projected.borrow()[0], rebuilt, "the trace rebuilds the Projector's exact prompt");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// A thought ticked at 900 s and admitted at 1000 s: what landed at 950 s while the models ran is news to the next one.
|
||||||
|
#[test]
|
||||||
|
fn what_lands_while_she_thinks_is_news_to_the_next_thought() {
|
||||||
|
let mind = TestMind::seeded();
|
||||||
|
mind.admit_at(t(950));
|
||||||
|
mind.committed(vec![memory("m9", MemorySort::Decided, "Landed while the Projector ran", &[])]);
|
||||||
|
mind.admit_at(t(1000));
|
||||||
|
mind.committed(vec![thought_doc(&ticked(900), "A drift that took a hundred seconds")]);
|
||||||
|
let model = Fake::saying("I turn it over.", &["A new drift"]);
|
||||||
|
let persona = persona();
|
||||||
|
let wrap = Wrap::over(&mind, &[0.1]).with_forge(vec![push(2, 1000), push(1, 950), push(0, 899)]);
|
||||||
|
let Outcome::Thought { thought, admitted: true } = tick(&organs(&wrap, &persona, &model), &|| released(), Mode::Live, t(1901)).unwrap() else { panic!("news past the gap") };
|
||||||
|
assert!(thought.stimulus.0.contains("Landed while the Projector ran"), "{}", thought.stimulus.0);
|
||||||
|
assert!(thought.stimulus.0.contains("2 commits pushed by libby"), "the pushes at 950 s and 1000 s, not the one at 899 s: {}", thought.stimulus.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_capped_repo_is_judged_by_its_own_events_and_disclosed_when_they_do_not_decide_it() {
|
||||||
|
let mind = mind_with_thought(1000);
|
||||||
|
let read = PersonaRead { state: state(), receipt_id: "r".into() };
|
||||||
|
let cultlib = |id, secs| RepoEvent { repo: "GameCult/CultLib".into(), ..push(id, secs) };
|
||||||
|
let mut two = Wrap::over(&mind, &[]).with_forge(vec![push(2, 1500), cultlib(1, 500)]);
|
||||||
|
two.forge.as_mut().unwrap().capped = vec!["GameCult/Huginn".into(), "GameCult/CultLib".into()];
|
||||||
|
let seed = seed_of(&two, &read, Some(t(1000)), t(1901)).unwrap();
|
||||||
|
assert!(seed.stimulus.to_lowercase().contains("page limit") && seed.stimulus.contains("GameCult/Huginn"), "the repo no event decides stays disclosed: {}", seed.stimulus);
|
||||||
|
assert!(!seed.stimulus.contains("GameCult/CultLib"), "an event before her thought decides CultLib's cap: {}", seed.stimulus);
|
||||||
|
let mut flooded = Wrap::over(&mind, &[]).with_forge(vec![]);
|
||||||
|
flooded.forge.as_mut().unwrap().capped = vec!["GameCult/Huginn".into()];
|
||||||
|
let seed = seed_of(&flooded, &read, Some(t(1000)), t(1901)).unwrap();
|
||||||
|
assert!(seed.news && seed.stimulus.to_lowercase().contains("page limit"), "a cap with no event at all is disclosed: {}", seed.stimulus);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_quiet_hour_reads_the_forge_at_each_gap_mark_only() {
|
||||||
|
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]).with_forge(vec![push(1, 500)]);
|
||||||
|
let organs = organs(&wrap, &persona, &model);
|
||||||
|
let mut secs = 1000 + STREAM_POLL_S;
|
||||||
|
while secs < 1000 + STREAM_IDLE_GAP_S {
|
||||||
|
assert_eq!(tick(&organs, &|| released(), Mode::Live, t(secs)).unwrap(), Outcome::NotDue, "at {secs}");
|
||||||
|
secs += STREAM_POLL_S;
|
||||||
|
}
|
||||||
|
assert_eq!(wrap.forge_reads.get(), 3, "one read for each mark crossed at 900, 1800 and 2700 s");
|
||||||
|
assert_eq!(model.calls(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn identical_memory_lines_are_each_cited_once() {
|
||||||
|
let mind = TestMind::seeded();
|
||||||
|
mind.admit_at(t(100));
|
||||||
|
mind.committed((0..12).map(|at| memory(&format!("m{at:02}"), MemorySort::Learned, &format!("the same thing {}", "x".repeat(290)), &["@libby:chat"])).collect());
|
||||||
|
let read = PersonaRead { state: state(), receipt_id: "r".into() };
|
||||||
|
let seed = seed_of(&Wrap::over(&mind, &[]), &read, Some(t(300)), t(400)).unwrap();
|
||||||
|
let shown = seed.context.lines().filter(|line| line.contains("the same thing")).count();
|
||||||
|
let cited = seed.reads.iter().filter(|read| read.kind == PipelineKind::Memory).count();
|
||||||
|
assert!(shown >= 1 && cited == shown, "{shown} lines shown, {cited} cited");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The environment's proxy settings are read when an agent is built, so the check runs in a child process that has them.
|
||||||
|
#[test]
|
||||||
|
fn the_stream_agent_ignores_the_proxy_environment() {
|
||||||
|
if std::env::var("STREAM_PROXY_CHILD").is_ok() {
|
||||||
|
let plain: ureq::Agent = ureq::Agent::config_builder().build().into();
|
||||||
|
assert!(plain.config().proxy().is_some(), "the child's environment names a proxy");
|
||||||
|
let stream = OllamaStream::open("http://127.0.0.1:9", "p", "b").unwrap();
|
||||||
|
assert!(stream.agent.config().proxy().is_none(), "the stream's agent took the environment's proxy");
|
||||||
|
assert_eq!(stream.agent.config().max_redirects(), 0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let child = std::process::Command::new(std::env::current_exe().unwrap())
|
||||||
|
.args(["--exact", "stream::tests::the_stream_agent_ignores_the_proxy_environment", "--nocapture"])
|
||||||
|
.env("STREAM_PROXY_CHILD", "1")
|
||||||
|
.env("ALL_PROXY", "http://192.0.2.1:3128")
|
||||||
|
.env("HTTP_PROXY", "http://192.0.2.1:3128")
|
||||||
|
.output()
|
||||||
|
.unwrap();
|
||||||
|
assert!(child.status.success(), "{}{}", String::from_utf8_lossy(&child.stdout), String::from_utf8_lossy(&child.stderr));
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue