Consolidate NPC location and item event instructions

This commit is contained in:
2026-08-05 14:48:55 +00:00
parent ab70347c5d
commit 856b26b718
15 changed files with 119 additions and 59 deletions

View File

@@ -1,7 +1,11 @@
Return one event only when the transcript establishes a meaningful item or
currency occurrence. Use a concise observed item name and preserve the stated
currency denomination. Set `quantity` to the explicitly stated integer, or to
`null` when the transcript does not state one.
Extract meaningful Dungeons & Dragons item and currency discoveries and changes
in party possession established by the transcript. This is an event history,
not an inventory or ledger: do not calculate balances, resolve item identity
across records, or infer ownership that the transcript does not establish.
Record a stated quantity as an integer and leave it null when the transcript
does not state one. Use a concise observed item name and preserve the stated
currency denomination.
Use `discovered` when the party learns of or encounters an item without
establishing possession. Use `acquired` when the party or a party member gains
@@ -22,5 +26,4 @@ transfer. Do not emit a transfer for a gift, sale, or payment outside the party.
Ordinary non-depleting use is not an event. Do not infer acquisition from a
discovery, or discovery from an acquisition: emit both only when each is
independently established. Every event needs at least one narrow transcript
range. Return no lore, inventory totals, aliases, or unstated holder details.
independently established.

View File

@@ -29,8 +29,6 @@ messages:
type: ephemeral
- role: user
content_file: ./sharedassets/common-dnd-extraction-evidence.md
- role: user
content_file: ./task.md
- role: user
content_file: ./instructions.md
cache_control:

View File

@@ -1,7 +0,0 @@
Extract meaningful Dungeons & Dragons item and currency events established by
the transcript. Record only discoveries and changes in party possession, with
the transcript ranges that support each event.
This is an event history, not an inventory or ledger. Do not calculate balances,
resolve item identity across records, or infer ownership that the transcript
does not establish.

View File

@@ -1,6 +1,10 @@
Return only observed location display names and narrow transcript source ranges.
Extract physical places established by the provided Dungeons & Dragons
transcript. Include planes, regions, settlements, districts, buildings, rooms,
landmarks, routes, and geographic features.
A generic label such as "the tavern" is allowed only when the transcript uses
it for a specific place. Keep aliases and nested places when the transcript
identifies them; do not merge or invent qualifiers for similarly named places.
Exclude people, creatures, objects, organizations, abstract concepts, and
places merely inferred from an event. Omit uncertain or unsupported places.
Campaign references may clarify terms already present in the transcript, but
they are not evidence and must never supply a source range.

View File

@@ -29,8 +29,6 @@ messages:
type: ephemeral
- role: user
content_file: ./sharedassets/common-dnd-extraction-evidence.md
- role: user
content_file: ./task.md
- role: user
content_file: ./instructions.md
cache_control:

View File

@@ -1,8 +0,0 @@
Extract physical places established by the provided Dungeons & Dragons
transcript and cite where each place is identified.
Include planes, regions, settlements, districts, buildings, rooms, landmarks,
routes, and geographic features. A generic label such as "the tavern" is
allowed only when the transcript uses it for a specific place. Keep aliases and
nested places when the transcript identifies them; do not merge or invent
qualifiers for similarly named places.

View File

@@ -1,8 +1,15 @@
For every NPC record, return only the observed display name and transcript
units that support that identity.
Extract the individually identifiable Dungeons & Dragons non-player characters
established by the provided transcript.
Return no other details or lore inferred from general D&D knowledge. Do not
invent a label for an anonymous creature, crowd, or generic role.
Include an in-world non-PC participant only when the transcript gives it a
proper name or a stable, individually distinguishing title or alias.
Preserve observed display spelling. Return at least one narrow source range for
every record.
Exclude human players, transcript speakers, and the GM as out-of-world people;
player characters identified by the player or party references; incidental or
hypothetical name drops; corrected transcription mistakes; anonymous or generic
roles; indistinguishable crowds or groups; invented descriptive labels; and
temporary summoned creatures or spell effects without a persistent individual
identity.
Preserve observed display spelling. Do not invent a label for an anonymous
creature, crowd, or generic role.

View File

@@ -29,8 +29,6 @@ messages:
type: ephemeral
- role: user
content_file: ./sharedassets/common-dnd-extraction-evidence.md
- role: user
content_file: ./task.md
- role: user
content_file: ./instructions.md
cache_control:

View File

@@ -1,12 +0,0 @@
Extract the individually identifiable Dungeons & Dragons non-player characters
established by the provided transcript and cite where each identity appears.
Include an in-world non-PC participant only when the transcript gives it a
proper name or a stable, individually distinguishing title or alias.
Exclude human players, transcript speakers, and the GM as out-of-world people,
player characters identified by the player or party references, incidental or
hypothetical name drops, corrected transcription mistakes, anonymous or
generic roles, indistinguishable crowds or groups, invented descriptive labels,
and temporary summoned creatures or spell effects without a persistent
individual identity.

