The AI innkeeper
A model voices the innkeeper. Umwelt chooses what may enter her prompt, and checks what she says before the player reads it.
| Candidate | Cost | Priority | Result |
|---|---|---|---|
| She watched the wreckmarta-wreck | 16 | 5 | admitted |
| The wreck, as a scenewreck | 8 | 4 | admitted |
| Bram says the captain livedmarta-captain | over-budget | ||
| Canon: the ship sankcanon-sank | not-held | ||
| Silas lit a false lightsilas-light | not-held | ||
| Her brother was aboardmarta-tobin | withheld | ||
| The ledger: a hold full of rummarta-rum | future | ||
| The rescue, as a scenerescue | not-held |
What may enter Marta’s prompt on the evening of day 3: the answer of context.admit, candidate by candidate.
The idea
A model voicing a character knows whatever is in its prompt. Retrieval finds what looks relevant, and that includes the narrator’s Facts, other characters’ secrets and things that have not happened yet. Umwelt sits between the two: it admits only what this character holds at this moment, within your budget, and says why everything else stayed out. After the model answers, it checks each claim the answer makes.
Your application does
- find candidates (search, embeddings, rules)
- estimate what each one costs
- write the prompt from what was admitted
- call the model, and extract claims from its reply
- decide what to do with a line that fails
Umwelt decides and checks
- what Marta holds, at this point in the story
- which candidates fit, by priority
- a reason for every refusal
- a verdict for every claim in the reply
No model runs here. The reply in this recipe is written into the program, so it runs anywhere. With a real model, everything around it stays the same.
The Drowned Lantern
A harbor tavern, two nights after the Albatross went down on the reef. Marta, who keeps the tavern, watched it break up from the harbor wall. Her brother Tobin was aboard, and she cannot bring herself to think about it.
She has heard the captain survived. She does not know that Silas, the lighthouse keeper, lit a false light that night, nor that the ship was running rum: she finds the ledger on day 5. A player sits down and asks her about the wreck.
Retrieval
const sank = claim.entity('albatross', 'sank-on', 'reef');
const falseLight = claim.unary('silas', 'lit-false-light');
const survived = claim.unary('rourke', 'survived');
const tobinAboard = claim.entity('tobin', 'sailed-on', 'albatross');
const rum = claim.value('albatross', 'carried', 'rum');
// A scene is one event. Its records take their position and provenance from it.
const scene = (id: string, title: string, tick: number, participants: string[], records: RecordInput[]) =>
client.commit({ projectId, attemptId: id, event: { id, type: 'scene', title, position: at(tick), participants, provenance }, records });
await scene('wreck', 'The Albatross goes down', 1, ['marta', 'silas'], [
fact(sank, { id: 'canon-sank' }),
fact(falseLight, { id: 'canon-light' }),
fact(tobinAboard, { id: 'canon-tobin' }),
knows('marta', sank, witnessed('wreck'), { id: 'marta-wreck', text: 'I watched the Albatross break on the reef from the harbor wall.' }),
knows('marta', tobinAboard, witnessed('wreck'), { id: 'marta-tobin', text: 'My brother Tobin was aboard her.', accessibility: 'suppressed' }),
knows('silas', falseLight, witnessed('wreck'), { id: 'silas-light', text: 'I lit the false light that drew her onto the reef.' }),
]);
await scene('rescue', 'A man on the shingle', 2, ['bram', 'rourke'], [
fact(survived, { id: 'canon-survived' }),
fact(rum, { id: 'canon-rum' }),
knows('bram', survived, witnessed('rescue'), { id: 'bram-saw', text: 'I pulled the captain out of the surf myself.' }),
]);
await client.transmit({ projectId, attemptId: 'gossip', from: 'bram', to: 'marta', sourceRecordId: 'bram-saw', intent: 'honest',
attitude: 'believes', confidence: 0.6, position: at(2), recordId: 'marta-captain',
text: 'Bram swears the captain crawled ashore alive.' });
await scene('ledger', 'The ledger', 5, ['marta'], [
knows('marta', rum, witnessed('ledger'), { id: 'marta-rum', text: 'The ledger says her hold was full of rum.' }),
]);var sank = Claim.Entity("albatross", "sank-on", "reef");
var falseLight = Claim.Unary("silas", "lit-false-light");
var survived = Claim.Unary("rourke", "survived");
var tobinAboard = Claim.Entity("tobin", "sailed-on", "albatross");
var rum = Claim.Value("albatross", "carried", "rum");
// A scene is one event. Its records take their position and provenance from it.
void Scene(string id, string title, long tick, List<string> participants, List<RecordInput> records) => engine.Commit(new CommitInput
{
ProjectId = projectId, AttemptId = id,
Event = new EventInput { Id = id, Type = "scene", Title = title, Position = Position.At(tick), Participants = participants, Provenance = provenance },
Records = records,
});
Scene("wreck", "The Albatross goes down", 1, new() { "marta", "silas" }, new()
{
Records.Fact(sank, id: "canon-sank"),
Records.Fact(falseLight, id: "canon-light"),
Records.Fact(tobinAboard, id: "canon-tobin"),
Records.Knows("marta", sank, Acquired.Witnessed("wreck"), id: "marta-wreck", text: "I watched the Albatross break on the reef from the harbor wall."),
Records.Knows("marta", tobinAboard, Acquired.Witnessed("wreck"), id: "marta-tobin", text: "My brother Tobin was aboard her.", accessibility: Accessibility.Suppressed),
Records.Knows("silas", falseLight, Acquired.Witnessed("wreck"), id: "silas-light", text: "I lit the false light that drew her onto the reef."),
});
Scene("rescue", "A man on the shingle", 2, new() { "bram", "rourke" }, new()
{
Records.Fact(survived, id: "canon-survived"),
Records.Fact(rum, id: "canon-rum"),
Records.Knows("bram", survived, Acquired.Witnessed("rescue"), id: "bram-saw", text: "I pulled the captain out of the surf myself."),
});
engine.Transmit(new TransmitInput
{
ProjectId = projectId, AttemptId = "gossip", From = "bram", To = "marta", SourceRecordId = "bram-saw", Intent = Intent.Honest,
Attitude = Attitude.Believes, Confidence = 0.6, Position = Position.At(2), RecordId = "marta-captain",
Text = "Bram swears the captain crawled ashore alive.",
});
Scene("ledger", "The ledger", 5, new() { "marta" }, new()
{
Records.Knows("marta", rum, Acquired.Witnessed("ledger"), id: "marta-rum", text: "The ledger says her hold was full of rum."),
});Search from the narrator’s view finds Facts and events; search from Marta’s view finds only her own Holdings that have text. Your retrieval gathers candidates from both and more, and gives each a cost and a priority.
// Evening of day 3. The player asks Marta about the wreck. Your retrieval gathers candidates:
// what the narrator's search finds, what Marta's own search finds, and a few records it ranks high.
const now = { tick: 3 };
const narrator = await client.search({ projectId, text: 'albatross', at: now });
const hers = await client.search({ projectId, text: 'albatross', at: now, filter: { holder: 'marta' } });
print('search', { narrator: narrator.items.map((i) => i.docId), marta: hers.items.map((i) => i.docId) });
// Your cost: a rough token count of the text you would put in the prompt.
const texts: Record<string, string> = {
'marta-wreck': 'I watched the Albatross break on the reef from the harbor wall.',
'marta-captain': 'Bram swears the captain crawled ashore alive.',
'marta-tobin': 'My brother Tobin was aboard her.',
'silas-light': 'I lit the false light that drew her onto the reef.',
'marta-rum': 'The ledger says her hold was full of rum.',
'canon-sank': 'The Albatross sank on the reef.',
wreck: 'Scene: the Albatross goes down.',
};
const cost = (id: string) => Math.ceil((texts[id] ?? id).length / 4);
const candidates: ContextCandidate[] = [
{ id: 'marta-wreck', recordId: 'marta-wreck', cost: cost('marta-wreck'), priority: 5 },
{ id: 'wreck', eventId: 'wreck', cost: cost('wreck'), priority: 4 },
{ id: 'marta-captain', recordId: 'marta-captain', cost: cost('marta-captain'), priority: 3 },
{ id: 'canon-sank', recordId: 'canon-sank', cost: cost('canon-sank'), priority: 3 },
{ id: 'silas-light', recordId: 'silas-light', cost: cost('silas-light'), priority: 2 },
{ id: 'marta-tobin', recordId: 'marta-tobin', cost: cost('marta-tobin'), priority: 2 },
{ id: 'marta-rum', recordId: 'marta-rum', cost: cost('marta-rum'), priority: 1 },
{ id: 'rescue', eventId: 'rescue', cost: 9, priority: 1 },
];// Evening of day 3. The player asks Marta about the wreck. Your retrieval gathers candidates:
// what the narrator's search finds, what Marta's own search finds, and a few records it ranks high.
var now = new At { Tick = 3 };
var narrator = engine.Search(new SearchPayload { ProjectId = projectId, Text = "albatross", At = now });
var hers = engine.Search(new SearchPayload { ProjectId = projectId, Text = "albatross", At = now, Filter = new SearchFilter { Holder = "marta" } });
Print("search", new { narrator = narrator.Items.Select(i => i.DocId), marta = hers.Items.Select(i => i.DocId) });
// Your cost: a rough token count of the text you would put in the prompt.
var texts = new Dictionary<string, string>
{
["marta-wreck"] = "I watched the Albatross break on the reef from the harbor wall.",
["marta-captain"] = "Bram swears the captain crawled ashore alive.",
["marta-tobin"] = "My brother Tobin was aboard her.",
["silas-light"] = "I lit the false light that drew her onto the reef.",
["marta-rum"] = "The ledger says her hold was full of rum.",
["canon-sank"] = "The Albatross sank on the reef.",
["wreck"] = "Scene: the Albatross goes down.",
};
uint Cost(string id) => (uint)Math.Ceiling((texts.TryGetValue(id, out var text) ? text : id).Length / 4.0);
ContextCandidate Record(string id, int priority) => new() { Id = id, RecordId = id, Cost = Cost(id), Priority = priority };
var candidates = new List<ContextCandidate>
{
Record("marta-wreck", 5),
new() { Id = "wreck", EventId = "wreck", Cost = Cost("wreck"), Priority = 4 },
Record("marta-captain", 3), Record("canon-sank", 3), Record("silas-light", 2), Record("marta-tobin", 2), Record("marta-rum", 1),
new() { Id = "rescue", EventId = "rescue", Cost = 9, Priority = 1 },
};{
"narrator": ["canon-rum", "wreck", "canon-sank", "canon-tobin"],
"marta": ["marta-wreck"]
}Admission
One call, with Marta as the holder, the evening of day 3 as the position, and a budget of 30.
const admitted = await client.contextAdmit({ projectId, holder: 'marta', at: now, budget: 30, candidates });
print('admit', admitted);
// Your prompt, from what was admitted, highest priority first.
const prompt = ['You are Marta, innkeeper of the Drowned Lantern. You know only this:',
...admitted.admitted.map((c) => `- ${texts[c.id] ?? c.id}`)].join('\n');
print('prompt', prompt);var admitted = engine.ContextAdmit(new ContextAdmitInput { ProjectId = projectId, Holder = "marta", At = now, Budget = 30, Candidates = candidates });
Print("admit", admitted);
// Your prompt, from what was admitted, highest priority first.
var prompt = string.Join("\n", admitted.Admitted.Select(c => $"- {(texts.TryGetValue(c.Id, out var text) ? text : c.Id)}")
.Prepend("You are Marta, innkeeper of the Drowned Lantern. You know only this:"));
Print("prompt", prompt);- Bram says the captain lived:
over-budget, did not fit in what was left of the budget. - Canon: the ship sank:
not-held, not hers: a Fact, someone else’s secret, or a scene she was not at. - Silas lit a false light:
not-held, not hers: a Fact, someone else’s secret, or a scene she was not at. - Her brother was aboard:
withheld, hers, but suppressed. - The ledger: a hold full of rum:
future, she only learns it on day 5. - The rescue, as a scene:
not-held, not hers: a Fact, someone else’s secret, or a scene she was not at.
You are Marta, innkeeper of the Drowned Lantern. You know only this:
- I watched the Albatross break on the reef from the harbor wall.
- Scene: the Albatross goes down.Being there is not knowing. The rescue scene is refused: Marta was not there. An event is admitted for a holder only when they hold something witnessed at it.
Checking the reply
The model answers. Your extractor turns each sentence into a claim, and check.utterance checks them all at once.
// The model answers the player. (Here the reply is written into the program, so the recipe
// runs without a model.) Your extractor turns each sentence into a claim; Umwelt checks it.
const reply: [string, Claim][] = [
['Aye, the Albatross broke up on the reef, I saw it with my own eyes.', sank],
['They say her captain made it to shore.', survived],
['And between us, old Silas lit a false light that night.', falseLight],
['Mind you, she was running rum.', rum],
];
const check = await client.checkUtterance({ projectId, holder: 'marta', at: now, claims: reply.map(([, claim]) => claim) });
const lines = reply.map(([line], i) => ({ line, verdict: check.items[i].verdict, canon: check.items[i].canon }));
print('reply', lines);
// Your rule: keep what she holds, cut the rest and ask the model again.
print('kept', lines.filter((l) => l.verdict === 'holds').map((l) => l.line));
await client.close();// The model answers the player. (Here the reply is written into the program, so the recipe
// runs without a model.) Your extractor turns each sentence into a claim; Umwelt checks it.
var reply = new (string Line, Claim Claim)[]
{
("Aye, the Albatross broke up on the reef, I saw it with my own eyes.", sank),
("They say her captain made it to shore.", survived),
("And between us, old Silas lit a false light that night.", falseLight),
("Mind you, she was running rum.", rum),
};
var check = engine.CheckUtterance(new CheckUtteranceInput { ProjectId = projectId, Holder = "marta", At = now, Claims = reply.Select(r => r.Claim).ToList() });
var lines = reply.Select((r, i) => new { line = r.Line, verdict = check.Items[i].Verdict, canon = check.Items[i].Canon }).ToList();
Print("reply", lines);
// Your rule: keep what she holds, cut the rest and ask the model again.
Print("kept", lines.Where(l => l.verdict == UtteranceVerdict.Holds).Select(l => l.line));Aye, the Albatross broke up on the reef, I saw it with my own eyes.holdscanon agrees
They say her captain made it to shore.holdscanon agrees
And between us, old Silas lit a false light that night.unknowncanon agrees
Mind you, she was running rum.futurecanon agrees
Every line agrees with canon, and two of them are still wrong for Marta. The false light is unknown to her: a spoiler. The rum is future: she learns it on day 5. Your rule cuts both and asks the model again.
Canon never grants knowledge. A true sentence is not one the character can say. The check is against what Marta holds, and canon is only reported beside it.
Try next
semanticSearch for retrieval; admission stays the same.classification and refuse some with excludeClassifications.transmit, so the player character holds it too.context.admit takes the narrator’s view: everything visible at the position.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, knows, witnessed } from '@kallamo/umwelt';
import type { Claim, ContextCandidate, RecordInput } from '@kallamo/umwelt';
const client = createClient({ file: 'drowned-lantern.umw', create: true });
const projectId = 'drowned-lantern';
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: 'The Drowned Lantern', timescale: 'day' },
entityTypes: [
{ id: 'person', name: 'Person', kind: 'agent' },
{ id: 'thing', name: 'Thing', kind: 'other' },
],
entities: [
{ id: 'marta', typeId: 'person', name: 'Marta, the innkeeper' },
{ id: 'silas', typeId: 'person', name: 'Silas, the lighthouse keeper' },
{ id: 'bram', typeId: 'person', name: 'Bram, a sailor' },
{ id: 'rourke', typeId: 'person', name: 'Captain Rourke' },
{ id: 'tobin', typeId: 'person', name: 'Tobin, Marta’s brother' },
{ id: 'albatross', typeId: 'thing', name: 'The Albatross' },
{ id: 'reef', typeId: 'thing', name: 'The reef' },
],
predicates: [
{ id: 'sank-on', label: 'sank on', object: 'entity', cardinality: 'many' },
{ id: 'lit-false-light', label: 'lit false light', object: 'none', cardinality: 'many' },
{ id: 'survived', label: 'survived', object: 'none', cardinality: 'many' },
{ id: 'sailed-on', label: 'sailed on', object: 'entity', cardinality: 'many' },
{ id: 'carried', label: 'carried', object: 'value', cardinality: 'many' },
],
});
const sank = claim.entity('albatross', 'sank-on', 'reef');
const falseLight = claim.unary('silas', 'lit-false-light');
const survived = claim.unary('rourke', 'survived');
const tobinAboard = claim.entity('tobin', 'sailed-on', 'albatross');
const rum = claim.value('albatross', 'carried', 'rum');
// A scene is one event. Its records take their position and provenance from it.
const scene = (id: string, title: string, tick: number, participants: string[], records: RecordInput[]) =>
client.commit({ projectId, attemptId: id, event: { id, type: 'scene', title, position: at(tick), participants, provenance }, records });
await scene('wreck', 'The Albatross goes down', 1, ['marta', 'silas'], [
fact(sank, { id: 'canon-sank' }),
fact(falseLight, { id: 'canon-light' }),
fact(tobinAboard, { id: 'canon-tobin' }),
knows('marta', sank, witnessed('wreck'), { id: 'marta-wreck', text: 'I watched the Albatross break on the reef from the harbor wall.' }),
knows('marta', tobinAboard, witnessed('wreck'), { id: 'marta-tobin', text: 'My brother Tobin was aboard her.', accessibility: 'suppressed' }),
knows('silas', falseLight, witnessed('wreck'), { id: 'silas-light', text: 'I lit the false light that drew her onto the reef.' }),
]);
await scene('rescue', 'A man on the shingle', 2, ['bram', 'rourke'], [
fact(survived, { id: 'canon-survived' }),
fact(rum, { id: 'canon-rum' }),
knows('bram', survived, witnessed('rescue'), { id: 'bram-saw', text: 'I pulled the captain out of the surf myself.' }),
]);
await client.transmit({ projectId, attemptId: 'gossip', from: 'bram', to: 'marta', sourceRecordId: 'bram-saw', intent: 'honest',
attitude: 'believes', confidence: 0.6, position: at(2), recordId: 'marta-captain',
text: 'Bram swears the captain crawled ashore alive.' });
await scene('ledger', 'The ledger', 5, ['marta'], [
knows('marta', rum, witnessed('ledger'), { id: 'marta-rum', text: 'The ledger says her hold was full of rum.' }),
]);
// Evening of day 3. The player asks Marta about the wreck. Your retrieval gathers candidates:
// what the narrator's search finds, what Marta's own search finds, and a few records it ranks high.
const now = { tick: 3 };
const narrator = await client.search({ projectId, text: 'albatross', at: now });
const hers = await client.search({ projectId, text: 'albatross', at: now, filter: { holder: 'marta' } });
print('search', { narrator: narrator.items.map((i) => i.docId), marta: hers.items.map((i) => i.docId) });
// Your cost: a rough token count of the text you would put in the prompt.
const texts: Record<string, string> = {
'marta-wreck': 'I watched the Albatross break on the reef from the harbor wall.',
'marta-captain': 'Bram swears the captain crawled ashore alive.',
'marta-tobin': 'My brother Tobin was aboard her.',
'silas-light': 'I lit the false light that drew her onto the reef.',
'marta-rum': 'The ledger says her hold was full of rum.',
'canon-sank': 'The Albatross sank on the reef.',
wreck: 'Scene: the Albatross goes down.',
};
const cost = (id: string) => Math.ceil((texts[id] ?? id).length / 4);
const candidates: ContextCandidate[] = [
{ id: 'marta-wreck', recordId: 'marta-wreck', cost: cost('marta-wreck'), priority: 5 },
{ id: 'wreck', eventId: 'wreck', cost: cost('wreck'), priority: 4 },
{ id: 'marta-captain', recordId: 'marta-captain', cost: cost('marta-captain'), priority: 3 },
{ id: 'canon-sank', recordId: 'canon-sank', cost: cost('canon-sank'), priority: 3 },
{ id: 'silas-light', recordId: 'silas-light', cost: cost('silas-light'), priority: 2 },
{ id: 'marta-tobin', recordId: 'marta-tobin', cost: cost('marta-tobin'), priority: 2 },
{ id: 'marta-rum', recordId: 'marta-rum', cost: cost('marta-rum'), priority: 1 },
{ id: 'rescue', eventId: 'rescue', cost: 9, priority: 1 },
];
const admitted = await client.contextAdmit({ projectId, holder: 'marta', at: now, budget: 30, candidates });
print('admit', admitted);
// Your prompt, from what was admitted, highest priority first.
const prompt = ['You are Marta, innkeeper of the Drowned Lantern. You know only this:',
...admitted.admitted.map((c) => `- ${texts[c.id] ?? c.id}`)].join('\n');
print('prompt', prompt);
// The model answers the player. (Here the reply is written into the program, so the recipe
// runs without a model.) Your extractor turns each sentence into a claim; Umwelt checks it.
const reply: [string, Claim][] = [
['Aye, the Albatross broke up on the reef, I saw it with my own eyes.', sank],
['They say her captain made it to shore.', survived],
['And between us, old Silas lit a false light that night.', falseLight],
['Mind you, she was running rum.', rum],
];
const check = await client.checkUtterance({ projectId, holder: 'marta', at: now, claims: reply.map(([, claim]) => claim) });
const lines = reply.map(([line], i) => ({ line, verdict: check.items[i].verdict, canon: check.items[i].canon }));
print('reply', lines);
// Your rule: keep what she holds, cut the rest and ask the model again.
print('kept', lines.filter((l) => l.verdict === 'holds').map((l) => l.line));
await client.close();using System.Text.Json;
using System.Text.Json.Serialization;
using Kallamo.Umwelt;
using var engine = UmweltEngine.Create("drowned-lantern.umw");
const string projectId = "drowned-lantern";
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 = "The Drowned Lantern", Timescale = "day" },
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 = "marta", TypeId = "person", Name = "Marta, the innkeeper" },
new WorldEntity { Id = "silas", TypeId = "person", Name = "Silas, the lighthouse keeper" },
new WorldEntity { Id = "bram", TypeId = "person", Name = "Bram, a sailor" },
new WorldEntity { Id = "rourke", TypeId = "person", Name = "Captain Rourke" },
new WorldEntity { Id = "tobin", TypeId = "person", Name = "Tobin, Marta’s brother" },
new WorldEntity { Id = "albatross", TypeId = "thing", Name = "The Albatross" },
new WorldEntity { Id = "reef", TypeId = "thing", Name = "The reef" },
},
Predicates = new()
{
new PredicateDef { Id = "sank-on", Label = "sank on", Object = ObjectKind.Entity, Cardinality = Cardinality.Many },
new PredicateDef { Id = "lit-false-light", Label = "lit false light", Object = ObjectKind.None, Cardinality = Cardinality.Many },
new PredicateDef { Id = "survived", Label = "survived", Object = ObjectKind.None, Cardinality = Cardinality.Many },
new PredicateDef { Id = "sailed-on", Label = "sailed on", Object = ObjectKind.Entity, Cardinality = Cardinality.Many },
new PredicateDef { Id = "carried", Label = "carried", Object = ObjectKind.Value, Cardinality = Cardinality.Many },
},
});
var sank = Claim.Entity("albatross", "sank-on", "reef");
var falseLight = Claim.Unary("silas", "lit-false-light");
var survived = Claim.Unary("rourke", "survived");
var tobinAboard = Claim.Entity("tobin", "sailed-on", "albatross");
var rum = Claim.Value("albatross", "carried", "rum");
// A scene is one event. Its records take their position and provenance from it.
void Scene(string id, string title, long tick, List<string> participants, List<RecordInput> records) => engine.Commit(new CommitInput
{
ProjectId = projectId, AttemptId = id,
Event = new EventInput { Id = id, Type = "scene", Title = title, Position = Position.At(tick), Participants = participants, Provenance = provenance },
Records = records,
});
Scene("wreck", "The Albatross goes down", 1, new() { "marta", "silas" }, new()
{
Records.Fact(sank, id: "canon-sank"),
Records.Fact(falseLight, id: "canon-light"),
Records.Fact(tobinAboard, id: "canon-tobin"),
Records.Knows("marta", sank, Acquired.Witnessed("wreck"), id: "marta-wreck", text: "I watched the Albatross break on the reef from the harbor wall."),
Records.Knows("marta", tobinAboard, Acquired.Witnessed("wreck"), id: "marta-tobin", text: "My brother Tobin was aboard her.", accessibility: Accessibility.Suppressed),
Records.Knows("silas", falseLight, Acquired.Witnessed("wreck"), id: "silas-light", text: "I lit the false light that drew her onto the reef."),
});
Scene("rescue", "A man on the shingle", 2, new() { "bram", "rourke" }, new()
{
Records.Fact(survived, id: "canon-survived"),
Records.Fact(rum, id: "canon-rum"),
Records.Knows("bram", survived, Acquired.Witnessed("rescue"), id: "bram-saw", text: "I pulled the captain out of the surf myself."),
});
engine.Transmit(new TransmitInput
{
ProjectId = projectId, AttemptId = "gossip", From = "bram", To = "marta", SourceRecordId = "bram-saw", Intent = Intent.Honest,
Attitude = Attitude.Believes, Confidence = 0.6, Position = Position.At(2), RecordId = "marta-captain",
Text = "Bram swears the captain crawled ashore alive.",
});
Scene("ledger", "The ledger", 5, new() { "marta" }, new()
{
Records.Knows("marta", rum, Acquired.Witnessed("ledger"), id: "marta-rum", text: "The ledger says her hold was full of rum."),
});
// Evening of day 3. The player asks Marta about the wreck. Your retrieval gathers candidates:
// what the narrator's search finds, what Marta's own search finds, and a few records it ranks high.
var now = new At { Tick = 3 };
var narrator = engine.Search(new SearchPayload { ProjectId = projectId, Text = "albatross", At = now });
var hers = engine.Search(new SearchPayload { ProjectId = projectId, Text = "albatross", At = now, Filter = new SearchFilter { Holder = "marta" } });
Print("search", new { narrator = narrator.Items.Select(i => i.DocId), marta = hers.Items.Select(i => i.DocId) });
// Your cost: a rough token count of the text you would put in the prompt.
var texts = new Dictionary<string, string>
{
["marta-wreck"] = "I watched the Albatross break on the reef from the harbor wall.",
["marta-captain"] = "Bram swears the captain crawled ashore alive.",
["marta-tobin"] = "My brother Tobin was aboard her.",
["silas-light"] = "I lit the false light that drew her onto the reef.",
["marta-rum"] = "The ledger says her hold was full of rum.",
["canon-sank"] = "The Albatross sank on the reef.",
["wreck"] = "Scene: the Albatross goes down.",
};
uint Cost(string id) => (uint)Math.Ceiling((texts.TryGetValue(id, out var text) ? text : id).Length / 4.0);
ContextCandidate Record(string id, int priority) => new() { Id = id, RecordId = id, Cost = Cost(id), Priority = priority };
var candidates = new List<ContextCandidate>
{
Record("marta-wreck", 5),
new() { Id = "wreck", EventId = "wreck", Cost = Cost("wreck"), Priority = 4 },
Record("marta-captain", 3), Record("canon-sank", 3), Record("silas-light", 2), Record("marta-tobin", 2), Record("marta-rum", 1),
new() { Id = "rescue", EventId = "rescue", Cost = 9, Priority = 1 },
};
var admitted = engine.ContextAdmit(new ContextAdmitInput { ProjectId = projectId, Holder = "marta", At = now, Budget = 30, Candidates = candidates });
Print("admit", admitted);
// Your prompt, from what was admitted, highest priority first.
var prompt = string.Join("\n", admitted.Admitted.Select(c => $"- {(texts.TryGetValue(c.Id, out var text) ? text : c.Id)}")
.Prepend("You are Marta, innkeeper of the Drowned Lantern. You know only this:"));
Print("prompt", prompt);
// The model answers the player. (Here the reply is written into the program, so the recipe
// runs without a model.) Your extractor turns each sentence into a claim; Umwelt checks it.
var reply = new (string Line, Claim Claim)[]
{
("Aye, the Albatross broke up on the reef, I saw it with my own eyes.", sank),
("They say her captain made it to shore.", survived),
("And between us, old Silas lit a false light that night.", falseLight),
("Mind you, she was running rum.", rum),
};
var check = engine.CheckUtterance(new CheckUtteranceInput { ProjectId = projectId, Holder = "marta", At = now, Claims = reply.Select(r => r.Claim).ToList() });
var lines = reply.Select((r, i) => new { line = r.Line, verdict = check.Items[i].Verdict, canon = check.Items[i].Canon }).ToList();
Print("reply", lines);
// Your rule: keep what she holds, cut the rest and ask the model again.
Print("kept", lines.Where(l => l.verdict == UtteranceVerdict.Holds).Select(l => l.line));