Extract shared D&D combat policy

This commit is contained in:
2026-08-28 00:13:44 +00:00
parent 0f2b23dce1
commit fcfff3ad15
8 changed files with 62 additions and 22 deletions

View File

@@ -5,9 +5,7 @@ into multiple scenes or use facts that are not supported by it.
Return one kind, one concise title, and one concise summary. Choose exactly one Return one kind, one concise title, and one concise summary. Choose exactly one
kind: kind:
- combat: active combat materially organizes the scene, including - combat: a scene classified as combat under the shared combat policy.
initiative-like exchanges or sustained hostile action. Planning a fight or
discussing a completed fight is not combat by itself.
- narrative: current-session in-world play that is not principally active - narrative: current-session in-world play that is not principally active
combat, a prior-session recap, or sustained out-of-character session combat, a prior-session recap, or sustained out-of-character session
discussion. This includes exploration, travel, dialogue, investigation, discussion. This includes exploration, travel, dialogue, investigation,
@@ -20,15 +18,13 @@ kind:
play. play.
Narrative is the default for actual current-session gameplay that does not meet Narrative is the default for actual current-session gameplay that does not meet
another definition. When the accepted chunk is mixed: another definition. When the accepted chunk has no substantive active combat:
1. use combat when active combat is a substantive central activity, even with 1. use recap when recounting a previous session is the chunk's
brief setup, rules clarification, or immediate aftermath;
2. otherwise use recap when recounting a previous session is the chunk's
primary table purpose; primary table purpose;
3. otherwise use meta when sustained out-of-character session discussion is 2. otherwise use meta when sustained out-of-character session discussion is
primary and in-world progression is no more than incidental; and primary and in-world progression is no more than incidental; and
4. use narrative for all remaining current-session in-world play. 3. use narrative for all remaining current-session in-world play.
Brief table talk, dice resolution, rules clarification, jokes, or Brief table talk, dice resolution, rules clarification, jokes, or
administrative comments do not make a gameplay scene meta. A short recollection administrative comments do not make a gameplay scene meta. A short recollection

View File

@@ -27,6 +27,8 @@ messages:
content_file: ./sharedassets/common-dnd-transcript-chunk.md content_file: ./sharedassets/common-dnd-transcript-chunk.md
cache_control: cache_control:
type: ephemeral type: ephemeral
- role: user
content_file: ./sharedassets/common-dnd-scene-combat-policy.md
- role: user - role: user
content_file: ./instructions.md content_file: ./instructions.md
cache_control: cache_control:

View File

@@ -0,0 +1,13 @@
Use `combat` only when substantive active combat materially organizes the
scene. Active combat includes initiative or turn exchanges, attacks, combat
spells, damage, saves, movement, or similarly sustained hostile action.
Do not use `combat` for planning or preparing for a possible fight; threats,
hostile dialogue, or a tense confrontation; immediate aftermath, looting,
healing, or discussion of a completed fight; a recap or in-world recollection
of earlier combat; or out-of-character rules discussion without active
encounter play.
When a chunk contains substantive active combat alongside brief setup, rules
clarification, interruption, phase transition, or immediate aftermath, classify
it as `combat`.

View File

@@ -59,6 +59,8 @@ before beginning the next one.
## Stage 1: Extract The Shared Combat Policy ## Stage 1: Extract The Shared Combat Policy
✅ Complete
### Goal ### Goal
Give the existing extractor and future validator one byte-identical owner for Give the existing extractor and future validator one byte-identical owner for

View File

@@ -24,6 +24,7 @@ var promptAssetManifest = shared.PromptAssetManifest{
"common-dnd-identity.md", "common-dnd-identity.md",
"common-dnd-references.md", "common-dnd-references.md",
"common-dnd-transcript-chunk.md", "common-dnd-transcript-chunk.md",
"common-dnd-scene-combat-policy.md",
}, },
} }

View File

