UmweltDocs
Recipes/The AI innkeeper

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.

Budget: 24 of 30 used2 admitted · 6 refused
CandidateCostPriorityResult
She watched the wreckmarta-wreck165admitted
The wreck, as a scenewreck84admitted
Bram says the captain livedmarta-captainover-budget
Canon: the ship sankcanon-sanknot-held
Silas lit a false lightsilas-lightnot-held
Her brother was aboardmarta-tobinwithheld
The ledger: a hold full of rummarta-rumfuture
The rescue, as a scenerescuenot-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

drowned-lantern.ts
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.' }),
]);
Program.cs
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.

drowned-lantern.tsYour code
// 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 },
];
Program.csYour code
// 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 },
};
What each search found
{
  "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.

drowned-lantern.ts
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);
Program.cs
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.
The prompt
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.

drowned-lantern.tsYour code
// 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();
Program.csYour code
// 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));
MartaAye, the Albatross broke up on the reef, I saw it with my own eyes.holdscanon agrees
MartaThey say her captain made it to shore.holdscanon agrees
MartaAnd between us, old Silas lit a false light that night.unknowncanon agrees
MartaMind 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

Search by meaningFill vectors with your model and use semanticSearch for retrieval; admission stays the same.
Spoiler classesTag candidates with a classification and refuse some with excludeClassifications.
What she lets slipWhen Marta tells the player something, record it with transmit, so the player character holds it too.
A narrator’s summaryWithout a holder, 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.

drowned-lantern.ts
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();
Program.cs
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));

More recipes

Was this page helpful?