diff --git a/docs/internal/llm.md b/docs/internal/llm.md index 288dad3..c542d37 100644 --- a/docs/internal/llm.md +++ b/docs/internal/llm.md @@ -98,6 +98,18 @@ D&D prompt content. `internal/framework/promptfs` provides the domain-neutral filesystem composition helper used to combine module-owned files with shared domain prompt fragments. +The D&D spell, NPC, and combat-turn extractors each declare an ordered prompt +asset manifest. The manifest lists the package-owned YAML and Markdown files, +then the exact shared fragments rendered by that prompt; the same ordered list +drives both filesystem mounting and the prompt fingerprint. Shared extraction +evidence and in-world identity messages precede the cacheable transcript and +campaign-reference messages. Spell and combat prompts then render immediate +resolution and the cacheable NPC registry before their lane-specific material; +the NPC prompt renders its task and instructions instead. The identity, +transcript, and campaign-reference messages are ephemeral in all three prompts, +and the NPC registry is ephemeral where it is used by spell and combat prompts. +Unused shared assets are neither mounted nor fingerprinted. + Schema helpers load embedded JSON Schema with identity and digest metadata, return defensive copies, and expose a diagnostics map that omits schema bytes. The small framework registry contains only generic test schemas; production @@ -113,8 +125,8 @@ input and an optional `application/json` `npcs` input. The extractor generates the catalog input from its prepared effective catalog as `{"spell_names":[...]}` using sorted canonical names only. The shared D&D prompt assets include a generic NPC grounding fragment directly -after the campaign reference message for both extractors. When an NPC registry -is bound, the +after the campaign reference message for spell and combat prompts. When an NPC +registry is bound, the domain registry boundary strictly decodes and identity-validates one durable artifact, re-encodes canonical JSON, and generates a semantic digest over those bytes. The unbound input is exactly `{"npcs":[]}`. Input digests cover diff --git a/docs/internal/modules.md b/docs/internal/modules.md index e3af44a..887712a 100644 --- a/docs/internal/modules.md +++ b/docs/internal/modules.md @@ -47,9 +47,16 @@ LLM-backed extensions own their prompt definitions and response schemas under package-local embedded assets. Shared filesystem composition belongs in `internal/framework/promptfs`; reusable D&D prompt fragments, reference declarations, prompt-input assembly, and source-unit helpers belong in -`internal/modules/dnd/shared`, which also owns bounded D&D diagnostics. Stage -contracts expose only Notarius structured- -completion types, not Scriptorium public types. +`internal/modules/dnd/shared`, which also owns bounded D&D diagnostics. The +spell, NPC, and combat-turn extractors use ordered package-local prompt +manifests for both rendering and prompt fingerprinting, so only the shared +fragments each prompt actually renders participate in either operation. Their +common prompt order is system, extraction evidence, in-world identity, +transcript, and campaign references, with ephemeral cache control on identity, +transcript, and references. Spell and combat prompts append immediate +resolution and an ephemeral NPC registry before lane-specific material; the +NPC prompt appends its task and instructions. Stage contracts expose only +Notarius structured-completion types, not Scriptorium public types. Reference material may inform a module or prompt but must not become source evidence. The resolver and materializer behavior is described in @@ -182,15 +189,14 @@ source evidence. The prompt limits each cast to its declaration and immediate resolution; it does not follow summoned creatures, persistent effects, or other downstream -consequences through the scene. Source references must collectively support all -reported facts, using separate narrow ranges when immediate evidence is -non-contiguous. The prompt grounds caster names in in-world identities, using -the player and party references to disambiguate transcript speakers without -treating those references as event evidence. Effects describe the session as -played: model rules knowledge cannot supplement or correct the transcript, and -nonstandard adjudication is attributed to the GM or table rather than stated as -a universal rule. Structural source validation remains deterministic; semantic -claim completeness is enforced through extraction policy and evaluation. +consequences through the scene. Shared extraction-evidence and identity rules +require transcript-supported factual claims and the most specific in-world +caster identity, while campaign references only disambiguate source text. +Effects describe the session as played: model rules knowledge cannot supplement +or correct the transcript, and nonstandard adjudication is attributed to the GM +or table rather than stated as a universal rule. Structural source validation +remains deterministic; semantic claim completeness is enforced through +extraction policy and evaluation. Both the extractor and deterministic catalog validator expose the effective base-plus-overlay semantic digest as scoped prepared-component @@ -223,12 +229,16 @@ The NPC extractor maps private model output to the canonical `dnd.NPCList`, assigns source identity and deterministic NPC IDs, and preserves source references for deterministic validation. It uses the shared campaign references only for disambiguation and does not consume the optional NPC -registry slot. Its prompt and private response schema are package-owned. +registry slot. Its prompt and private response schema are package-owned. The +prompt uses the common evidence, identity, transcript, and campaign-reference +messages, then the NPC-specific task and instructions; only the identity, +transcript, and campaign-reference messages carry ephemeral cache control. ### `internal/modules/dnd/extract/combatturns` The combat extractor prepares one structured request per supplied chunk using -the shared transcript, campaign-reference, and NPC-grounding prompt inputs. It +the shared extraction-evidence, identity, transcript, campaign-reference, +immediate-resolution, and NPC-grounding prompt inputs. It maps the private response to `dnd.CombatTurnList`, assigns the current source identity, removes exact duplicate source ranges, and orders turns by valid source-document position while preserving malformed candidate fields for @@ -236,8 +246,11 @@ deterministic validators. Its package-owned private response schema enforces only the structural JSON envelope; semantic artifact constraints remain with the validator chain. Its prepared metadata and checkpoint fingerprints contain only prompt/schema/mapping identities plus an optional NPC registry digest. -The package exposes typed registration and is included in the production D&D -registrar with the default combat extraction chain. +The prompt renders immediate resolution and the NPC registry before the +combat-specific task and instructions; identity, transcript, campaign +references, and the NPC registry use ephemeral cache control. The package +exposes typed registration and is included in the production D&D registrar with +the default combat extraction chain. The combat normalizer accepts only the optional structured NPC registry. Campaign references remain extractor-only LLM context and are not materialized diff --git a/internal/modules/dnd/extract/combatturns/assets/prompts/dnd.combat_turns.yaml b/internal/modules/dnd/extract/combatturns/assets/prompts/dnd.combat_turns.yaml index 9e18f76..4a721cc 100644 --- a/internal/modules/dnd/extract/combatturns/assets/prompts/dnd.combat_turns.yaml +++ b/internal/modules/dnd/extract/combatturns/assets/prompts/dnd.combat_turns.yaml @@ -20,6 +20,12 @@ inputs: messages: - role: system content_file: ./sharedassets/common-dnd-system.md + - role: user + content_file: ./sharedassets/common-dnd-extraction-evidence.md + - role: user + content_file: ./sharedassets/common-dnd-identity.md + cache_control: + type: ephemeral - role: user content_file: ./sharedassets/common-dnd-transcript.md cache_control: @@ -28,8 +34,12 @@ messages: content_file: ./sharedassets/common-dnd-references.md cache_control: type: ephemeral + - role: user + content_file: ./sharedassets/common-dnd-immediate-resolution.md - role: user content_file: ./sharedassets/common-dnd-npcs.md + cache_control: + type: ephemeral - role: user content_file: ./task.md - role: user diff --git a/internal/modules/dnd/extract/combatturns/assets/prompts/instructions.md b/internal/modules/dnd/extract/combatturns/assets/prompts/instructions.md index e9a56fe..358e46c 100644 --- a/internal/modules/dnd/extract/combatturns/assets/prompts/instructions.md +++ b/internal/modules/dnd/extract/combatturns/assets/prompts/instructions.md @@ -1,12 +1,5 @@ -Return exactly one JSON object and no explanatory text. - Return the combat_turns array even when no combat turn is established. Return one or more actions for every turn. Use one of the supported turn_kind and -action category values. Set round to null when the transcript does not state -an explicit or unambiguous positive round number. Set resolution to null when -the transcript establishes the declaration but not an immediate resolution. - -Every source reference must contain start_unit_id and end_unit_id from the -provided transcript. Do not add source_id; the extraction mapper assigns the -current source identity. Do not include fields not defined by the response -schema. +action category values. Set round to null when the transcript does not state an +explicit or unambiguous positive round number. Set resolution to null when the +transcript establishes the declaration but not an immediate resolution. diff --git a/internal/modules/dnd/extract/combatturns/assets/prompts/task.md b/internal/modules/dnd/extract/combatturns/assets/prompts/task.md index c4d731f..a6bcbd4 100644 --- a/internal/modules/dnd/extract/combatturns/assets/prompts/task.md +++ b/internal/modules/dnd/extract/combatturns/assets/prompts/task.md @@ -5,26 +5,15 @@ participant takes a combat turn or performs a discrete interrupting combat event. Reactions, legendary actions, lair actions, and other out-of-turn events belong at the point where they occur in transcript chronology. -Report only the declaration and its immediate observed resolution. Immediate -resolution may include directly associated rolls, damage, healing, movement, -conditions, target outcomes, or an interruption. Do not follow consequences -that occur on later turns or elsewhere in the scene. - Exclude initiative setup without a turn or combat event, tactical planning, table talk, rules lookup, hypothetical actions, abandoned declarations, recap of combat outside the current passage, and downstream consequences. -Use only the supplied transcript as evidence. Do not infer a round, target, -roll, amount, condition, outcome, or action classification from D&D rules -knowledge. Preserve the session as played; attribute relevant nonstandard -rulings to the GM or table. +Do not infer a round, target, roll, amount, condition, outcome, or action +classification from D&D rules knowledge. Preserve the session as played; +attribute relevant nonstandard rulings to the GM or table. -The actor must be the in-world character or creature, not a player, transcript -speaker, or GM. Use player, party, glossary, and NPC reference material only -to disambiguate identities. Reference material is context, never combat -evidence. Unmatched actors and targets remain permitted. +Unmatched actors and targets remain permitted. For every factual detail in a turn, cite all supporting transcript units in the -turn-level source_refs collection. Use narrow ranges when evidence is -non-contiguous. Numeric source-unit IDs identify transcript units; they do not -establish chronology outside the supplied transcript. +turn-level source_refs collection. diff --git a/internal/modules/dnd/extract/combatturns/scriptorium_assets.go b/internal/modules/dnd/extract/combatturns/scriptorium_assets.go index 1ca35fd..4ed03b6 100644 --- a/internal/modules/dnd/extract/combatturns/scriptorium_assets.go +++ b/internal/modules/dnd/extract/combatturns/scriptorium_assets.go @@ -20,8 +20,11 @@ var promptAssetManifest = shared.PromptAssetManifest{ }, SharedFiles: []string{ "common-dnd-system.md", + "common-dnd-extraction-evidence.md", + "common-dnd-identity.md", "common-dnd-transcript.md", "common-dnd-references.md", + "common-dnd-immediate-resolution.md", "common-dnd-npcs.md", }, } diff --git a/internal/modules/dnd/extract/npcs/assets/prompts/dnd.npcs.yaml b/internal/modules/dnd/extract/npcs/assets/prompts/dnd.npcs.yaml index 12eccfb..8cb01ce 100644 --- a/internal/modules/dnd/extract/npcs/assets/prompts/dnd.npcs.yaml +++ b/internal/modules/dnd/extract/npcs/assets/prompts/dnd.npcs.yaml @@ -17,6 +17,12 @@ inputs: messages: - role: system content_file: ./sharedassets/common-dnd-system.md + - role: user + content_file: ./sharedassets/common-dnd-extraction-evidence.md + - role: user + content_file: ./sharedassets/common-dnd-identity.md + cache_control: + type: ephemeral - role: user content_file: ./sharedassets/common-dnd-transcript.md cache_control: diff --git a/internal/modules/dnd/extract/npcs/assets/prompts/instructions.md b/internal/modules/dnd/extract/npcs/assets/prompts/instructions.md index d4ec175..c81c787 100644 --- a/internal/modules/dnd/extract/npcs/assets/prompts/instructions.md +++ b/internal/modules/dnd/extract/npcs/assets/prompts/instructions.md @@ -1,18 +1,5 @@ -Return exactly one JSON object and no explanatory text. - -For every NPC record, cite one or more transcript source-unit ranges that -collectively support the canonical name, every alias, the description, and -every relationship. Use integer start_unit_id and end_unit_id values from the -transcript. Do not provide source_id; the extractor assigns it automatically. -Use narrow ranges when evidence is not contiguous and do not bridge unrelated -conversation with a broad range. - -A canonical name must be the most specific in-world identity supported by the -transcript. Never use a human player, transcript speaker, or GM name when an -associated in-world character or creature is identified. Player, party, and -glossary references may disambiguate identities, but they are not transcript -evidence and cannot establish that an NPC appeared, acted, or was discussed. -Do not return a person or entity mentioned only in those references. +For every NPC record, cite transcript units that support the canonical name, +every alias, the description, and every relationship. Descriptions must be short session records, not biographies, statistics, alignment, motivations, or lore inferred from general D&D knowledge. Do not diff --git a/internal/modules/dnd/extract/npcs/assets/prompts/task.md b/internal/modules/dnd/extract/npcs/assets/prompts/task.md index 650dfe2..4d42b10 100644 --- a/internal/modules/dnd/extract/npcs/assets/prompts/task.md +++ b/internal/modules/dnd/extract/npcs/assets/prompts/task.md @@ -5,12 +5,11 @@ Include an in-world non-PC participant when the transcript establishes that it appears, acts, speaks, or is materially discussed and gives it a proper name, a stable alias or title, or an individually useful distinguishing description. -Exclude human players, transcript speakers, the GM as an out-of-world person, +Exclude human players, transcript speakers, and the GM as out-of-world people, player characters identified by the player or party references, incidental or hypothetical name drops, corrected transcription mistakes, characters mentioned only by reference material, indistinguishable crowds or groups, and temporary summoned creatures or spell effects without a persistent individual identity. -Return canonical in-world names rather than player names or transcript speaker -names. Keep each description concise and limited to facts established by the +Keep each description concise and limited to facts established by the transcript. Include only explicitly supported aliases and relationships. diff --git a/internal/modules/dnd/extract/npcs/scriptorium_assets.go b/internal/modules/dnd/extract/npcs/scriptorium_assets.go index 4a1fd53..81f1798 100644 --- a/internal/modules/dnd/extract/npcs/scriptorium_assets.go +++ b/internal/modules/dnd/extract/npcs/scriptorium_assets.go @@ -20,6 +20,8 @@ var promptAssetManifest = shared.PromptAssetManifest{ }, SharedFiles: []string{ "common-dnd-system.md", + "common-dnd-extraction-evidence.md", + "common-dnd-identity.md", "common-dnd-transcript.md", "common-dnd-references.md", }, diff --git a/internal/modules/dnd/extract/spells/assets/prompts/dnd.spells.yaml b/internal/modules/dnd/extract/spells/assets/prompts/dnd.spells.yaml index 9c5fc79..3c820f4 100644 --- a/internal/modules/dnd/extract/spells/assets/prompts/dnd.spells.yaml +++ b/internal/modules/dnd/extract/spells/assets/prompts/dnd.spells.yaml @@ -23,6 +23,12 @@ inputs: messages: - role: system content_file: ./sharedassets/common-dnd-system.md + - role: user + content_file: ./sharedassets/common-dnd-extraction-evidence.md + - role: user + content_file: ./sharedassets/common-dnd-identity.md + cache_control: + type: ephemeral - role: user content_file: ./sharedassets/common-dnd-transcript.md cache_control: @@ -31,8 +37,12 @@ messages: content_file: ./sharedassets/common-dnd-references.md cache_control: type: ephemeral + - role: user + content_file: ./sharedassets/common-dnd-immediate-resolution.md - role: user content_file: ./sharedassets/common-dnd-npcs.md + cache_control: + type: ephemeral - role: user content_file: ./catalog.md - role: user diff --git a/internal/modules/dnd/extract/spells/assets/prompts/instructions.md b/internal/modules/dnd/extract/spells/assets/prompts/instructions.md index 7f24c20..3fc29cc 100644 --- a/internal/modules/dnd/extract/spells/assets/prompts/instructions.md +++ b/internal/modules/dnd/extract/spells/assets/prompts/instructions.md @@ -1,40 +1,29 @@ -Source references must use integer source-unit IDs from the transcript. Provide -start_unit_id and end_unit_id for each source reference; the extractor assigns -source_id automatically. +For each spell cast, source references must collectively support the caster, +spell, effect, and narrative_description. If a detail is not supported by the +cited transcript units, omit that detail or describe only the supported attempt +or declaration. -Report only the casting declaration or action and its immediate resolution. -This may include an immediately resolved target, saving throw, damage, healing, -condition, interruption, or outcome. Do not follow summoned creatures, -persistent effects, or other downstream consequences through the rest of the -scene. +For spells, do not follow summoned creatures, persistent effects, or other +downstream consequences through the rest of the scene. -For every detail you report, cite all supporting transcript units. The source -references for an artifact must collectively support every factual claim in -caster, spell, effect, and narrative_description. When immediate supporting -evidence is non-contiguous, provide multiple narrow source references. Do not -use one broad range merely to bridge unrelated table conversation. If a detail -is not supported by the cited transcript units, omit that detail or describe -only the supported attempt or declaration. +Return only D&D spell-cast artifacts. For each spell cast, identify the +in-world caster, spell name, effect, narrative description, and source +references. -Return only D&D spell-cast artifacts. For each spell cast, identify the in-world -caster, spell name, effect, narrative description, and source references. - -The caster must be the canonical in-world character or creature, not the human -player, transcript speaker, or GM. Use the player and party references together -with transcript context to map first-person player speech to the associated -player character and use the canonical character name from the references. -Likewise, attribute a spell narrated by the GM to the in-world creature that -casts it. Never return a player or GM name when the associated in-world caster -can be identified. If the caster cannot be resolved, use only the most specific -in-world identity supported by the transcript; do not invent a name. +Use the player and party references together with transcript context to map +first-person player speech to the associated player character and use the +canonical character name from the references. Likewise, attribute a spell +narrated by the GM to the in-world creature that casts it. If the caster cannot +be resolved, use only the most specific in-world identity supported by the +transcript; do not invent a name. Use the canonical spell-name catalog to select spell names. Do not return a spell name absent from that catalog, even when it is suggested by general D&D knowledge or reference material. -Use player, party, and glossary reference material only to clarify source text; -references are not source evidence for a spell cast. Do not return spells, -casters, or effects that are mentioned only in reference material. +Use player, party, and glossary reference material only to clarify source text. +Do not return spells, casters, or effects that are mentioned only in reference +material. Effects and narrative descriptions are session records, not rules summaries. Report only mechanics, explanations, and outcomes established by the cited @@ -42,5 +31,3 @@ transcript units. Preserve the table's observed resolution without silently correcting it from general D&D knowledge. If the transcript gives a possibly nonstandard rationale, use wording such as "the GM rules" or "the table resolves" rather than asserting that rationale as a universal rule. - -Return exactly one JSON object and no explanatory text. diff --git a/internal/modules/dnd/extract/spells/scriptorium_assets.go b/internal/modules/dnd/extract/spells/scriptorium_assets.go index 49f6f37..d309695 100644 --- a/internal/modules/dnd/extract/spells/scriptorium_assets.go +++ b/internal/modules/dnd/extract/spells/scriptorium_assets.go @@ -21,8 +21,11 @@ var promptAssetManifest = shared.PromptAssetManifest{ }, SharedFiles: []string{ "common-dnd-system.md", + "common-dnd-extraction-evidence.md", + "common-dnd-identity.md", "common-dnd-transcript.md", "common-dnd-references.md", + "common-dnd-immediate-resolution.md", "common-dnd-npcs.md", }, } diff --git a/internal/modules/dnd/extract/spells/scriptorium_assets_test.go b/internal/modules/dnd/extract/spells/scriptorium_assets_test.go index 13da182..9bbfd86 100644 --- a/internal/modules/dnd/extract/spells/scriptorium_assets_test.go +++ b/internal/modules/dnd/extract/spells/scriptorium_assets_test.go @@ -46,9 +46,12 @@ func TestScriptoriumPromptPreparesWithMissingOptionalReferences(t *testing.T) { transcript := []byte(`{"id":"session-1","segments":[]}`) prepared := prepareSpellsPrompt(t, transcript, " ", " ", " ") - if !strings.Contains(prepared.Messages[2].Content, " ") { - t.Fatalf("reference message did not include empty optional reference placeholders") + for _, message := range prepared.Messages { + if strings.Contains(message.Content, "Player list reference:") && strings.Contains(message.Content, "Party roster reference:") && strings.Contains(message.Content, "Glossary reference:") { + return + } } + t.Fatalf("reference message did not render empty optional reference placeholders") } func TestScriptoriumPromptDiagnosticsOmitRawMaterials(t *testing.T) { diff --git a/internal/modules/dnd/shared/assets.go b/internal/modules/dnd/shared/assets.go index 7ceb40d..a751eff 100644 --- a/internal/modules/dnd/shared/assets.go +++ b/internal/modules/dnd/shared/assets.go @@ -23,10 +23,13 @@ type PromptAssetManifest struct { } var sharedPromptPaths = map[string]string{ - "common-dnd-system.md": "assets/prompts/common-dnd-system.md", - "common-dnd-transcript.md": "assets/prompts/common-dnd-transcript.md", - "common-dnd-references.md": "assets/prompts/common-dnd-references.md", - "common-dnd-npcs.md": "assets/prompts/common-dnd-npcs.md", + "common-dnd-system.md": "assets/prompts/common-dnd-system.md", + "common-dnd-extraction-evidence.md": "assets/prompts/common-dnd-extraction-evidence.md", + "common-dnd-identity.md": "assets/prompts/common-dnd-identity.md", + "common-dnd-transcript.md": "assets/prompts/common-dnd-transcript.md", + "common-dnd-references.md": "assets/prompts/common-dnd-references.md", + "common-dnd-immediate-resolution.md": "assets/prompts/common-dnd-immediate-resolution.md", + "common-dnd-npcs.md": "assets/prompts/common-dnd-npcs.md", } func (manifest PromptAssetManifest) PromptFS(moduleFS fs.FS) (fs.FS, error) { diff --git a/internal/modules/dnd/shared/assets/prompts/common-dnd-extraction-evidence.md b/internal/modules/dnd/shared/assets/prompts/common-dnd-extraction-evidence.md new file mode 100644 index 0000000..24b560b --- /dev/null +++ b/internal/modules/dnd/shared/assets/prompts/common-dnd-extraction-evidence.md @@ -0,0 +1,14 @@ +Transcript units are the only evidence for extracted events and factual claims. +Campaign and registry references may disambiguate names, aliases, speakers, or +other identities, but they do not establish events, participation, effects, or +source evidence. + +Every reported factual claim must be supported by cited transcript units. Use +integer `start_unit_id` and `end_unit_id` values from the transcript. Omit +`source_id`; Notarius assigns the current source identity. + +When supporting evidence is non-contiguous, use multiple narrow ranges rather +than a broad range that bridges unrelated conversation. + +Return exactly one JSON object and no explanatory text. Output only the +configured JSON object and fields defined by its response schema. diff --git a/internal/modules/dnd/shared/assets/prompts/common-dnd-identity.md b/internal/modules/dnd/shared/assets/prompts/common-dnd-identity.md new file mode 100644 index 0000000..727f83b --- /dev/null +++ b/internal/modules/dnd/shared/assets/prompts/common-dnd-identity.md @@ -0,0 +1,7 @@ +Use the most specific supported in-world character or creature identity for +each actor or participant. Do not identify a human player, transcript speaker, +or the GM as an out-of-world person when an in-world identity is supported. + +Player, party, glossary, campaign, and NPC registry references may disambiguate +an identity, but a reference alone cannot establish that the identity +participated in the transcript. diff --git a/internal/modules/dnd/shared/assets/prompts/common-dnd-immediate-resolution.md b/internal/modules/dnd/shared/assets/prompts/common-dnd-immediate-resolution.md new file mode 100644 index 0000000..89c105e --- /dev/null +++ b/internal/modules/dnd/shared/assets/prompts/common-dnd-immediate-resolution.md @@ -0,0 +1,7 @@ +Report only a declaration or action and its immediate observed resolution. +Immediate resolution may include directly associated rolls, damage, healing, +movement, conditions, target outcomes, interruptions, or other outcomes shown +with that declaration or action. + +Do not follow consequences that occur on later turns or elsewhere in the +scene.