Skip to content

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 SeedTypeRegistry and SeedEvaluator
  • Archetype-based quest buildingQuestArchetype + ObjectivePattern + NarrativeArc drive template selection and QuestBuilder assembly
  • LLM narrative generation — async QuestNarrativeGenerator with TemplateFallbackGenerator, rolling token budget (15,000/hr), and ContentFilter on all output
  • Player personalization (Phase 3) — PlayerProfile tracking with PlayStyle enum, QuestPersonalizer adapts difficulty/rewards, DeliveryPlanner selects QuestDeliveryMethod
  • Consequence propagationConsequenceSystem applies WorldMutationBatch on 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__.py exporting all public types
  • [ ] Create models.py for core data models
  • [ ] Create stubs.py for upstream dependency stubs: StorySignal dataclass (signal_type: str, source_entity: UUID, importance: float, context: dict[str, Any], timestamp: datetime), emit_test_signal(world, signal_type, npc_id) helper, stub GoalsComponent and NeedsComponent with no-op implementations. Enables development and testing independent of Doc 03/07.
  • [ ] Create seeds.py for seed detection logic
  • [ ] Create archetypes.py for archetype definitions
  • [ ] Create builder.py for QuestBuilder and ObjectiveBuilder
  • [ ] Create fallback.py for TemplateFallbackGenerator
  • [ ] Create history.py for QuestHistory and QuestHistoryEntry
  • [ ] Create constants.py for 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 = str type alias in models.py, with built-in constants in QuestSeedTypes class: 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 via SeedTypeRegistry in StdlibContentPack.on_load(), not as built-in constants
  • [ ] Define QuestSeed dataclass in models.py with 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 ObjectivePattern as str constants in models.py: FETCH, CLEAR, ESCORT, INVESTIGATE, DELIVER, NEGOTIATE, CRAFT_AND_DELIVER, MULTI_STAGE, CHOICE
  • [ ] Define NarrativeArc as str constants in models.py: THREE_ACT, MYSTERY, JOURNEY, DILEMMA, ESCALATION
  • [ ] Define QuestArchetype dataclass in models.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 GeneratedQuest dataclass in models.py: id (str, slug-format matching Quest.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 QuestDialogue dataclass: offer (str), progress (str), completion (str)
  • [ ] QuestBranch and BranchOptionDeferred to future phase. Branching quests require UI/client support for presenting choices. Start all archetypes as LINEAR only.
  • [ ] Define ConsequenceType enum (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 QuestOutcome enum (str, Enum): PERFECT, COMPLETED, PARTIAL_SUCCESS, PYRRHIC_VICTORY, FAILED, EXPIRED, ABANDONED
  • [ ] PlayStyle enumDeferred to Phase 3 (Personalization).
  • [ ] Define QuestDeliveryMethod enum (str, Enum): DIRECT_OFFER, LETTER, RUMOR, NOTICE_BOARD, DISCOVERY
  • [ ] BranchingPattern enumDeferred. All quests are LINEAR initially.
  • [ ] ProgressionMode enumRemoved. Never consumed by any system. Defer to future multiplayer/group feature if needed.
  • [ ] Define DifficultyTier dataclass: tier (int), label (str), multiplier (float), recommended_level (int)
  • [ ] Define QuestConsequence dataclass: consequence_type (ConsequenceType), target_npc (UUID | None), target_faction (str | None), description (str), params (dict[str, Any]) — payload validated by ConsequencePropagator based on consequence_type (e.g., GOSSIP_INJECTION requires "content" key; RELATIONSHIP_CHANGE requires "delta" key). Replaces 10 optional fields with a single typed dict.
  • [ ] Define WorldMutation dataclass: mutation_type (str), target_entity (UUID | None), target_key (str | None), delta (float | None), payload (dict[str, Any]), description (str = "")
  • [ ] Define WorldMutationBatch class: 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
  • [ ] PlayerProfile dataclassDeferred to Phase 3 (Personalization). Define when implementing QuestPersonalizer.
  • [ ] QuestDelivery dataclassDeferred to Phase 3 (Personalization). Define when implementing DeliveryPlanner.
  • [ ] Define AntiPattern dataclass: 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 QuestSeed default values
  • [ ] Test GeneratedQuest construction with all fields
  • [ ] Test QuestConsequence and WorldMutation field types

1.3 Seed Detection and Registry

Package: maid-stdlib | Priority: P1 | Dependencies: 1.2

  • [ ] Define SeedEvaluationStrategy protocol in seeds.py:
  • async evaluate(self, signal: StorySignal, world: World, quest_history: QuestHistory) -> QuestSeed | None
  • [ ] Define SeedEvaluator class in seeds.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 strategies
  • register_strategy(self, strategy: SeedEvaluationStrategy) -> None
  • async 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 SeedTypeRegistry class in seeds.py:
  • __init__(self) — auto-registers all built-in types from QuestSeedTypes class
  • register(self, type_id: str, description: str) -> None — content packs call this for custom types
  • is_registered(self, type_id: str) -> bool
  • list_types(self) -> list[str]
  • _types: dict[str, str] — type_id → description
  • [ ] Implement CompoundSeedComposer class in seeds.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 NpcNeedEvaluator as SeedEvaluationStrategy in seeds.py:
  • Checks NPC NeedsComponent (from design doc 07 — stub if not available)
  • Returns QuestSeed with seed_type="npc_need"
  • [ ] Implement NpcGoalEvaluator as SeedEvaluationStrategy in seeds.py:
  • Checks NPC GoalsComponent (from design doc 03 — stub if not available)
  • Returns QuestSeed with seed_type="npc_goal"
  • [ ] Implement ChainFollowupEvaluator as SeedEvaluationStrategy in seeds.py:
  • Listens for QuestTurnedInEvent, generates chain_followup seeds
  • 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 SeedTypeRegistry register and retrieve
  • [ ] Test SeedTypeRegistry list_types returns all registered
  • [ ] Test CompoundSeedComposer filters below MIN_IMPORTANCE
  • [ ] Test ChainFollowupEvaluator dampening calculation
  • [ ] Test ChainFollowupEvaluator respects 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:
  • QuestArchetype model remains framework data in models.py
  • select_archetype(seed: QuestSeed, archetypes: list[QuestArchetype]) -> QuestArchetype | None remains in maid-stdlib
  • [ ] Define Classic RPG default archetype instances in packages/maid-classic-rpg/src/maid_classic_rpg/data/quests/archetypes.py as module-level list DEFAULT_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_ARCHETYPES during ClassicRPGContentPack.on_load() (do not hardcode archetype instances in maid-stdlib)
  • [ ] Implement select_archetype(seed: QuestSeed, archetypes: list[QuestArchetype]) -> QuestArchetype | None in archetypes.py:
  • Matches seed.seed_type against archetype.applicable_seed_types
  • Selects highest priority match, breaking ties randomly
  • [ ] Implement ObjectiveBuilder class in builder.py:
  • build_objectives(self, seed: QuestSeed, pattern: str, world: World) -> list[QuestObjective]
  • register(self, pattern: str, builder_callable: Callable) -> None to 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 QuestBuilder class in builder.py:
  • async build(self, seed: QuestSeed, archetype: QuestArchetype, world: World) -> GeneratedQuest | None
  • Uses ObjectiveBuilder to generate objectives
  • Creates Quest model instance with generated quest_id (UUID4 hex), objectives, rewards
  • Sets giver_npc_id from seed.primary_npc
  • Generates default dialogue placeholders (overwritten by LLM in Phase 2)
  • Wraps result in GeneratedQuest dataclass
  • [ ] Write unit tests in packages/maid-stdlib/tests/quests/generation/test_archetypes.py:
  • [ ] Test select_archetype matches seed type correctly
  • [ ] Test select_archetype returns None for 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_ARCHETYPES have valid patterns and arcs
  • [ ] Write unit tests in packages/maid-stdlib/tests/quests/generation/test_builder.py:
  • [ ] Test QuestBuilder.build returns GeneratedQuest with valid Quest
  • [ ] Test ObjectiveBuilder.register and dispatch
  • [ ] Test each built-in objective builder produces correct ObjectiveType values
  • [ ] Test QuestBuilder sets giver_npc_id from 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() in packages/maid-classic-rpg/src/maid_classic_rpg/systems/quests/manager.py to emit QuestAcceptedEvent(character_id, quest_id) via EventBus after adding quest to quest log
  • [ ] Modify abandon_quest() to emit QuestAbandonedEvent(character_id, quest_id) via EventBus after removing from active
  • [ ] Add expire_quest(self, character_id: UUID, quest_id: str) -> bool method to QuestManager: sets state to FAILED, emits QuestFailedEvent(character_id, quest_id, reason="expired")
  • [ ] Add register_generated_quest(self, quest: Quest) -> None method: stores quest in internal registry for lookup, validates quest_id uniqueness
  • [ ] Add invalidate_quest(self, quest_id: str) -> None method: removes quest from available quests, cleans up any pending offers
  • [ ] Ensure turn_in_quest() in rewards.py continues to emit QuestTurnedInEvent — verify it already does
  • [ ] Add outcome: QuestOutcome field to QuestTurnedInEvent in packages/maid-classic-rpg/src/maid_classic_rpg/events/core.py (default: QuestOutcome.COMPLETED for backward compatibility)
  • [ ] Add _grade_outcome(self, quest: Quest, progress: QuestProgress) -> QuestOutcome private method to RewardDistributor (NOT QuestManagerRewardDistributor.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 QuestManager holds EventBus reference — add event_bus parameter to __init__ if not present
  • [ ] Write unit tests in packages/maid-classic-rpg/tests/quests/test_manager_events.py:
  • [ ] Test accept_quest emits QuestAcceptedEvent
  • [ ] Test abandon_quest emits QuestAbandonedEvent
  • [ ] Test expire_quest emits QuestFailedEvent with reason="expired"
  • [ ] Test register_generated_quest stores and retrieves quest
  • [ ] Test invalidate_quest removes quest from registry
  • [ ] Test QuestTurnedInEvent includes outcome field
  • [ ] Test _grade_outcome returns PERFECT when all objectives complete and under time limit

1.6 Quest History and Tracking

Package: maid-stdlib | Priority: P1 | Dependencies: 1.2, 1.5

  • [ ] Define QuestHistoryEntry dataclass in history.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 QuestHistory class in history.py:
  • __init__(self, document_store: DocumentStore) — stores DocumentStore reference, init _entries: dict[str, QuestHistoryEntry] and _active_ids: set[str]
  • record(self, quest: GeneratedQuest) -> None — creates QuestHistoryEntry from quest's seed_type, importance, quality scores
  • recent_quest_from_npc(self, npc_id: UUID, window_seconds: float) -> bool
  • recent_types(self, count: int) -> list[QuestSeedType] — returns seed types of N most recent quests
  • active_generated_quest_ids(self) -> set[str]
  • get(self, quest_id: str) -> QuestHistoryEntry | None
  • count_active_in_region(self, region_id: str) -> int
  • count_pending_offers(self, character_id: UUID) -> int
  • [ ] QuestProgressTrackerRemoved. Pyrrhic Victory detection simplified to check character health at turn-in time in _grade_outcome(). No persistent DamageDealtEvent subscriber needed.
  • [ ] Register DocumentStore schemas via register_schema() (NOT ensure_collection()) in generation package init:
  • "generated_quests": stores GeneratedQuest documents
  • "quest_history": stores QuestHistoryEntry documents
  • [ ] Write unit tests in packages/maid-stdlib/tests/quests/generation/test_history.py:
  • [ ] Test QuestHistory.record and get
  • [ ] Test recent_quest_from_npc returns correct boolean for cooldown window
  • [ ] Test recent_types returns 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 TemplateFallbackGenerator class in fallback.py (framework only):
  • generate_narrative(self, seed: QuestSeed, archetype: QuestArchetype) -> str
  • generate_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.Template with 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_narrative returns non-empty string when templates are registered
  • [ ] Test generate_dialogue returns 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.4
  • MIN_INVOLVED_NPCS: int = 1
  • MAX_ACTIVE_SEEDS: int = 10
  • COOLDOWN_PER_NPC: int = 3600
  • VARIETY_WINDOW: int = 5
  • MAX_PENDING_QUESTS: int = 10
  • MAX_SEED_ATTEMPTS: int = 3
  • MAX_CONCURRENT_BUILDS: int = 3
  • MAX_ACTIVE_QUESTS_PER_REGION: int = 3
  • MAX_PENDING_OFFERS_PER_PLAYER: int = 2
  • TOKEN_BUDGET_WINDOW: float = 3600.0
  • TOKEN_BUDGET_LIMIT: int = 15_000
  • CONSEQUENCE_SIGNAL_BUDGET: int = 5
  • MAX_CHAIN_DEPTH: int = 5
  • CHAIN_DAMPENING: float = 0.7
  • MAX_TOKENS_PER_NARRATIVE: int = 500
  • MAX_TOKENS_PER_DIALOGUE: int = 400
  • GOSSIP_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) in packages/maid-stdlib/src/maid_stdlib/systems/quests/generation/system.py:
  • priority: ClassVar[int] = 230 — avoids collision with TradeSystem (210) and BankSystem (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 with SIGNAL_DEDUP_WINDOW = 60.0
  • _global_signal_timestamps: list[float] — global rolling-window throttle with GLOBAL_SIGNAL_MAX = 30 per GLOBAL_SIGNAL_WINDOW = 300.0
  • _token_usage: list[tuple[float, int]] — rolling token budget tracking
  • _ticks_since_generation: int = 0 — generation cycle counter, initialized in startup()
  • _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 SeedTypeRegistry with default evaluators
  • Load DEFAULT_ARCHETYPES
  • Initialize ObjectiveBuilder with built-in builders
  • Subscribe to QuestTurnedInEvent for chain detection
  • Subscribe to QuestAcceptedEvent and QuestAbandonedEvent for history tracking
  • [ ] Implement async update(self, delta: float) -> None:
  • Deduplicate _pending_seeds by 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_seen entries older than SIGNAL_DEDUP_WINDOW every 100 ticks
  • Cap _processed_seed_ids at 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_at passed)
  • Sort _pending_seeds by importance descending
  • Launch up to MAX_QUESTS_PER_CYCLE background builds via asyncio.create_task(_background_build(...)), respecting MAX_CONCURRENT_BUILDS
  • Pass force_template=True when 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) via asyncio.wait_for(..., timeout=30.0)
  • On LLM failure/timeout: fall back to TemplateFallbackGenerator
  • Run ContentFilter check on all generated text; on failure, fall back to template
  • Run QuestValidator.validate() and QuestQualityFilter.filter()
  • Track actual token usage from CompletionResult.usage in rolling window
  • On success: call _register_and_deliver(quest)
  • On failure: increment seed.attempt_count, re-queue if < MAX_SEED_ATTEMPTS
  • Decrement _active_builds in finally block
  • [ ] Implement async _register_and_deliver(self, quest: GeneratedQuest) -> None:
  • Check per-region density (MAX_ACTIVE_QUESTS_PER_REGION); skip if saturated
  • Convert GeneratedQuest to Quest Pydantic model via _to_quest_model() — maps quest_giver_dialogue to accept_dialogue/progress_dialogue/complete_dialogue string 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 via DIRECT_OFFER if quest giver NPC is nearby (personalization deferred to Phase 3)
  • Record in QuestHistory
  • [ ] Implement _to_quest_model(self, quest: GeneratedQuest) -> Quest:
  • Maps GeneratedQuest.idQuest.quest_id (both str)
  • Maps quest_giver_dialogue.offerQuest.accept_dialogue
  • Maps quest_giver_dialogue.progressQuest.progress_dialogue
  • Maps quest_giver_dialogue.completionQuest.complete_dialogue
  • Converts objectives to QuestObjective model 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 maps deliver_to_npc_id and deliver_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 emit QuestExpiredInternalEvent
  • [ ] 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_seen and caps _processed_seed_ids
  • [ ] Test _build_and_register retries on failure up to MAX_SEED_ATTEMPTS
  • [ ] Test _build_and_register emits QuestGeneratedEvent on success
  • [ ] Test shutdown cancels pending tasks

1.10 New Events

Package: maid-stdlib | Priority: P1 | Dependencies: 1.2

  • [ ] Define QuestGeneratedEvent in packages/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 QuestOfferedEvent in events/quest_generation.py: quest_id (str), character_id (UUID), delivery_method (QuestDeliveryMethod), match_score (float)
  • [ ] Define QuestChainEvent in events/quest_generation.py: original_quest_id (str), chain_signal (str), chain_type (str), chain_depth (int)
  • [ ] Define QuestExpiredInternalEvent in events/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: QuestManager event 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 QuestNarrativeGenerator class in packages/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) in narrative.py for LLM output validation:
  • title: str
  • description: str
  • backstory: str
  • stakes: str
  • Fields validated via Pydantic
  • [ ] Define QuestDialogueModel(MAIDBaseModel) in narrative.py for LLM output validation:
  • accept_dialogue: str
  • progress_dialogue: str
  • complete_dialogue: str
  • turn_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: deque of (timestamp, token_count) tuples
  • _budget_remaining(self) -> int: sum tokens in last TOKEN_BUDGET_WINDOW seconds, return TOKEN_BUDGET_LIMIT minus sum
  • Reject generation if budget insufficient for estimated cost
  • [ ] Apply ContentFilter.check_output() on all LLM responses before use
  • [ ] Fall back to TemplateFallbackGenerator on any LLM error (timeout, rate limit, content filter rejection, budget exhaustion)
  • [ ] Access LLM via CompletionResult.content (NOT .text) — CompletionResult has content: str attribute
  • [ ] Write unit tests in packages/maid-stdlib/tests/quests/generation/test_narrative.py (all tests use MockLLMProvider — no real LLM calls):
  • [ ] Test generate calls LLMProvider.complete with correct CompletionOptions
  • [ ] Test generate falls back to template on LLM error
  • [ ] Test generate_dialogue validates output via QuestDialogueModel
  • [ ] Test rolling token budget rejects when exhausted
  • [ ] Test ContentFilter.check_output applied to LLM response
  • [ ] Test XML isolation tags present in prompt
  • [ ] Test CompletionResult.content used (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. Eliminates cache.py module and "narrative_cache" DocumentStore collection.

2.3 Quest Validation and Quality Filter

Package: maid-stdlib | Priority: P1 | Dependencies: 1.2, 1.4

  • [ ] Implement QuestValidator class in packages/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_id is non-empty, giver_npc_id entity exists in world (via world.entities.get(id) — NOT world.entity_exists() which does not exist), all objective target_ids exist, destination_room_ids exist, at least MIN_INVOLVED_NPCS involved
  • Returns empty list if valid
  • [ ] Implement QuestQualityFilter class in validator.py:
  • filter(self, quest: GeneratedQuest, history: QuestHistory) -> tuple[bool, list[str]]
  • Returns (passes, reasons)
  • Checks against anti-patterns:
    • "duplicate_archetype": same archetype_id in last VARIETY_WINDOW quests
    • "npc_overuse": same NPC gave quest within COOLDOWN_PER_NPC seconds
    • "region_saturation": region already has MAX_ACTIVE_QUESTS_PER_REGION active quests
    • "trivial_reward": reward experience < 1 or gold < 1
    • "impossible_objective": objective references non-existent entity
  • [ ] Define DEFAULT_ANTI_PATTERNS list[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 QuestValidator catches missing NPC
  • [ ] Test QuestValidator catches missing room
  • [ ] Test QuestValidator passes valid quest
  • [ ] Test QuestQualityFilter catches duplicate archetype
  • [ ] Test QuestQualityFilter catches NPC overuse
  • [ ] Test QuestQualityFilter catches 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 ConsequencePropagator class in packages/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 cycle
  • plan_mutation(self, consequence: QuestConsequence, character_id: UUID, outcome_scale: float, world: World) -> WorldMutation | None — sole public entry point
  • Maps each ConsequenceType to a WorldMutation via match statement, reading values from consequence.params:
    • NPC_GOAL_ADVANCE: delta=params["goal_progress"] * outcome_scale
    • NPC_GOAL_COMPLETE: marks NPC goal as complete
    • NPC_NEED_SATISFY: delta=params["need_delta"] * outcome_scale
    • RELATIONSHIP_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_scale
    • SOCIAL_INFLUENCE: delta=params["social_influence_delta"] * outcome_scale
    • GOSSIP_INJECTION: payload={"content": params["content"], "confidence": 0.9} — confidence capped at GOSSIP_CONFIDENCE_CAP
    • MEMORY_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 at CONSEQUENCE_SIGNAL_BUDGET
  • [ ] Register DocumentStore collections:
  • "quest_consequences": stores WorldMutationBatch documents
  • "quest_branch_choices"Deferred (branching removed from Phase 1)
  • [ ] Write unit tests in packages/maid-stdlib/tests/quests/generation/test_consequences.py:
  • [ ] Test plan_mutation returns WorldMutation for each ConsequenceType
  • [ ] Test plan_mutation returns None when target entity doesn't exist
  • [ ] Test GOSSIP_INJECTION confidence capped at 0.9
  • [ ] Test MEMORY_CREATION payload includes visibility: "public" tag
  • [ ] Test RELATIONSHIP_CHANGE applies outcome_scale multiplier
  • [ ] Test CONSEQUENCE_SIGNAL_BUDGET caps signals from detect_consequence_signals
  • [ ] Test OUTCOME_SCALE maps: 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) in packages/maid-stdlib/src/maid_stdlib/systems/quests/generation/consequence_system.py:
  • priority: ClassVar[int] = 240 — avoids collision with TradeSystem (210) and BankSystem (220)
  • _propagator: ConsequencePropagator
  • _pending_mutations: asyncio.Queue[WorldMutationBatch]
  • [ ] Implement async startup(self) -> None:
  • Subscribe to QuestTurnedInEvent — reads graded outcome from 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 consequences or failure_consequences based on outcome
  • Compute outcome_scale from ConsequencePropagator.OUTCOME_SCALE
  • Build WorldMutationBatch via propagator.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 resulting StorySignalEvents
  • [ ] Implement async update(self, delta: float) -> None:
  • Drain _pending_mutations queue
  • Apply each WorldMutationBatch atomically
  • Emit QuestChainEvent for 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 QuestTurnedInEvent reads graded outcome from event (not re-computed)
  • [ ] Test _propagate retrieves metadata from "generated_quests" side-table
  • [ ] Test _propagate skips hand-authored quests (no side-table entry)
  • [ ] Test WorldMutationBatch validation failure logs and returns without applying
  • [ ] Test QuestAbandonedEvent injects 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_register to check for chain potential:
  • After successful build, if outcome is COMPLETED or PERFECT, evaluate chain seed
  • Call ChainFollowupEvaluator to generate follow-up QuestSeed
  • 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 | None field to QuestHistoryEntry
  • 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_depth incremented correctly
  • [ ] Test chain dampening reduces importance
  • [ ] Test max chain depth prevents further chaining
  • [ ] Test QuestChainEvent emitted with correct fields

3.4 NPC Memory and Gossip Integration

Package: maid-stdlib | Priority: P2 | Dependencies: 3.1

  • [ ] Implement gossip injection in ConsequencePropagator:
  • GOSSIP_INJECTION creates 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_CREATION stores structured event in entity metadata
  • Format: {"event": str, "timestamp": datetime, "importance": float, "related_entities": list[UUID]}
  • If NPC memory system available (AutonomySystem from 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 PlayStyle enum (str, Enum): COMBAT, SOCIAL, EXPLORER, CRAFTER, BALANCED
  • [ ] Define PlayerProfile dataclass: 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 QuestDelivery dataclass: method (QuestDeliveryMethod), delivery_npc (UUID | None), delivery_room (UUID | None), delivery_text (str), discovery_hint (str | None)
  • [ ] Implement QuestPersonalizer class in packages/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 PlayerProfile construction from QuestHistory:
  • build_profile(self, character_id: UUID, history: QuestHistory) -> PlayerProfile
  • [ ] Implement DeliveryPlanner class in packages/maid-stdlib/src/maid_stdlib/systems/quests/generation/delivery.py:
  • plan(self, quest: GeneratedQuest, profile: PlayerProfile, world: World) -> QuestDelivery
  • Selects QuestDeliveryMethod based 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) in packages/maid-stdlib/src/maid_stdlib/systems/quests/generation/grounding.py:
  • priority: ClassVar[int] = 245 — avoids collision with TradeSystem (210) and BankSystem (220)
  • _check_interval: float = 60.0 seconds
  • _time_since_check: float = 0.0
  • [ ] Implement async update(self, delta: float) -> None:
  • Accumulate delta in _time_since_check
  • When _time_since_check >= _check_interval:
    • Iterate all active generated quests from QuestManager
    • For each quest, verify: giver_npc_id entity exists (via world.entities.get(id)), all objective target_ids exist, all destination_room_ids exist
    • If any reference invalid: call QuestManager.invalidate_quest(quest_id), emit QuestExpiredInternalEvent(quest_id, reason="grounding_failure")
    • Log warnings for invalidated quests
    • Reset _time_since_check
  • [ ] Subscribe to EntityDestroyedEvent (from packages/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 EntityDestroyedEvent triggers reactive invalidation
  • [ ] Test QuestExpiredInternalEvent emitted 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_pressure returns correct normalized values
  • [ ] Test spike detection activates on burst
  • [ ] Test budget_status reflects 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 compose detects regional crisis pattern
  • [ ] Test compose detects 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 a QuestSeed with importance=1.0, synthesizes minimal StorySignal context 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-generated returns formatted output
  • [ ] Test seed-status shows queue sizes
  • [ ] Test inspect shows full quest details
  • [ ] Test force-generate creates 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() in packages/maid-stdlib/src/maid_stdlib/pack.py:
  • Add QuestGenerationSystem to returned systems list
  • Add ConsequenceSystem to returned systems list
  • Add QuestGroundingMonitor to returned systems list
  • [ ] Update StdlibContentPack.get_events() to include new events:
  • QuestGeneratedEvent, QuestOfferedEvent, QuestChainEvent, QuestExpiredInternalEvent
  • [ ] Update StdlibContentPack.register_document_schemas() to register generation schemas via register_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.md with 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, GoalsComponent do 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 module generation/stubs.py with: StorySignal dataclass (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 stub GoalsComponent/NeedsComponent. Without this, no development or testing can proceed.
  • Design Doc 07 (NPC Needs): NeedsComponent does not exist yet. NpcNeedEvaluator uses the stub from generation/stubs.py. Same pattern as doc 03 — no blocker.
  • Design Doc 01 (Durable Persistence): DocumentStore uses register_schema(name, schema) (NOT register_collection or ensure_collection). Generated quest persistence depends on existing infrastructure. No blocker.
  • QuestManager event 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.
  • StdlibContentPack updates (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)