thought kind: her stream of thought is a document (cut stream-thoughts step 1)

PipelineThought epiphany.pipeline.thought.v2, instance-rooted, validated as
its chosen candidate; ForeignInstance refusal, Answered-by-FollowUp or Recorded
resolutions, facts, a reads citation role, text index. Schemas, golden sample,
huginn schema list regenerated.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Eureka Hands 2026-11-06 22:04:35 +00:00
commit f65a7449fb
24 changed files with 726 additions and 32 deletions

View file

@ -30,6 +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
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

View file

@ -118,7 +118,7 @@ macro_rules! unbounded_scalars {
)*};
}
unbounded_scalars!(u16, u32, u64, bool);
unbounded_scalars!(u8, u16, u32, u64, bool);
fn bound(field: &str, limit: usize, actual: usize) -> Result<(), PipelineRefusal> {
if actual > limit {
@ -622,6 +622,34 @@ fn reply_is_well_formed(reply: &PipelineReply, at: &str) -> Result<(), PipelineR
Ok(())
}
/// The most candidates a continuation may offer, and the nearness that means
/// she had no earlier thought to be near.
pub const THOUGHT_CANDIDATES_MAX: usize = 4;
pub const THOUGHT_NEAREST_NONE_PERMILLE: u16 = 1000;
/// A thought says something it was chosen to say: its text is not blank and is
/// the chosen candidate, every candidate has its nearness, and no nearness is
/// over 1000 thousandths.
fn thought_is_well_formed(thought: &PipelineThought, at: &str) -> Result<(), PipelineRefusal> {
if thought.text.0.trim().is_empty() {
return Err(format_error(&format!("{at}.text"), "a thought with no words"));
}
if thought.candidates.is_empty() || thought.candidates.len() > THOUGHT_CANDIDATES_MAX {
return Err(format_error(&format!("{at}.candidates"), &format!("{} candidates", thought.candidates.len())));
}
if thought.nearest_permille.len() != thought.candidates.len() {
return Err(format_error(&format!("{at}.nearest_permille"), "a nearness list that is not one per candidate"));
}
if let Some(over) = thought.nearest_permille.iter().position(|near| *near > THOUGHT_NEAREST_NONE_PERMILLE) {
return Err(format_error(&format!("{at}.nearest_permille[{over}]"), &thought.nearest_permille[over].to_string()));
}
match thought.candidates.get(usize::from(thought.chosen)) {
None => Err(format_error(&format!("{at}.chosen"), &thought.chosen.to_string())),
Some(chosen) if chosen.0 != thought.text.0 => Err(format_error(&format!("{at}.text"), "a thought that is not its chosen candidate")),
Some(_) => Ok(()),
}
}
/// The most thousandths a memory's salience may carry.
pub const SALIENCE_MAX_PERMILLE: u16 = 1000;
@ -841,6 +869,22 @@ value_types! {
/// Instance-rooted: the key is `<instance>:reply:<label>`. Not semantically
/// indexed. Whether the run is hers is admission's rule.
pub struct PipelineReply { instance: Slug, label: Label, run: PipelineRef, answers: Vec<PipelineRef>[64], text: Para } => reply_is_well_formed
/// One tick of her stream of thought, offline: the words a base model
/// continued from a projection of her own state, with the whole trace that
/// made them (the seed, the projection, every candidate and how near each
/// was to her earlier thoughts). `text` is the chosen candidate. `reads`
/// are the documents the seed was drawn from. A thought is divergence for
/// delivery to weigh, never a memory: its words reach a later seed only
/// 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.
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,
} => 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
/// her and another Mind or person. `text` is her own words. `about` names
@ -1181,6 +1225,8 @@ pipeline_kinds! {
"epiphany.pipeline.idea.v2", "EpiphanyPipelineIdeaDocument";
Reply(PipelineReply) => EpiphanyPipelineReplyDocument, "reply",
"epiphany.pipeline.reply.v2", "EpiphanyPipelineReplyDocument";
Thought(PipelineThought) => EpiphanyPipelineThoughtDocument, "thought",
"epiphany.pipeline.thought.v2", "EpiphanyPipelineThoughtDocument";
}
/// Parses a full document id: the grammar read backwards. A key and an id are
@ -1378,6 +1424,7 @@ pub fn pipeline_key(document: &PipelineDocument) -> Result<String, PipelineRefus
D::Run(value) => ("run.instance", value.instance.0.as_str(), local(&key_field, document_kind, &[&value.label.0])?),
D::Pulse(value) => ("pulse.instance", value.instance.0.as_str(), local(&key_field, document_kind, &[&value.label.0])?),
D::Reply(value) => ("reply.instance", value.instance.0.as_str(), local(&key_field, document_kind, &[&value.label.0])?),
D::Thought(value) => ("thought.instance", value.instance.0.as_str(), local(&key_field, document_kind, &[&value.label.0])?),
D::Memory(value) => ("memory.instance", value.instance.0.as_str(), local(&key_field, document_kind, &[&value.label.0])?),
D::Research(value) => (campaign_field.as_str(), value.campaign.0.as_str(), local(&key_field, document_kind, &[&value.pass.0, &value.label.0])?),
D::Idea(value) => (campaign_field.as_str(), value.campaign.0.as_str(), local(&key_field, document_kind, &[&value.pass.0, &value.label.0])?),
@ -1658,6 +1705,17 @@ mod tests {
answers: vec![PipelineRef { kind: PipelineKind::FollowUp, id: id("follow_up", "ask-the-self") }],
text: "The shader pass is done; the sounds are Libby's.".into(),
}), format!("{INSTANCE}:reply:mind-2026-10-09-1-r1")),
(D::Thought(PipelineThought {
instance: slug(INSTANCE), label: l("t1793000000"),
text: "The sounds are Libby's; the shaders were never the hard part.".into(),
reads: vec![PipelineRef { kind: PipelineKind::Memory, id: Short(format!("{INSTANCE}:memory:2026-10-31t041200-t0-m0")) }],
persona_receipt: s("mind-commit-0a1b"), identity: "Eureka, she/her, plain-spoken.".into(),
context: "Values: coherence over velocity.".into(), stimulus: "Libby asked about the sounds.".into(),
projector_model: s("projector:latest"), projection: "I keep turning over who holds the sounds.".into(), base_model: s("base:latest"),
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,
}), format!("{INSTANCE}:thought:t1793000000")),
]
}
@ -1992,6 +2050,35 @@ mod tests {
}
}
/// A thought is its chosen candidate, with one nearness per candidate, in bounds.
#[test]
fn malformed_thought_refused() {
let validated = |change: &dyn Fn(&mut PipelineThought)| {
let PipelineDocument::Thought(mut thought) = samples().remove(19).0 else { unreachable!() };
change(&mut thought);
PipelineDocument::Thought(thought).validate()
};
assert_eq!(validated(&|_| {}), Ok(()));
assert_eq!(validated(&|thought| { thought.chosen = 1; thought.text = thought.candidates[1].0.as_str().into(); }), Ok(()));
assert_eq!(validated(&|thought| { thought.candidates.truncate(1); thought.nearest_permille.truncate(1); }), Ok(()));
assert_eq!(validated(&|thought| thought.nearest_permille[0] = 1000), Ok(()), "1000 means no earlier thought");
for (change, field) in [
(&(|thought: &mut PipelineThought| { thought.text = " \n".into(); thought.candidates[0] = " \n".into(); }) as &dyn Fn(&mut PipelineThought), "thought.text"),
(&|thought: &mut PipelineThought| thought.text = "Another thing entirely.".into(), "thought.text"),
(&|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.candidates.clear(); thought.nearest_permille.clear(); }, "thought.candidates"),
] {
assert!(matches!(validated(change), Err(PipelineRefusal::InvalidFormat { field: f, .. }) if f == field), "{field}");
}
let five = |thought: &mut PipelineThought| {
thought.candidates = vec![thought.text.0.as_str().into(); 5];
thought.nearest_permille = vec![0; 5];
};
assert!(matches!(validated(&five), Err(PipelineRefusal::FieldBound { .. })), "five candidates are over the bound");
}
/// A pulse votes a tempo in bounds, declares an urgency in bounds, or does both; a pulse that does neither is refused.
#[test]
fn a_pulse_is_in_bounds_and_says_something() {
@ -3691,11 +3778,11 @@ mod tests {
};
let document_schema = serde_json::to_value(schemars::schema_for!(PipelineDocument))?;
let variants = document_schema["oneOf"].as_array().expect("the document schema is a oneOf");
assert_eq!(PipelineKind::ALL.len(), 19, "nineteen kinds");
assert_eq!(PipelineKind::ALL.len(), 20, "twenty kinds");
assert_eq!(
variants.iter().map(tag).collect::<Vec<_>>(),
PipelineKind::ALL.iter().map(|kind| kind.name().to_owned()).collect::<Vec<_>>(),
"the document schema lists exactly the nineteen kinds, by name"
"the document schema lists exactly the twenty kinds, by name"
);
let refusal_schema = serde_json::to_value(schemars::schema_for!(PipelineRefusal))?;

View file

@ -20,7 +20,7 @@ macro_rules! kinds {
kinds!(
campaign, target, question, ruling, cut_spec, cut_report, verdict, finding, follow_up, resolution, instance, stewardship,
hand_off, run, pulse, memory, research, idea, reply,
hand_off, run, pulse, memory, research, idea, reply, thought,
);
/// The schema named, or the names when none is: the exit code is 2 for a name

View file

@ -1140,12 +1140,12 @@ fn schema_prints_the_published_files() {
let names = run(&[], &["schema"], None);
assert_eq!(names.code, 0);
let names: Vec<&str> = names.out.lines().collect();
assert_eq!(names.len(), 20, "{names:?}");
assert_eq!(names.len(), 21, "{names:?}");
assert_eq!(names[0], "campaign");
assert_eq!(names[14], "pulse");
assert_eq!(names[15], "memory");
assert_eq!((names[16], names[17]), ("research", "idea"));
assert_eq!((names[18], names[19]), ("reply", "selection"));
assert_eq!((names[18], names[19], names[20]), ("reply", "thought", "selection"));
for kind in PipelineKind::ALL {
assert!(names.contains(&kind.name()), "{}", kind.name());
}

View file

@ -314,6 +314,7 @@ fn refuse_foreign_instance(document: &PipelineDocument, mind: &Slug) -> Result<(
PipelineDocument::Run(value) if value.instance != *mind => Err(foreign(&value.instance)),
PipelineDocument::Pulse(value) if value.instance != *mind => Err(foreign(&value.instance)),
PipelineDocument::Reply(value) if value.instance != *mind => Err(foreign(&value.instance)),
PipelineDocument::Thought(value) if value.instance != *mind => Err(foreign(&value.instance)),
PipelineDocument::Memory(value) if value.instance != *mind => Err(foreign(&value.instance)),
PipelineDocument::HandOff(value) if value.from_instance != *mind && value.to_instance != *mind => {
Err(foreign(&value.from_instance))
@ -624,7 +625,7 @@ fn check(docs: &Docs<'_>, staged: &Staged, mind: &Slug) -> Result<(), MindRefusa
let replay = docs.in_image(K::Run, key) == Some(&staged.document);
run_rule(docs, key, run, replay)
}
D::FollowUp(_) | D::Instance(_) | D::Pulse(_) | D::Reply(_) | D::Memory(_) | D::Research(_) | D::Idea(_) => Ok(()),
D::FollowUp(_) | D::Instance(_) | D::Pulse(_) | D::Reply(_) | D::Thought(_) | D::Memory(_) | D::Research(_) | D::Idea(_) => Ok(()),
}
}
@ -730,6 +731,8 @@ fn matrix(subject: PipelineKind, outcome: &ResolutionOutcome) -> bool {
(K::Idea, O::Recorded { .. } | O::Withdrawn { .. }) => true,
(K::Reply, O::Answered { by }) => by.kind == K::FollowUp,
(K::Reply, O::Recorded { .. }) => true,
(K::Thought, O::Answered { by }) => by.kind == K::FollowUp,
(K::Thought, O::Recorded { .. }) => true,
(K::Resolution, O::Withdrawn { .. }) => true,
_ => false,
}
@ -2498,6 +2501,44 @@ mod tests {
committed(admit(&mut mind, vec![reply(INSTANCE)]));
}
/// A thought is her own: one of another mind is a ForeignInstance, her own is admitted.
#[test]
fn a_thought_of_another_mind_is_a_foreign_instance_and_her_own_is_admitted() {
let mut mind = with_specs();
let D::Thought(mut foreign) = crate::fixtures::thought("t1", "A drift.", &[]) else { unreachable!() };
foreign.instance = slug(OTHER_INSTANCE);
assert_eq!(
refusal(admit(&mut mind, vec![D::Thought(foreign)])),
MindRefusal::ForeignInstance { declared: OTHER_INSTANCE.into(), mind: INSTANCE.into() }
);
committed(admit(&mut mind, vec![crate::fixtures::thought("t1", "A drift.", &[])]));
}
/// A thought resolves Answered by a follow-up or Recorded; no other outcome.
#[test]
fn a_thought_resolves_answered_by_a_follow_up_or_recorded_and_nothing_else() {
let incompatible = |outcome: &str| MindRefusal::IncompatibleResolution { subject_kind: K::Thought, outcome: outcome.into() };
let subject = || r(K::Thought, &format!("{INSTANCE}:thought:t1"));
let fu = || r(K::FollowUp, &id("follow_up", "FU1"));
let open = || {
let mut mind = with_specs();
committed(admit(&mut mind, vec![D::Ruling(ruling("R1")), follow_up("FU1", r(K::Ruling, &id("ruling", "R1")))]));
committed(admit(&mut mind, vec![crate::fixtures::thought("t1", "A drift.", &[])]));
mind
};
committed(admit(&mut open(), vec![resolution(subject(), ResolutionOutcome::Answered { by: fu() })]));
committed(admit(&mut open(), vec![resolution(subject(), ResolutionOutcome::Recorded { reason: "weighed".into() })]));
let refused = [
(ResolutionOutcome::Answered { by: r(K::Ruling, &id("ruling", "R1")) }, "Answered"),
(ResolutionOutcome::Fixed { commit: sha(), by: None }, "Fixed"),
(ResolutionOutcome::Deferred { to: fu() }, "Deferred"),
(withdrawn(), "Withdrawn"),
];
for (outcome, name) in refused {
assert_eq!(refusal(admit(&mut open(), vec![resolution(subject(), outcome)])), incompatible(name), "{name}");
}
}
fn memory_key(label: &str) -> String {
format!("{INSTANCE}:memory:{label}")
}

View file

@ -344,7 +344,8 @@ pub(crate) fn line_of(document: &PipelineDocument) -> Option<Lineage<'_>> {
| D::HandOff(_)
| D::Run(_)
| D::Pulse(_)
| D::Reply(_) => None,
| D::Reply(_)
| D::Thought(_) => None,
}
}
@ -394,10 +395,11 @@ pub enum CitationRole {
RestsOn,
Toward,
Run,
Reads,
}
impl CitationRole {
pub const ALL: [CitationRole; 22] = [
pub const ALL: [CitationRole; 23] = [
Self::RaisedIn,
Self::Answers,
Self::Rulings,
@ -420,6 +422,7 @@ impl CitationRole {
Self::RestsOn,
Self::Toward,
Self::Run,
Self::Reads,
];
/// The wire spelling, equal to the referring field's name.
@ -447,6 +450,7 @@ impl CitationRole {
Self::RestsOn => "rests_on",
Self::Toward => "toward",
Self::Run => "run",
Self::Reads => "reads",
}
}
@ -472,6 +476,7 @@ impl CitationRole {
Self::Serves | Self::Sources => K::Research,
Self::RestsOn | Self::Toward => K::Idea,
Self::Run => K::Reply,
Self::Reads => K::Thought,
}
}
@ -500,7 +505,8 @@ impl CitationRole {
| Self::Serves
| Self::Sources
| Self::RestsOn
| Self::Toward => PipelineKind::ALL.to_vec(),
| Self::Toward
| Self::Reads => PipelineKind::ALL.to_vec(),
}
}
}
@ -573,6 +579,7 @@ pub(crate) fn citations(document: &PipelineDocument) -> Vec<(CitationRole, Pipel
),
D::Run(run) => edges.extend(run.claims.iter().map(|to| (R::Claims, to.clone()))),
D::Reply(reply) => edges.push((R::Run, reply.run.clone())),
D::Thought(thought) => edges.extend(thought.reads.iter().map(|to| (R::Reads, to.clone()))),
D::Memory(memory) => edges.extend(memory.consolidates.iter().map(|to| (R::Consolidates, to.clone()))),
D::Research(research) => {
edges.extend(research.serves.iter().map(|to| (R::Serves, to.clone())));

View file

@ -9,7 +9,7 @@ use eureka_pipeline::{
FindingConfidence, FindingOrigin, FindingSeverity, Label, Line, MemoryOrigin, MemorySort, MutationRecord, OrgRepo, PipelineCampaign,
PipelineCutReport, PipelineCutSpec, PipelineDocument, PipelineFinding, PipelineFollowUp, PipelineHandOff,
PipelineInstance, PipelineKind, PipelineMemory, PipelineQuestion, PipelineRef, PipelineResolution, PipelineRuling,
PipelineRun, PipelineStewardship, PipelineTarget, PipelineVerdict, Promise, QuestionOption, ReportCommit,
PipelineThought, PipelineRun, PipelineStewardship, PipelineTarget, PipelineVerdict, Promise, QuestionOption, ReportCommit,
ResolutionOutcome, RulingAuthority, RunOperator, RunTurn, Sha, Short, Slug, StructuralDelta, TargetInvariant, VerdictClaim,
};
@ -354,6 +354,31 @@ fn run_of(label: &str, operated_by: RunOperator, turn: RunTurn, claims: &[Pipeli
})
}
/// One of her thoughts: the chosen candidate is `text`, the only candidate, as near as 1000 (no earlier thought).
pub(crate) fn thought(label: &str, text: &str, reads: &[PipelineRef]) -> PipelineDocument {
PipelineDocument::Thought(PipelineThought {
instance: slug(INSTANCE),
label: l(label),
text: text.into(),
reads: reads.to_vec(),
persona_receipt: s("mind-commit-0"),
identity: "Eureka".into(),
context: "".into(),
stimulus: "".into(),
projector_model: s("projector"),
projection: "A projection.".into(),
base_model: s("base"),
temperature_permille: 1000,
min_p_permille: 50,
max_tokens: 160,
candidates: vec![text.into()],
nearest_permille: vec![1000],
chosen: 0,
projector_ms: 1,
continuer_ms: 1,
})
}
/// One of her memories: the sort and the text, about `about`, learned in a room, not direct.
pub(crate) fn memory(label: &str, sort: MemorySort, text: &str, about: &[&str]) -> PipelineDocument {
PipelineDocument::Memory(PipelineMemory {

View file

@ -85,6 +85,7 @@ pub fn index_text(document: &PipelineDocument) -> Option<String> {
D::Resolution(resolution) => lines.push(&resolution.rationale.0),
D::HandOff(hand_off) => lines.push(&hand_off.reason.0),
D::Memory(memory) => lines.push(&memory.text.0),
D::Thought(thought) => lines.push(&thought.text.0),
D::Research(research) => {
lines.push(&research.claim.0);
lines.push(&research.detail.0);

View file

@ -148,6 +148,7 @@ pub enum PipelineFacts {
Run { label: Label, operated_by: RunOperator, turn: RunTurn, started_on: Date, claims: Vec<PipelineRef> },
Pulse { label: Label, tempo_vote_beat_s: Option<u32>, #[serde(default)] tempo_vote_withdrawn: bool, urgency_permille: Option<u16> },
Reply { label: Label, run: PipelineRef, answers: Vec<PipelineRef> },
Thought { label: Label, chosen: u8, nearest_permille: u16 },
Memory { label: Label, sort: MemorySort, about: Vec<Short>, origin: MemoryOrigin, salience_permille: u16 },
Research { pass: Label, label: Label, claim: Line, inferred: bool, observed_on: Date },
Idea { pass: Label, label: Label, title: Title, proposed_on: Date },
@ -256,6 +257,11 @@ impl PipelineFacts {
urgency_permille: value.urgency_permille,
},
D::Reply(value) => Self::Reply { label: value.label.clone(), run: value.run.clone(), answers: value.answers.clone() },
D::Thought(value) => Self::Thought {
label: value.label.clone(),
chosen: value.chosen,
nearest_permille: value.nearest_permille.get(usize::from(value.chosen)).copied().unwrap_or(1000),
},
D::Memory(value) => Self::Memory {
label: value.label.clone(),
sort: value.sort,
@ -1702,6 +1708,16 @@ mod tests {
/// One edge list: every role's edge comes out of the hop, typed by the
/// role and the ends' kinds.
#[test]
fn a_thoughts_facts_carry_the_chosen_candidates_nearness() {
let D::Thought(mut thought) = crate::fixtures::thought("t1", "A.", &[]) else { unreachable!() };
thought.candidates.push("B.".into());
thought.nearest_permille = vec![300, 450];
thought.chosen = 1;
thought.text = "B.".into();
assert_eq!(PipelineFacts::of(&D::Thought(thought)), PipelineFacts::Thought { label: "t1".into(), chosen: 1, nearest_permille: 450 });
}
#[test]
fn every_role_yields_its_edge() {
let mut mind = seeded();
@ -1742,6 +1758,8 @@ mod tests {
committed(admit(&mut mind, vec![D::Reply(eureka_pipeline::PipelineReply {
instance: INSTANCE.into(), label: "w1-r1".into(), run: r(K::Run, &run_id), answers: vec![r(K::FollowUp, &fu)], text: "Done.".into(),
})]));
let thought_id = format!("{INSTANCE}:thought:t1");
committed(admit(&mut mind, vec![crate::fixtures::thought("t1", "A drift.", &[r(K::Finding, &f1), r(K::Question, &q1)])]));
let (older, newer) = (format!("{INSTANCE}:memory:m1"), format!("{INSTANCE}:memory:m2"));
committed(admit(&mut mind, vec![memory("m1", MemorySort::Idea, "one", &[])]));
let D::Memory(mut consolidating) = memory("m2", MemorySort::Idea, "two", &[]) else { unreachable!() };
@ -1792,13 +1810,15 @@ mod tests {
(CitationRole::Claims, &run_id, &spec_r2),
(CitationRole::Claims, &run_id, &fu),
(CitationRole::Run, &reply_id, &run_id),
(CitationRole::Reads, &thought_id, &f1),
(CitationRole::Reads, &thought_id, &q1),
(CitationRole::Consolidates, &newer, &older),
(CitationRole::Serves, &note, &q1),
(CitationRole::Sources, &note, &f1),
(CitationRole::RestsOn, &way, &note),
(CitationRole::Toward, &way, &q1),
];
assert_eq!(CitationRole::ALL.len(), 22);
assert_eq!(CitationRole::ALL.len(), 23);
for role in CitationRole::ALL {
let selection =
Selection { cited: Some(Incoming { role: role.name().into(), exists: true }), ..Selection::default() };

View file

@ -244,7 +244,7 @@ impl SelectionRow {
D::FollowUp(follow_up) => vec![follow_up.repo.identity()],
D::Stewardship(stewardship) => vec![stewardship.repo.identity()],
D::HandOff(hand_off) => vec![hand_off.repo.identity()],
D::Target(_) | D::Question(_) | D::Ruling(_) | D::Verdict(_) | D::Finding(_) | D::Instance(_) | D::Resolution(_) | D::Run(_) | D::Pulse(_) | D::Reply(_) | D::Memory(_) | D::Research(_) | D::Idea(_) => {
D::Target(_) | D::Question(_) | D::Ruling(_) | D::Verdict(_) | D::Finding(_) | D::Instance(_) | D::Resolution(_) | D::Run(_) | D::Pulse(_) | D::Reply(_) | D::Thought(_) | D::Memory(_) | D::Research(_) | D::Idea(_) => {
Vec::new()
}
},
@ -329,7 +329,7 @@ impl Row for SelectionRow {
}
}
/// The organ's closed lists: nineteen schemas, twenty aliases, twenty-two roles.
/// The organ's closed lists: twenty schemas, twenty aliases, twenty-three roles.
/// A schema's name is its id, so the substrate's alias matching resolves
/// nothing beyond exact ids.
pub(crate) struct Vocabulary;
@ -375,7 +375,7 @@ fn invalid(field: impl Into<String>, value: &str, message: impl Into<String>) ->
/// because `OrgRepo` identity is case-insensitive.
///
/// Refuses, naming the field the client sent: a `schemas` entry that is not
/// one of the nineteen ids; a `keys` entry that is not a full id whose kind
/// one of the twenty ids; a `keys` entry that is not a full id whose kind
/// segment reads and passes the leaf's ref grammar; an `any_of` value outside
/// its alias's domain; and a `cites.target` whose key fails the ref grammar
/// for its schema's kind (V24's rule, at this door). Whether the target
@ -442,7 +442,7 @@ mod tests {
/// refused at the substrate's door.
#[test]
fn each_kind_declares_exactly_its_aliases_and_none_is_numeric() {
let table: [(K, &[&str]); 19] = [
let table: [(K, &[&str]); 20] = [
(K::Campaign, &["repo"]),
(K::Target, &[]),
(K::Question, &[]),
@ -462,6 +462,7 @@ mod tests {
(K::Research, &["pass"]),
(K::Idea, &["pass"]),
(K::Reply, &[]),
(K::Thought, &[]),
];
for (kind, extra) in table {
let want = [&["root", "in_force", "faculty"][..], extra].concat();
@ -474,10 +475,10 @@ mod tests {
/// Each role is declared by the one kind whose field carries it, and its
/// targets are the kinds that field is typed to, "any" spelled as all
/// nineteen.
/// twenty.
#[test]
fn each_kind_declares_the_roles_it_carries_and_their_targets() {
let table: [(K, &[&str]); 19] = [
let table: [(K, &[&str]); 20] = [
(K::Campaign, &[]),
(K::Target, &[]),
(K::Question, &["raised_in"]),
@ -497,13 +498,14 @@ mod tests {
(K::Research, &["serves", "sources"]),
(K::Idea, &["rests_on", "toward"]),
(K::Reply, &["run"]),
(K::Thought, &["reads"]),
];
for (kind, roles) in table {
assert_eq!(sorted(Vocabulary.declared_roles(kind.type_id())), strings(roles), "{kind:?}");
}
assert_eq!(Vocabulary.target_leaves("answers"), vec![K::Question.type_id().to_string()]);
assert_eq!(Vocabulary.target_leaves("rulings"), vec![K::Ruling.type_id().to_string()]);
assert_eq!(Vocabulary.target_leaves("subject").len(), 19);
assert_eq!(Vocabulary.target_leaves("subject").len(), 20);
assert_eq!(
sorted(Vocabulary.target_leaves("claims")),
sorted([K::CutSpec, K::Finding, K::FollowUp].iter().map(|kind| kind.type_id().to_string()).collect())

View file

@ -176,7 +176,8 @@
"Memory",
"Research",
"Idea",
"Reply"
"Reply",
"Thought"
],
"type": "string"
},

View file

@ -70,7 +70,8 @@
"Memory",
"Research",
"Idea",
"Reply"
"Reply",
"Thought"
],
"type": "string"
},

View file

@ -35,7 +35,8 @@
"Memory",
"Research",
"Idea",
"Reply"
"Reply",
"Thought"
],
"type": "string"
},

View file

@ -131,7 +131,8 @@
"Memory",
"Research",
"Idea",
"Reply"
"Reply",
"Thought"
],
"type": "string"
},

View file

@ -40,7 +40,8 @@
"Memory",
"Research",
"Idea",
"Reply"
"Reply",
"Thought"
],
"type": "string"
},

View file

@ -30,7 +30,8 @@
"Memory",
"Research",
"Idea",
"Reply"
"Reply",
"Thought"
],
"type": "string"
},

View file

@ -102,7 +102,8 @@
"Memory",
"Research",
"Idea",
"Reply"
"Reply",
"Thought"
],
"type": "string"
},

View file

@ -35,7 +35,8 @@
"Memory",
"Research",
"Idea",
"Reply"
"Reply",
"Thought"
],
"type": "string"
},

View file

@ -73,7 +73,8 @@
"Memory",
"Research",
"Idea",
"Reply"
"Reply",
"Thought"
],
"type": "string"
},

View file

@ -30,7 +30,8 @@
"Memory",
"Research",
"Idea",
"Reply"
"Reply",
"Thought"
],
"type": "string"
},

View file

@ -0,0 +1,186 @@
{
"$defs": {
"Label": {
"description": "A key label: `[A-Za-z0-9_-]{1,64}`.",
"maxLength": 64,
"type": "string"
},
"Line": {
"description": "Text of at most 1,000 UTF-8 bytes.",
"maxLength": 1000,
"type": "string"
},
"Para": {
"description": "Text of at most 4,000 UTF-8 bytes; longer narrative is cited by `DocRef`.",
"maxLength": 4000,
"type": "string"
},
"PipelineKind": {
"enum": [
"Campaign",
"Target",
"Question",
"Ruling",
"CutSpec",
"CutReport",
"Verdict",
"Finding",
"FollowUp",
"Resolution",
"Instance",
"Stewardship",
"HandOff",
"Run",
"Pulse",
"Memory",
"Research",
"Idea",
"Reply",
"Thought"
],
"type": "string"
},
"PipelineRef": {
"description": "A reference to another document. The id is parsed as a full pipeline id of\nthe declared `kind`, so the kind and the id's kind segment cannot disagree.",
"properties": {
"id": {
"$ref": "#/$defs/Short"
},
"kind": {
"$ref": "#/$defs/PipelineKind"
}
},
"required": [
"kind",
"id"
],
"type": "object"
},
"Short": {
"description": "Text of at most 200 UTF-8 bytes.",
"maxLength": 200,
"type": "string"
},
"Slug": {
"description": "A campaign slug or key segment: label parts joined by `.`.",
"maxLength": 64,
"type": "string"
}
},
"$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.",
"properties": {
"base_model": {
"$ref": "#/$defs/Short"
},
"candidates": {
"items": {
"$ref": "#/$defs/Line"
},
"maxItems": 4,
"type": "array"
},
"chosen": {
"format": "uint8",
"maximum": 255,
"minimum": 0,
"type": "integer"
},
"context": {
"$ref": "#/$defs/Para"
},
"continuer_ms": {
"format": "uint32",
"minimum": 0,
"type": "integer"
},
"identity": {
"$ref": "#/$defs/Line"
},
"instance": {
"$ref": "#/$defs/Slug"
},
"label": {
"$ref": "#/$defs/Label"
},
"max_tokens": {
"format": "uint16",
"maximum": 65535,
"minimum": 0,
"type": "integer"
},
"min_p_permille": {
"format": "uint16",
"maximum": 65535,
"minimum": 0,
"type": "integer"
},
"nearest_permille": {
"items": {
"format": "uint16",
"maximum": 65535,
"minimum": 0,
"type": "integer"
},
"maxItems": 4,
"type": "array"
},
"persona_receipt": {
"$ref": "#/$defs/Short"
},
"projection": {
"$ref": "#/$defs/Para"
},
"projector_model": {
"$ref": "#/$defs/Short"
},
"projector_ms": {
"format": "uint32",
"minimum": 0,
"type": "integer"
},
"reads": {
"items": {
"$ref": "#/$defs/PipelineRef"
},
"maxItems": 64,
"type": "array"
},
"stimulus": {
"$ref": "#/$defs/Para"
},
"temperature_permille": {
"format": "uint16",
"maximum": 65535,
"minimum": 0,
"type": "integer"
},
"text": {
"$ref": "#/$defs/Para"
}
},
"required": [
"instance",
"label",
"text",
"reads",
"persona_receipt",
"identity",
"context",
"stimulus",
"projector_model",
"projection",
"base_model",
"temperature_permille",
"min_p_permille",
"max_tokens",
"candidates",
"nearest_permille",
"chosen",
"projector_ms",
"continuer_ms"
],
"title": "PipelineThought",
"type": "object"
}

View file

@ -1326,6 +1326,22 @@
"kind",
"value"
]
},
{
"type": "object",
"properties": {
"kind": {
"type": "string",
"const": "thought"
},
"value": {
"$ref": "#/$defs/PipelineThought"
}
},
"required": [
"kind",
"value"
]
}
]
},
@ -1580,7 +1596,8 @@
"Memory",
"Research",
"Idea",
"Reply"
"Reply",
"Thought"
]
},
"PipelineMemory": {
@ -2097,6 +2114,121 @@
"doc"
]
},
"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.",
"type": "object",
"properties": {
"base_model": {
"$ref": "#/$defs/Short"
},
"candidates": {
"type": "array",
"items": {
"$ref": "#/$defs/Line"
},
"maxItems": 4
},
"chosen": {
"type": "integer",
"format": "uint8",
"maximum": 255,
"minimum": 0
},
"context": {
"$ref": "#/$defs/Para"
},
"continuer_ms": {
"type": "integer",
"format": "uint32",
"minimum": 0
},
"identity": {
"$ref": "#/$defs/Line"
},
"instance": {
"$ref": "#/$defs/Slug"
},
"label": {
"$ref": "#/$defs/Label"
},
"max_tokens": {
"type": "integer",
"format": "uint16",
"maximum": 65535,
"minimum": 0
},
"min_p_permille": {
"type": "integer",
"format": "uint16",
"maximum": 65535,
"minimum": 0
},
"nearest_permille": {
"type": "array",
"items": {
"type": "integer",
"format": "uint16",
"maximum": 65535,
"minimum": 0
},
"maxItems": 4
},
"persona_receipt": {
"$ref": "#/$defs/Short"
},
"projection": {
"$ref": "#/$defs/Para"
},
"projector_model": {
"$ref": "#/$defs/Short"
},
"projector_ms": {
"type": "integer",
"format": "uint32",
"minimum": 0
},
"reads": {
"type": "array",
"items": {
"$ref": "#/$defs/PipelineRef"
},
"maxItems": 64
},
"stimulus": {
"$ref": "#/$defs/Para"
},
"temperature_permille": {
"type": "integer",
"format": "uint16",
"maximum": 65535,
"minimum": 0
},
"text": {
"$ref": "#/$defs/Para"
}
},
"required": [
"instance",
"label",
"text",
"reads",
"persona_receipt",
"identity",
"context",
"stimulus",
"projector_model",
"projection",
"base_model",
"temperature_permille",
"min_p_permille",
"max_tokens",
"candidates",
"nearest_permille",
"chosen",
"projector_ms",
"continuer_ms"
]
},
"PipelineVerdict": {
"type": "object",
"properties": {

View file

@ -261,7 +261,8 @@
"sources",
"rests_on",
"toward",
"run"
"run",
"reads"
]
},
"ClaimOutcome": {
@ -2629,6 +2630,22 @@
"kind",
"value"
]
},
{
"type": "object",
"properties": {
"kind": {
"type": "string",
"const": "thought"
},
"value": {
"$ref": "#/$defs/PipelineThought"
}
},
"required": [
"kind",
"value"
]
}
]
},
@ -3382,6 +3399,44 @@
"value"
]
},
{
"type": "object",
"properties": {
"kind": {
"type": "string",
"const": "thought"
},
"value": {
"type": "object",
"properties": {
"chosen": {
"type": "integer",
"format": "uint8",
"maximum": 255,
"minimum": 0
},
"label": {
"$ref": "#/$defs/Label"
},
"nearest_permille": {
"type": "integer",
"format": "uint16",
"maximum": 65535,
"minimum": 0
}
},
"required": [
"label",
"chosen",
"nearest_permille"
]
}
},
"required": [
"kind",
"value"
]
},
{
"type": "object",
"properties": {
@ -3757,7 +3812,8 @@
"Memory",
"Research",
"Idea",
"Reply"
"Reply",
"Thought"
]
},
"PipelineMemory": {
@ -4561,6 +4617,121 @@
"doc"
]
},
"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.",
"type": "object",
"properties": {
"base_model": {
"$ref": "#/$defs/Short"
},
"candidates": {
"type": "array",
"items": {
"$ref": "#/$defs/Line"
},
"maxItems": 4
},
"chosen": {
"type": "integer",
"format": "uint8",
"maximum": 255,
"minimum": 0
},
"context": {
"$ref": "#/$defs/Para"
},
"continuer_ms": {
"type": "integer",
"format": "uint32",
"minimum": 0
},
"identity": {
"$ref": "#/$defs/Line"
},
"instance": {
"$ref": "#/$defs/Slug"
},
"label": {
"$ref": "#/$defs/Label"
},
"max_tokens": {
"type": "integer",
"format": "uint16",
"maximum": 65535,
"minimum": 0
},
"min_p_permille": {
"type": "integer",
"format": "uint16",
"maximum": 65535,
"minimum": 0
},
"nearest_permille": {
"type": "array",
"items": {
"type": "integer",
"format": "uint16",
"maximum": 65535,
"minimum": 0
},
"maxItems": 4
},
"persona_receipt": {
"$ref": "#/$defs/Short"
},
"projection": {
"$ref": "#/$defs/Para"
},
"projector_model": {
"$ref": "#/$defs/Short"
},
"projector_ms": {
"type": "integer",
"format": "uint32",
"minimum": 0
},
"reads": {
"type": "array",
"items": {
"$ref": "#/$defs/PipelineRef"
},
"maxItems": 64
},
"stimulus": {
"$ref": "#/$defs/Para"
},
"temperature_permille": {
"type": "integer",
"format": "uint16",
"maximum": 65535,
"minimum": 0
},
"text": {
"$ref": "#/$defs/Para"
}
},
"required": [
"instance",
"label",
"text",
"reads",
"persona_receipt",
"identity",
"context",
"stimulus",
"projector_model",
"projection",
"base_model",
"temperature_permille",
"min_p_permille",
"max_tokens",
"candidates",
"nearest_permille",
"chosen",
"projector_ms",
"continuer_ms"
]
},
"PipelineVerdict": {
"type": "object",
"properties": {

View file

@ -221,6 +221,17 @@
"title": "Epiphany Pipeline Reply v2",
"path": "epiphany.pipeline.reply.v2.schema.json"
},
{
"schemaId": "https://gamecult.dev/epiphany/cultnet/epiphany.pipeline.thought.v2.schema.json",
"kind": "document_payload",
"wireContracts": [
"cultnet.schema.v0"
],
"schemaVersion": "epiphany.pipeline.thought.v2",
"documentType": "epiphany.pipeline.thought.v2",
"title": "Epiphany Pipeline Thought v2",
"path": "epiphany.pipeline.thought.v2.schema.json"
},
{
"schemaId": "huginn.mind_request.v1",
"kind": "wire_message",