Implement operation-time D&D NPC artifact handoff

This commit is contained in:
2026-07-21 22:12:01 +00:00
parent c437682407
commit 9184072839
12 changed files with 606 additions and 27 deletions

View File

@@ -65,7 +65,7 @@ type Extractor struct {
llm contracts.StructuredLLMClient
effectiveCatalog spellcatalog.EffectiveCatalog
catalogPromptInput contracts.LLMInputMaterial
npcRegistry *npcregistry.Registry
npcResolver *npcregistry.Resolver
promptSHA string
responseSchemaSHA string
}
@@ -89,7 +89,7 @@ func New(llmClient contracts.StructuredLLMClient, _ Options, references ...contr
if err != nil {
return nil, extractorErrorf("prepare spell catalog prompt input: %w", err)
}
npcRegistry, err := npcregistry.Resolve(referenceSet)
npcResolver, err := npcregistry.NewResolver(referenceSet)
if err != nil {
return nil, extractorErrorf("prepare NPC registry prompt input: %w", err)
}
@@ -105,7 +105,7 @@ func New(llmClient contracts.StructuredLLMClient, _ Options, references ...contr
llm: llmClient,
effectiveCatalog: effectiveCatalog,
catalogPromptInput: catalogPromptInput,
npcRegistry: npcRegistry,
npcResolver: npcResolver,
promptSHA: promptSHA,
responseSchemaSHA: responseSchema.SHA256,
}, nil
@@ -133,9 +133,10 @@ func (e *Extractor) ManifestMetadata() map[string]any {
"response_schema_version": SchemaVersion,
"response_schema_sha256": e.responseSchemaSHA,
}
if e.npcRegistry.Bound() {
metadata["npc_registry_digest"] = e.npcRegistry.Digest()
metadata["npc_count"] = e.npcRegistry.Count()
seeded := e.npcResolver.Seeded()
if seeded.Bound() {
metadata["npc_registry_digest"] = seeded.Digest()
metadata["npc_count"] = seeded.Count()
}
return metadata
}
@@ -149,8 +150,9 @@ func (e *Extractor) CheckpointFingerprints() []pipeline.CheckpointFingerprint {
{Name: "prompt", Value: e.promptSHA},
{Name: "response_schema", Value: e.responseSchemaSHA},
}
if e.npcRegistry.Bound() {
fingerprints = append(fingerprints, pipeline.CheckpointFingerprint{Name: "npc_registry", Value: e.npcRegistry.Digest()})
seeded := e.npcResolver.Seeded()
if seeded.Bound() {
fingerprints = append(fingerprints, pipeline.CheckpointFingerprint{Name: "npc_registry", Value: seeded.Digest()})
}
return fingerprints
}
@@ -181,11 +183,15 @@ func (e *Extractor) Extract(ctx context.Context, req contracts.TypedExtractionRe
if err != nil {
return contracts.TypedExtractionResult[dnd.SpellList]{}, extractorErrorf("%w", err)
}
npcRegistry, err := e.npcResolver.Resolve(req.References)
if err != nil {
return contracts.TypedExtractionResult[dnd.SpellList]{}, extractorErrorf("resolve NPC registry: %w", err)
}
var response extractionResponse
inputs := shared.PromptInputs(sourceInput, req.References)
inputs[spellcatalog.SpellCatalogReferenceSlot] = e.catalogPromptInput.Clone()
inputs[NPCRegistryReferenceSlot] = e.npcRegistry.PromptInput()
inputs[NPCRegistryReferenceSlot] = npcRegistry.PromptInput()
if _, err := e.llm.CompleteStructured(ctx, contracts.StructuredCompletionRequest{
StageName: Key,
PromptID: PromptID,

View File

@@ -84,6 +84,32 @@ func TestSpellExtractorPreservesSemanticNPCRegistryFingerprintAndPromptWiring(t
}
}
func TestSpellExtractorResolvesOperationNPCOverrideWithoutSingletonMetadata(t *testing.T) {
canonical, err := npccodec.New().Encode(registryFixture())
if err != nil {
t.Fatalf("encode NPC registry: %v", err)
}
client := &fakeSpellsLLMClient{response: extractionResponse{SpellCasts: []spellCastResponse{}}}
extractor := newExtractor(t, client)
request := extractionRequest()
request.References = spellNPCRegistryReference(canonical, "file:///generated.json")
if _, err := extractor.Extract(context.Background(), request); err != nil {
t.Fatalf("Extract() error = %v", err)
}
input := client.requests[0].Inputs[NPCRegistryReferenceSlot]
if input.Digest == "" || string(input.Content) != string(canonical) || input.OriginURI != "" {
t.Fatalf("operation NPC input = %#v, want generated canonical grounding without provenance", input)
}
if metadata := extractor.ManifestMetadata(); metadata["npc_registry_digest"] != nil || metadata["npc_count"] != nil {
t.Fatalf("singleton metadata = %#v, want no operation-varying NPC identity", metadata)
}
for _, fingerprint := range extractor.CheckpointFingerprints() {
if fingerprint.Name == "npc_registry" {
t.Fatalf("singleton fingerprints = %#v, want no operation-varying NPC identity", extractor.CheckpointFingerprints())
}
}
}
func registryFixture() dnd.NPCList {
return dnd.NPCList{NPCs: []dnd.NPC{{
ID: identity.DeriveID("Mira Thorn"),