View File

@@ -17,7 +17,6 @@ var promptAssetManifest = shared.PromptAssetManifest{
ModuleDir: "dnd.item_events",
ModuleFiles: []promptfs.ModulePromptFile{
{Name: "prompt.yaml", Path: "prompts/prompt.yaml"},
{Name: "task.md", Path: "prompts/task.md"},
{Name: "instructions.md", Path: "prompts/instructions.md"},
},
SharedFiles: []string{

View File

@@ -31,8 +31,8 @@ func TestPromptAssetsPrepareItemEventPrompt(t *testing.T) {
prepared, err := engine.Prepare(context.Background(), promptkit.RunRequest{
PromptID: PromptID, PromptVersion: SchemaVersion, ProfileID: "item-events-test-profile",
Inputs: map[string]promptkit.ArtifactRef{
"transcript": promptkit.InlineWithURI("file:///session.json", `{"segments":[1]}`),
"players": promptkit.Inline(" "),
"transcript": promptkit.InlineWithURI("file:///session.json", `{"units":[{"sentinel":"item-event-transcript"}]}`),
"players": promptkit.Inline("item-event-player"),
"party": promptkit.Inline(" "),
"glossary": promptkit.Inline(" "),
},
@@ -43,6 +43,36 @@ func TestPromptAssetsPrepareItemEventPrompt(t *testing.T) {
if prepared.PromptID != PromptID || prepared.OutputContract.SchemaPath != "dnd_item_events_llm.v1.json" {
t.Fatalf("prepared prompt = %#v", prepared)
}
if len(prepared.Messages) < 5 {
t.Fatalf("prepared messages = %#v, want shared policy, references, transcript, and instructions", prepared.Messages)
}
referenceIndex := -1
transcriptIndex := -1
for index, message := range prepared.Messages {
if strings.Contains(message.Content, "item-event-player") {
referenceIndex = index
}
if strings.Contains(message.Content, "item-event-transcript") {
transcriptIndex = index
}
}
instructionsIndex := len(prepared.Messages) - 1
if referenceIndex < 0 || transcriptIndex <= referenceIndex || transcriptIndex >= instructionsIndex {
t.Fatalf("message order = references %d, transcript %d, instructions %d; want that order", referenceIndex, transcriptIndex, instructionsIndex)
}
for _, index := range []int{referenceIndex, transcriptIndex, instructionsIndex} {
if cache := prepared.Messages[index].CacheControl; cache == nil || cache.Type != promptkit.CacheControlEphemeral {
t.Fatalf("message %d cache control = %#v, want ephemeral", index, cache)
}
}
for index, message := range prepared.Messages {
if index != referenceIndex && strings.Contains(message.Content, "item-event-player") {
t.Errorf("message %d unexpectedly rendered campaign-reference input", index)
}
if index != transcriptIndex && strings.Contains(message.Content, "item-event-transcript") {
t.Errorf("message %d unexpectedly rendered transcript input", index)
}
}
}
func TestPromptAssetsDoNotLeakIntoMetadata(t *testing.T) {

View File

@@ -17,7 +17,6 @@ var promptAssetManifest = shared.PromptAssetManifest{
ModuleDir: PromptID,
ModuleFiles: []promptfs.ModulePromptFile{
{Name: "prompt.yaml", Path: "prompts/prompt.yaml"},
{Name: "task.md", Path: "prompts/task.md"},
{Name: "instructions.md", Path: "prompts/instructions.md"},
},
SharedFiles: []string{

View File

@@ -31,7 +31,7 @@ func TestRegisterPromptAssetsPreparesLocationPrompt(t *testing.T) {
t.Fatalf("NewEngine() error = %v", err)
}
prepared, err := engine.Prepare(context.Background(), promptkit.RunRequest{PromptID: PromptID, PromptVersion: SchemaVersion, ProfileID: "location-test-profile", Inputs: map[string]promptkit.ArtifactRef{
"transcript": promptkit.Inline(`{"units":[1]}`), "players": promptkit.Inline(" "), "party": promptkit.Inline(" "), "glossary": promptkit.Inline(" "),
"transcript": promptkit.Inline(`{"units":[{"sentinel":"location-transcript"}]}`), "players": promptkit.Inline("location-player"), "party": promptkit.Inline(" "), "glossary": promptkit.Inline(" "),
}})
if err != nil {
t.Fatalf("Prepare() error = %v", err)
@@ -39,12 +39,34 @@ func TestRegisterPromptAssetsPreparesLocationPrompt(t *testing.T) {
if prepared.OutputContract.SchemaPath != "dnd_locations_llm.v1.json" {
t.Fatalf("output contract = %#v", prepared.OutputContract)
}
if len(prepared.Messages) != 7 || !strings.Contains(prepared.Messages[3].Content, `"units"`) || strings.Contains(prepared.Messages[3].Content, "location-test") {
t.Fatalf("prepared messages = %#v, want rendered transcript only in transcript message", prepared.Messages)
if len(prepared.Messages) < 5 {
t.Fatalf("prepared messages = %#v, want shared policy, references, transcript, and instructions", prepared.Messages)
}
for _, index := range []int{2, 3, 6} {
referenceIndex := -1
transcriptIndex := -1
for index, message := range prepared.Messages {
if strings.Contains(message.Content, "location-player") {
referenceIndex = index
}
if strings.Contains(message.Content, "location-transcript") {
transcriptIndex = index
}
}
instructionsIndex := len(prepared.Messages) - 1
if referenceIndex < 0 || transcriptIndex <= referenceIndex || transcriptIndex >= instructionsIndex {
t.Fatalf("message order = references %d, transcript %d, instructions %d; want that order", referenceIndex, transcriptIndex, instructionsIndex)
}
for _, index := range []int{referenceIndex, transcriptIndex, instructionsIndex} {
if prepared.Messages[index].CacheControl == nil || prepared.Messages[index].CacheControl.Type != promptkit.CacheControlEphemeral {
t.Fatalf("message %d cache control = %#v, want ephemeral", index, prepared.Messages[index].CacheControl)
}
}
for index, message := range prepared.Messages {
if index != referenceIndex && strings.Contains(message.Content, "location-player") {
t.Errorf("message %d unexpectedly rendered campaign-reference input", index)
}
if index != transcriptIndex && strings.Contains(message.Content, "location-transcript") {
t.Errorf("message %d unexpectedly rendered transcript input", index)
}
}
}

View File

@@ -17,7 +17,6 @@ var promptAssetManifest = shared.PromptAssetManifest{
ModuleDir: "dnd.npcs",
ModuleFiles: []promptfs.ModulePromptFile{
{Name: "prompt.yaml", Path: "prompts/prompt.yaml"},
{Name: "task.md", Path: "prompts/task.md"},
{Name: "instructions.md", Path: "prompts/instructions.md"},
},
SharedFiles: []string{

View File

@@ -30,8 +30,8 @@ func TestRegisterPromptAssetsAndPrepareNPCPrompt(t *testing.T) {
prepared, err := engine.Prepare(context.Background(), promptkit.RunRequest{
PromptID: PromptID, PromptVersion: SchemaVersion, ProfileID: "npc-test-profile",
Inputs: map[string]promptkit.ArtifactRef{
"transcript": promptkit.InlineWithURI("file:///session.json", `{"units":[1]}`),
"players": promptkit.Inline(" "),
"transcript": promptkit.InlineWithURI("file:///session.json", `{"units":[{"sentinel":"npc-transcript"}]}`),
"players": promptkit.Inline("npc-player"),
"party": promptkit.Inline(" "),
"glossary": promptkit.Inline(" "),
},
@@ -42,6 +42,36 @@ func TestRegisterPromptAssetsAndPrepareNPCPrompt(t *testing.T) {
if prepared.PromptID != PromptID || prepared.OutputContract.SchemaPath != "dnd_npcs_llm.v1.json" {
t.Fatalf("prepared prompt = %#v, want NPC prompt identity and wiring", prepared)
}
if len(prepared.Messages) < 5 {
t.Fatalf("prepared messages = %#v, want shared policy, references, transcript, and instructions", prepared.Messages)
}
referenceIndex := -1
transcriptIndex := -1
for index, message := range prepared.Messages {
if strings.Contains(message.Content, "npc-player") {
referenceIndex = index
}
if strings.Contains(message.Content, "npc-transcript") {
transcriptIndex = index
}
}
instructionsIndex := len(prepared.Messages) - 1
if referenceIndex < 0 || transcriptIndex <= referenceIndex || transcriptIndex >= instructionsIndex {
t.Fatalf("message order = references %d, transcript %d, instructions %d; want that order", referenceIndex, transcriptIndex, instructionsIndex)
}
for _, index := range []int{referenceIndex, transcriptIndex, instructionsIndex} {
if cache := prepared.Messages[index].CacheControl; cache == nil || cache.Type != promptkit.CacheControlEphemeral {
t.Fatalf("message %d cache control = %#v, want ephemeral", index, cache)
}
}
for index, message := range prepared.Messages {
if index != referenceIndex && strings.Contains(message.Content, "npc-player") {
t.Errorf("message %d unexpectedly rendered campaign-reference input", index)
}
if index != transcriptIndex && strings.Contains(message.Content, "npc-transcript") {
t.Errorf("message %d unexpectedly rendered transcript input", index)
}
}
}
func TestPromptMetadataAndDiagnosticsDoNotContainRawAssets(t *testing.T) {