Simplify NPC normalization prompt guidance

This commit is contained in:
2026-07-26 02:40:08 +00:00
parent 5ad661f95f
commit d3c4d6f133
4 changed files with 39 additions and 22 deletions

View File

@@ -157,18 +157,18 @@ messages remain canonical shared assets rather than copied package text.
### D&D NPC Normalization Prompt Ordering And Cache Boundaries
NPC normalization has a distinct prompt and response-schema identity from NPC
extraction. Its stable message tiers are the common D&D system and identity
assets, followed by package-owned task and normalization instructions. Cache
boundaries follow the shared identity tier and the package instructions. The
variable tail contains the private candidate-name-and-range input and a
windowed transcript input whose cited units provide local context; neither has
a cache boundary because it changes with the document.
extraction. Its stable message tiers are the common D&D system asset, followed
by package-owned task and normalization instructions. Cache boundaries follow
the shared system tier and the package instructions. The variable tail contains
the private candidate-name-and-range input and a windowed transcript input
whose cited units provide local context; neither has a cache boundary because
it changes with the document.
This prompt intentionally omits extraction-evidence and campaign-reference
assets: it reconciles existing records rather than extracting events or adding
evidence. Its package-owned manifest and schema identity are fingerprinted
separately, so a normalization prompt or schema change cannot reuse a prior
normalization checkpoint.
This prompt intentionally omits shared identity guidance,
extraction-evidence, and campaign-reference assets: it reconciles existing
records rather than extracting events or adding evidence. Its package-owned
manifest and schema identity are fingerprinted separately, so a normalization
prompt or schema change cannot reuse a prior normalization checkpoint.
Shared wording belongs in the canonical assets under
`internal/modules/dnd/shared`; extraction packages reference those assets in

View File

@@ -11,8 +11,6 @@ inputs:
messages:
- role: system
content_file: ./sharedassets/common-dnd-system.md
- role: user
content_file: ./sharedassets/common-dnd-identity.md
cache_control:
type: ephemeral
- role: user

View File

@@ -21,7 +21,6 @@ var promptAssetManifest = shared.PromptAssetManifest{
},
SharedFiles: []string{
"common-dnd-system.md",
"common-dnd-identity.md",
"common-dnd-transcript.md",
},
}

View File

@@ -2,6 +2,7 @@ package npcs
import (
"context"
"reflect"
"strings"
"testing"
"time"
@@ -11,6 +12,12 @@ import (
)
func TestRegisterPromptAssetsPreparesNormalizationPrompt(t *testing.T) {
if want := []string{"common-dnd-system.md", "common-dnd-transcript.md"}; !reflect.DeepEqual(promptAssetManifest.SharedFiles, want) {
t.Fatalf("shared prompt assets = %#v, want %#v", promptAssetManifest.SharedFiles, want)
}
if promptHash, err := scriptoriumPromptMetadata(); err != nil || promptHash == "" {
t.Fatalf("scriptoriumPromptMetadata() = %q, %v; want prompt fingerprint", promptHash, err)
}
registry := llm.NewAssetRegistry()
if err := RegisterPromptAssets(registry); err != nil {
t.Fatalf("RegisterPromptAssets() error = %v", err)
@@ -39,23 +46,36 @@ func TestRegisterPromptAssetsPreparesNormalizationPrompt(t *testing.T) {
if prepared.PromptID != PromptID || prepared.OutputContract.SchemaPath != "dnd_npcs_normalize_llm.v1.json" {
t.Fatalf("prepared prompt = %#v, want normalization prompt identity and schema", prepared)
}
if len(prepared.Messages) != 6 {
t.Fatalf("prepared messages = %d, want 6", len(prepared.Messages))
if len(prepared.Messages) != 5 {
t.Fatalf("prepared messages = %d, want 5", len(prepared.Messages))
}
for _, index := range []int{1, 3} {
for index, role := range []string{"system", "user", "user", "user", "user"} {
if prepared.Messages[index].Role != role {
t.Errorf("message %d role = %q, want %q", index, prepared.Messages[index].Role, role)
}
}
for _, index := range []int{0, 2} {
if cache := prepared.Messages[index].CacheControl; cache == nil || cache.Type != scriptorium.CacheControlEphemeral {
t.Errorf("message %d cache control = %#v, want ephemeral", index, cache)
}
}
for _, index := range []int{0, 2, 4, 5} {
for _, index := range []int{1, 3, 4} {
if cache := prepared.Messages[index].CacheControl; cache != nil {
t.Errorf("message %d cache control = %#v, want nil", index, cache)
}
}
if !strings.Contains(prepared.Messages[4].Content, `"Mira"`) || strings.Contains(prepared.Messages[4].Content, `"windows"`) {
t.Fatalf("candidate message = %q, want only rendered candidates", prepared.Messages[4].Content)
if !strings.Contains(prepared.Messages[3].Content, `"Mira"`) || strings.Contains(prepared.Messages[3].Content, `"windows"`) {
t.Fatalf("candidate message = %q, want only rendered candidates", prepared.Messages[3].Content)
}
if !strings.Contains(prepared.Messages[5].Content, `"windows"`) || strings.Contains(prepared.Messages[5].Content, `"Mira"`) {
t.Fatalf("transcript message = %q, want only rendered transcript", prepared.Messages[5].Content)
if !strings.Contains(prepared.Messages[4].Content, `"windows"`) || strings.Contains(prepared.Messages[4].Content, `"Mira"`) {
t.Fatalf("transcript message = %q, want only rendered transcript", prepared.Messages[4].Content)
}
for index, message := range prepared.Messages {
if index != 3 && strings.Contains(message.Content, `"Mira"`) {
t.Errorf("message %d unexpectedly rendered candidate input", index)
}
if index != 4 && strings.Contains(message.Content, `"windows"`) {
t.Errorf("message %d unexpectedly rendered transcript input", index)
}
}
}