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

View File

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

View File

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

View File

@@ -2,6 +2,7 @@ package npcs
import ( import (
"context" "context"
"reflect"
"strings" "strings"
"testing" "testing"
"time" "time"
@@ -11,6 +12,12 @@ import (
) )
func TestRegisterPromptAssetsPreparesNormalizationPrompt(t *testing.T) { 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() registry := llm.NewAssetRegistry()
if err := RegisterPromptAssets(registry); err != nil { if err := RegisterPromptAssets(registry); err != nil {
t.Fatalf("RegisterPromptAssets() error = %v", err) 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" { 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) t.Fatalf("prepared prompt = %#v, want normalization prompt identity and schema", prepared)
} }
if len(prepared.Messages) != 6 { if len(prepared.Messages) != 5 {
t.Fatalf("prepared messages = %d, want 6", len(prepared.Messages)) 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 { if cache := prepared.Messages[index].CacheControl; cache == nil || cache.Type != scriptorium.CacheControlEphemeral {
t.Errorf("message %d cache control = %#v, want ephemeral", index, cache) 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 { if cache := prepared.Messages[index].CacheControl; cache != nil {
t.Errorf("message %d cache control = %#v, want nil", index, cache) 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"`) { 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[4].Content) 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"`) { 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[5].Content) 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)
}
} }
} }