diff --git a/assets/dnd/item-events/prompts/instructions.md b/assets/dnd/item-events/prompts/instructions.md index 2bbb421..8d9c700 100644 --- a/assets/dnd/item-events/prompts/instructions.md +++ b/assets/dnd/item-events/prompts/instructions.md @@ -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. diff --git a/assets/dnd/item-events/prompts/prompt.yaml b/assets/dnd/item-events/prompts/prompt.yaml index 2bf4df9..f098a45 100644 --- a/assets/dnd/item-events/prompts/prompt.yaml +++ b/assets/dnd/item-events/prompts/prompt.yaml @@ -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: diff --git a/assets/dnd/item-events/prompts/task.md b/assets/dnd/item-events/prompts/task.md deleted file mode 100644 index fe267c0..0000000 --- a/assets/dnd/item-events/prompts/task.md +++ /dev/null @@ -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. diff --git a/assets/dnd/locations/extract/prompts/instructions.md b/assets/dnd/locations/extract/prompts/instructions.md index aefa071..9ba187a 100644 --- a/assets/dnd/locations/extract/prompts/instructions.md +++ b/assets/dnd/locations/extract/prompts/instructions.md @@ -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. diff --git a/assets/dnd/locations/extract/prompts/prompt.yaml b/assets/dnd/locations/extract/prompts/prompt.yaml index ffac003..8b48b37 100644 --- a/assets/dnd/locations/extract/prompts/prompt.yaml +++ b/assets/dnd/locations/extract/prompts/prompt.yaml @@ -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: diff --git a/assets/dnd/locations/extract/prompts/task.md b/assets/dnd/locations/extract/prompts/task.md deleted file mode 100644 index 5b82c2f..0000000 --- a/assets/dnd/locations/extract/prompts/task.md +++ /dev/null @@ -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. diff --git a/assets/dnd/npcs/extract/prompts/instructions.md b/assets/dnd/npcs/extract/prompts/instructions.md index f41a847..32ccf92 100644 --- a/assets/dnd/npcs/extract/prompts/instructions.md +++ b/assets/dnd/npcs/extract/prompts/instructions.md @@ -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. diff --git a/assets/dnd/npcs/extract/prompts/prompt.yaml b/assets/dnd/npcs/extract/prompts/prompt.yaml index 703f974..ec539d7 100644 --- a/assets/dnd/npcs/extract/prompts/prompt.yaml +++ b/assets/dnd/npcs/extract/prompts/prompt.yaml @@ -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: diff --git a/assets/dnd/npcs/extract/prompts/task.md b/assets/dnd/npcs/extract/prompts/task.md deleted file mode 100644 index 30c087d..0000000 --- a/assets/dnd/npcs/extract/prompts/task.md +++ /dev/null @@ -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. diff --git a/internal/modules/dnd/extract/itemevents/prompt_assets.go b/internal/modules/dnd/extract/itemevents/prompt_assets.go index dc60503..b5cde98 100644 --- a/internal/modules/dnd/extract/itemevents/prompt_assets.go +++ b/internal/modules/dnd/extract/itemevents/prompt_assets.go @@ -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{ diff --git a/internal/modules/dnd/extract/itemevents/prompt_assets_test.go b/internal/modules/dnd/extract/itemevents/prompt_assets_test.go index 3176df3..dbcab5b 100644 --- a/internal/modules/dnd/extract/itemevents/prompt_assets_test.go +++ b/internal/modules/dnd/extract/itemevents/prompt_assets_test.go @@ -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) { diff --git a/internal/modules/dnd/extract/locations/prompt_assets.go b/internal/modules/dnd/extract/locations/prompt_assets.go index 921637b..2969241 100644 --- a/internal/modules/dnd/extract/locations/prompt_assets.go +++ b/internal/modules/dnd/extract/locations/prompt_assets.go @@ -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{ diff --git a/internal/modules/dnd/extract/locations/prompt_assets_test.go b/internal/modules/dnd/extract/locations/prompt_assets_test.go index d0e791b..0549e61 100644 --- a/internal/modules/dnd/extract/locations/prompt_assets_test.go +++ b/internal/modules/dnd/extract/locations/prompt_assets_test.go @@ -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) + } + } } diff --git a/internal/modules/dnd/extract/npcs/prompt_assets.go b/internal/modules/dnd/extract/npcs/prompt_assets.go index 40ffacc..2dda79d 100644 --- a/internal/modules/dnd/extract/npcs/prompt_assets.go +++ b/internal/modules/dnd/extract/npcs/prompt_assets.go @@ -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{ diff --git a/internal/modules/dnd/extract/npcs/prompt_assets_test.go b/internal/modules/dnd/extract/npcs/prompt_assets_test.go index 23c27e2..8fcdb89 100644 --- a/internal/modules/dnd/extract/npcs/prompt_assets_test.go +++ b/internal/modules/dnd/extract/npcs/prompt_assets_test.go @@ -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) {