Simplify enemy event grounding ownership

This commit is contained in:
2026-08-03 22:54:11 +00:00
parent e6b7c61f45
commit e15007fffb
2 changed files with 41 additions and 27 deletions

View File

@@ -16,6 +16,8 @@ import (
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd"
combatturncodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/combatturns"
interactioncodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/npcinteractions"
npccodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/npcs"
scenecodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/scenedescriptions"
npcregistry "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/npcs/registry"
sceneregistry "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/scenedescriptions/registry"
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared"
@@ -27,6 +29,7 @@ const (
CombatTurnReferenceSlot = "combat_turns"
NPCInteractionReferenceSlot = "npc_interactions"
ReferenceMaxBytes = 1048576
promptProjectionMediaType = "application/json"
)
var referenceSlotDescriptions = shared.ReferenceSlotDescriptions{
@@ -43,7 +46,7 @@ func referenceSlots() []contracts.ReferenceSlot {
Name: NPCRegistryReferenceSlot,
Description: "Required normalized NPC registry used only for enemy-subject grounding, never as event evidence.",
Required: true,
AcceptedMediaTypes: []string{combatturncodec.MediaType},
AcceptedMediaTypes: []string{npccodec.MediaType},
AcceptedArtifactKinds: []contracts.ArtifactKind{dnd.NPCListKind},
MaxBytes: ReferenceMaxBytes,
},
@@ -51,7 +54,7 @@ func referenceSlots() []contracts.ReferenceSlot {
Name: SceneDescriptionReferenceSlot,
Description: "Required scene descriptions used only to determine exact combat eligibility, never as event evidence.",
Required: true,
AcceptedMediaTypes: []string{combatturncodec.MediaType},
AcceptedMediaTypes: []string{scenecodec.MediaType},
AcceptedArtifactKinds: []contracts.ArtifactKind{dnd.SceneDescriptionListKind},
MaxBytes: ReferenceMaxBytes,
},
@@ -88,10 +91,9 @@ type groundingResolver struct {
}
type grounding struct {
npcInput contracts.LLMInputMaterial
combatTurnInput contracts.LLMInputMaterial
npcInteractionInput contracts.LLMInputMaterial
sceneEligibilityView *sceneregistry.Registry
npcInput contracts.LLMInputMaterial
combatTurnInput contracts.LLMInputMaterial
npcInteractionInput contracts.LLMInputMaterial
}
func newGroundingResolver(references contracts.ReferenceSet) (*groundingResolver, error) {
@@ -130,10 +132,6 @@ func (r *groundingResolver) Resolve(references contracts.ReferenceSet) (groundin
if !npcs.Bound() {
return grounding{}, fmt.Errorf("NPC registry reference is required")
}
scenes, err := r.resolveScenes(references)
if err != nil {
return grounding{}, err
}
combatTurns, err := resolveInput(references, CombatTurnReferenceSlot, r.combatTurns, prepareCombatTurnInput)
if err != nil {
return grounding{}, err
@@ -143,10 +141,9 @@ func (r *groundingResolver) Resolve(references contracts.ReferenceSet) (groundin
return grounding{}, err
}
return grounding{
npcInput: npcs.PromptInput(),
combatTurnInput: combatTurns,
npcInteractionInput: npcInteractions,
sceneEligibilityView: scenes,
npcInput: npcs.PromptInput(),
combatTurnInput: combatTurns,
npcInteractionInput: npcInteractions,
}, nil
}
@@ -197,10 +194,6 @@ func (g grounding) PromptInputs() contracts.LLMInputSet {
}
}
func (g grounding) SceneMatch(chunk *source.Chunk) sceneregistry.ChunkMatch {
return g.sceneEligibilityView.Match(chunk)
}
func prepareCombatTurnInput(references contracts.ReferenceSet) (*contracts.LLMInputMaterial, error) {
item, ok, err := referenceItem(references, CombatTurnReferenceSlot, combatturncodec.MediaType)
if err != nil || !ok {
@@ -295,6 +288,6 @@ func projectNPCInteractions(interactions []dnd.NPCInteraction) []npcInteractionP
func newPromptInput(name string, content []byte) *contracts.LLMInputMaterial {
sum := sha256.Sum256(content)
material := contracts.NewLLMInputMaterial(name, combatturncodec.MediaType, content, "sha256:"+hex.EncodeToString(sum[:]), "")
material := contracts.NewLLMInputMaterial(name, promptProjectionMediaType, content, "sha256:"+hex.EncodeToString(sum[:]), "")
return &material
}

View File

@@ -104,15 +104,27 @@ func TestGroundingResolvesGeneratedReferencesAndSceneEligibility(t *testing.T) {
if got := string(resolved.PromptInputs()[NPCRegistryReferenceSlot].Content); got != `{"npcs":[{"name":"Grimjaw"}]}` {
t.Fatalf("generated NPC projection = %s", got)
}
if resolved.SceneMatch(combatChunk()) != (sceneregistry.ChunkMatch{State: sceneregistry.MatchExact, Kind: dnd.SceneKindNarrative}) {
t.Fatalf("generated scene match = %#v", resolved.SceneMatch(combatChunk()))
match, err := resolver.SceneMatch(generated, combatChunk())
if err != nil {
t.Fatal(err)
}
if resolved.SceneMatch(&source.Chunk{ID: "other", Ref: combatChunk().Ref}).State != sceneregistry.MatchMissing {
if match != (sceneregistry.ChunkMatch{State: sceneregistry.MatchExact, Kind: dnd.SceneKindNarrative}) {
t.Fatalf("generated scene match = %#v", match)
}
match, err = resolver.SceneMatch(generated, &source.Chunk{ID: "other", Ref: combatChunk().Ref})
if err != nil {
t.Fatal(err)
}
if match.State != sceneregistry.MatchMissing {
t.Fatal("missing scene was not reported")
}
mismatched := combatChunk()
mismatched.Ref.EndUnitID++
if resolved.SceneMatch(mismatched).State != sceneregistry.MatchMismatched {
match, err = resolver.SceneMatch(generated, mismatched)
if err != nil {
t.Fatal(err)
}
if match.State != sceneregistry.MatchMismatched {
t.Fatal("mismatched scene was not reported")
}
@@ -123,8 +135,12 @@ func TestGroundingResolvesGeneratedReferencesAndSceneEligibility(t *testing.T) {
if got := string(static.PromptInputs()[NPCRegistryReferenceSlot].Content); got != `{"npcs":[{"name":"Ashfang"}]}` {
t.Fatalf("static NPC projection changed after generated resolution: %s", got)
}
if static.SceneMatch(combatChunk()) != (sceneregistry.ChunkMatch{State: sceneregistry.MatchExact, Kind: dnd.SceneKindCombat}) {
t.Fatalf("static scene match = %#v", static.SceneMatch(combatChunk()))
match, err = resolver.SceneMatch(contracts.ReferenceSet{}, combatChunk())
if err != nil {
t.Fatal(err)
}
if match != (sceneregistry.ChunkMatch{State: sceneregistry.MatchExact, Kind: dnd.SceneKindCombat}) {
t.Fatalf("static scene match = %#v", match)
}
}
@@ -144,8 +160,13 @@ func TestGroundingRejectsMissingAndInvalidReferences(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if _, err := resolver.Resolve(contracts.ReferenceSet{}); err == nil || !strings.Contains(err.Error(), test.want) {
t.Fatalf("Resolve() error = %v, want missing %q reference", err, test.slot)
if test.slot == SceneDescriptionReferenceSlot {
_, err = resolver.SceneMatch(contracts.ReferenceSet{}, combatChunk())
} else {
_, err = resolver.Resolve(contracts.ReferenceSet{})
}
if err == nil || !strings.Contains(err.Error(), test.want) {
t.Fatalf("grounding error = %v, want missing %q reference", err, test.slot)
}
})
}