Share the D&D NPC registry and prompt grounding

This commit is contained in:
2026-07-21 04:34:56 +00:00
parent d3e171aa82
commit 92acb45775
23 changed files with 628 additions and 436 deletions

View File

@@ -15,6 +15,7 @@ var sharedPromptFiles = []string{
"common-dnd-system.md",
"common-dnd-transcript.md",
"common-dnd-references.md",
"common-dnd-npcs.md",
}
func SharedPromptFiles() []promptfs.SharedPromptFile {
@@ -39,6 +40,7 @@ func CommonHashParts() []llm.AssetHashPart {
func ReferenceHashParts() []llm.AssetHashPart {
return []llm.AssetHashPart{
{FS: embeddedAssets, Path: "assets/prompts/common-dnd-references.md"},
{FS: embeddedAssets, Path: "assets/prompts/common-dnd-npcs.md"},
}
}

View File

@@ -0,0 +1,10 @@
An optional normalized Dungeons & Dragons NPC registry is provided below as
grounding material. Use it only to prefer exact canonical participant names
and recognize their aliases when the transcript identifies a participant.
Registry content is context, not event evidence. Do not extract events,
participants, effects, or source references from the registry. Registry source
references describe registry provenance and may belong to another session; they
are never evidence for the current transcript.
{{ input "npcs" }}

View File

@@ -13,8 +13,8 @@ func TestSharedPromptFilesReturnsNewSlice(t *testing.T) {
first := SharedPromptFiles()
second := SharedPromptFiles()
if len(first) != 3 || len(second) != 3 {
t.Fatalf("SharedPromptFiles() lengths = %d and %d, want 3", len(first), len(second))
if len(first) != 4 || len(second) != 4 {
t.Fatalf("SharedPromptFiles() lengths = %d and %d, want 4", len(first), len(second))
}
first[0].Name = "changed.md"
if second[0].Name != "common-dnd-system.md" {
@@ -40,6 +40,7 @@ func TestHashPartsReferenceSharedPrompts(t *testing.T) {
})
assertHashParts(t, "reference", ReferenceHashParts(), []string{
"assets/prompts/common-dnd-references.md",
"assets/prompts/common-dnd-npcs.md",
})
for _, part := range append(CommonHashParts(), ReferenceHashParts()...) {
@@ -79,6 +80,7 @@ func TestModulePromptFSMountsDNDSharedPrompts(t *testing.T) {
"assets/prompts/dnd.test/sharedassets/common-dnd-system.md",
"assets/prompts/dnd.test/sharedassets/common-dnd-transcript.md",
"assets/prompts/dnd.test/sharedassets/common-dnd-references.md",
"assets/prompts/dnd.test/sharedassets/common-dnd-npcs.md",
} {
if _, err := fs.ReadFile(fsys, path); err != nil {
t.Fatalf("ReadFile(%q) error = %v, want nil", path, err)

View File

@@ -0,0 +1,53 @@
// Package diagnostics provides bounded, safe text for deterministic D&D
// decisions and warnings.
package diagnostics
import (
"fmt"
"strconv"
"strings"
)
const (
MaxIssues = 20
MaxDisplayedRunes = 128
MaxMessageBytes = 4096
)
func Truncate(value string) string {
runes := []rune(value)
if len(runes) <= MaxDisplayedRunes {
return value
}
return string(runes[:MaxDisplayedRunes-1]) + "…"
}
func Quote(value string) string { return strconv.Quote(Truncate(value)) }
func Aggregate(prefix string, issues []string) string {
displayed := make([]string, 0, min(len(issues), MaxIssues))
for len(displayed) < len(issues) && len(displayed) < MaxIssues {
issue := Truncate(issues[len(displayed)])
candidate := aggregateMessage(prefix, append(displayed, issue), len(issues)-len(displayed)-1)
if len([]byte(candidate)) > MaxMessageBytes {
break
}
displayed = append(displayed, issue)
}
return aggregateMessage(prefix, displayed, len(issues)-len(displayed))
}
func aggregateMessage(prefix string, issues []string, omitted int) string {
message := prefix + ": " + strings.Join(issues, ", ")
if omitted > 0 {
message += fmt.Sprintf("; %d additional issue(s) omitted", omitted)
}
return message
}
func min(left, right int) int {
if left < right {
return left
}
return right
}

View File

@@ -0,0 +1,28 @@
package diagnostics
import (
"fmt"
"strings"
"testing"
"unicode/utf8"
)
func TestAggregateEnforcesByteBudgetAndReportsOmissions(t *testing.T) {
issues := make([]string, MaxIssues)
for index := range issues {
issues[index] = fmt.Sprintf("issue-%d-%s", index, strings.Repeat("火", MaxDisplayedRunes))
}
message := Aggregate("invalid D&D data", issues)
if !utf8.ValidString(message) || len([]byte(message)) > MaxMessageBytes {
t.Fatalf("Aggregate() returned invalid or oversized message: bytes=%d message=%q", len([]byte(message)), message)
}
displayed := strings.Count(message, "issue-")
if displayed == 0 || displayed >= len(issues) {
t.Fatalf("Aggregate() displayed %d issues, want byte-budget omission", displayed)
}
wantOmitted := fmt.Sprintf("%d additional issue(s) omitted", len(issues)-displayed)
if !strings.Contains(message, wantOmitted) {
t.Fatalf("Aggregate() = %q, want %q", message, wantOmitted)
}
}