Document D&D prompt conventions and verification
This commit is contained in:
@@ -43,59 +43,63 @@ the contracts above define durable data.
|
||||
|
||||
## Prompt Construction
|
||||
|
||||
D&D LLM-facing content lives beneath `assets/dnd/`. New extractor content uses
|
||||
its feature subtree; when a family has both extraction and normalization
|
||||
content, keep those in its `extract` and `normalize` subtrees. The owning module
|
||||
still defines the ordered manifest and registers the resulting scoped filesystem.
|
||||
Shared fragments belong to the D&D shared implementation and are selected by
|
||||
name, never copied into individual module subtrees.
|
||||
D&D LLM-facing content lives beneath `assets/dnd/`. Each module contributes a
|
||||
local `prompt.yaml` declaration and `instructions.md`; input-specific files
|
||||
such as a catalog, registry, grounding projection, or candidate collection are
|
||||
local only when that module needs them. New extractor content uses its feature
|
||||
subtree, while families with both extraction and normalization content use their
|
||||
`extract` and `normalize` subtrees. Shared visual-provenance fragments use
|
||||
the `common-dnd-` prefix.
|
||||
|
||||
D&D extractors assemble prompts from an ordered manifest of shared and
|
||||
module-selected assets. The location extractor and occurrence extractor reuse
|
||||
the shared D&D system, evidence, identity, reference, and transcript assets
|
||||
instead of copying their text into individual modules. A manifest’s declared
|
||||
sequence, including cache-control placement, is part of the prompt behavior.
|
||||
The owning module’s manifest is the source of truth for which local and shared
|
||||
assets are selected, their mount paths, their message order, cache controls,
|
||||
and the files included in its prompt fingerprint. Shared fragments belong to
|
||||
the D&D shared implementation and are selected by name rather than copied into
|
||||
module directories. The root `assets` package is a content-only boundary; its
|
||||
physical ownership and rationale are defined by
|
||||
[ADR-0011](../adr/0011-centralize-llm-assets.md).
|
||||
|
||||
Put each rule at its narrowest owner:
|
||||
|
||||
- universal behavior belongs in the shared system asset;
|
||||
- D&D-family behavior belongs in a selected `common-dnd-` asset;
|
||||
- rules for an input projection belong with that input asset;
|
||||
- lane-specific policy belongs in the module’s `instructions.md`; and
|
||||
- transport-envelope shape belongs in the private response schema.
|
||||
|
||||
A rule is eligible for the system prompt only when every D&D LLM prompt needs
|
||||
it regardless of lane, inputs, or response shape. Module instructions must not
|
||||
repeat rules selected from shared assets or schemas. Reintroduce such repetition
|
||||
only after observational evaluation with representative transcripts shows that
|
||||
it improves results at the intended target models and cost; structural prompt
|
||||
tests alone are not that evidence.
|
||||
|
||||
Every maintained D&D LLM prompt selects `dnd-extraction` as its default
|
||||
profile. The D&D registrar registers that fallback profile with the maintained
|
||||
OpenRouter model, timeout, and service-tier policy. An operator may provide a
|
||||
complete profile with the same ID through the configured PromptKit source; that
|
||||
definition replaces the fallback rather than merging with it. The fallback
|
||||
leaves reasoning and optional sampling controls unspecified. Deployment profile
|
||||
selection and the maintained operator example are documented in
|
||||
profile. The D&D registrar registers the fallback, while an operator can
|
||||
replace it with a complete profile of the same ID from the configured PromptKit
|
||||
source. Deployment profile selection is documented in
|
||||
[Configuration](../config.md#promptkit-profiles).
|
||||
|
||||
All extraction prompts share this four-message rendered prefix: the system
|
||||
message without cache control, the identity message without cache control, the
|
||||
campaign-reference message with ephemeral cache control, and the chunk
|
||||
transcript message with ephemeral cache control. This gives equivalent
|
||||
extraction requests the same reusable prefix through their source material.
|
||||
The transcript assets have distinct consumers. Scene chunking consumes the
|
||||
complete-session `common-dnd-transcript-full.md`; extraction prompts consume
|
||||
the current-chunk `common-dnd-transcript-chunk.md`; and NPC and location
|
||||
normalization consume `common-dnd-transcript-windows.md` alongside their
|
||||
candidate collections. Player, party, glossary, and compatible campaign
|
||||
references provide disambiguating context, not evidence. Reference material is
|
||||
canonically ordered before rendering so equivalent inputs remain stable.
|
||||
|
||||
Extraction-evidence policy, generated NPC registries, spell catalogs, module
|
||||
tasks, and instructions follow the transcript because they are not universal
|
||||
across all extraction lanes. The final instructions message carries ephemeral
|
||||
cache control; evidence, registry, catalog, and task messages do not. Preserve
|
||||
this division when changing an extractor or its assets so prompt-cache behavior
|
||||
remains stable.
|
||||
Extraction prompts render the common system and identity messages first, then
|
||||
cached campaign references and the cached chunk transcript. Evidence policy and
|
||||
any lane-specific registry, catalog, or grounding projection follow that
|
||||
prefix. The final module instructions message is ephemeral. This keeps the
|
||||
reusable extraction prefix identical while preserving the lane-specific suffix.
|
||||
|
||||
The other D&D LLM prompts intentionally follow different patterns. Scene
|
||||
chunking has no sibling extraction lane with which to share its full transcript,
|
||||
so it renders campaign references before its task and instructions, then places
|
||||
the cacheable full transcript last. NPC and location normalization share the
|
||||
entity-reconciliation response schema and safety boundary while retaining their
|
||||
own task and identity rules. NPC normalization keeps its task and
|
||||
cacheable instructions before the candidate collection, followed by the
|
||||
cacheable transcript windows: candidates must be available before their
|
||||
supporting evidence is evaluated, and those windows are not a cross-lane
|
||||
prefix. Mounted assets and their declared message order determine the prompt
|
||||
fingerprint, so intentional prompt edits continue to invalidate stale
|
||||
checkpoints.
|
||||
|
||||
All extractors use the shared prompt-input preparation rules. The current chunk
|
||||
is copied into transcript material; player, party, glossary, and compatible
|
||||
campaign references are context for disambiguation, not source evidence.
|
||||
Reference prompt material is canonically ordered before it is rendered, which
|
||||
keeps equivalent inputs stable across runs.
|
||||
Scene chunking intentionally uses a different order: system, cached campaign
|
||||
references, uncached module instructions, then the final ephemeral full
|
||||
transcript. Entity normalization also has its own order: system, uncached
|
||||
module instructions, ephemeral reconciliation policy, uncached candidates, and
|
||||
final ephemeral transcript windows. These orders and cache controls are prompt
|
||||
behavior; change them only through the owning manifest and prompt declaration.
|
||||
|
||||
## Evidence, Candidates, And Normalization
|
||||
|
||||
|
||||
@@ -185,13 +185,11 @@ func TestAssetRegistryFallbackProfileDigestTracksContentWithoutLeakingIt(t *test
|
||||
func TestAssetRegistryCombinesNamespacedPromptSources(t *testing.T) {
|
||||
registry := NewAssetRegistry()
|
||||
mustRegisterPromptFS(t, registry, fstest.MapFS{
|
||||
"dnd.spells/dnd.spells.yaml": {Data: []byte(validPromptYAML("schema.json"))},
|
||||
"dnd.spells/task.md": {Data: []byte("spell task")},
|
||||
"dnd.spells/prompt.yaml": {Data: []byte(validPromptYAML("schema.json"))},
|
||||
"dnd.spells/instructions.md": {Data: []byte("spell instructions")},
|
||||
}, ".")
|
||||
mustRegisterPromptFS(t, registry, fstest.MapFS{
|
||||
"dnd.scenes/dnd.scenes.yaml": {Data: []byte(validPromptYAML("schema.json"))},
|
||||
"dnd.scenes/task.md": {Data: []byte("scene task")},
|
||||
"dnd.scenes/prompt.yaml": {Data: []byte(validPromptYAML("schema.json"))},
|
||||
"dnd.scenes/instructions.md": {Data: []byte("scene instructions")},
|
||||
}, ".")
|
||||
|
||||
@@ -200,11 +198,9 @@ func TestAssetRegistryCombinesNamespacedPromptSources(t *testing.T) {
|
||||
t.Fatalf("PromptFS() error = %v, want nil", err)
|
||||
}
|
||||
for _, name := range []string{
|
||||
"dnd.spells/dnd.spells.yaml",
|
||||
"dnd.spells/task.md",
|
||||
"dnd.spells/prompt.yaml",
|
||||
"dnd.spells/instructions.md",
|
||||
"dnd.scenes/dnd.scenes.yaml",
|
||||
"dnd.scenes/task.md",
|
||||
"dnd.scenes/prompt.yaml",
|
||||
"dnd.scenes/instructions.md",
|
||||
} {
|
||||
if _, err := fsys.Open(name); err != nil {
|
||||
|
||||
@@ -14,11 +14,11 @@ func TestModulePromptFSCombinesModuleAndSharedPrompts(t *testing.T) {
|
||||
"nested/transcript.md": {Data: []byte("transcript")},
|
||||
}
|
||||
fsys, err := ModulePromptFS("module.test", fstest.MapFS{
|
||||
"assets/prompts/module.test.yaml": {Data: []byte("id: module.test")},
|
||||
"assets/prompts/task.md": {Data: []byte("task")},
|
||||
"assets/prompts/prompt.yaml": {Data: []byte("id: module.test")},
|
||||
"assets/prompts/instructions.md": {Data: []byte("instructions")},
|
||||
}, []ModulePromptFile{
|
||||
{Name: "module.test.yaml", Path: "assets/prompts/module.test.yaml"},
|
||||
{Name: "task.md", Path: "assets/prompts/task.md"},
|
||||
{Name: "prompt.yaml", Path: "assets/prompts/prompt.yaml"},
|
||||
{Name: "instructions.md", Path: "assets/prompts/instructions.md"},
|
||||
},
|
||||
SharedPromptFile{Name: "system.md", FS: sharedFS, Path: "shared/system.md"},
|
||||
SharedPromptFile{Name: "reference.md", FS: sharedFS, Path: "shared/reference.md"},
|
||||
@@ -29,8 +29,8 @@ func TestModulePromptFSCombinesModuleAndSharedPrompts(t *testing.T) {
|
||||
}
|
||||
|
||||
tests := map[string]string{
|
||||
"assets/prompts/module.test/module.test.yaml": "id: module.test",
|
||||
"assets/prompts/module.test/task.md": "task",
|
||||
"assets/prompts/module.test/prompt.yaml": "id: module.test",
|
||||
"assets/prompts/module.test/instructions.md": "instructions",
|
||||
"assets/prompts/module.test/sharedassets/system.md": "system",
|
||||
"assets/prompts/module.test/sharedassets/reference.md": "reference",
|
||||
"assets/prompts/module.test/sharedassets/transcript.md": "transcript",
|
||||
@@ -63,18 +63,18 @@ func TestModulePromptFSCombinesModuleAndSharedPrompts(t *testing.T) {
|
||||
|
||||
func TestModulePromptFSRejectsMissingModuleFile(t *testing.T) {
|
||||
_, err := ModulePromptFS("module.test", fstest.MapFS{}, []ModulePromptFile{
|
||||
{Name: "task.md", Path: "assets/prompts/task.md"},
|
||||
{Name: "instructions.md", Path: "assets/prompts/instructions.md"},
|
||||
})
|
||||
if err == nil || !strings.Contains(err.Error(), "read module prompt asset assets/prompts/task.md") {
|
||||
if err == nil || !strings.Contains(err.Error(), "read module prompt asset assets/prompts/instructions.md") {
|
||||
t.Fatalf("ModulePromptFS() error = %v, want missing module asset context", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestModulePromptFSRejectsMissingSharedFile(t *testing.T) {
|
||||
_, err := ModulePromptFS("module.test", fstest.MapFS{
|
||||
"assets/prompts/task.md": {Data: []byte("task")},
|
||||
"assets/prompts/instructions.md": {Data: []byte("instructions")},
|
||||
}, []ModulePromptFile{
|
||||
{Name: "task.md", Path: "assets/prompts/task.md"},
|
||||
{Name: "instructions.md", Path: "assets/prompts/instructions.md"},
|
||||
}, SharedPromptFile{Name: "system.md", FS: fstest.MapFS{}, Path: "shared/system.md"})
|
||||
if err == nil || !strings.Contains(err.Error(), "read shared prompt asset shared/system.md") {
|
||||
t.Fatalf("ModulePromptFS() error = %v, want missing shared asset context", err)
|
||||
@@ -83,9 +83,9 @@ func TestModulePromptFSRejectsMissingSharedFile(t *testing.T) {
|
||||
|
||||
func TestModulePromptFSRejectsNestedVirtualFileName(t *testing.T) {
|
||||
_, err := ModulePromptFS("module.test", fstest.MapFS{
|
||||
"assets/prompts/task.md": {Data: []byte("task")},
|
||||
"assets/prompts/instructions.md": {Data: []byte("instructions")},
|
||||
}, []ModulePromptFile{
|
||||
{Name: "nested/task.md", Path: "assets/prompts/task.md"},
|
||||
{Name: "nested/instructions.md", Path: "assets/prompts/instructions.md"},
|
||||
})
|
||||
if err == nil || !strings.Contains(err.Error(), "must not contain path separators") {
|
||||
t.Fatalf("ModulePromptFS() error = %v, want nested file name error", err)
|
||||
@@ -94,9 +94,9 @@ func TestModulePromptFSRejectsNestedVirtualFileName(t *testing.T) {
|
||||
|
||||
func TestModulePromptFSRejectsNestedSharedFileName(t *testing.T) {
|
||||
_, err := ModulePromptFS("module.test", fstest.MapFS{
|
||||
"assets/prompts/task.md": {Data: []byte("task")},
|
||||
"assets/prompts/instructions.md": {Data: []byte("instructions")},
|
||||
}, []ModulePromptFile{
|
||||
{Name: "task.md", Path: "assets/prompts/task.md"},
|
||||
{Name: "instructions.md", Path: "assets/prompts/instructions.md"},
|
||||
}, SharedPromptFile{Name: "nested/system.md", FS: fstest.MapFS{}, Path: "shared/system.md"})
|
||||
if err == nil || !strings.Contains(err.Error(), "shared prompt file name") || !strings.Contains(err.Error(), "must not contain path separators") {
|
||||
t.Fatalf("ModulePromptFS() error = %v, want nested shared file name error", err)
|
||||
|
||||
@@ -25,9 +25,6 @@ func TestPromptAssetsPrepareTranscriptWithInstructionOrdering(t *testing.T) {
|
||||
if prepared.OutputContract.SchemaPath != "dnd_scenes_llm.v1.json" {
|
||||
t.Fatalf("scene output schema path = %q, want private LLM schema", prepared.OutputContract.SchemaPath)
|
||||
}
|
||||
if len(prepared.Messages) < 4 {
|
||||
t.Fatalf("prepared messages = %#v, want system, references, instructions, and transcript", prepared.Messages)
|
||||
}
|
||||
if prepared.Messages[0].Role != "system" || prepared.Messages[1].Role != "user" {
|
||||
t.Fatalf("initial prompt roles = %#v, want system followed by campaign references", prepared.Messages[:2])
|
||||
}
|
||||
|
||||
@@ -62,9 +62,6 @@ func TestPromptAssetsPrepareRequiredInputs(t *testing.T) {
|
||||
if prepared.PromptID != PromptID || prepared.OutputContract.SchemaPath != "dnd_combat_turns_llm.v1.json" {
|
||||
t.Fatalf("prepared prompt = %#v, want combat prompt identity and schema", prepared)
|
||||
}
|
||||
if len(prepared.Messages) < 6 {
|
||||
t.Fatalf("prepared messages = %#v, want shared context, NPC grounding, and instructions", prepared.Messages)
|
||||
}
|
||||
referenceIndex := -1
|
||||
transcriptIndex := -1
|
||||
npcIndex := -1
|
||||
|
||||
@@ -41,7 +41,7 @@ func TestRegisterPromptAssetsAndPrepareEnemyEventPrompt(t *testing.T) {
|
||||
groundingIndices = append(groundingIndices, index)
|
||||
}
|
||||
}
|
||||
instructionIndex := renderedMessageIndex(t, prepared.Messages, "Extract Dungeons & Dragons enemy events")
|
||||
instructionIndex := len(prepared.Messages) - 1
|
||||
for _, index := range groundingIndices {
|
||||
if instructionIndex <= index {
|
||||
t.Fatalf("instruction index = %d, want after grounding index %d", instructionIndex, index)
|
||||
|
||||
@@ -3,7 +3,6 @@ package itemevents
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -43,9 +42,6 @@ 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 {
|
||||
@@ -91,16 +87,3 @@ func TestPromptAssetsDoNotLeakIntoMetadata(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPromptManifestReusesOnlySharedAssets(t *testing.T) {
|
||||
want := []string{
|
||||
"common-dnd-system.md",
|
||||
"common-dnd-extraction-evidence.md",
|
||||
"common-dnd-identity.md",
|
||||
"common-dnd-references.md",
|
||||
"common-dnd-transcript-chunk.md",
|
||||
}
|
||||
if !reflect.DeepEqual(promptAssetManifest.SharedFiles, want) {
|
||||
t.Fatalf("shared assets = %#v, want %#v", promptAssetManifest.SharedFiles, want)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package locations
|
||||
|
||||
import (
|
||||
"context"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -12,11 +11,6 @@ import (
|
||||
)
|
||||
|
||||
func TestRegisterPromptAssetsPreparesLocationPrompt(t *testing.T) {
|
||||
for _, name := range []string{"common-dnd-system.md", "common-dnd-identity.md", "common-dnd-references.md", "common-dnd-transcript-chunk.md", "common-dnd-extraction-evidence.md"} {
|
||||
if !slices.Contains(promptAssetManifest.SharedFiles, name) {
|
||||
t.Fatalf("shared prompt assets = %#v, missing %q", promptAssetManifest.SharedFiles, name)
|
||||
}
|
||||
}
|
||||
registry := llm.NewAssetRegistry()
|
||||
if err := RegisterPromptAssets(registry); err != nil {
|
||||
t.Fatalf("RegisterPromptAssets() error = %v", err)
|
||||
@@ -39,9 +33,6 @@ 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) < 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 {
|
||||
|
||||
@@ -51,9 +51,6 @@ func TestRegisterPromptAssetsAndPrepareInteractionPrompt(t *testing.T) {
|
||||
if prepared.PromptID != PromptID || prepared.OutputContract.SchemaPath != "dnd_npc_interactions_llm.v1.json" {
|
||||
t.Fatalf("prepared prompt = %#v", prepared)
|
||||
}
|
||||
if len(prepared.Messages) < 6 {
|
||||
t.Fatalf("prepared messages = %#v, want shared context, NPC grounding, and instructions", prepared.Messages)
|
||||
}
|
||||
referenceIndex := -1
|
||||
transcriptIndex := -1
|
||||
npcIndex := -1
|
||||
|
||||
@@ -42,9 +42,6 @@ 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 {
|
||||
|
||||
@@ -42,9 +42,6 @@ func TestRegisterPromptAssetsPreparesSceneDescriptionPrompt(t *testing.T) {
|
||||
if prepared.PromptID != PromptID || prepared.OutputContract.SchemaPath != "dnd_scene_descriptions_llm.v1.json" {
|
||||
t.Fatalf("prepared prompt = %#v, want scene-description prompt identity and schema 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 {
|
||||
|
||||
@@ -21,9 +21,6 @@ func TestPromptAssetsPrepareSpellPrompt(t *testing.T) {
|
||||
if prepared.OutputContract.SchemaPath != "dnd_spells_llm.v1.json" {
|
||||
t.Fatalf("schema path = %q, want LLM-only schema", prepared.OutputContract.SchemaPath)
|
||||
}
|
||||
if len(prepared.Messages) < 7 {
|
||||
t.Fatalf("prepared messages = %#v, want shared context, grounded inputs, and instructions", prepared.Messages)
|
||||
}
|
||||
indices := map[string]int{
|
||||
"spell-player": -1,
|
||||
"spell-transcript": -1,
|
||||
|
||||
@@ -12,18 +12,6 @@ import (
|
||||
)
|
||||
|
||||
func TestRegisterPromptAssetsPreparesLocationNormalizationPrompt(t *testing.T) {
|
||||
hasInstructions := false
|
||||
for _, file := range promptAssetManifest.ModuleFiles {
|
||||
if file.Name == "task.md" {
|
||||
t.Fatalf("module prompt files = %#v, want no retired task asset", promptAssetManifest.ModuleFiles)
|
||||
}
|
||||
if file.Name == "instructions.md" {
|
||||
hasInstructions = true
|
||||
}
|
||||
}
|
||||
if !hasInstructions {
|
||||
t.Fatalf("module prompt files = %#v, want instructions asset", promptAssetManifest.ModuleFiles)
|
||||
}
|
||||
registry := llm.NewAssetRegistry()
|
||||
if err := entityreconcile.RegisterSchemaAssets(registry); err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -44,7 +32,7 @@ func TestRegisterPromptAssetsPreparesLocationNormalizationPrompt(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if prepared.OutputContract.SchemaPath != "dnd_entity_reconcile_llm.v1.json" || len(prepared.Messages) < 5 {
|
||||
if prepared.OutputContract.SchemaPath != "dnd_entity_reconcile_llm.v1.json" {
|
||||
t.Fatalf("prepared prompt = %#v", prepared)
|
||||
}
|
||||
for _, index := range []int{2, 4} {
|
||||
|
||||
@@ -2,7 +2,6 @@ package npcs
|
||||
|
||||
import (
|
||||
"context"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -13,21 +12,6 @@ import (
|
||||
)
|
||||
|
||||
func TestRegisterPromptAssetsPreparesNormalizationPrompt(t *testing.T) {
|
||||
if want := []string{"common-dnd-system.md", "common-dnd-entity-reconciliation.md", "common-dnd-transcript-windows.md"}; !reflect.DeepEqual(promptAssetManifest.SharedFiles, want) {
|
||||
t.Fatalf("shared prompt assets = %#v, want %#v", promptAssetManifest.SharedFiles, want)
|
||||
}
|
||||
hasInstructions := false
|
||||
for _, file := range promptAssetManifest.ModuleFiles {
|
||||
if file.Name == "task.md" {
|
||||
t.Fatalf("module prompt files = %#v, want no retired task asset", promptAssetManifest.ModuleFiles)
|
||||
}
|
||||
if file.Name == "instructions.md" {
|
||||
hasInstructions = true
|
||||
}
|
||||
}
|
||||
if !hasInstructions {
|
||||
t.Fatalf("module prompt files = %#v, want instructions asset", promptAssetManifest.ModuleFiles)
|
||||
}
|
||||
if promptHash, err := promptAssetMetadata(); err != nil || promptHash == "" {
|
||||
t.Fatalf("promptAssetMetadata() = %q, %v; want prompt fingerprint", promptHash, err)
|
||||
}
|
||||
@@ -62,9 +46,6 @@ func TestRegisterPromptAssetsPreparesNormalizationPrompt(t *testing.T) {
|
||||
if prepared.PromptID != PromptID || prepared.OutputContract.SchemaPath != "dnd_entity_reconcile_llm.v1.json" {
|
||||
t.Fatalf("prepared prompt = %#v, want normalization prompt identity and schema", prepared)
|
||||
}
|
||||
if len(prepared.Messages) < 5 {
|
||||
t.Fatalf("prepared messages = %#v, want system, instructions, reconciliation policy, candidates, and transcript windows", prepared.Messages)
|
||||
}
|
||||
if prepared.Messages[0].Role != "system" {
|
||||
t.Fatalf("initial message role = %q, want system", prepared.Messages[0].Role)
|
||||
}
|
||||
|
||||
@@ -15,8 +15,8 @@ func TestPromptAssetManifestPromptFS(t *testing.T) {
|
||||
manifest := PromptAssetManifest{
|
||||
ModuleDir: "dnd.test",
|
||||
ModuleFiles: []promptfs.ModulePromptFile{
|
||||
{Name: "dnd.test.yaml", Path: "assets/prompts/dnd.test.yaml"},
|
||||
{Name: "task.md", Path: "assets/prompts/task.md"},
|
||||
{Name: "prompt.yaml", Path: "assets/prompts/prompt.yaml"},
|
||||
{Name: "instructions.md", Path: "assets/prompts/instructions.md"},
|
||||
},
|
||||
SharedFiles: []string{
|
||||
"common-dnd-system.md",
|
||||
@@ -27,16 +27,16 @@ func TestPromptAssetManifestPromptFS(t *testing.T) {
|
||||
}
|
||||
|
||||
fsys, err := manifest.PromptFS(fstest.MapFS{
|
||||
"assets/prompts/dnd.test.yaml": {Data: []byte("id: dnd.test")},
|
||||
"assets/prompts/task.md": {Data: []byte("task")},
|
||||
"assets/prompts/prompt.yaml": {Data: []byte("id: dnd.test")},
|
||||
"assets/prompts/instructions.md": {Data: []byte("instructions")},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("PromptFS() error = %v, want nil", err)
|
||||
}
|
||||
|
||||
wantModule := map[string]string{
|
||||
"assets/prompts/dnd.test/dnd.test.yaml": "id: dnd.test",
|
||||
"assets/prompts/dnd.test/task.md": "task",
|
||||
"assets/prompts/dnd.test/prompt.yaml": "id: dnd.test",
|
||||
"assets/prompts/dnd.test/instructions.md": "instructions",
|
||||
}
|
||||
for path, wantContent := range wantModule {
|
||||
content, err := fs.ReadFile(fsys, path)
|
||||
@@ -88,11 +88,11 @@ func TestPromptAssetManifestRejectsInvalidSharedNames(t *testing.T) {
|
||||
_, err := (PromptAssetManifest{
|
||||
ModuleDir: "dnd.test",
|
||||
ModuleFiles: []promptfs.ModulePromptFile{
|
||||
{Name: "dnd.test.yaml", Path: "assets/prompts/dnd.test.yaml"},
|
||||
{Name: "prompt.yaml", Path: "assets/prompts/prompt.yaml"},
|
||||
},
|
||||
SharedFiles: test.sharedFiles,
|
||||
}).PromptFS(fstest.MapFS{
|
||||
"assets/prompts/dnd.test.yaml": {Data: []byte("id: dnd.test")},
|
||||
"assets/prompts/prompt.yaml": {Data: []byte("id: dnd.test")},
|
||||
})
|
||||
if err == nil || !strings.Contains(err.Error(), test.wantError) {
|
||||
t.Fatalf("PromptFS() error = %v, want %q", err, test.wantError)
|
||||
@@ -105,10 +105,10 @@ func TestPromptAssetManifestRejectsMissingModuleFile(t *testing.T) {
|
||||
_, err := (PromptAssetManifest{
|
||||
ModuleDir: "dnd.test",
|
||||
ModuleFiles: []promptfs.ModulePromptFile{
|
||||
{Name: "task.md", Path: "assets/prompts/task.md"},
|
||||
{Name: "instructions.md", Path: "assets/prompts/instructions.md"},
|
||||
},
|
||||
}).PromptFS(fstest.MapFS{})
|
||||
if err == nil || !strings.Contains(err.Error(), "read module prompt asset assets/prompts/task.md") {
|
||||
if err == nil || !strings.Contains(err.Error(), "read module prompt asset assets/prompts/instructions.md") {
|
||||
t.Fatalf("PromptFS() error = %v, want missing module asset context", err)
|
||||
}
|
||||
}
|
||||
@@ -129,11 +129,11 @@ func TestPromptAssetManifestRejectsMissingSharedFile(t *testing.T) {
|
||||
_, err := (PromptAssetManifest{
|
||||
ModuleDir: "dnd.test",
|
||||
ModuleFiles: []promptfs.ModulePromptFile{
|
||||
{Name: "dnd.test.yaml", Path: "assets/prompts/dnd.test.yaml"},
|
||||
{Name: "prompt.yaml", Path: "assets/prompts/prompt.yaml"},
|
||||
},
|
||||
SharedFiles: []string{name},
|
||||
}).PromptFS(fstest.MapFS{
|
||||
"assets/prompts/dnd.test.yaml": {Data: []byte("id: dnd.test")},
|
||||
"assets/prompts/prompt.yaml": {Data: []byte("id: dnd.test")},
|
||||
})
|
||||
if err == nil || !strings.Contains(err.Error(), "read shared prompt asset "+path) {
|
||||
t.Fatalf("PromptFS() error = %v, want missing shared asset context", err)
|
||||
@@ -142,14 +142,14 @@ func TestPromptAssetManifestRejectsMissingSharedFile(t *testing.T) {
|
||||
|
||||
func TestPromptAssetManifestHashMatchesManifestParts(t *testing.T) {
|
||||
moduleFS := fstest.MapFS{
|
||||
"assets/prompts/dnd.test.yaml": {Data: []byte("id: dnd.test")},
|
||||
"assets/prompts/task.md": {Data: []byte("task")},
|
||||
"assets/prompts/prompt.yaml": {Data: []byte("id: dnd.test")},
|
||||
"assets/prompts/instructions.md": {Data: []byte("instructions")},
|
||||
}
|
||||
manifest := PromptAssetManifest{
|
||||
ModuleDir: "dnd.test",
|
||||
ModuleFiles: []promptfs.ModulePromptFile{
|
||||
{Name: "dnd.test.yaml", Path: "assets/prompts/dnd.test.yaml"},
|
||||
{Name: "task.md", Path: "assets/prompts/task.md"},
|
||||
{Name: "prompt.yaml", Path: "assets/prompts/prompt.yaml"},
|
||||
{Name: "instructions.md", Path: "assets/prompts/instructions.md"},
|
||||
},
|
||||
SharedFiles: []string{
|
||||
"common-dnd-transcript-chunk.md",
|
||||
@@ -166,8 +166,8 @@ func TestPromptAssetManifestHashMatchesManifestParts(t *testing.T) {
|
||||
t.Fatalf("sharedAssetFS() error = %v, want nil", err)
|
||||
}
|
||||
want, err := llm.HashAssets([]llm.AssetHashPart{
|
||||
{FS: moduleFS, Path: "assets/prompts/dnd.test.yaml"},
|
||||
{FS: moduleFS, Path: "assets/prompts/task.md"},
|
||||
{FS: moduleFS, Path: "assets/prompts/prompt.yaml"},
|
||||
{FS: moduleFS, Path: "assets/prompts/instructions.md"},
|
||||
{FS: sharedFS, Path: "prompts/common-dnd-transcript-chunk.md"},
|
||||
{FS: sharedFS, Path: "prompts/common-dnd-system.md"},
|
||||
})
|
||||
@@ -178,8 +178,8 @@ func TestPromptAssetManifestHashMatchesManifestParts(t *testing.T) {
|
||||
t.Fatalf("Hash() = %q, want independently assembled manifest hash %q", got, want)
|
||||
}
|
||||
withUnused, err := llm.HashAssets([]llm.AssetHashPart{
|
||||
{FS: moduleFS, Path: "assets/prompts/dnd.test.yaml"},
|
||||
{FS: moduleFS, Path: "assets/prompts/task.md"},
|
||||
{FS: moduleFS, Path: "assets/prompts/prompt.yaml"},
|
||||
{FS: moduleFS, Path: "assets/prompts/instructions.md"},
|
||||
{FS: sharedFS, Path: "prompts/common-dnd-transcript-chunk.md"},
|
||||
{FS: sharedFS, Path: "prompts/common-dnd-system.md"},
|
||||
{FS: sharedFS, Path: "prompts/common-dnd-npcs.md"},
|
||||
|
||||
@@ -61,8 +61,8 @@ func TestRunnerProcessesSeriatimInputWithDNDSpellsExtractor(t *testing.T) {
|
||||
t.Fatalf("len(NormalizeOutputs) = %d, want 1", len(output.NormalizeOutputs))
|
||||
}
|
||||
serializedOutput := output.NormalizeOutputs[0]
|
||||
if serializedOutput.LaneID != "spells" || serializedOutput.Artifact.Schema.ID != spells.ResponseSchemaID || serializedOutput.Artifact.Schema.Version != spells.SchemaVersion {
|
||||
t.Fatalf("serialized output envelope = %#v, want dnd spells schema on spells lane", serializedOutput)
|
||||
if serializedOutput.LaneID != "spells" || serializedOutput.Artifact.Schema.ID != spellcodec.SchemaID || serializedOutput.Artifact.Schema.Version != spellcodec.SchemaVersion {
|
||||
t.Fatalf("serialized output envelope = %#v, want durable spell artifact schema on spells lane", serializedOutput)
|
||||
}
|
||||
response := decodeRunnerSpellResponse(t, serializedOutput.Artifact.Content)
|
||||
if len(response.SpellCasts) != 2 {
|
||||
|
||||
Reference in New Issue
Block a user