Automated Quest Generation — Implementation Plan¶
Design Document: docs/designs/v3.1/08-automated-quest-generation.md
Priority: P1 — High
Estimated Duration: 14 weeks across 4 phases (Phase 1–4)
Summary¶
This plan implements the Automated Quest Generation system for MAID, enabling the game engine to dynamically create, personalize, and deliver quests driven by NPC signals, world events, and player behavior. The implementation adds:
- QuestSeed detection — event-driven seed extraction from NPC signals via
SeedTypeRegistryandSeedEvaluator - Archetype-based quest building —
QuestArchetype+ObjectivePattern+NarrativeArcdrive template selection andQuestBuilderassembly - LLM narrative generation — async
QuestNarrativeGeneratorwithTemplateFallbackGenerator, rolling token budget (15,000/hr), andContentFilteron all output - Player personalization (Phase 3) —
PlayerProfiletracking withPlayStyleenum,QuestPersonalizeradapts difficulty/rewards,DeliveryPlannerselectsQuestDeliveryMethod - Consequence propagation —
ConsequenceSystemappliesWorldMutationBatchon quest outcomes, injects gossip, modifies NPC goals/memories - Quest chaining — completed quests seed follow-up
QuestSeeds with dampened importance (0.7× per depth, max depth 5) - QuestGroundingMonitor — periodic validation that generated quest targets (NPCs, rooms, items) still exist in
World
Key prerequisite: QuestManager currently emits NO events at state transitions. Phase 1 adds missing event emissions.
Existing infrastructure leveraged:
- QuestManager at packages/maid-classic-rpg/src/maid_classic_rpg/systems/quests/manager.py
- ObjectiveTracker at packages/maid-classic-rpg/src/maid_classic_rpg/systems/quests/objectives.py
- RewardDistributor at packages/maid-classic-rpg/src/maid_classic_rpg/systems/quests/rewards.py
- Quest models at packages/maid-classic-rpg/src/maid_classic_rpg/models/quest.py
- Quest events at packages/maid-classic-rpg/src/maid_classic_rpg/events/core.py
- LLMProvider at packages/maid-engine/src/maid_engine/ai/providers/base.py
- ContentFilter at packages/maid-engine/src/maid_engine/ai/safety.py
- RateLimiter at packages/maid-engine/src/maid_engine/ai/rate_limiter.py
- DocumentStore at packages/maid-engine/src/maid_engine/storage/document_store.py
- EventBus at packages/maid-engine/src/maid_engine/core/events.py
- World at packages/maid-engine/src/maid_engine/core/world.py
- MAIDBaseModel at packages/maid-stdlib/src/maid_stdlib/models/base.py
Phase 1: Core Infrastructure (Weeks 1–4)¶
1.1 Generation Package Structure¶
Package:
maid-stdlib| Priority: P1 | Dependencies: none
- [ ] Create directory
packages/maid-stdlib/src/maid_stdlib/systems/quests/generation/ - [ ] Create
__init__.pyexporting all public types - [ ] Create
models.pyfor core data models - [ ] Create
stubs.pyfor upstream dependency stubs:StorySignaldataclass (signal_type: str,source_entity: UUID,importance: float,context: dict[str, Any],timestamp: datetime),emit_test_signal(world, signal_type, npc_id)helper, stubGoalsComponentandNeedsComponentwith no-op implementations. Enables development and testing independent of Doc 03/07. - [ ] Create
seeds.pyfor seed detection logic - [ ] Create
archetypes.pyfor archetype definitions - [ ] Create
builder.pyforQuestBuilderandObjectiveBuilder - [ ] Create
fallback.pyforTemplateFallbackGenerator - [ ] Create
history.pyforQuestHistoryandQuestHistoryEntry - [ ] Create
constants.pyfor all generation constants - [ ] Create tests directory:
packages/maid-stdlib/tests/quests/generation/
1.2 Core Data Models¶
Package:
maid-stdlib| Priority: P0 | Dependencies: 1.1
- [ ] Define
QuestSeedType = strtype alias inmodels.py, with built-in constants inQuestSeedTypesclass:THREAT_RESPONSE,RESOURCE_CRISIS,MYSTERY,GOAL_ASSISTANCE,CONSEQUENCE_CHAIN(5 core types). Remaining types (RIVALRY_INTERVENTION,FACTION_CONFLICT,TRADE_MISSION,ALLIANCE_QUEST,DISCOVERY_EXPEDITION,JUSTICE,POWER_STRUGGLE,RESCUE,PLAYER_REPUTATION,CRAFTING_REQUEST,SOCIAL_MANIPULATION,COLD_CASE) are registered as content pack extensions viaSeedTypeRegistryinStdlibContentPack.on_load(), not as built-in constants - [ ] Define
QuestSeeddataclass inmodels.pywith fields:source_signals(list[str]),seed_type(QuestSeedType),importance(float),urgency(float),primary_npc(UUID | None),primary_npc_motivation(str),backstory(str),stakes(str),location(UUID | None),threat_source(str),evidence_location(UUID | None),involved_npcs(list[UUID]),context_keys(dict[str, str]),created_at(datetime),expires_at(datetime | None),attempt_count(int = 0),chain_depth(int = 0) - [ ] Define
ObjectivePatternasstrconstants inmodels.py:FETCH,CLEAR,ESCORT,INVESTIGATE,DELIVER,NEGOTIATE,CRAFT_AND_DELIVER,MULTI_STAGE,CHOICE - [ ] Define
NarrativeArcasstrconstants inmodels.py:THREE_ACT,MYSTERY,JOURNEY,DILEMMA,ESCALATION - [ ] Define
QuestArchetypedataclass inmodels.py:archetype_id(str),name(str),applicable_seed_types(list[QuestSeedType]),objective_patterns(list[str]),narrative_arcs(list[str]),default_difficulty(float),reward_scale(float) - [ ] Define
GeneratedQuestdataclass inmodels.py:id(str, slug-format matchingQuest.quest_id),seed_id(UUID),seed_type(QuestSeedType),archetype_id(str),importance(float),title(str),summary(str),detailed_description(str),objectives(list[QuestObjective]),quest_giver_id(UUID),quest_giver_motivation(str),quest_giver_dialogue(QuestDialogue),involved_npcs(dict[UUID, str]),involved_locations(list[UUID]),rewards(QuestReward),consequences(list[QuestConsequence]),failure_consequences(list[QuestConsequence]),difficulty(DifficultyTier),time_limit(float | None),expiry(datetime),chain_potential(float),chain_depth(int = 0),grounding_score(float),coherence_score(float),novelty_score(float) - [ ] Define
QuestDialoguedataclass:offer(str),progress(str),completion(str) - [ ]
— Deferred to future phase. Branching quests require UI/client support for presenting choices. Start all archetypes asQuestBranchandBranchOptionLINEARonly. - [ ] Define
ConsequenceTypeenum (str,Enum):NPC_GOAL_ADVANCE,NPC_GOAL_COMPLETE,NPC_NEED_SATISFY,RELATIONSHIP_CHANGE,FACTION_STANDING,ECONOMIC_CHANGE,SOCIAL_INFLUENCE,GOSSIP_INJECTION,MEMORY_CREATION,WORLD_STATE - [ ] Define
QuestOutcomeenum (str,Enum):PERFECT,COMPLETED,PARTIAL_SUCCESS,PYRRHIC_VICTORY,FAILED,EXPIRED,ABANDONED - [ ]
— Deferred to Phase 3 (Personalization).PlayStyleenum - [ ] Define
QuestDeliveryMethodenum (str,Enum):DIRECT_OFFER,LETTER,RUMOR,NOTICE_BOARD,DISCOVERY - [ ]
— Deferred. All quests areBranchingPatternenumLINEARinitially. - [ ]
— Removed. Never consumed by any system. Defer to future multiplayer/group feature if needed.ProgressionModeenum - [ ] Define
DifficultyTierdataclass:tier(int),label(str),multiplier(float),recommended_level(int) - [ ] Define
QuestConsequencedataclass:consequence_type(ConsequenceType),target_npc(UUID | None),target_faction(str | None),description(str),params(dict[str, Any]) — payload validated byConsequencePropagatorbased onconsequence_type(e.g.,GOSSIP_INJECTIONrequires"content"key;RELATIONSHIP_CHANGErequires"delta"key). Replaces 10 optional fields with a single typed dict. - [ ] Define
WorldMutationdataclass:mutation_type(str),target_entity(UUID | None),target_key(str | None),delta(float | None),payload(dict[str, Any]),description(str = "") - [ ] Define
WorldMutationBatchclass:add(mutation: WorldMutation) -> None,validate(world: World) -> bool,apply(world: World) -> list[WorldMutation]— validates entity existence, bounds (faction_standing in [-1, 1], needs in [0, 1]), conflict detection; applies atomically with rollback on failure - [ ]
— Deferred to Phase 3 (Personalization). Define when implementingPlayerProfiledataclassQuestPersonalizer. - [ ]
— Deferred to Phase 3 (Personalization). Define when implementingQuestDeliverydataclassDeliveryPlanner. - [ ] Define
AntiPatterndataclass:name(str),description(str),check(Callable[[GeneratedQuest], bool]) - [ ] Write unit tests in
packages/maid-stdlib/tests/quests/generation/test_models.py: - [ ] Test all enum members serialize/deserialize correctly
- [ ] Test
QuestSeeddefault values - [ ] Test
GeneratedQuestconstruction with all fields - [ ] Test
QuestConsequenceandWorldMutationfield types
1.3 Seed Detection and Registry¶
Package:
maid-stdlib| Priority: P1 | Dependencies: 1.2
- [ ] Define
SeedEvaluationStrategyprotocol inseeds.py: async evaluate(self, signal: StorySignal, world: World, quest_history: QuestHistory) -> QuestSeed | None- [ ] Define
SeedEvaluatorclass inseeds.py: MIN_IMPORTANCE: float = 0.4,MIN_INVOLVED_NPCS: int = 1,MAX_ACTIVE_SEEDS: int = 10,COOLDOWN_PER_NPC: int = 3600,VARIETY_WINDOW: int = 5_strategies: list[SeedEvaluationStrategy]— registered evaluation strategiesregister_strategy(self, strategy: SeedEvaluationStrategy) -> Noneasync evaluate(self, signal: StorySignal, world: World, quest_history: QuestHistory) -> QuestSeed | None— delegates to strategies in order, applies shared filters (importance threshold, NPC cooldown, seed type variety)- Filters: importance threshold, NPC cooldown, seed type variety, feasibility check
- Validates required context keys from StorySignal → QuestSeedType contract
- Builds enriched seed with grounding context via
_build_seed()— populates typed fields (threat_source, evidence_location, etc.) at seed creation time, no fallback dict - [ ] Implement
SeedTypeRegistryclass inseeds.py: __init__(self)— auto-registers all built-in types fromQuestSeedTypesclassregister(self, type_id: str, description: str) -> None— content packs call this for custom typesis_registered(self, type_id: str) -> boollist_types(self) -> list[str]_types: dict[str, str]— type_id → description- [ ] Implement
CompoundSeedComposerclass inseeds.py: async compose(self, signals: list[str], world: World) -> list[QuestSeed]- Combines multiple related signals into compound seeds
- Filters by
MIN_IMPORTANCE = 0.4 - [ ] Implement
NpcNeedEvaluatorasSeedEvaluationStrategyinseeds.py: - Checks NPC
NeedsComponent(from design doc 07 — stub if not available) - Returns
QuestSeedwithseed_type="npc_need" - [ ] Implement
NpcGoalEvaluatorasSeedEvaluationStrategyinseeds.py: - Checks NPC
GoalsComponent(from design doc 03 — stub if not available) - Returns
QuestSeedwithseed_type="npc_goal" - [ ] Implement
ChainFollowupEvaluatorasSeedEvaluationStrategyinseeds.py: - Listens for
QuestTurnedInEvent, generateschain_followupseeds - Applies chain dampening:
importance *= 0.7 ** chain_depth - Respects MAX
chain_depth = 5 - [ ] Write unit tests in
packages/maid-stdlib/tests/quests/generation/test_seeds.py: - [ ] Test
SeedTypeRegistryregister and retrieve - [ ] Test
SeedTypeRegistrylist_typesreturns all registered - [ ] Test
CompoundSeedComposerfilters belowMIN_IMPORTANCE - [ ] Test
ChainFollowupEvaluatordampening calculation - [ ] Test
ChainFollowupEvaluatorrespects max depth
1.4 Archetype System and Objective Builders¶
Packages:
maid-stdlib(framework),maid-classic-rpg(content) | Priority: P1 | Dependencies: 1.2
- [ ] Keep archetype framework in
packages/maid-stdlib/src/maid_stdlib/systems/quests/generation/archetypes.py: QuestArchetypemodel remains framework data inmodels.pyselect_archetype(seed: QuestSeed, archetypes: list[QuestArchetype]) -> QuestArchetype | Noneremains inmaid-stdlib- [ ] Define Classic RPG default archetype instances in
packages/maid-classic-rpg/src/maid_classic_rpg/data/quests/archetypes.pyas module-level listDEFAULT_ARCHETYPES: list[QuestArchetype]: "threat_elimination": applicable to[THREAT_RESPONSE, RESOURCE_CRISIS], patterns[CLEAR], arcs[THREE_ACT], requires_combat=True"diplomatic_resolution": applicable to[RIVALRY_INTERVENTION, FACTION_CONFLICT, POWER_STRUGGLE], patterns[NEGOTIATE], arcs[DILEMMA], requires_social=True"supply_chain": applicable to[RESOURCE_CRISIS, TRADE_MISSION, GOAL_ASSISTANCE], patterns[CRAFT_AND_DELIVER], arcs[JOURNEY], requires_exploration=True"investigation": applicable to[MYSTERY, JUSTICE, DISCOVERY_EXPEDITION], patterns[INVESTIGATE], arcs[MYSTERY], requires_social=True, requires_exploration=True"rescue_mission": applicable to[RESCUE, THREAT_RESPONSE], patterns[ESCORT], arcs[ESCALATION], requires_combat=True"faction_choice": applicable to[FACTION_CONFLICT, POWER_STRUGGLE, RIVALRY_INTERVENTION], patterns[CHOICE], arcs[DILEMMA], requires_social=True- [ ] Register Classic RPG
DEFAULT_ARCHETYPESduringClassicRPGContentPack.on_load()(do not hardcode archetype instances inmaid-stdlib) - [ ] Implement
select_archetype(seed: QuestSeed, archetypes: list[QuestArchetype]) -> QuestArchetype | Noneinarchetypes.py: - Matches
seed.seed_typeagainstarchetype.applicable_seed_types - Selects highest priority match, breaking ties randomly
- [ ] Implement
ObjectiveBuilderclass inbuilder.py: build_objectives(self, seed: QuestSeed, pattern: str, world: World) -> list[QuestObjective]register(self, pattern: str, builder_callable: Callable) -> Noneto register per-pattern builders_builders: dict[str, Callable]private storage- [ ] Implement built-in objective builders for each
ObjectivePattern: _build_fetch(seed, world) -> list[QuestObjective]: creates COLLECT + DELIVER objectives_build_clear(seed, world) -> list[QuestObjective]: creates KILL objectives for target area_build_escort(seed, world) -> list[QuestObjective]: creates ESCORT + VISIT objectives_build_investigate(seed, world) -> list[QuestObjective]: creates VISIT + TALK objectives_build_deliver(seed, world) -> list[QuestObjective]: creates COLLECT + DELIVER objectives_build_negotiate(seed, world) -> list[QuestObjective]: creates TALK objectives for multiple NPCs_build_craft_and_deliver(seed, world) -> list[QuestObjective]: creates COLLECT + CUSTOM("craft") + DELIVER_build_multi_stage(seed, world) -> list[QuestObjective]: chains 2-3 sub-patterns sequentially_build_choice(seed, world) -> list[QuestObjective]: creates parallel optional objectives- [ ] Implement
QuestBuilderclass inbuilder.py: async build(self, seed: QuestSeed, archetype: QuestArchetype, world: World) -> GeneratedQuest | None- Uses
ObjectiveBuilderto generate objectives - Creates
Questmodel instance with generatedquest_id(UUID4 hex), objectives, rewards - Sets
giver_npc_idfromseed.primary_npc - Generates default dialogue placeholders (overwritten by LLM in Phase 2)
- Wraps result in
GeneratedQuestdataclass - [ ] Write unit tests in
packages/maid-stdlib/tests/quests/generation/test_archetypes.py: - [ ] Test
select_archetypematches seed type correctly - [ ] Test
select_archetypereturnsNonefor unmatched seed type - [ ] Test injected archetype lists have valid patterns and arcs
- [ ] Write content tests in
packages/maid-classic-rpg/tests/quests/generation/test_archetypes_content.py: - [ ] Test Classic RPG
DEFAULT_ARCHETYPEShave valid patterns and arcs - [ ] Write unit tests in
packages/maid-stdlib/tests/quests/generation/test_builder.py: - [ ] Test
QuestBuilder.buildreturnsGeneratedQuestwith validQuest - [ ] Test
ObjectiveBuilder.registerand dispatch - [ ] Test each built-in objective builder produces correct
ObjectiveTypevalues - [ ] Test
QuestBuildersetsgiver_npc_idfrom seed
1.5 QuestManager Event Emissions¶
Package:
maid-classic-rpg| Priority: P0 | Dependencies: none
Currently QuestManager.accept_quest(), abandon_quest(), and state transitions emit NO events. The generation system depends on these events for chain detection and history tracking.
- [ ] Modify
accept_quest()inpackages/maid-classic-rpg/src/maid_classic_rpg/systems/quests/manager.pyto emitQuestAcceptedEvent(character_id, quest_id)viaEventBusafter adding quest to quest log - [ ] Modify
abandon_quest()to emitQuestAbandonedEvent(character_id, quest_id)viaEventBusafter removing from active - [ ] Add
expire_quest(self, character_id: UUID, quest_id: str) -> boolmethod toQuestManager: sets state to FAILED, emitsQuestFailedEvent(character_id, quest_id, reason="expired") - [ ] Add
register_generated_quest(self, quest: Quest) -> Nonemethod: stores quest in internal registry for lookup, validatesquest_iduniqueness - [ ] Add
invalidate_quest(self, quest_id: str) -> Nonemethod: removes quest from available quests, cleans up any pending offers - [ ] Ensure
turn_in_quest()inrewards.pycontinues to emitQuestTurnedInEvent— verify it already does - [ ] Add
outcome: QuestOutcomefield toQuestTurnedInEventinpackages/maid-classic-rpg/src/maid_classic_rpg/events/core.py(default:QuestOutcome.COMPLETEDfor backward compatibility) - [ ] Add
_grade_outcome(self, quest: Quest, progress: QuestProgress) -> QuestOutcomeprivate method toRewardDistributor(NOTQuestManager—RewardDistributor.turn_in_quest()emits the event, so grading must happen there before emission): PERFECT = all required + all optional complete; COMPLETED = all required complete; PARTIAL_SUCCESS = ≥50% required complete; PYRRHIC_VICTORY = all required complete but character health ≤ 10% of max at turn-in time (simple check, no persistent tracking needed); FAILED = <50% required - [ ] Ensure
QuestManagerholdsEventBusreference — addevent_busparameter to__init__if not present - [ ] Write unit tests in
packages/maid-classic-rpg/tests/quests/test_manager_events.py: - [ ] Test
accept_questemitsQuestAcceptedEvent - [ ] Test
abandon_questemitsQuestAbandonedEvent - [ ] Test
expire_questemitsQuestFailedEventwithreason="expired" - [ ] Test
register_generated_queststores and retrieves quest - [ ] Test
invalidate_questremoves quest from registry - [ ] Test
QuestTurnedInEventincludesoutcomefield - [ ] Test
_grade_outcomereturnsPERFECTwhen all objectives complete and under time limit
1.6 Quest History and Tracking¶
Package:
maid-stdlib| Priority: P1 | Dependencies: 1.2, 1.5
- [ ] Define
QuestHistoryEntrydataclass inhistory.py:quest_id(str),seed_type(QuestSeedType),archetype_id(str),quest_giver_id(UUID),character_id(UUID | None),outcome(QuestOutcome | None),importance(float),grounding_score(float),coherence_score(float),novelty_score(float),created_at(datetime),accepted_at(datetime | None = None),completed_at(datetime | None = None),chain_depth(int = 0),chain_parent_id(str | None = None) - [ ] Define
QuestHistoryclass inhistory.py: __init__(self, document_store: DocumentStore)— storesDocumentStorereference, init_entries: dict[str, QuestHistoryEntry]and_active_ids: set[str]record(self, quest: GeneratedQuest) -> None— createsQuestHistoryEntryfrom quest'sseed_type,importance, quality scoresrecent_quest_from_npc(self, npc_id: UUID, window_seconds: float) -> boolrecent_types(self, count: int) -> list[QuestSeedType]— returns seed types of N most recent questsactive_generated_quest_ids(self) -> set[str]get(self, quest_id: str) -> QuestHistoryEntry | Nonecount_active_in_region(self, region_id: str) -> intcount_pending_offers(self, character_id: UUID) -> int- [ ]
— Removed. Pyrrhic Victory detection simplified to check character health at turn-in time inQuestProgressTracker_grade_outcome(). No persistentDamageDealtEventsubscriber needed. - [ ] Register
DocumentStoreschemas viaregister_schema()(NOTensure_collection()) in generation package init: "generated_quests": storesGeneratedQuestdocuments"quest_history": storesQuestHistoryEntrydocuments- [ ] Write unit tests in
packages/maid-stdlib/tests/quests/generation/test_history.py: - [ ] Test
QuestHistory.recordandget - [ ] Test
recent_quest_from_npcreturns correct boolean for cooldown window - [ ] Test
recent_typesreturns correct seed types for variety window - [ ] Test
count_active_in_region
1.7 Template Fallback Generator¶
Packages:
maid-stdlib(framework),maid-classic-rpg(content) | Priority: P1 | Dependencies: 1.2, 1.4
- [ ] Implement
TemplateFallbackGeneratorclass infallback.py(framework only): generate_narrative(self, seed: QuestSeed, archetype: QuestArchetype) -> strgenerate_dialogue(self, seed: QuestSeed, stage: str) -> dict[str, str]— returns dict with keys:accept_dialogue,progress_dialogue,complete_dialogue,turn_in_dialogue- Accepts registered templates (do not hardcode RPG narrative/dialogue text in
maid-stdlib) - Uses
string.Templatewith registered narrative templates per archetype - Templates reference
seed.primary_npc_motivation,seed.stakes,seed.backstory - [ ] Define Classic RPG narrative/dialogue template content in
packages/maid-classic-rpg/src/maid_classic_rpg/data/quests/templates.py: - Create at least 3 narrative templates per archetype (18 total minimum)
- Create dialogue templates for each stage (accept, progress, complete, turn_in)
- Include RPG-flavored text such as threat/emergency hooks and adventurer-facing completion dialogue
- [ ] Register Classic RPG templates during
ClassicRPGContentPack.on_load() - [ ] Write unit tests in
packages/maid-stdlib/tests/quests/generation/test_fallback.py: - [ ] Test
generate_narrativereturns non-empty string when templates are registered - [ ] Test
generate_dialoguereturns all required keys - [ ] Test template substitution uses seed fields correctly
1.8 Constants and Configuration¶
Package:
maid-stdlib| Priority: P1 | Dependencies: 1.1
- [ ] Define all constants in
constants.py: MIN_IMPORTANCE: float = 0.4MIN_INVOLVED_NPCS: int = 1MAX_ACTIVE_SEEDS: int = 10COOLDOWN_PER_NPC: int = 3600VARIETY_WINDOW: int = 5MAX_PENDING_QUESTS: int = 10MAX_SEED_ATTEMPTS: int = 3MAX_CONCURRENT_BUILDS: int = 3MAX_ACTIVE_QUESTS_PER_REGION: int = 3MAX_PENDING_OFFERS_PER_PLAYER: int = 2TOKEN_BUDGET_WINDOW: float = 3600.0TOKEN_BUDGET_LIMIT: int = 15_000CONSEQUENCE_SIGNAL_BUDGET: int = 5MAX_CHAIN_DEPTH: int = 5CHAIN_DAMPENING: float = 0.7MAX_TOKENS_PER_NARRATIVE: int = 500MAX_TOKENS_PER_DIALOGUE: int = 400GOSSIP_CONFIDENCE_CAP: float = 0.9- [ ] Write unit test verifying all constants are importable and have correct types
1.9 QuestGenerationSystem Skeleton¶
Package:
maid-stdlib| Priority: P1 | Dependencies: 1.2, 1.3, 1.4, 1.5
- [ ] Define
QuestGenerationSystem(System)inpackages/maid-stdlib/src/maid_stdlib/systems/quests/generation/system.py: priority: ClassVar[int] = 230— avoids collision withTradeSystem(210) andBankSystem(220)_pending_seeds: list[QuestSeed]— seeds awaiting processing (simple list, not double-buffered; EventBus is single-threaded async)_signal_seen: dict[tuple[str, UUID], float]— per-NPC signal dedup withSIGNAL_DEDUP_WINDOW = 60.0_global_signal_timestamps: list[float]— global rolling-window throttle withGLOBAL_SIGNAL_MAX = 30perGLOBAL_SIGNAL_WINDOW = 300.0_token_usage: list[tuple[float, int]]— rolling token budget tracking_ticks_since_generation: int = 0— generation cycle counter, initialized instartup()_in_flight: dict[UUID, asyncio.Task]— background build task tracking_processed_seed_ids: set[UUID]— deduplication set (capped at 1000 entries via LRU eviction)_seed_registry: SeedTypeRegistry_archetype_list: list[QuestArchetype]_quest_builder: QuestBuilder_objective_builder: ObjectiveBuilder_quest_history: QuestHistory_fallback_generator: TemplateFallbackGenerator- [ ] Implement
async startup(self) -> None: - Initialize
SeedTypeRegistrywith default evaluators - Load
DEFAULT_ARCHETYPES - Initialize
ObjectiveBuilderwith built-in builders - Subscribe to
QuestTurnedInEventfor chain detection - Subscribe to
QuestAcceptedEventandQuestAbandonedEventfor history tracking - [ ] Implement
async update(self, delta: float) -> None: - Deduplicate
_pending_seedsby seed ID - Collect results from completed background build tasks in
_in_flight - Call
_check_quest_expiry()for active generated quests - Detect seed spike (
len(_pending_seeds) > QUEUE_SPIKE_THRESHOLD) → force template-only mode - Prune rolling token budget window; compute
budget_exhausted - Prune
_signal_seenentries older thanSIGNAL_DEDUP_WINDOWevery 100 ticks - Cap
_processed_seed_idsat 1000 entries (evict oldest on overflow) - Rate limit: increment
_ticks_since_generation, skip if <GENERATION_INTERVAL_TICKS(unless high-importance seed >= 0.85) - Evict expired / exhausted seeds (attempt_count >= MAX_SEED_ATTEMPTS, or
expires_atpassed) - Sort
_pending_seedsby importance descending - Launch up to
MAX_QUESTS_PER_CYCLEbackground builds viaasyncio.create_task(_background_build(...)), respectingMAX_CONCURRENT_BUILDS - Pass
force_template=Truewhen queue spike or budget exhausted - Never block the tick loop — all LLM calls happen in background tasks
- [ ] Implement
async _build_and_register(self, seed: QuestSeed, archetype: QuestArchetype) -> None: - Call
quest_builder.build(seed, archetype, world)viaasyncio.wait_for(..., timeout=30.0) - On LLM failure/timeout: fall back to
TemplateFallbackGenerator - Run
ContentFiltercheck on all generated text; on failure, fall back to template - Run
QuestValidator.validate()andQuestQualityFilter.filter() - Track actual token usage from
CompletionResult.usagein rolling window - On success: call
_register_and_deliver(quest) - On failure: increment
seed.attempt_count, re-queue if <MAX_SEED_ATTEMPTS - Decrement
_active_buildsinfinallyblock - [ ] Implement
async _register_and_deliver(self, quest: GeneratedQuest) -> None: - Check per-region density (
MAX_ACTIVE_QUESTS_PER_REGION); skip if saturated - Convert
GeneratedQuesttoQuestPydantic model via_to_quest_model()— mapsquest_giver_dialoguetoaccept_dialogue/progress_dialogue/complete_dialoguestring fields - Call
QuestManager.register_generated_quest(quest_model) - Store generation metadata (consequences, branches, quality scores) in
"generated_quests"DocumentStore side-table - Emit
QuestGeneratedEvent - For each online character: check
MAX_PENDING_OFFERS_PER_PLAYER, deliver viaDIRECT_OFFERif quest giver NPC is nearby (personalization deferred to Phase 3) - Record in
QuestHistory - [ ] Implement
_to_quest_model(self, quest: GeneratedQuest) -> Quest: - Maps
GeneratedQuest.id→Quest.quest_id(both str) - Maps
quest_giver_dialogue.offer→Quest.accept_dialogue - Maps
quest_giver_dialogue.progress→Quest.progress_dialogue - Maps
quest_giver_dialogue.completion→Quest.complete_dialogue - Converts objectives to
QuestObjectivemodel with:objective_id=obj.id,objective_type,target_name=obj.target,target_count=obj.quantity,required=not obj.optional,order=obj.order. For DELIVER objectives, also mapsdeliver_to_npc_idanddeliver_item_template_id. - Sets
tags=["generated"]to distinguish from hand-authored quests - [ ] Implement
async _check_quest_expiry(self) -> None: - Check all active generated quest IDs from
QuestHistory - For each expired quest, call
QuestManager.expire_quest()and emitQuestExpiredInternalEvent - [ ] Implement
async shutdown(self) -> None: - Cancel any pending build tasks
- Unsubscribe from events
- [ ] Write unit tests in
packages/maid-stdlib/tests/quests/generation/test_system.py: - [ ] Test startup initializes all components
- [ ] Test update drains seed list with validation
- [ ] Test update respects
MAX_CONCURRENT_BUILDS - [ ] Test update prunes
_signal_seenand caps_processed_seed_ids - [ ] Test
_build_and_registerretries on failure up toMAX_SEED_ATTEMPTS - [ ] Test
_build_and_registeremitsQuestGeneratedEventon success - [ ] Test shutdown cancels pending tasks
1.10 New Events¶
Package:
maid-stdlib| Priority: P1 | Dependencies: 1.2
- [ ] Define
QuestGeneratedEventinpackages/maid-stdlib/src/maid_stdlib/events/quest_generation.py:quest_id(str),seed_type(QuestSeedType),archetype_id(str),quest_giver_id(UUID),importance(float) - [ ] Define
QuestOfferedEventinevents/quest_generation.py:quest_id(str),character_id(UUID),delivery_method(QuestDeliveryMethod),match_score(float) - [ ] Define
QuestChainEventinevents/quest_generation.py:original_quest_id(str),chain_signal(str),chain_type(str),chain_depth(int) - [ ] Define
QuestExpiredInternalEventinevents/quest_generation.py:quest_id(str) - [ ] Write tests verifying all new events can be instantiated and have correct fields
1.11 Phase 1 Integration Tests¶
Package:
maid-stdlib| Priority: P1 | Dependencies: 1.1–1.10
- [ ] Write integration test: seed → archetype selection → objective building →
GeneratedQuest - [ ] Write integration test:
QuestManagerevent emissions trigger history recording - [ ] Write integration test: seed list processes seeds without blocking tick loop
- [ ] Write integration test: fallback generator produces valid quest dialogue for all archetypes
Phase 2: LLM Integration and Validation (Weeks 5–8)¶
2.1 LLM Narrative Generation¶
Package:
maid-stdlib| Priority: P1 | Dependencies: 1.2, 1.7
- [ ] Implement
QuestNarrativeGeneratorclass inpackages/maid-stdlib/src/maid_stdlib/systems/quests/generation/narrative.py: __init__(self, provider: LLMProvider, content_filter: ContentFilter, rate_limiter: RateLimiter, fallback: TemplateFallbackGenerator)async generate(self, seed: QuestSeed, archetype: QuestArchetype, world_context: WorldContext) -> tuple[str, int]— returns(narrative_text, token_cost)async generate_dialogue(self, seed: QuestSeed, narrative: str) -> tuple[dict[str, str], int]— returns(dialogue_dict, token_cost)- Uses 2 LLM calls per quest: one for narrative, one for dialogue
- Each call wrapped in
asyncio.create_task(), never blocks tick - [ ] Define
QuestNarrativeModel(MAIDBaseModel)innarrative.pyfor LLM output validation: title: strdescription: strbackstory: strstakes: str- Fields validated via Pydantic
- [ ] Define
QuestDialogueModel(MAIDBaseModel)innarrative.pyfor LLM output validation: accept_dialogue: strprogress_dialogue: strcomplete_dialogue: strturn_in_dialogue: str- [ ] Implement prompt construction with XML isolation tags for prompt injection defense:
- Wrap user-facing content (NPC names, location names) in
<user_content>...</user_content>tags - System prompt instructs LLM to treat
<user_content>as data, not instructions - Use
CompletionOptions(max_tokens=MAX_TOKENS_PER_NARRATIVE, temperature=0.8)for narrative - Use
CompletionOptions(max_tokens=MAX_TOKENS_PER_DIALOGUE, temperature=0.7)for dialogue - [ ] Implement rolling token budget tracking:
_token_window: dequeof(timestamp, token_count)tuples_budget_remaining(self) -> int: sum tokens in lastTOKEN_BUDGET_WINDOWseconds, returnTOKEN_BUDGET_LIMITminus sum- Reject generation if budget insufficient for estimated cost
- [ ] Apply
ContentFilter.check_output()on all LLM responses before use - [ ] Fall back to
TemplateFallbackGeneratoron any LLM error (timeout, rate limit, content filter rejection, budget exhaustion) - [ ] Access LLM via
CompletionResult.content(NOT.text) —CompletionResulthascontent: strattribute - [ ] Write unit tests in
packages/maid-stdlib/tests/quests/generation/test_narrative.py(all tests useMockLLMProvider— no real LLM calls): - [ ] Test
generatecallsLLMProvider.completewith correctCompletionOptions - [ ] Test
generatefalls back to template on LLM error - [ ] Test
generate_dialoguevalidates output viaQuestDialogueModel - [ ] Test rolling token budget rejects when exhausted
- [ ] Test
ContentFilter.check_outputapplied to LLM response - [ ] Test XML isolation tags present in prompt
- [ ] Test
CompletionResult.contentused (not.text)
2.2 Narrative Context Cache — REMOVED¶
Rationale: With 2–5 quests/day and unique NPC context per seed, cache hit rate approaches zero. The cache key (
seed_type:npc:archetype) ignores world state, so hits would return stale narratives. If caching proves necessary later, it can be added trivially. Eliminatescache.pymodule and"narrative_cache"DocumentStore collection.
2.3 Quest Validation and Quality Filter¶
Package:
maid-stdlib| Priority: P1 | Dependencies: 1.2, 1.4
- [ ] Implement
QuestValidatorclass inpackages/maid-stdlib/src/maid_stdlib/systems/quests/generation/validator.py: validate(self, quest: GeneratedQuest, world: World) -> list[str]— returns list of validation errors- Checks:
quest.quest.quest_idis non-empty,giver_npc_identity exists in world (viaworld.entities.get(id)— NOTworld.entity_exists()which does not exist), all objectivetarget_ids exist,destination_room_ids exist, at leastMIN_INVOLVED_NPCSinvolved - Returns empty list if valid
- [ ] Implement
QuestQualityFilterclass invalidator.py: filter(self, quest: GeneratedQuest, history: QuestHistory) -> tuple[bool, list[str]]- Returns
(passes, reasons) - Checks against anti-patterns:
"duplicate_archetype": samearchetype_idin lastVARIETY_WINDOWquests"npc_overuse": same NPC gave quest withinCOOLDOWN_PER_NPCseconds"region_saturation": region already hasMAX_ACTIVE_QUESTS_PER_REGIONactive quests"trivial_reward": reward experience < 1 or gold < 1"impossible_objective": objective references non-existent entity
- [ ] Define
DEFAULT_ANTI_PATTERNSlist[AntiPattern]with the above checks - [ ] Allow content packs to register additional anti-patterns (e.g., RPG-specific reward threshold or economy balance checks)
- [ ] Write unit tests in
packages/maid-stdlib/tests/quests/generation/test_validator.py: - [ ] Test
QuestValidatorcatches missing NPC - [ ] Test
QuestValidatorcatches missing room - [ ] Test
QuestValidatorpasses valid quest - [ ] Test
QuestQualityFiltercatches duplicate archetype - [ ] Test
QuestQualityFiltercatches NPC overuse - [ ] Test
QuestQualityFiltercatches region saturation
2.4 Phase 2 Integration Tests¶
Package:
maid-stdlib| Priority: P1 | Dependencies: 2.1–2.3
- [ ] Write integration test: seed → build → LLM narrative → validate → deliver (via
DIRECT_OFFER) - [ ] Write integration test: LLM failure triggers fallback with valid output
- [ ] Write integration test: token budget exhaustion prevents new builds until window rolls
- [ ] Write integration test: quality filter rejects duplicate quests within variety window
- [ ] All Phase 2 tests use a deterministic
MockLLMProvider— no real LLM calls in CI
Phase 3: Consequences and Chaining (Weeks 9–11)¶
3.1 Consequence System¶
Package:
maid-stdlib| Priority: P1 | Dependencies: 1.2, 1.5
- [ ] Define
ConsequencePropagatorclass inpackages/maid-stdlib/src/maid_stdlib/systems/quests/generation/consequences.py: OUTCOME_SCALE: ClassVar[dict[QuestOutcome, float]]={PERFECT: 1.25, COMPLETED: 1.0, PARTIAL_SUCCESS: 0.5, PYRRHIC_VICTORY: 0.3}CONSEQUENCE_SIGNAL_BUDGET: int = 5— max new signals per propagation cycleplan_mutation(self, consequence: QuestConsequence, character_id: UUID, outcome_scale: float, world: World) -> WorldMutation | None— sole public entry point- Maps each
ConsequenceTypeto aWorldMutationviamatchstatement, reading values fromconsequence.params:NPC_GOAL_ADVANCE:delta=params["goal_progress"] * outcome_scaleNPC_GOAL_COMPLETE: marks NPC goal as completeNPC_NEED_SATISFY:delta=params["need_delta"] * outcome_scaleRELATIONSHIP_CHANGE:delta=params["relationship_delta"] * outcome_scale,target_key=str(character_id)FACTION_STANDING:delta=params["faction_delta"] * outcome_scale,payload={"character_id": str(character_id)}ECONOMIC_CHANGE:delta=params["economic_amount"] * outcome_scaleSOCIAL_INFLUENCE:delta=params["social_influence_delta"] * outcome_scaleGOSSIP_INJECTION:payload={"content": params["content"], "confidence": 0.9}— confidence capped atGOSSIP_CONFIDENCE_CAPMEMORY_CREATION:payload={"content": params["content"], "tags": ["quest"], "visibility": "public"}WORLD_STATE:payload={"value": params["value"]}
async detect_consequence_signals(quest, outcome, character_id, world) -> list[StorySignal]— detects chain potential, capped atCONSEQUENCE_SIGNAL_BUDGET- [ ] Register
DocumentStorecollections: "quest_consequences": storesWorldMutationBatchdocuments— Deferred (branching removed from Phase 1)"quest_branch_choices"- [ ] Write unit tests in
packages/maid-stdlib/tests/quests/generation/test_consequences.py: - [ ] Test
plan_mutationreturnsWorldMutationfor eachConsequenceType - [ ] Test
plan_mutationreturnsNonewhen target entity doesn't exist - [ ] Test
GOSSIP_INJECTIONconfidence capped at 0.9 - [ ] Test
MEMORY_CREATIONpayload includesvisibility: "public"tag - [ ] Test
RELATIONSHIP_CHANGEappliesoutcome_scalemultiplier - [ ] Test
CONSEQUENCE_SIGNAL_BUDGETcaps signals fromdetect_consequence_signals - [ ] Test
OUTCOME_SCALEmaps: PERFECT=1.25, COMPLETED=1.0, PARTIAL_SUCCESS=0.5, PYRRHIC_VICTORY=0.3
3.2 ConsequenceSystem ECS System¶
Package:
maid-stdlib| Priority: P1 | Dependencies: 3.1, 1.5
- [ ] Define
ConsequenceSystem(System)inpackages/maid-stdlib/src/maid_stdlib/systems/quests/generation/consequence_system.py: priority: ClassVar[int] = 240— avoids collision withTradeSystem(210) andBankSystem(220)_propagator: ConsequencePropagator_pending_mutations: asyncio.Queue[WorldMutationBatch]- [ ] Implement
async startup(self) -> None: - Subscribe to
QuestTurnedInEvent— reads gradedoutcomefrom event (NOT re-computed), calls_propagate(quest_id, event.outcome, event.character_id) - Subscribe to
QuestFailedEvent— calls_propagate(quest_id, QuestOutcome.FAILED, event.character_id) - Subscribe to
QuestAbandonedEvent— calls_propagate(quest_id, QuestOutcome.ABANDONED, event.character_id), injects relationship penalty (-0.15) and gossip injection - Subscribe to
QuestExpiredInternalEvent— calls_propagate(quest_id, QuestOutcome.EXPIRED, character_id)with failure consequences - [ ] Implement
async _propagate(self, quest_id: str, outcome: QuestOutcome, character_id: UUID) -> None: - Retrieve generation metadata from
"generated_quests"DocumentStore side-table; skip if not found (hand-authored quest) - Select
consequencesorfailure_consequencesbased on outcome - Compute
outcome_scalefromConsequencePropagator.OUTCOME_SCALE - Build
WorldMutationBatchviapropagator.plan_mutation()for each consequence - Validate batch via
batch.validate(world); log and return on validation failure - Apply atomically via
await batch.apply(world); batch handles rollback on failure - Persist applied mutations to
"quest_consequences"DocumentStore collection - Call
propagator.detect_consequence_signals()and emit resultingStorySignalEvents - [ ] Implement
async update(self, delta: float) -> None: - Drain
_pending_mutationsqueue - Apply each
WorldMutationBatchatomically - Emit
QuestChainEventfor chain triggers - [ ] Implement
async shutdown(self) -> None: - Drain remaining mutations without applying
- Unsubscribe from events
- [ ] Write unit tests in
packages/maid-stdlib/tests/quests/generation/test_consequence_system.py: - [ ] Test startup subscribes to quest outcome events
- [ ] Test
QuestTurnedInEventreads graded outcome from event (not re-computed) - [ ] Test
_propagateretrieves metadata from"generated_quests"side-table - [ ] Test
_propagateskips hand-authored quests (no side-table entry) - [ ] Test
WorldMutationBatchvalidation failure logs and returns without applying - [ ] Test
QuestAbandonedEventinjects relationship penalty and gossip - [ ] Test shutdown drains queue
3.3 Quest Chain Generation¶
Package:
maid-stdlib| Priority: P1 | Dependencies: 1.3, 3.2
- [ ] Extend
QuestGenerationSystem._build_and_registerto check for chain potential: - After successful build, if outcome is
COMPLETEDorPERFECT, evaluate chain seed - Call
ChainFollowupEvaluatorto generate follow-upQuestSeed - Enqueue follow-up seed with
chain_depth + 1 - Emit
QuestChainEvent(parent_quest_id, child_quest_id, chain_depth) - [ ] Add chain tracking to
QuestHistory: - Add
chain_id: str | Nonefield toQuestHistoryEntry - Add
get_chain(self, chain_id: str) -> list[QuestHistoryEntry]method - [ ] Add narrative continuity to chain quests:
- Pass parent quest narrative context to LLM prompt for follow-up
- Reference parent quest events in follow-up backstory
- [ ] Write unit tests in
packages/maid-stdlib/tests/quests/generation/test_chains.py: - [ ] Test chain seed generated on quest completion
- [ ] Test
chain_depthincremented correctly - [ ] Test chain dampening reduces importance
- [ ] Test max chain depth prevents further chaining
- [ ] Test
QuestChainEventemitted with correct fields
3.4 NPC Memory and Gossip Integration¶
Package:
maid-stdlib| Priority: P2 | Dependencies: 3.1
- [ ] Implement gossip injection in
ConsequencePropagator: GOSSIP_INJECTIONcreates a gossip message string describing quest outcome- Gossip confidence =
min(outcome_quality * 0.5 + 0.3, GOSSIP_CONFIDENCE_CAP) - Stores gossip in NPC dialogue state for retrieval by
NPCDialogueSystem - If NPC memory system (design doc 07) not available, store as simple dict on entity
- [ ] Implement memory creation stub:
MEMORY_CREATIONstores structured event in entity metadata- Format:
{"event": str, "timestamp": datetime, "importance": float, "related_entities": list[UUID]} - If NPC memory system available (
AutonomySystemfrom doc 03), use its API - Otherwise store as list in entity metadata dict
- [ ] Write unit tests:
- [ ] Test gossip confidence calculation
- [ ] Test gossip message stored on NPC entity
- [ ] Test memory creation stores structured event
3.5 Player Personalization and Delivery (moved from Phase 2)¶
Package:
maid-stdlib| Priority: P2 | Dependencies: 1.2, 1.6, 3.1
Personalization is additive — the system works without it (all qualifying quests use DIRECT_OFFER). Moving here reduces Phase 2 scope and derisks LLM integration.
- [ ] Define
PlayStyleenum (str,Enum):COMBAT,SOCIAL,EXPLORER,CRAFTER,BALANCED - [ ] Define
PlayerProfiledataclass:character_id(UUID),play_style(PlayStyle),preferred_difficulty(float),quest_completion_rate(float),average_quest_duration(float),favorite_objective_types(list[str]),disliked_objective_types(list[str]) - [ ] Define
QuestDeliverydataclass:method(QuestDeliveryMethod),delivery_npc(UUID | None),delivery_room(UUID | None),delivery_text(str),discovery_hint(str | None) - [ ] Implement
QuestPersonalizerclass inpackages/maid-stdlib/src/maid_stdlib/systems/quests/generation/personalizer.py: personalize(self, quest: GeneratedQuest, profile: PlayerProfile) -> GeneratedQuest- Adjusts difficulty and rewards based on
profile.preferred_difficulty - Filters/prefers objective types based on profile
- [ ] Implement
PlayerProfileconstruction fromQuestHistory: build_profile(self, character_id: UUID, history: QuestHistory) -> PlayerProfile- [ ] Implement
DeliveryPlannerclass inpackages/maid-stdlib/src/maid_stdlib/systems/quests/generation/delivery.py: plan(self, quest: GeneratedQuest, profile: PlayerProfile, world: World) -> QuestDelivery- Selects
QuestDeliveryMethodbased on NPC proximity and play style - [ ] Write unit tests for personalization and delivery
3.6 Phase 3 Integration Tests¶
Package:
maid-stdlib| Priority: P1 | Dependencies: 3.1–3.5
- [ ] Write integration test: quest completion → consequence propagation → world state change
- [ ] Write integration test: quest completion → chain seed → follow-up quest generation
- [ ] Write integration test: quest failure → failure consequences applied
- [ ] Write integration test: gossip injection → NPC has gossip in dialogue state
- [ ] Write integration test: full feedback loop — quest completion → consequence application → story signal emission → seed evaluation → new quest generation. Must verify chain depth incrementing and importance dampening across at least 2 chain links. This is the highest-risk architectural feature and requires explicit coverage.
Phase 4: Monitoring, Admin Tools, and Polish (Weeks 12–14)¶
4.1 QuestGroundingMonitor¶
Package:
maid-stdlib| Priority: P1 | Dependencies: 1.9, 2.3
- [ ] Define
QuestGroundingMonitor(System)inpackages/maid-stdlib/src/maid_stdlib/systems/quests/generation/grounding.py: priority: ClassVar[int] = 245— avoids collision withTradeSystem(210) andBankSystem(220)_check_interval: float = 60.0seconds_time_since_check: float = 0.0- [ ] Implement
async update(self, delta: float) -> None: - Accumulate
deltain_time_since_check - When
_time_since_check >= _check_interval:- Iterate all active generated quests from
QuestManager - For each quest, verify:
giver_npc_identity exists (viaworld.entities.get(id)), all objectivetarget_ids exist, alldestination_room_ids exist - If any reference invalid: call
QuestManager.invalidate_quest(quest_id), emitQuestExpiredInternalEvent(quest_id, reason="grounding_failure") - Log warnings for invalidated quests
- Reset
_time_since_check
- Iterate all active generated quests from
- [ ] Subscribe to
EntityDestroyedEvent(frompackages/maid-engine/src/maid_engine/core/events.py) for reactive invalidation: - On entity destruction, check if any active quest references that entity
- Immediately invalidate affected quests
- [ ] Write unit tests in
packages/maid-stdlib/tests/quests/generation/test_grounding.py: - [ ] Test periodic check invalidates quest with missing NPC
- [ ] Test periodic check invalidates quest with missing room
- [ ] Test periodic check passes valid quest
- [ ] Test
EntityDestroyedEventtriggers reactive invalidation - [ ] Test
QuestExpiredInternalEventemitted on invalidation
4.2 Backpressure and Telemetry¶
Package:
maid-stdlib| Priority: P1 | Dependencies: 1.9, 2.1
- [ ] Add backpressure monitoring to
QuestGenerationSystem: - Track
len(_pending_seeds)and active builds each tick - If
len(_pending_seeds) > MAX_ACTIVE_SEEDS * 0.8: log warning"seed list near capacity" - If no builds completing and seeds accumulating: pause seed intake, log
"build pipeline stalled" - Expose
queue_pressure(self) -> dict[str, float]method returning normalized pressures - [ ] Add token budget telemetry:
- Track
tokens_used_this_window,tokens_remaining,builds_completed,builds_failed - Expose
budget_status(self) -> dict[str, int | float]method - [ ] Add spike detection:
- If more than 5 seeds arrive within 10 seconds, throttle to 1 per tick
- Log
"seed spike detected, throttling"at WARNING level - [ ] Write unit tests in
packages/maid-stdlib/tests/quests/generation/test_backpressure.py: - [ ] Test
queue_pressurereturns correct normalized values - [ ] Test spike detection activates on burst
- [ ] Test
budget_statusreflects token usage - [ ] Test pipeline stall detection
4.3 Compound Seeds¶
Package:
maid-stdlib| Priority: P2 | Dependencies: 1.3
- [ ] Extend
CompoundSeedComposer.compose()to detect correlating signals: - Two NPC need signals in the same region → compound
"regional_crisis"seed - NPC goal + world event in same timeframe → compound
"opportunity"seed - Multiple player actions toward same target → compound
"player_driven"seed - [ ] Add compound-specific archetype matching in
select_archetype() - [ ] Write unit tests in
packages/maid-stdlib/tests/quests/generation/test_compound.py: - [ ] Test
composedetects regional crisis pattern - [ ] Test
composedetects opportunity pattern - [ ] Test compound seed selects correct archetype
4.4 Admin Commands¶
Package:
maid-stdlib| Priority: P2 | Dependencies: 1.9, 2.3, 4.1, 4.2
- [ ] Register admin commands in
StdlibContentPack.register_commands(): @quest list-generated [--active|--pending|--expired]: lists generated quests with status@quest seed-status: shows seed queue size, build queue size, active builds count@quest inspect <quest_id>: shows full quest details including seed, archetype, narrative, branches@quest force-generate <seed_type> [--npc <npc_id>] [--archetype <id>]: manually creates aQuestSeedwithimportance=1.0, synthesizes minimalStorySignalcontext from specified NPC's current state, bypasses quality filters (but NOT content safety), and processes immediately via_build_and_register. Useful for debugging and content testing.@quest quality-report [--last <N>]: shows quality scores and anti-pattern hits for recent quests@quest grounding-check: triggers immediate grounding validation@quest backpressure: shows queue pressures, token budget, spike status@quest chain <quest_id>: shows quest chain tree- [ ] All commands require ADMIN access level lock:
locks="perm(admin)" - [ ] Write unit tests in
packages/maid-stdlib/tests/quests/generation/test_admin_commands.py: - [ ] Test
list-generatedreturns formatted output - [ ] Test
seed-statusshows queue sizes - [ ] Test
inspectshows full quest details - [ ] Test
force-generatecreates and processes seed - [ ] Test commands require admin permission
4.5 ContentPack Integration¶
Packages:
maid-stdlib(systems),maid-classic-rpg(content) | Priority: P1 | Dependencies: 1.9, 3.2, 4.1
- [ ] Update
StdlibContentPack.get_systems()inpackages/maid-stdlib/src/maid_stdlib/pack.py: - Add
QuestGenerationSystemto returned systems list - Add
ConsequenceSystemto returned systems list - Add
QuestGroundingMonitorto returned systems list - [ ] Update
StdlibContentPack.get_events()to include new events: QuestGeneratedEvent,QuestOfferedEvent,QuestChainEvent,QuestExpiredInternalEvent- [ ] Update
StdlibContentPack.register_document_schemas()to register generation schemas viaregister_schema(): "generated_quests","quest_history","quest_consequences"- [ ] Update
StdlibContentPack.on_load()to initialize generation framework wiring (no RPG content defaults) - [ ] Update
ClassicRPGContentPack.on_load()to register content-specific generation data: - Default quest archetypes (
threat_elimination,diplomatic_resolution,supply_chain,investigation,rescue_mission,faction_choice) - Narrative/dialogue template text (RPG-flavored template strings)
- Content-specific seed types (
RIVALRY_INTERVENTION,FACTION_CONFLICT,TRADE_MISSION,ALLIANCE_QUEST,DISCOVERY_EXPEDITION,JUSTICE,POWER_STRUGGLE,RESCUE,PLAYER_REPUTATION,CRAFTING_REQUEST,SOCIAL_MANIPULATION,COLD_CASE) - Content-specific anti-patterns (e.g., RPG reward threshold checks beyond framework defaults)
- [ ] Write integration tests verifying stdlib systems load and Classic RPG content registration occurs on pack load
4.6 Documentation¶
Package:
maid-stdlib| Priority: P2 | Dependencies: 4.1–4.5
- [ ] Write
docs/guides/quest-generation.md: - Architecture overview with system diagram
- Seed detection pipeline description
- Archetype and objective builder extension guide
- LLM integration and fallback behavior
- Consequence propagation flow
- Quest chaining mechanics
- Admin command reference
- Configuration constants reference
- [ ] Add docstrings to all public classes and methods (Google-style)
- [ ] Update
CHANGELOG.mdwith quest generation feature entry
4.7 Phase 4 Integration Tests¶
Package:
maid-stdlib| Priority: P1 | Dependencies: 4.1–4.5
- [ ] Write integration test: full lifecycle seed → build → offer → accept → complete → consequence → chain
- [ ] Write integration test: grounding monitor invalidates quest when NPC destroyed
- [ ] Write integration test: backpressure throttles under load
- [ ] Write integration test: content pack loads and initializes all generation systems
- [ ] Write integration test: admin commands produce correct output
- [ ] Write integration test: compound seeds generate multi-NPC quests
Success Criteria¶
- [ ] Generated quests have valid objectives referencing existing world entities
- [ ] LLM narrative generation completes in <2s per quest (async, non-blocking)
- [ ] Template fallback produces playable quests when LLM unavailable
- [ ] Token budget stays within 15,000 tokens per rolling hour
- [ ] Quest variety: no duplicate archetype in last 5 quests per player
- [ ] NPC cooldown: minimum 1 hour between quests from same NPC
- [ ] Maximum 3 active generated quests per region
- [ ] Maximum 2 pending quest offers per player
- [ ] Quest chains limited to depth 5 with 0.7x importance dampening
- [ ] Grounding monitor detects and invalidates stale references within 60 seconds
- [ ] Consequence propagation applies world mutations within 1 tick of quest completion
- [ ] All generation code passes MyPy strict mode
- [ ] Test coverage >80% for all generation modules
- [ ] No tick loop blocking — all LLM calls use
asyncio.create_task() - [ ] Content safety filter applied to all LLM-generated quest text
- [ ] Admin commands functional for monitoring and debugging generation pipeline
Prerequisites / Blockers from Other Design Docs¶
- Design Doc 03 (NPC Autonomy):
StorySignal,AutonomySystem,GoalsComponentdo not exist yet. Seed evaluators that depend on these use stubs with clear interfaces that will be wired when doc 03 is implemented. Phase 1.1 must create a concrete stub modulegeneration/stubs.pywith:StorySignaldataclass (fields:signal_type: str,source_entity: UUID,importance: float,context: dict[str, Any],timestamp: datetime),emit_test_signal(world, signal_type, npc_id)helper, and stubGoalsComponent/NeedsComponent. Without this, no development or testing can proceed. - Design Doc 07 (NPC Needs):
NeedsComponentdoes not exist yet.NpcNeedEvaluatoruses the stub fromgeneration/stubs.py. Same pattern as doc 03 — no blocker. - Design Doc 01 (Durable Persistence):
DocumentStoreusesregister_schema(name, schema)(NOTregister_collectionorensure_collection). Generated quest persistence depends on existing infrastructure. No blocker. QuestManagerevent emissions (task 1.5) are an internal prerequisite that must complete first — before any other Phase 1 task that depends on events. This is P0 priority and should be the first task executed.StdlibContentPackupdates (task 4.5) require all systems to be implemented first, handled by Phase 4 ordering.
World API note: The actual World API uses world.entities.get(entity_id) → Entity | None, then entity.get(ComponentType) / entity.has(ComponentType). Methods like world.entity_exists(), world.get_component(), world.get_name() do NOT exist. All code in this plan must use the real Entity API. Consider adding a thin WorldHelper utility module with convenience wrappers if the pattern becomes verbose.
DocumentStore error handling: All await document_store.put/get(...) calls must be wrapped in try/except with logging. Quest generation should degrade gracefully (skip persistence, continue in-memory) if the store is unavailable. An unhandled DocumentStore exception would kill the tick loop.
Dependencies Summary¶
Phase 1 (Weeks 1-4):
1.5 QuestManager Events (P0, no dependencies — EXECUTE FIRST)
1.1 Package Structure + StorySignal stub (generation/stubs.py)
1.2 Core Data Models (depends on 1.1)
1.3 Seed Detection (depends on 1.2)
1.4 Archetype System (depends on 1.2)
1.6 Quest History (depends on 1.2, 1.5)
1.7 Template Fallback (depends on 1.2, 1.4)
1.8 Constants (depends on 1.1)
1.9 System Skeleton (depends on 1.2, 1.3, 1.4, 1.5)
1.10 New Events (depends on 1.2)
1.11 Integration Tests (depends on 1.1-1.10)
Phase 2 (Weeks 5-8, depends on Phase 1):
2.1 LLM Narrative (depends on 1.2, 1.7) — use MockLLMProvider in tests
2.2 ~~Narrative Cache~~ REMOVED
2.3 Validation/Quality (depends on 1.2, 1.4)
2.4 Integration Tests (depends on 2.1-2.3)
Phase 3 (Weeks 9-11, depends on Phase 2):
3.1 Consequence Propagator (depends on 1.2, 1.5)
3.2 ConsequenceSystem (depends on 3.1, 1.5)
3.3 Chain Generation (depends on 1.3, 3.2)
3.4 NPC Memory/Gossip (depends on 3.1)
3.5 Personalization + Delivery (depends on 1.2, 1.6, 3.1) — moved from Phase 2
3.6 Integration Tests incl. full feedback loop (depends on 3.1-3.5)
Phase 4 (Weeks 12-14, depends on Phase 3):
4.1 Grounding Monitor (depends on 1.9, 2.3)
4.2 Backpressure/Telemetry (depends on 1.9, 2.1)
4.3 Compound Seeds (depends on 1.3)
4.4 Admin Commands (depends on 1.9, 2.3, 4.1, 4.2)
4.5 ContentPack Integration (depends on 1.9, 3.2, 4.1)
4.6 Documentation (depends on 4.1-4.5)
4.7 Integration Tests (depends on 4.1-4.5)