@@ -42,6 +42,23 @@ func TestRegisterPromptAssetsPreparesSceneDescriptionPrompt(t *testing.T) {
if prepared.PromptID != PromptID || prepared.OutputContract.SchemaPath != "dnd_scene_descriptions_llm.v1.json" { 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) t.Fatalf("prepared prompt = %#v, want scene-description prompt identity and schema wiring", prepared)
} }
const combatPolicySentinel = "substantive active combat materially organizes the"
transcriptIndex := -1
policyIndex := -1
instructionsIndex := len(prepared.Messages) - 1
policyOccurrences := 0
for index, message := range prepared.Messages {
if strings.Contains(message.Content, "scene-description-transcript") {
transcriptIndex = index
}
if count := strings.Count(message.Content, combatPolicySentinel); count > 0 {
policyIndex = index
policyOccurrences += count
}
}
if policyOccurrences != 1 || policyIndex <= transcriptIndex || policyIndex >= instructionsIndex {
t.Fatalf("combat policy appeared %d times at message %d; want once after transcript message %d and before instructions message %d", policyOccurrences, policyIndex, transcriptIndex, instructionsIndex)
}
} }
func TestPromptMetadataAndDiagnosticsDoNotContainRawAssets(t *testing.T) { func TestPromptMetadataAndDiagnosticsDoNotContainRawAssets(t *testing.T) {

View File

@@ -30,6 +30,7 @@ func TestExtractionPromptComposition(t *testing.T) {
npcSentinel = "npc-registry-sentinel" npcSentinel = "npc-registry-sentinel"
catalogSentinel = "spell-catalog-sentinel" catalogSentinel = "spell-catalog-sentinel"
evidenceSentinel = "Transcript units are the only evidence" evidenceSentinel = "Transcript units are the only evidence"
combatPolicySentinel = "substantive active combat materially organizes the"
) )
registry := llm.NewAssetRegistry() registry := llm.NewAssetRegistry()
if err := registerPromptAssets(registry); err != nil { if err := registerPromptAssets(registry); err != nil {
@@ -48,6 +49,7 @@ func TestExtractionPromptComposition(t *testing.T) {
promptVersion string promptVersion string
inputs map[string]promptkit.ArtifactRef inputs map[string]promptkit.ArtifactRef
suffixGroups [][]string suffixGroups [][]string
middleSentinel string
}{ }{
{name: "npcs", promptID: npcextract.PromptID, promptVersion: npcextract.SchemaVersion, inputs: commonInputs, suffixGroups: [][]string{{evidenceSentinel}}}, {name: "npcs", promptID: npcextract.PromptID, promptVersion: npcextract.SchemaVersion, inputs: commonInputs, suffixGroups: [][]string{{evidenceSentinel}}},
{name: "locations", promptID: locationextract.PromptID, promptVersion: locationextract.SchemaVersion, inputs: commonInputs, suffixGroups: [][]string{{evidenceSentinel}}}, {name: "locations", promptID: locationextract.PromptID, promptVersion: locationextract.SchemaVersion, inputs: commonInputs, suffixGroups: [][]string{{evidenceSentinel}}},
@@ -55,7 +57,7 @@ func TestExtractionPromptComposition(t *testing.T) {
"item_registry": promptkit.Inline(`{"items":[{"id":"item-registry-sentinel","name":"Torch"}]}`), "item_registry": promptkit.Inline(`{"items":[{"id":"item-registry-sentinel","name":"Torch"}]}`),
}), suffixGroups: [][]string{{evidenceSentinel}, {"item-registry-sentinel"}}}, }), suffixGroups: [][]string{{evidenceSentinel}, {"item-registry-sentinel"}}},
{name: "item registry", promptID: itemregistryextract.PromptID, promptVersion: itemregistryextract.SchemaVersion, inputs: commonInputs, suffixGroups: [][]string{{evidenceSentinel}}}, {name: "item registry", promptID: itemregistryextract.PromptID, promptVersion: itemregistryextract.SchemaVersion, inputs: commonInputs, suffixGroups: [][]string{{evidenceSentinel}}},
{name: "scene descriptions", promptID: scenedescriptionextract.PromptID, promptVersion: scenedescriptionextract.SchemaVersion, inputs: commonInputs}, {name: "scene descriptions", promptID: scenedescriptionextract.PromptID, promptVersion: scenedescriptionextract.SchemaVersion, inputs: commonInputs, middleSentinel: combatPolicySentinel},
{ {
name: "combat turns", name: "combat turns",
promptID: combatextract.PromptID, promptID: combatextract.PromptID,
@@ -155,6 +157,12 @@ func TestExtractionPromptComposition(t *testing.T) {
previousIndex = groupIndex previousIndex = groupIndex
} }
instructionIndex := len(prepared.Messages) - 1 instructionIndex := len(prepared.Messages) - 1
if testCase.middleSentinel != "" {
policyIndex := renderedInputMessageIndex(t, prepared.Messages, testCase.middleSentinel)
if policyIndex <= transcriptIndex || policyIndex >= instructionIndex {
t.Fatalf("combat policy rendered at message %d, want after transcript message %d and before instructions message %d", policyIndex, transcriptIndex, instructionIndex)
}
}
if instructionIndex <= previousIndex { if instructionIndex <= previousIndex {
t.Fatalf("instructions rendered at message %d, want after lane input message %d", instructionIndex, previousIndex) t.Fatalf("instructions rendered at message %d, want after lane input message %d", instructionIndex, previousIndex)
} }

View File

@@ -27,6 +27,7 @@ var sharedPromptPaths = map[string]string{
"common-dnd-identity.md": "prompts/common-dnd-identity.md", "common-dnd-identity.md": "prompts/common-dnd-identity.md",
"common-dnd-transcript-full.md": "prompts/common-dnd-transcript-full.md", "common-dnd-transcript-full.md": "prompts/common-dnd-transcript-full.md",
"common-dnd-transcript-chunk.md": "prompts/common-dnd-transcript-chunk.md", "common-dnd-transcript-chunk.md": "prompts/common-dnd-transcript-chunk.md",
"common-dnd-scene-combat-policy.md": "prompts/common-dnd-scene-combat-policy.md",
"common-dnd-references.md": "prompts/common-dnd-references.md", "common-dnd-references.md": "prompts/common-dnd-references.md",
"common-dnd-npc-registry.md": "prompts/common-dnd-npc-registry.md", "common-dnd-npc-registry.md": "prompts/common-dnd-npc-registry.md",
} }