A memory that fades
Each night your forgetting curve wears memories down. One is pushed out of reach, one is gone, and the history still remembers them.
Each bar is the confidence of Nell’s Holding that night, read back with holdings. Striped: suppressed. Empty: forgotten.
The idea
How fast a memory fades is a design decision: a number slips sooner than a face, and fear can bury what it cannot bear. That curve is your code. What Umwelt keeps is what the curve did, night by night, as records you can read at any point in the story.
Your game decides
- how vivid each memory was
- how much of it survives each night
- when a memory is lost for good
- what trauma hides, and what brings it back
Umwelt records
- a new confidence, as a superseding copy
suppressedandavailableagainforgotten, from that night on- everything before, still readable
The night of the storm
Hollow Pines, a hunting lodge in the storm. Nell and Jonah. Jonah goes down into the cellar, and Nell sees something move behind him.
She remembers four things. Canon agrees with three of them: the radio never worked.
Night zero
One commit: what happened, as Facts, and what Nell remembers, as Holdings with the attitude remembers. Her memory of the radio is a valid Holding even though canon says otherwise.
// The night of the storm. Nell remembers four things; one of them is wrong.
const memories: Record<string, { claim: Claim; salience: number }> = {
code: { claim: claim.value('cellar-door', 'code', '4471'), salience: 0.72 },
jonah: { claim: claim.entity('jonah', 'went-into', 'cellar'), salience: 0.92 },
radio: { claim: claim.unary('radio', 'works'), salience: 0.85 },
shape: { claim: claim.entity('something', 'moved-in', 'cellar'), salience: 0.98 },
};
await client.commit({
projectId, attemptId: 'storm',
event: { id: 'storm', type: 'scene', title: 'The night of the storm', position: at(0), participants: ['nell', 'jonah'], provenance },
records: [
fact(memories.code.claim, { id: 'canon-code' }),
fact(memories.jonah.claim, { id: 'canon-jonah' }),
fact(negate(memories.radio.claim), { id: 'canon-radio' }),
fact(memories.shape.claim, { id: 'canon-shape' }),
...Object.entries(memories).map(([key, memory]) => remembers('nell', memory.claim, witnessed('storm'), { id: `${key}-0` })),
],
});// The night of the storm. Nell remembers four things; one of them is wrong.
var memories = new Dictionary<string, (Claim Claim, double Salience)>
{
["code"] = (Claim.Value("cellar-door", "code", "4471"), 0.72),
["jonah"] = (Claim.Entity("jonah", "went-into", "cellar"), 0.92),
["radio"] = (Claim.Unary("radio", "works"), 0.85),
["shape"] = (Claim.Entity("something", "moved-in", "cellar"), 0.98),
};
var storm = Position.At(0);
var records = new List<RecordInput>
{
Records.Fact(memories["code"].Claim, id: "canon-code"),
Records.Fact(memories["jonah"].Claim, id: "canon-jonah"),
Records.Fact(memories["radio"].Claim.Negated(), id: "canon-radio"),
Records.Fact(memories["shape"].Claim, id: "canon-shape"),
};
records.AddRange(memories.Select(memory => Records.Remembers("nell", memory.Value.Claim, Acquired.Witnessed("storm"), id: $"{memory.Key}-0")));
engine.Commit(new CommitInput
{
ProjectId = projectId,
AttemptId = "storm",
Event = new EventInput { Id = "storm", Type = "scene", Title = "The night of the storm", Position = storm, Participants = new() { "nell", "jonah" }, Provenance = provenance },
Records = records,
});The forgetting curve
Each night your code reads what Nell still holds, multiplies each confidence by how vivid the memory was, and records the result. A memory below 0.35 is forgotten. The shape in the cellar is pushed out of reach on night two, and comes back on night five.
// Your forgetting curve: each night a memory keeps a share of its confidence, set by how
// vivid it was. Below 0.35 it is gone. What Nell cannot bear to remember is pushed away on
// night 2, and comes back on night 5, when she sees the cellar door again.
// A convention of this recipe, not of the engine: each Holding's id starts with the memory it
// belongs to (`code-3` is `code` on night 3). A real game keeps that link in its own data.
const keyOf = (recordId: string) => recordId.split('-')[0];
for (const night of [1, 2, 3, 4, 5, 6]) {
const { items } = await client.holdings({ projectId, holder: 'nell', at: { tick: night }, filter: { includeWithheld: true } });
const adjustments: HoldingAdjustment[] = [];
const forgotten: string[] = [];
for (const { record } of items) {
if (record.kind !== 'holding') continue;
const key = keyOf(record.id);
const confidence = Math.round(record.confidence * memories[key].salience * 100) / 100;
if (confidence < 0.35) { forgotten.push(record.id); continue; }
const accessibility = key === 'shape' && night === 2 ? 'suppressed' : key === 'shape' && night === 5 ? 'available' : record.accessibility;
adjustments.push({ recordId: record.id, newRecordId: `${key}-${night}`, confidence, accessibility });
}
if (adjustments.length) {
await client.holdingAdjustBatch({ projectId, attemptId: `night-${night}`, position: at(night), adjustments });
}
if (forgotten.length) {
await client.transitionBatch({ projectId, attemptId: `forget-${night}`, position: at(night), recordIds: forgotten, status: 'forgotten' });
}
}// Your forgetting curve: each night a memory keeps a share of its confidence, set by how
// vivid it was. Below 0.35 it is gone. What Nell cannot bear to remember is pushed away on
// night 2, and comes back on night 5, when she sees the cellar door again.
// A convention of this recipe, not of the engine: each Holding's id starts with the memory it
// belongs to (code-3 is code on night 3). A real game keeps that link in its own data.
string KeyOf(string recordId) => recordId.Split('-')[0];
List<HoldingItem> HoldingsAt(long night, bool withheld) => engine.Holdings(new HoldingsPayload
{
ProjectId = projectId, Holder = "nell", At = new At { Tick = night }, Filter = new HoldingFilter { IncludeWithheld = withheld },
}).Items;
foreach (var night in new long[] { 1, 2, 3, 4, 5, 6 })
{
var adjustments = new List<HoldingAdjustment>();
var forgotten = new List<string>();
foreach (var record in HoldingsAt(night, withheld: true).Select(item => item.Record).OfType<Record.Holding>())
{
var key = KeyOf(record.Id);
var confidence = Math.Round(record.Confidence * memories[key].Salience * 100, MidpointRounding.AwayFromZero) / 100;
if (confidence < 0.35) { forgotten.Add(record.Id); continue; }
var accessibility = key == "shape" && night == 2 ? Accessibility.Suppressed
: key == "shape" && night == 5 ? Accessibility.Available
: record.Accessibility;
adjustments.Add(new HoldingAdjustment { RecordId = record.Id, NewRecordId = $"{key}-{night}", Confidence = confidence, Accessibility = accessibility });
}
if (adjustments.Count > 0)
{
engine.HoldingAdjustBatch(new AdjustHoldingsInput { ProjectId = projectId, AttemptId = $"night-{night}", Position = Position.At(night), Adjustments = adjustments });
}
if (forgotten.Count > 0)
{
engine.TransitionBatch(new TransitionBatchInput
{
ProjectId = projectId, AttemptId = $"forget-{night}", Position = Position.At(night), RecordIds = forgotten, Status = TransitionStatus.Forgotten,
});
}
}History stays readable. Each adjustment supersedes the old Holding with a copy. Read night one and the door code is still there, at 0.72.
{
"attitude": "remembers",
"confidence": 0.72,
"holds": true,
"holdsAs": "same",
"polarity": true,
"recordId": "code-1"
}Ask Nell
// What Nell holds about each memory, night by night: the curve, read back from Umwelt.
const curve: Record<string, (number | string)[][]> = {};
for (const key of Object.keys(memories)) curve[key] = [];
for (const night of [0, 1, 2, 3, 4, 5, 6]) {
const { items } = await client.holdings({ projectId, holder: 'nell', at: { tick: night }, filter: { includeWithheld: true } });
for (const key of Object.keys(memories)) {
const item = items.find(({ record }) => keyOf(record.id) === key);
curve[key].push(item?.record.kind === 'holding' ? [item.record.confidence, item.record.accessibility] : [0, 'forgotten']);
}
}
print('curve', curve);
const night3 = await client.holdings({ projectId, holder: 'nell', at: { tick: 3 } });
print('night-3', { visible: night3.items.length, withheldCount: night3.withheldCount });
print('night-1-code', await client.holds({ projectId, holder: 'nell', claim: memories.code.claim, at: { tick: 1 } }));
const said = await client.checkUtterance({
projectId, holder: 'nell', at: { tick: 4 },
claims: [memories.code.claim, memories.jonah.claim, memories.radio.claim, memories.shape.claim],
});
print('night-4-says', said.items.map(({ verdict, canon, confidence }) => ({ verdict, canon, confidence })));
const candidates = [{ id: 'the-shape', recordId: 'shape-4' }, { id: 'jonah', recordId: 'jonah-4' }];
print('prompt-as-nell', await client.contextAdmit({ projectId, holder: 'nell', at: { tick: 4 }, candidates }));
print('prompt-as-narrator', await client.contextAdmit({
projectId, holder: 'nell', at: { tick: 4 }, candidates, includeWithheld: true, authorizedNarrator: true,
}));
await client.close();// What Nell holds about each memory, night by night: the curve, read back from Umwelt.
var curve = memories.Keys.ToDictionary(key => key, _ => new List<object[]>());
foreach (var night in new long[] { 0, 1, 2, 3, 4, 5, 6 })
{
var held = HoldingsAt(night, withheld: true).Select(item => item.Record).OfType<Record.Holding>().ToList();
foreach (var key in memories.Keys)
{
var record = held.FirstOrDefault(r => KeyOf(r.Id) == key);
curve[key].Add(record is null ? new object[] { 0, "forgotten" } : new object[] { record.Confidence, record.Accessibility });
}
}
Print("curve", curve);
var night3 = engine.Holdings(new HoldingsPayload { ProjectId = projectId, Holder = "nell", At = new At { Tick = 3 } });
Print("night-3", new { visible = night3.Items.Count, withheldCount = night3.WithheldCount });
Print("night-1-code", engine.Holds(new HoldsPayload { ProjectId = projectId, Holder = "nell", Claim = memories["code"].Claim, At = new At { Tick = 1 } }));
var said = engine.CheckUtterance(new CheckUtteranceInput
{
ProjectId = projectId, Holder = "nell", At = new At { Tick = 4 },
Claims = memories.Values.Select(memory => memory.Claim).ToList(),
});
Print("night-4-says", said.Items.Select(item => new { verdict = item.Verdict, canon = item.Canon, confidence = item.Confidence }));
var candidates = new List<ContextCandidate> { new() { Id = "the-shape", RecordId = "shape-4" }, new() { Id = "jonah", RecordId = "jonah-4" } };
Print("prompt-as-nell", engine.ContextAdmit(new ContextAdmitInput { ProjectId = projectId, Holder = "nell", At = new At { Tick = 4 }, Candidates = candidates }));
Print("prompt-as-narrator", engine.ContextAdmit(new ContextAdmitInput
{
ProjectId = projectId, Holder = "nell", At = new At { Tick = 4 }, Candidates = candidates, IncludeWithheld = true, AuthorizedNarrator = true,
}));What she lets herself remember
On night three, holdings returns three memories and says one more is there but out of reach.
{ "visible": 3, "withheldCount": 1 }What she says on night four
The code is 4471.unknowncanon agrees
Jonah went into the cellar.holdscanon agrees
The radio works.holdscanon contradicts
Something moved in the cellar.withheldcanon agrees
The code is gone, so she no longer holds it: unknown, although canon agrees. She still believes the radio works, and canon says it does not. The shape is withheld: she holds it, out of reach.
Who may see the buried memory
Building a prompt for Nell herself, context.admit refuses the withheld memory. A narrator who is allowed to see it asks with includeWithheld and authorizedNarrator.
{
"admitted": [{ "cost": 1, "id": "jonah", "priority": 0, "recordId": "jonah-4" }],
"rejected": [{ "id": "the-shape", "reason": "withheld", "recordId": "shape-4" }],
"usedBudget": 1
}{
"admitted": [
{
"cost": 1,
"id": "the-shape",
"priority": 0,
"recordId": "shape-4"
},
{ "cost": 1, "id": "jonah", "priority": 0, "recordId": "jonah-4" }
],
"rejected": [],
"usedBudget": 2
}Try next
contradictions catch it.evidence.compare.at: show the player Nell’s memories as they were on any night.The whole program
One file that builds the world, runs it and prints every answer on this page. The TypeScript program and the .NET one print the same answers: the script that runs them for these pages checks it.
import { at, by, claim, createClient, fact, negate, remembers, witnessed } from '@kallamo/umwelt';
import type { Claim, HoldingAdjustment } from '@kallamo/umwelt';
const client = createClient({ file: 'hollow-pines.umw', create: true });
const projectId = 'hollow-pines';
const provenance = by.human();
const print = (label: string, value: unknown) => console.log(`@@${label}\n${JSON.stringify(value)}`);
// The world, declared once: sending the same manifest again changes nothing.
await client.worldEnsure({
project: { id: projectId, name: 'Hollow Pines', timescale: 'night' },
entityTypes: [
{ id: 'person', name: 'Person', kind: 'agent' },
{ id: 'thing', name: 'Thing', kind: 'other' },
],
entities: [
{ id: 'nell', typeId: 'person', name: 'Nell' },
{ id: 'jonah', typeId: 'person', name: 'Jonah' },
{ id: 'cellar', typeId: 'thing', name: 'The cellar' },
{ id: 'cellar-door', typeId: 'thing', name: 'The cellar door' },
{ id: 'radio', typeId: 'thing', name: 'The radio' },
{ id: 'something', typeId: 'thing', name: 'Something' },
],
predicates: [
{ id: 'code', label: 'code', object: 'value', cardinality: 'one' },
{ id: 'went-into', label: 'went into', object: 'entity', cardinality: 'many' },
{ id: 'works', label: 'works', object: 'none', cardinality: 'one' },
{ id: 'moved-in', label: 'moved in', object: 'entity', cardinality: 'many' },
],
});
// The night of the storm. Nell remembers four things; one of them is wrong.
const memories: Record<string, { claim: Claim; salience: number }> = {
code: { claim: claim.value('cellar-door', 'code', '4471'), salience: 0.72 },
jonah: { claim: claim.entity('jonah', 'went-into', 'cellar'), salience: 0.92 },
radio: { claim: claim.unary('radio', 'works'), salience: 0.85 },
shape: { claim: claim.entity('something', 'moved-in', 'cellar'), salience: 0.98 },
};
await client.commit({
projectId, attemptId: 'storm',
event: { id: 'storm', type: 'scene', title: 'The night of the storm', position: at(0), participants: ['nell', 'jonah'], provenance },
records: [
fact(memories.code.claim, { id: 'canon-code' }),
fact(memories.jonah.claim, { id: 'canon-jonah' }),
fact(negate(memories.radio.claim), { id: 'canon-radio' }),
fact(memories.shape.claim, { id: 'canon-shape' }),
...Object.entries(memories).map(([key, memory]) => remembers('nell', memory.claim, witnessed('storm'), { id: `${key}-0` })),
],
});
// Your forgetting curve: each night a memory keeps a share of its confidence, set by how
// vivid it was. Below 0.35 it is gone. What Nell cannot bear to remember is pushed away on
// night 2, and comes back on night 5, when she sees the cellar door again.
// A convention of this recipe, not of the engine: each Holding's id starts with the memory it
// belongs to (`code-3` is `code` on night 3). A real game keeps that link in its own data.
const keyOf = (recordId: string) => recordId.split('-')[0];
for (const night of [1, 2, 3, 4, 5, 6]) {
const { items } = await client.holdings({ projectId, holder: 'nell', at: { tick: night }, filter: { includeWithheld: true } });
const adjustments: HoldingAdjustment[] = [];
const forgotten: string[] = [];
for (const { record } of items) {
if (record.kind !== 'holding') continue;
const key = keyOf(record.id);
const confidence = Math.round(record.confidence * memories[key].salience * 100) / 100;
if (confidence < 0.35) { forgotten.push(record.id); continue; }
const accessibility = key === 'shape' && night === 2 ? 'suppressed' : key === 'shape' && night === 5 ? 'available' : record.accessibility;
adjustments.push({ recordId: record.id, newRecordId: `${key}-${night}`, confidence, accessibility });
}
if (adjustments.length) {
await client.holdingAdjustBatch({ projectId, attemptId: `night-${night}`, position: at(night), adjustments });
}
if (forgotten.length) {
await client.transitionBatch({ projectId, attemptId: `forget-${night}`, position: at(night), recordIds: forgotten, status: 'forgotten' });
}
}
// What Nell holds about each memory, night by night: the curve, read back from Umwelt.
const curve: Record<string, (number | string)[][]> = {};
for (const key of Object.keys(memories)) curve[key] = [];
for (const night of [0, 1, 2, 3, 4, 5, 6]) {
const { items } = await client.holdings({ projectId, holder: 'nell', at: { tick: night }, filter: { includeWithheld: true } });
for (const key of Object.keys(memories)) {
const item = items.find(({ record }) => keyOf(record.id) === key);
curve[key].push(item?.record.kind === 'holding' ? [item.record.confidence, item.record.accessibility] : [0, 'forgotten']);
}
}
print('curve', curve);
const night3 = await client.holdings({ projectId, holder: 'nell', at: { tick: 3 } });
print('night-3', { visible: night3.items.length, withheldCount: night3.withheldCount });
print('night-1-code', await client.holds({ projectId, holder: 'nell', claim: memories.code.claim, at: { tick: 1 } }));
const said = await client.checkUtterance({
projectId, holder: 'nell', at: { tick: 4 },
claims: [memories.code.claim, memories.jonah.claim, memories.radio.claim, memories.shape.claim],
});
print('night-4-says', said.items.map(({ verdict, canon, confidence }) => ({ verdict, canon, confidence })));
const candidates = [{ id: 'the-shape', recordId: 'shape-4' }, { id: 'jonah', recordId: 'jonah-4' }];
print('prompt-as-nell', await client.contextAdmit({ projectId, holder: 'nell', at: { tick: 4 }, candidates }));
print('prompt-as-narrator', await client.contextAdmit({
projectId, holder: 'nell', at: { tick: 4 }, candidates, includeWithheld: true, authorizedNarrator: true,
}));
await client.close();using System.Text.Json;
using System.Text.Json.Serialization;
using Kallamo.Umwelt;
using var engine = UmweltEngine.Create("hollow-pines.umw");
const string projectId = "hollow-pines";
var provenance = Provenance.Human();
var json = new JsonSerializerOptions
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
Converters = { new JsonStringEnumConverter(JsonNamingPolicy.KebabCaseLower) },
};
void Print(string label, object? value) => Console.WriteLine($"@@{label}\n{JsonSerializer.Serialize(value, json)}");
// The world, declared once: sending the same manifest again changes nothing.
engine.WorldEnsure(new EnsureWorldInput
{
Project = new WorldProject { Id = projectId, Name = "Hollow Pines", Timescale = "night" },
EntityTypes = new()
{
new WorldEntityType { Id = "person", Name = "Person", Kind = EntityKind.Agent },
new WorldEntityType { Id = "thing", Name = "Thing", Kind = EntityKind.Other },
},
Entities = new()
{
new WorldEntity { Id = "nell", TypeId = "person", Name = "Nell" },
new WorldEntity { Id = "jonah", TypeId = "person", Name = "Jonah" },
new WorldEntity { Id = "cellar", TypeId = "thing", Name = "The cellar" },
new WorldEntity { Id = "cellar-door", TypeId = "thing", Name = "The cellar door" },
new WorldEntity { Id = "radio", TypeId = "thing", Name = "The radio" },
new WorldEntity { Id = "something", TypeId = "thing", Name = "Something" },
},
Predicates = new()
{
new PredicateDef { Id = "code", Label = "code", Object = ObjectKind.Value, Cardinality = Cardinality.One },
new PredicateDef { Id = "went-into", Label = "went into", Object = ObjectKind.Entity, Cardinality = Cardinality.Many },
new PredicateDef { Id = "works", Label = "works", Object = ObjectKind.None, Cardinality = Cardinality.One },
new PredicateDef { Id = "moved-in", Label = "moved in", Object = ObjectKind.Entity, Cardinality = Cardinality.Many },
},
});
// The night of the storm. Nell remembers four things; one of them is wrong.
var memories = new Dictionary<string, (Claim Claim, double Salience)>
{
["code"] = (Claim.Value("cellar-door", "code", "4471"), 0.72),
["jonah"] = (Claim.Entity("jonah", "went-into", "cellar"), 0.92),
["radio"] = (Claim.Unary("radio", "works"), 0.85),
["shape"] = (Claim.Entity("something", "moved-in", "cellar"), 0.98),
};
var storm = Position.At(0);
var records = new List<RecordInput>
{
Records.Fact(memories["code"].Claim, id: "canon-code"),
Records.Fact(memories["jonah"].Claim, id: "canon-jonah"),
Records.Fact(memories["radio"].Claim.Negated(), id: "canon-radio"),
Records.Fact(memories["shape"].Claim, id: "canon-shape"),
};
records.AddRange(memories.Select(memory => Records.Remembers("nell", memory.Value.Claim, Acquired.Witnessed("storm"), id: $"{memory.Key}-0")));
engine.Commit(new CommitInput
{
ProjectId = projectId,
AttemptId = "storm",
Event = new EventInput { Id = "storm", Type = "scene", Title = "The night of the storm", Position = storm, Participants = new() { "nell", "jonah" }, Provenance = provenance },
Records = records,
});
// Your forgetting curve: each night a memory keeps a share of its confidence, set by how
// vivid it was. Below 0.35 it is gone. What Nell cannot bear to remember is pushed away on
// night 2, and comes back on night 5, when she sees the cellar door again.
// A convention of this recipe, not of the engine: each Holding's id starts with the memory it
// belongs to (code-3 is code on night 3). A real game keeps that link in its own data.
string KeyOf(string recordId) => recordId.Split('-')[0];
List<HoldingItem> HoldingsAt(long night, bool withheld) => engine.Holdings(new HoldingsPayload
{
ProjectId = projectId, Holder = "nell", At = new At { Tick = night }, Filter = new HoldingFilter { IncludeWithheld = withheld },
}).Items;
foreach (var night in new long[] { 1, 2, 3, 4, 5, 6 })
{
var adjustments = new List<HoldingAdjustment>();
var forgotten = new List<string>();
foreach (var record in HoldingsAt(night, withheld: true).Select(item => item.Record).OfType<Record.Holding>())
{
var key = KeyOf(record.Id);
var confidence = Math.Round(record.Confidence * memories[key].Salience * 100, MidpointRounding.AwayFromZero) / 100;
if (confidence < 0.35) { forgotten.Add(record.Id); continue; }
var accessibility = key == "shape" && night == 2 ? Accessibility.Suppressed
: key == "shape" && night == 5 ? Accessibility.Available
: record.Accessibility;
adjustments.Add(new HoldingAdjustment { RecordId = record.Id, NewRecordId = $"{key}-{night}", Confidence = confidence, Accessibility = accessibility });
}
if (adjustments.Count > 0)
{
engine.HoldingAdjustBatch(new AdjustHoldingsInput { ProjectId = projectId, AttemptId = $"night-{night}", Position = Position.At(night), Adjustments = adjustments });
}
if (forgotten.Count > 0)
{
engine.TransitionBatch(new TransitionBatchInput
{
ProjectId = projectId, AttemptId = $"forget-{night}", Position = Position.At(night), RecordIds = forgotten, Status = TransitionStatus.Forgotten,
});
}
}
// What Nell holds about each memory, night by night: the curve, read back from Umwelt.
var curve = memories.Keys.ToDictionary(key => key, _ => new List<object[]>());
foreach (var night in new long[] { 0, 1, 2, 3, 4, 5, 6 })
{
var held = HoldingsAt(night, withheld: true).Select(item => item.Record).OfType<Record.Holding>().ToList();
foreach (var key in memories.Keys)
{
var record = held.FirstOrDefault(r => KeyOf(r.Id) == key);
curve[key].Add(record is null ? new object[] { 0, "forgotten" } : new object[] { record.Confidence, record.Accessibility });
}
}
Print("curve", curve);
var night3 = engine.Holdings(new HoldingsPayload { ProjectId = projectId, Holder = "nell", At = new At { Tick = 3 } });
Print("night-3", new { visible = night3.Items.Count, withheldCount = night3.WithheldCount });
Print("night-1-code", engine.Holds(new HoldsPayload { ProjectId = projectId, Holder = "nell", Claim = memories["code"].Claim, At = new At { Tick = 1 } }));
var said = engine.CheckUtterance(new CheckUtteranceInput
{
ProjectId = projectId, Holder = "nell", At = new At { Tick = 4 },
Claims = memories.Values.Select(memory => memory.Claim).ToList(),
});
Print("night-4-says", said.Items.Select(item => new { verdict = item.Verdict, canon = item.Canon, confidence = item.Confidence }));
var candidates = new List<ContextCandidate> { new() { Id = "the-shape", RecordId = "shape-4" }, new() { Id = "jonah", RecordId = "jonah-4" } };
Print("prompt-as-nell", engine.ContextAdmit(new ContextAdmitInput { ProjectId = projectId, Holder = "nell", At = new At { Tick = 4 }, Candidates = candidates }));
Print("prompt-as-narrator", engine.ContextAdmit(new ContextAdmitInput
{
ProjectId = projectId, Holder = "nell", At = new At { Tick = 4 }, Candidates = candidates, IncludeWithheld = true, AuthorizedNarrator = true,
}));