Skip to content

NPC Autonomy & Living World — Implementation Plan

Design Document: docs/designs/v3.1/07-npc-autonomy-living-world.md (v3.1) Priority: P1 — Core Experience Estimated Timeline: 10 weeks (4 phases)


Summary

This plan implements a Goal-Driven NPC Autonomy system that transforms NPCs from reactive dialogue endpoints into autonomous agents with needs, daily routines, social interactions, and story-generating behavior. NPCs pursue personal goals, follow schedules, gossip with purpose, and create emergent narrative through utility-based decision-making.

Key deliverables: - Need & desire model with mood calculation (NeedsComponent, NeedCategory, NeedDecaySystem) - Goal system with typed predicates (GoalsComponent, GoalGenerator, GoalPredicate protocol) - Daily schedules driven by GameTimeSystem (ScheduleComponent, ScheduleSystem) - Social fabric with NPC-to-NPC interactions and gossip as behavioral driver (SocialComponent, SocialFabricSystem) - Utility-based autonomous action selection with tick budget governor (AutonomySystem, UtilityScorer, ActionExecutor) - Story signal detection for downstream quest generation (StorySignalDetector, StorySignalSystem) - NPC archetype system with YAML definitions and inheritance (NPCArchetype, NPCPersonality) - Bark system for ambient NPC flavor without LLM calls (BarkTemplate, BarkLibrary) - Background NPC catch-up via analytic advancement - 14 new event types for autonomy lifecycle - Persistence via DocumentStore (npc_needs, npc_goals, npc_schedule_overrides, npc_demotions) - Off-tick LLM queue with circuit breaker for narrative generation - @debug_brain admin command for NPC behavior inspection

Cross-document dependencies: - Doc 03 (NPC Memory & Relationships): Memory data drives goal generation (reactive goals from episodic memories), gossip reaction processing uses relationship trust, social queries derive friends/rivals/allies from Doc 03 relationship state - Doc 01 (Durable Persistence): DocumentStore integration for needs, goals, schedule overrides, and demotion state; dirty-tracking via EntityPersistenceManager for save scheduling


Phase 0: Validation & Preparation (Week 0)

Validate integration points with existing systems before building autonomy infrastructure.

0.1 Verify Existing System Integration Points

Package: maid-stdlib | Priority: P0 | Dependencies: None

  • [ ] Verify GameTimeSystem in packages/maid-classic-rpg/src/maid_classic_rpg/systems/world/time.py emits TimeChangeEvent with current_time: GameTime field (extract hour via event.current_time.hour)
  • [ ] Verify WeatherSystem in packages/maid-classic-rpg/src/maid_classic_rpg/systems/world/weather.py emits WeatherChangeEvent
  • [ ] Verify BehaviorSystem in packages/maid-classic-rpg/src/maid_classic_rpg/systems/npc/behavior.py at priority=100 handles combat states and will coexist with AutonomySystem at priority=115
  • [ ] Verify GridManager.find_path() in packages/maid-engine/src/maid_engine/core/grid.py (line 1672; class at line 621) is synchronous and can be called from NPC movement handler
  • [ ] Verify DocumentStore ABC in packages/maid-engine/src/maid_engine/storage/document_store.py (line 198) uses collection-based async API: register_schema(name, Model), get_collection(name), then await collection.get(doc_id) / await collection.create(doc_id, model) / await collection.update(doc_id, model) — documents are Pydantic models keyed by UUID, not raw dicts
  • [ ] Verify CircuitBreaker in packages/maid-engine/src/maid_engine/ai/circuit_breaker.py (line 53) can be wrapped for LLM autonomy calls
  • [ ] Verify NPCComponent in packages/maid-stdlib/src/maid_stdlib/components/core.py (line 408) can coexist with new NeedsComponent and GoalsComponent
  • [ ] Verify Component base class in packages/maid-engine/src/maid_engine/core/ecs/component.py (line 10) inherits from Pydantic BaseModel with validate_assignment=True, use_enum_values=True, extra="forbid" — all Component subclasses must use Pydantic field declarations (e.g., Field(default_factory=dict)), NOT dataclass-style mutable defaults; use_enum_values=True means enum fields store string values
  • [ ] Verify EntityManager.with_components() in packages/maid-engine/src/maid_engine/core/ecs/entity.py (line 278) returns Iterator[Entity], not Iterator[uuid.UUID] — iteration pattern is for entity in self.entities.with_components(X):
  • [ ] Verify World.grid (not World.grid_manager) returns GridManager instance; world.move_entity() is synchronous (not async)
  • [ ] Verify System ABC in packages/maid-engine/src/maid_engine/core/ecs/system.py (line 29) supports startup() for event subscription
  • [ ] Document any interface mismatches and required adjustments

0.2 System Priority Allocation Validation

Package: maid-stdlib | Priority: P0 | Dependencies: 0.1

  • [ ] Validate no priority conflicts with existing systems:
  • Existing: time=5, weather=10, world_events=20, area_reset=30, combat_body=45, combat_base/melee/dialogue/spawner=50, combat_ranged=51, skills=55, damage=55, effects=60, behavior=100, auction=150, guild=150, shop=200, trade=210, bank=220
  • New: ScheduleSystem=105, NeedDecaySystem=110, AutonomySystem=115, SocialFabricSystem=120, StorySignalSystem=130
  • [ ] Confirm new priorities fit between behavior=100 and auction/guild=150 without collision
  • [ ] Verify combat systems at priority ≤100 run before autonomy systems, ensuring combat takes precedence

Phase 1: Core Autonomy Foundation (Weeks 1–4)

1.1 Autonomy Data Models and Enums

Package: maid-stdlib | Priority: P0 | Dependencies: None

  • [ ] Create packages/maid-stdlib/src/maid_stdlib/models/npc/autonomy.py with:
  • [ ] NeedCategory(str, Enum) with values: SURVIVAL, ECONOMIC, PURPOSE, COMFORT (defer SOCIAL and AMBITION to v2 — archetype weights default to 0 for unused categories)
  • [ ] Need model (Pydantic, not dataclass — Component base is Pydantic) with fields: category: NeedCategory, value: float, decay_rate: float, last_satisfied: float (use GameTime for timestamps or float for elapsed durations — do not mix with raw tick counts)
  • [ ] NeedsComponent(Component) with fields: needs: dict[NeedCategory, Need] = Field(default_factory=dict), personality_weights: dict[NeedCategory, float] = Field(default_factory=dict), mood: float = 0.5, stress: float = 0.0 — use Field(default_factory=...) for mutable defaults
  • [ ] GoalCategory(str, Enum) with values: ACQUIRE, CRAFT, SOCIAL, PROTECT, EXPLORE, REVENGE, AMBITION, DUTY
  • [ ] GoalSource(str, Enum) with values: INNATE, DERIVED, REACTIVE, SOCIAL, QUEST
  • [ ] GoalPredicate(Protocol) with methods: evaluate(world: World, npc_id: uuid.UUID) -> bool, progress_estimate(world: World, npc_id: uuid.UUID) -> float
  • [ ] Built-in predicates: HasItemPredicate, RelationshipThresholdPredicate, LocationPredicate, CurrencyThresholdPredicate — each frozen dataclass implementing GoalPredicate
  • [ ] GoalCondition dataclass with fields: predicate: GoalPredicate, description: str
  • [ ] Goal dataclass with fields: id: uuid.UUID, category: GoalCategory, description: str, priority: float, progress: float, conditions: list[GoalCondition], deadline: int | None, created_at: int, source: GoalSource, target: uuid.UUID | str | None, required_resources: dict[str, int], preferred_helpers: list[uuid.UUID], blockers: list[str]
  • [ ] GoalsComponent(Component) with fields: active_goals: list[Goal] = Field(default_factory=list) (max 5), completed_goals: list[uuid.UUID] = Field(default_factory=list) (last 20), failed_goals: list[uuid.UUID] = Field(default_factory=list), goal_generation_cooldown: float = 0.0
  • [ ] ActivityType(str, Enum) with values: WORK, SLEEP, EAT, SOCIALIZE, PATROL, TRADE, CRAFT, WORSHIP, TRAIN, WANDER, CUSTOM
  • [ ] ScheduleCondition dataclass with fields: category: str, operator: str, value: str
  • [ ] ScheduleBlock dataclass with fields: start_hour: int, end_hour: int, activity: ActivityType, location: str | uuid.UUID, priority: float, conditions: list[ScheduleCondition]
  • [ ] ScheduleComponent(Component) with fields: blocks: list[ScheduleBlock] = Field(default_factory=list), current_activity: ActivityType | None = None, schedule_adherence: float = 0.8, override_until: float | None = None, override_reason: str | None = None
  • [ ] ActionType(str, Enum) — v1 core set: MOVE_TO, CRAFT_ITEM, SELL_ITEM, REST, EAT, SOCIALIZE, GOSSIP, PATROL, GUARD, INVESTIGATE (remaining types BUY_ITEM, SEEK_HELP, OFFER_HELP, FLEE, CONFRONT, COMPLAIN, PRAY, TRAIN, SCHEME, CELEBRATE deferred to v2 — ActionExecutor returns None for unhandled types, falling back to idle)
  • [ ] ActionState(str, Enum) with values: PENDING, RUNNING, COMPLETE, FAILED, INTERRUPTED
  • [ ] ActionCost frozen dataclass with fields: time_cost: float, risk: float, currency: int = 0, stamina: float = 0.0
  • [ ] Action dataclass with fields: action_type: ActionType, target: uuid.UUID | str | None, cost: ActionCost, prerequisites: list[GoalPredicate], need_effects: dict[NeedCategory, float], context: dict[str, Any]; methods: advances_goal(goal: Goal) -> bool, aligns_with(activity: ActivityType) -> bool
  • [ ] ScoredAction dataclass with fields: action: Action, utility: float, score_breakdown: dict[str, float] | None = None
  • [ ] ActiveAction class with fields: scored_action: ScoredAction, state: ActionState, started_at: int | None, ticks_elapsed: int; method: is_terminal() -> bool
  • [ ] NavigationIntent(Component) with fields: destination: uuid.UUID, reason: str
  • [ ] SocialIntent(Component) with fields: target_id: uuid.UUID, interaction_type: str, gossip_to_share: uuid.UUID | None = None
  • [ ] Write unit tests in packages/maid-stdlib/tests/test_autonomy_models.py:
  • [ ] test_need_category_values — verify all enum values
  • [ ] test_needs_component_defaults — verify mood=0.5, stress=0.0
  • [ ] test_goal_max_active — verify enforcement of max 5 active goals
  • [ ] test_schedule_block_overnight_wrap — verify end_hour < start_hour semantics
  • [ ] test_action_advances_goal — verify goal advancement detection
  • [ ] test_action_aligns_with_activity — verify schedule alignment
  • [ ] test_active_action_terminal_states — verify is_terminal() for COMPLETE/FAILED/INTERRUPTED
  • [ ] test_scored_action_separation — verify score is not stored on Action
  • [ ] test_goal_predicate_protocol — verify HasItemPredicate implements GoalPredicate
  • [ ] test_has_item_predicate_evaluate — verify inventory check
  • [ ] test_relationship_threshold_predicate — verify relationship dimension check
  • [ ] test_location_predicate — verify room matching
  • [ ] test_currency_threshold_predicate — verify gold amount check
  • [ ] test_navigation_intent_component — verify destination and reason fields
  • [ ] test_social_intent_component — verify target_id and interaction_type fields

1.2 NPC Archetype System and YAML Loading

Package: maid-stdlib (registry) + maid-classic-rpg (archetype definitions and YAML data) | Priority: P0 | Dependencies: 1.1

  • [ ] Create packages/maid-classic-rpg/src/maid_classic_rpg/models/npc/archetypes.py with:
  • [ ] NPCPersonality Pydantic model with fields: bravery: float = 0.5, greed: float = 0.5, sociability: float = 0.5, diligence: float = 0.5, ambition: float = 0.5 (defer curiosity, loyalty, temperament to v2 — these add little observable difference in a text MUD)
  • [ ] GoalTemplate dataclass with fields: category: GoalCategory, description: str, priority: float
  • [ ] NPCArchetype dataclass with fields: archetype_id: str, display_name: str, parent: str | None, need_weights: dict[NeedCategory, float], need_decay_rates: dict[NeedCategory, float], default_schedule: list[ScheduleBlock], innate_goals: list[GoalTemplate], forbidden_goals: list[GoalCategory], required_location_tags: list[str], max_need_delta_per_tick: float = 0.15, personality: NPCPersonality, preferred_interaction_types: list[str], gossip_tendency: float, social_initiative: float
  • [ ] Create packages/maid-stdlib/src/maid_stdlib/models/npc/archetypes.py with:
  • [ ] ArchetypeRegistry class with methods: register(archetype: NPCArchetype) -> None, get(archetype_id: str) -> NPCArchetype | None, resolve_inheritance(archetype: NPCArchetype) -> NPCArchetype, load_from_yaml(path: Path) -> list[NPCArchetype]
  • [ ] Inheritance resolution: child inherits all parent fields, overrides specified fields only
  • [ ] Create packages/maid-classic-rpg/data/npcs/archetypes.yaml with archetypes:
  • [ ] tradesperson base archetype (not instantiated directly)
  • [ ] blacksmith (parent: tradesperson)
  • [ ] merchant (parent: tradesperson)
  • [ ] guard archetype
  • [ ] innkeeper (parent: tradesperson)
  • [ ] farmer archetype
  • [ ] Write unit tests in packages/maid-stdlib/tests/test_archetypes.py:
  • [ ] test_archetype_yaml_loading — verify YAML parsing produces valid archetypes
  • [ ] test_archetype_inheritance — verify child inherits and overrides parent fields
  • [ ] test_archetype_registry_get — verify lookup by archetype_id
  • [ ] test_archetype_circular_inheritance — verify detection and error
  • [ ] test_archetype_personality_defaults — verify 0.5 defaults
  • [ ] test_archetype_schedule_parsing — verify schedule blocks from YAML
  • [ ] test_archetype_innate_goals — verify GoalTemplate list parsed
  • [ ] test_archetype_forbidden_goals — verify forbidden category list
  • [ ] test_archetype_need_weights_sum — verify weights are non-negative floats
  • [ ] test_archetype_memory_sharing — verify shared data across NPCs of same archetype

1.2a Spawner Integration for Autonomy Components

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

  • [ ] Modify SpawnerSystem or create a post-spawn hook to attach NeedsComponent, GoalsComponent, and ScheduleComponent based on the NPC's archetype when an NPC entity is spawned
  • [ ] Look up the NPC's archetype from ArchetypeRegistry, populate need weights/decay rates, default schedule, and innate goals from the archetype definition
  • [ ] If no archetype is specified, use sensible defaults (neutral needs, empty schedule, no goals)
  • [ ] Write unit tests:
  • [ ] test_spawned_npc_has_autonomy_components — verify NPC gets NeedsComponent, GoalsComponent, ScheduleComponent on spawn
  • [ ] test_spawned_npc_archetype_applied — verify archetype's need_weights and schedule applied
  • [ ] test_spawned_npc_no_archetype_defaults — verify fallback defaults when no archetype

1.2b Doc 03 Null Stubs

Package: maid-stdlib | Priority: P0 | Dependencies: None

Doc 03 (NPC Memory & Relationships) is designed but not implemented. These null-object stubs allow the autonomy system to compile, run, and test without Doc 03. When Doc 03 lands, these stubs are replaced by real implementations.

  • [ ] Create packages/maid-stdlib/src/maid_stdlib/systems/npc/stubs.py with:
  • [ ] NullMemoryProvider class — get_memories(npc_id) -> list returns [], get_important_memories(npc_id, threshold) -> list returns []
  • [ ] NullRelationshipProvider class — get_trust(a, b) -> float returns 0.5 (neutral), get_respect(a, b) -> float returns 0.5, get_friends(npc_id) -> list[uuid.UUID] returns [], get_rivals(npc_id) -> list[uuid.UUID] returns []
  • [ ] MemoryProvider(Protocol) and RelationshipProvider(Protocol) — interfaces that Doc 03 implementations will satisfy
  • [ ] Wire null providers as defaults in SocialFabricSystem, GoalGenerator, and GossipReactionProcessor; Doc 03 implementations replace them via dependency injection
  • [ ] Write unit tests:
  • [ ] test_null_memory_provider_returns_empty — verify empty list
  • [ ] test_null_relationship_provider_neutral — verify 0.5 trust/respect
  • [ ] test_autonomy_runs_without_doc03 — verify full tick cycle with null providers

1.3 Mood Calculation and Need Decay

Package: maid-stdlib | Priority: P0 | Dependencies: 1.1

  • [ ] Create packages/maid-stdlib/src/maid_stdlib/systems/npc/needs.py with:
  • [ ] calculate_mood(needs: NeedsComponent, recent_memories: list) -> float — weighted average of need satisfaction, ±20% memory modifier, clamped to [0.0, 1.0]
  • [ ] NeedDecaySystem(System) with priority: ClassVar[int] = 110:
    • [ ] async update(self, delta: float) -> None — for each NPC with NeedsComponent: decay needs by decay_rate * elapsed_game_hours, recalculate mood, emit NPCMoodChangedEvent on significant change (>0.1 delta)
    • [ ] Background NPCs: advance needs toward schedule-predicted baseline
  • [ ] clamp(value: float, min_val: float, max_val: float) -> float utility
  • [ ] Write unit tests in packages/maid-stdlib/tests/test_need_decay.py:
  • [ ] test_need_decay_over_time — verify decay reduces need value
  • [ ] test_mood_calculation_weighted_average — verify personality_weights affect mood
  • [ ] test_mood_clamped_to_bounds — verify mood stays in [0.0, 1.0]
  • [ ] test_mood_change_event_emitted — verify NPCMoodChangedEvent on significant shift
  • [ ] test_need_value_floor — verify need.value never drops below 0.0
  • [ ] test_mood_with_no_needs — verify default mood 0.5 when no needs configured
  • [ ] test_stress_accumulation — verify stress increases from unmet needs
  • [ ] test_max_need_delta_capped — verify archetype max_need_delta_per_tick prevents grief exploitation
  • [ ] test_background_npc_baseline_advance — verify background NPCs advance toward schedule baseline
  • [ ] test_mood_memory_modifier — verify recent memories shift mood by ±20%

1.4 Schedule System

Package: maid-stdlib | Priority: P0 | Dependencies: 1.1

  • [ ] Create packages/maid-stdlib/src/maid_stdlib/systems/npc/schedule.py with:
  • [ ] ScheduleSystem(System) with priority: ClassVar[int] = 105:
    • [ ] async startup(self) -> None — subscribe to TimeChangeEvent and WeatherChangeEvent
    • [ ] async _on_time_changed(self, event: TimeChangeEvent) -> None — extract hour via event.current_time.hour; for each NPC with ScheduleComponent: find applicable block, check conditions, compare schedule priority vs urgent need priority, emit NPCActivityChangedEvent on transition
    • [ ] _get_block_for_hour(schedule: ScheduleComponent, hour: int) -> ScheduleBlock | None — overnight-aware matching (end < start wraps across midnight); on gap (no matching block) default to WANDER; on overlap (multiple matching blocks) use highest priority field
    • [ ] _check_conditions(block: ScheduleBlock, weather: str, season: str) -> bool — evaluate typed ScheduleCondition list
    • [ ] async _transition_activity(entity_id: uuid.UUID, schedule: ScheduleComponent, new_block: ScheduleBlock) -> None — update current_activity, set NavigationIntent for relocation
  • [ ] Write unit tests in packages/maid-stdlib/tests/test_schedule_system.py:
  • [ ] test_schedule_block_matching — verify correct block for given hour
  • [ ] test_overnight_block_matching — verify block with end < start (e.g., sleep 20:00–06:00)
  • [ ] test_weather_condition_check — verify weather-based schedule overrides
  • [ ] test_activity_change_event — verify NPCActivityChangedEvent emission
  • [ ] test_schedule_override — verify override_until bypasses normal schedule
  • [ ] test_urgent_need_overrides_schedule — verify low-priority schedule yields to urgent need
  • [ ] test_schedule_navigation_intent — verify NavigationIntent set for relocation
  • [ ] test_schedule_no_transition_same_block — verify no event when already in correct block
  • [ ] test_schedule_multiple_conditions — verify AND semantics for condition list
  • [ ] test_season_condition_check — verify season-based schedule overrides
  • [ ] test_schedule_gap_default_wander — verify no matching block defaults to WANDER
  • [ ] test_schedule_overlap_highest_priority — verify highest-priority block wins on overlap

1.5 Utility Scorer and Action Selection

Package: maid-stdlib | Priority: P0 | Dependencies: 1.1, 1.3

  • [ ] Create packages/maid-stdlib/src/maid_stdlib/systems/npc/scoring.py with:
  • [ ] NPCDecisionContext frozen dataclass — snapshot of NPC state (only taken when NPC needs a new decision, not when RUNNING); fields: npc_id, needs, personality_weights, active_goals, current_activity, schedule_adherence, current_room, personality, current_action, in_combat, mood, stress; static method: snapshot(entity: Entity) -> NPCDecisionContext; method: personality_modifier(action_type: ActionType) -> float
  • [ ] WorldDecisionContext frozen dataclass with fields: current_hour, current_weather, current_season, nearby_entities, nearby_threats, available_resources
  • [ ] UtilityScorer class with method: score(action: Action, npc: NPCDecisionContext, world: WorldDecisionContext) -> ScoredAction — need satisfaction weighted by urgency and personality_weights, goal advancement bonus, schedule alignment bonus, social relationship modifiers, personality modifiers, cost penalties; filters negative scores at selection time
  • [ ] Write unit tests in packages/maid-stdlib/tests/test_utility_scorer.py:
  • [ ] test_score_need_satisfaction — verify higher urgency = higher score
  • [ ] test_score_goal_advancement — verify goal-advancing actions score higher
  • [ ] test_score_schedule_alignment — verify schedule-aligned actions get bonus
  • [ ] test_score_personality_modifier — verify personality affects score
  • [ ] test_score_cost_penalty — verify time/risk costs reduce score
  • [ ] test_score_deterministic — verify same inputs produce same score
  • [ ] test_decision_context_snapshot — verify snapshot is immutable copy
  • [ ] test_score_rival_interaction_bonus — verify slight preference for acting against rivals
  • [ ] test_negative_scores_filtered — verify negative utility actions excluded from selection

1.6 Action Executor and Intent Components

Package: maid-stdlib | Priority: P0 | Dependencies: 1.5

  • [ ] Create packages/maid-stdlib/src/maid_stdlib/systems/npc/executor.py with:
  • [ ] ActionResult dataclass with fields: success: bool, partial: bool = False, fallback: str | None = None
  • [ ] ActionExecutor class with method: async execute(npc_id: uuid.UUID, scored_action: ScoredAction, world: World) -> ActionResult — match on ActionType, set appropriate intent components (NavigationIntent, SocialIntent), emit IntentCancelledEvent before overwriting existing intents, update ActiveAction.state on completion/failure, emit ActionCompletedEvent/ActionFailedEvent, return None for unhandled action types (falls back to idle)
  • [ ] NPCMovementHandler as a registered System (priority=116, after AutonomySystem) with async update(self, delta: float) — queries all entities with NavigationIntent component, moves one step per tick via world.grid.find_path() (synchronous call), calls world.move_entity() (synchronous), removes NavigationIntent on arrival or failure
  • [ ] MovementResult dataclass with fields: success: bool, partial: bool = False, reason: str | None = None, remaining_path: list[uuid.UUID] | None = None
  • [ ] Write unit tests in packages/maid-stdlib/tests/test_action_executor.py:
  • [ ] test_execute_move_to — verify NavigationIntent is set
  • [ ] test_execute_craft_item — verify NPCCraftRequestEvent emitted
  • [ ] test_execute_socialize — verify SocialIntent is set
  • [ ] test_intent_cancelled_event — verify IntentCancelledEvent before overwrite
  • [ ] test_execute_fallback_on_failure — verify idle fallback on error
  • [ ] test_movement_handler_pathfinding — verify GridManager.find_path usage
  • [ ] test_movement_one_step_per_tick — verify NPCs don't teleport, move one room per tick
  • [ ] test_execute_rest_action — verify rest satisfies COMFORT need
  • [ ] test_execute_gossip_action — verify SocialIntent with gossip_to_share
  • [ ] test_action_state_lifecycle — verify PENDING → RUNNING → COMPLETE transition
  • [ ] test_action_interrupted_by_combat — verify INTERRUPTED state on combat entry

1.7 Autonomy System with Tick Budget Governor

Package: maid-stdlib | Priority: P0 | Dependencies: 1.3, 1.4, 1.5, 1.6

  • [ ] Create packages/maid-stdlib/src/maid_stdlib/systems/npc/autonomy.py with:
  • [ ] TickBudgetGovernor class with constants: SOFT_CEILING_MS = 4.0, HARD_CEILING_MS = 5.0, RECOVERY_TICKS = 10; methods: should_process(npc_id: uuid.UUID, elapsed_ms: float) -> bool, end_tick(elapsed_ms: float) -> None; fields: _dynamic_demotions: set[uuid.UUID], _ticks_under_budget: int
  • [ ] AutonomySystem(System) with priority: ClassVar[int] = 115, MAX_FAILURES = 3, PER_NPC_TIMEOUT = 0.002:
    • [ ] async startup(self) -> None — restore persisted demotion state and failure counts from DocumentStore collection npc_demotions (via registered Pydantic schema and await collection.get(doc_id))
    • [ ] async update(self, delta: float) -> None — tick budget loop: for each Active NPC (for entity in self.entities.with_components(NeedsComponent, GoalsComponent):), check TickBudgetGovernor.should_process(), skip demoted/deleted entities, build NPCDecisionContext.snapshot() (only when NPC needs new decision) and WorldDecisionContext, check ActiveAction lifecycle, generate candidates, score via UtilityScorer, select highest utility, execute via ActionExecutor, emit NPCActionEvent; process Nearby NPCs every 10th tick; process Background NPCs every 60th tick; call TickBudgetGovernor.end_tick()
    • [ ] Per-NPC error isolation with asyncio.wait_for 2ms timeout; failure counter; demotion to Background after 3 consecutive failures
    • [ ] Archetype-level circuit breaker: ARCHETYPE_FAILURE_THRESHOLD = 3; emit ArchetypeCircuitBreakerEvent when threshold exceeded
    • [ ] async _persist_demotions(self) -> None — persist to DocumentStore, wrapped in try/except
    • [ ] Entity deletion guard: skip deleted entities without counting as failure
    • [ ] Demotion hysteresis: 3 consecutive ticks outside Active range before demotion
    • [ ] Catch-up budget: max 5 NPCs/tick caught up from Background → Active
  • [ ] Tier assignment via room-based proximity and grid distance checks (using existing RoomIndex and world.grid.find_path()): incremental re-evaluation as players move; highest tier wins for multi-player proximity. AOI-specific optimization deferred until profiling shows tier-assignment bottlenecks
  • [ ] Write unit tests in packages/maid-stdlib/tests/test_autonomy_system.py:
  • [ ] test_tick_budget_soft_ceiling — verify demotion starts at 4ms
  • [ ] test_tick_budget_hard_ceiling — verify processing stops at 5ms
  • [ ] test_tick_budget_recovery — verify demoted NPCs restored after 10 ticks under budget
  • [ ] test_npc_error_demotion — verify demotion after 3 consecutive failures
  • [ ] test_archetype_circuit_breaker — verify archetype-level demotion
  • [ ] test_demotion_persistence — verify demotion state persisted to DocumentStore
  • [ ] test_deleted_entity_skip — verify deleted entities silently skipped
  • [ ] test_tier_assignment_highest_wins — verify multi-player proximity uses highest tier
  • [ ] test_demotion_hysteresis — verify 3-tick delay before Active→Nearby demotion
  • [ ] test_catchup_budget_limit — verify max 5 NPCs caught up per tick
  • [ ] test_tier_assignment_incremental — verify only changed rooms re-evaluated
  • [ ] test_combat_npc_skipped — verify NPCs in combat deferred to BehaviorSystem
  • [ ] test_action_selection_with_noise — verify slight randomization for variety

1.8 Bark System

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

  • [ ] Create framework code in packages/maid-stdlib/src/maid_stdlib/systems/npc/barks.py with:
  • [ ] BarkTemplate dataclass with fields: text: str, trigger_need: NeedCategory | None, need_threshold: float = 0.3, mood_range: tuple[float, float] = (0.0, 1.0), cooldown_minutes: float = 15.0
  • [ ] BarkLibrary class with method: select_bark(needs: NeedsComponent, mood: float, archetype_id: str) -> str | None — match by need threshold and mood range, enforce cooldown
  • [ ] load_barks_from_yaml(path: Path) -> dict[str, list[BarkTemplate]]
  • [ ] BarkSystem(System) with priority: ClassVar[int] = 125 (after AutonomySystem): queries NPCs with NeedsComponent in rooms containing players, calls BarkLibrary.select_bark(), emits bark text to players in the room via EventBus
  • [ ] Create content data in packages/maid-classic-rpg/data/npcs/barks.yaml with bark templates for blacksmith, merchant, guard, innkeeper, farmer
  • [ ] Write unit tests in packages/maid-stdlib/tests/test_barks.py:
  • [ ] test_bark_selection_by_need — verify trigger_need matching
  • [ ] test_bark_selection_by_mood — verify mood_range filtering
  • [ ] test_bark_cooldown — verify cooldown prevents repeated barks
  • [ ] test_bark_yaml_loading — verify YAML parsing
  • [ ] test_bark_no_match — verify None returned when no bark matches
  • [ ] test_bark_system_emits_to_players — verify barks displayed to players in the same room

1.9 Autonomy Events

Package: maid-stdlib | Priority: P0 | Dependencies: None

  • [ ] Add to packages/maid-stdlib/src/maid_stdlib/events/npc_autonomy.py:
  • [ ] NPCActivityChangedEvent(Event) with fields: npc_id: uuid.UUID, previous_activity: ActivityType | None, new_activity: ActivityType, location: uuid.UUID
  • [ ] NPCGoalCreatedEvent(Event) with fields: npc_id: uuid.UUID, goal_id: uuid.UUID, goal_category: GoalCategory
  • [ ] NPCGoalCompletedEvent(Event) with fields: npc_id: uuid.UUID, goal_id: uuid.UUID, goal_category: GoalCategory
  • [ ] NPCGoalFailedEvent(Event) with fields: npc_id: uuid.UUID, goal_id: uuid.UUID, goal_category: GoalCategory, reason: str
  • [ ] NPCSocialInteractionEvent(Event) with fields: initiator_id: uuid.UUID, target_id: uuid.UUID, interaction_type: str, outcome_type: str, player_witnessed: bool
  • [ ] StorySignalEvent(Event) with fields: signal_id: uuid.UUID, signal_type: str, importance: float, involved_npc_ids: list[uuid.UUID]
  • [ ] NPCMoodChangedEvent(Event) with fields: npc_id: uuid.UUID, previous_mood: float, new_mood: float, cause: str
  • [ ] ArchetypeCircuitBreakerEvent(Event) with fields: archetype_id: str, failed_npc_count: int
  • [ ] IntentCancelledEvent(Event) with fields: npc_id: uuid.UUID, cancelled_intent_type: str, reason: str
  • [ ] NarrativeReadyEvent(Event) with fields: interaction_id: uuid.UUID, narrative_text: str
  • [ ] TickBudgetExceededEvent(Event) with fields: elapsed_ms: float, npcs_processed: int, npcs_skipped: int
  • [ ] ActionCompletedEvent(Event) with fields: npc_id: uuid.UUID, action_type: ActionType, ticks_elapsed: int
  • [ ] ActionFailedEvent(Event) with fields: npc_id: uuid.UUID, action_type: ActionType, reason: str
  • [ ] NPCActionEvent(Event) with fields: npc_id: uuid.UUID, action_type: ActionType, target: uuid.UUID | str | None
  • [ ] Update packages/maid-stdlib/src/maid_stdlib/events/__init__.py to include autonomy events in get_stdlib_events()
  • [ ] Write unit tests in packages/maid-stdlib/tests/test_autonomy_events.py:
  • [ ] test_all_events_inherit_from_event — verify Event base class
  • [ ] test_event_field_types — verify field annotations match design

1.10 Content Pack Registration

Package: maid-stdlib (systems/events) + maid-classic-rpg (content data loading) | Priority: P0 | Dependencies: 1.2, 1.7, 1.8, 1.9

  • [ ] Update packages/maid-stdlib/src/maid_stdlib/pack.py StdlibContentPack:
  • [ ] Add ScheduleSystem(world) (priority=105), NeedDecaySystem(world) (priority=110), AutonomySystem(world) (priority=115), NPCMovementHandler(world) (priority=116), BarkSystem(world) (priority=125) to get_systems() return list
  • [ ] Add all 14 autonomy events to get_events() return list (register all events up front even if some aren't emitted until Phase 2–3 — missing event registration causes silent failures)
  • [ ] Update packages/maid-classic-rpg/src/maid_classic_rpg/pack.py ClassicRPGContentPack:
  • [ ] During on_load(), call ArchetypeRegistry.load_from_yaml() using packages/maid-classic-rpg/data/npcs/archetypes.yaml, then register loaded archetypes
  • [ ] During on_load(), load bark templates from packages/maid-classic-rpg/data/npcs/barks.yaml and wire them into the stdlib bark framework (BarkLibrary/BarkSystem)
  • [ ] Update packages/maid-stdlib/src/maid_stdlib/systems/__init__.py get_stdlib_systems() to include all new systems (this is the actual registration path, not only pack.py)

Phase 2: Social Fabric (Weeks 5–6)

2.1 Social Component and Interaction Types

Package: maid-stdlib | Priority: P0 | Dependencies: 1.1

The social layer extends Doc 03's gossip mechanism into a full NPC-to-NPC interaction system. NPCs don't just pass data — they have conversations, form opinions, make deals, and scheme. Friends, rivals, and allies are derived from Doc 03 relationship data at query time (not duplicated in SocialComponent).

  • [ ] Add to packages/maid-stdlib/src/maid_stdlib/models/npc/autonomy.py:
  • [ ] SocialInteractionType(str, Enum) with values: GOSSIP, TRADE, CONVERSATION, ARGUMENT, FAVOR, INTIMIDATION, SCHEMING, MENTORING, COURTSHIP
  • [ ] SocialComponent(Component) with fields: faction_standing: dict[str, float] = Field(default_factory=dict), social_influence: float = 0.5, last_social_interaction: float | None = None, social_cooldown: float = 0.0, interaction_reservation: uuid.UUID | None = None, reservation_expires: float | None = None
  • [ ] GossipIntentType(str, Enum) with values: WARN, DEFAME, BOAST, BOND, MANIPULATE, INFORM, SEEK_HELP
  • [ ] GossipIntent dataclass with fields: intent_type: GossipIntentType, target_npc_id: uuid.UUID | None, desired_outcome: str, source_event_id: uuid.UUID | None = None
  • [ ] BehaviorModification dataclass with fields: type: str, target_need: NeedCategory | None = None, amount: float = 0.0, goal: Goal | None = None, target_id: uuid.UUID | None = None, dimension: str | None = None, delta: float = 0.0
  • [ ] InteractionOutcome dataclass with fields: success: bool, modifications: list[BehaviorModification], narrative: str | None = None

2.2 Social Fabric System

Package: maid-stdlib | Priority: P0 | Dependencies: 2.1

  • [ ] Create packages/maid-stdlib/src/maid_stdlib/systems/npc/social.py with:
  • [ ] SocialFabricSystem(System) with priority: ClassVar[int] = 120:
    • [ ] Relationship caches: _friends_cache, _rivals_cache, _allies_cache — invalidated on relationship-change events (not per-tick); lazily populated from Doc 03 relationship data via RelationshipProvider (uses NullRelationshipProvider stub until Doc 03 lands)
    • [ ] async update(self, delta: float) -> None — sweep expired reservations, process pending social intents
    • [ ] get_friends(npc_id: uuid.UUID) -> list[uuid.UUID] — trust > 0.7 from Doc 03
    • [ ] get_rivals(npc_id: uuid.UUID) -> list[uuid.UUID] — trust < 0.3 and respect < 0.3
    • [ ] get_allies(npc_id: uuid.UUID) -> list[uuid.UUID] — loyalty > 0.6
    • [ ] reserve_interaction(initiator_id: uuid.UUID, target_id: uuid.UUID, duration: float) -> bool — prevent chase behavior, returns False if already reserved
  • [ ] Write unit tests in packages/maid-stdlib/tests/test_social_fabric.py:
  • [ ] test_cache_invalidation — verify caches cleared each tick
  • [ ] test_get_friends_threshold — verify trust > 0.7 filter
  • [ ] test_get_rivals_threshold — verify trust < 0.3 and respect < 0.3 filter
  • [ ] test_reservation_prevents_double_booking — verify second reserve returns False
  • [ ] test_reservation_expiry — verify expired reservations swept

2.3 Gossip Reaction Processing

Package: maid-stdlib | Priority: P1 | Dependencies: 2.1, Doc 03

Blocked on Doc 03. GossipReactionProcessor consumes GossipMessage objects from Doc 03 which do not exist yet. For v1, stub the processor with a protocol interface. Implement the full processor when Doc 03 lands.

  • [ ] Create packages/maid-stdlib/src/maid_stdlib/systems/npc/gossip.py with:
  • [ ] GossipReactionProcessor protocol/interface with method signature: async process_gossip_reaction(listener, gossip, intent) -> list[BehaviorModification]
  • [ ] NullGossipProcessor stub implementation that returns []
  • [ ] Constants: DEPTH_DAMPENING = 0.7, MAX_PROPAGATION_DEPTH = 5 (for future implementation)
  • [ ] Write unit tests in packages/maid-stdlib/tests/test_gossip_reactions.py:
  • [ ] test_null_gossip_processor_returns_empty — verify stub returns empty list
  • [ ] test_gossip_processor_protocol — verify protocol interface is correct

2.4 Social Interaction Resolver

Package: maid-stdlib | Priority: P0 | Dependencies: 2.2, 2.3

  • [ ] Add to packages/maid-stdlib/src/maid_stdlib/systems/npc/social.py:
  • [ ] SocialInteractionResolver class:
    • [ ] async resolve(initiator: NPCContext, target: NPCContext, interaction_type: SocialInteractionType, player_witnessed: bool) -> InteractionOutcome — deterministic rules for TRADE, GOSSIP, ARGUMENT, etc.; LLM narrative generation deferred to Phase 4 (§4.2)
    • [ ] Deterministic resolution only in Phase 2 — no LLM calls; narrative field set to None for unwitnessed interactions
  • [ ] Write unit tests in packages/maid-stdlib/tests/test_social_resolver.py:
  • [ ] test_resolve_trade — verify deterministic trade outcome
  • [ ] test_resolve_gossip — verify gossip reaction processing invoked (stub in v1)
  • [ ] test_deterministic_without_llm — verify all interactions use deterministic rules only (LLM deferred to Phase 4)
  • [ ] test_resolve_argument — verify argument outcome modifies relationship
  • [ ] test_resolve_favor — verify favor affects trust dimension
  • [ ] test_deterministic_without_llm — verify off-screen interactions use rules only

2.5 Content Pack Social Registration

Package: maid-stdlib | Priority: P0 | Dependencies: 2.2

  • [ ] Update packages/maid-stdlib/src/maid_stdlib/pack.py:
  • [ ] Add SocialFabricSystem(world) (priority=120) to get_systems() return list

Phase 3: Story Signals & Goal Lifecycle (Weeks 7–8)

3.1 Goal Generator

Package: maid-stdlib | Priority: P0 | Dependencies: 1.1

  • [ ] Create packages/maid-stdlib/src/maid_stdlib/systems/npc/goals.py with:
  • [ ] GoalGenerator class with constant: EVALUATE_TIMEOUT = 0.002 (2ms):
    • [ ] async evaluate(npc_id: uuid.UUID, needs: NeedsComponent, memories: list, relationships: dict, world_context: WorldContext) -> list[Goal] — generate candidates with timeout via asyncio.wait_for, deduplicate by (category, source, target, description_hash)
    • [ ] _derive_goal_from_need(category: NeedCategory, need: Need, world_context: WorldContext) -> Goal | None — generate when need.value < 0.3
    • [ ] _derive_goal_from_memory(memory, relationships: dict) -> Goal | None — generate reactive goals from high-importance memories (importance > 0.7)
  • [ ] GoalPredicateRegistry class — register serializer/deserializer pairs for custom predicates
  • [ ] Write unit tests in packages/maid-stdlib/tests/test_goal_generator.py:
  • [ ] test_goal_from_low_need — verify goal generated when need < 0.3
  • [ ] test_goal_from_memory — verify reactive goal from important memory
  • [ ] test_goal_deduplication — verify duplicate goals filtered
  • [ ] test_goal_timeout — verify empty list on timeout
  • [ ] test_goal_max_active_limit — verify max 5 active goals enforced

3.2 Goal Lifecycle Management

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

  • [ ] Add to packages/maid-stdlib/src/maid_stdlib/systems/npc/goals.py:
  • [ ] GoalLifecycleManager class:
    • [ ] async update_goals(entity_id: uuid.UUID, goals: GoalsComponent, world: World) -> None — check each goal's conditions via predicate evaluation, update progress, handle deadline expiry, emit NPCGoalCompletedEvent or NPCGoalFailedEvent, move to completed/failed lists
    • [ ] add_goal(goals: GoalsComponent, new_goal: Goal) -> bool — enforce max 5, replace lowest-priority if full
    • [ ] Persist goal changes to DocumentStore collection npc_goals
  • [ ] Write unit tests in packages/maid-stdlib/tests/test_goal_lifecycle.py:
  • [ ] test_goal_completion_on_predicate_satisfied — verify COMPLETE state
  • [ ] test_goal_failure_on_deadline — verify FAILED on deadline expiry
  • [ ] test_goal_completion_event — verify NPCGoalCompletedEvent emitted
  • [ ] test_goal_replacement_lowest_priority — verify lowest-priority replaced when at max
  • [ ] test_goal_persistence — verify DocumentStore write on change

3.3 Story Signal Detector

Package: maid-stdlib | Priority: P0 | Dependencies: 1.9

  • [ ] Create packages/maid-stdlib/src/maid_stdlib/systems/npc/signals.py with:
  • [ ] StorySignalType(str, Enum) with values: RIVALRY_ESCALATION, FACTION_TENSION, THREAT_DETECTED, RESOURCE_SCARCITY, TRADE_OPPORTUNITY, ALLIANCE_FORMING, DISCOVERY, GOAL_BLOCKED, BETRAYAL, SECRET_REVEALED, POWER_SHIFT, UNREQUITED, CRISIS, CELEBRATION, MIGRATION
  • [ ] StorySignal dataclass with fields: id: uuid.UUID, signal_type: StorySignalType, importance: float, involved_npcs: list[uuid.UUID], involved_players: list[uuid.UUID], location: uuid.UUID, context: dict[str, Any], created_at: int, expires_at: int
  • [ ] StorySignalDetector class with constants: DETECTION_INTERVAL_TICKS = 30, MAX_BUFFER_SIZE = 1000:
    • [ ] async startup(self) -> None — subscribe to NPCGoalCreatedEvent, NPCGoalFailedEvent, NPCSocialInteractionEvent, NPCMoodChangedEvent, NPCActivityChangedEvent
    • [ ] async _accumulate(event: Event) -> None — append to buffer, trim to MAX_BUFFER_SIZE
    • [ ] async detect(self) -> list[StorySignal] — atomic swap buffer, run v1 pattern detectors: _detect_blocked_goals (uses goal data only), _detect_rivalry_escalation (uses basic relationship data), _detect_resource_conflicts (uses need data only); remaining detectors (_detect_alliance_formation, _detect_spreading_rumors, _detect_secret_exposure, _detect_faction_tensions, _detect_power_shifts) deferred to v2 — StorySignalType enum can stay large (unused types are harmless)
  • [ ] StorySignalSystem(System) with priority: ClassVar[int] = 130:
    • [ ] async update(self, delta: float) -> None — every 30 ticks, call detector.detect(), emit StorySignalEvent for each detected signal
  • [ ] Write unit tests in packages/maid-stdlib/tests/test_story_signals.py:
  • [ ] test_rivalry_escalation_detection — verify declining trust triggers signal
  • [ ] test_blocked_goal_detection — verify goal with blockers emits GOAL_BLOCKED
  • [ ] test_signal_buffer_bounded — verify MAX_BUFFER_SIZE enforcement
  • [ ] test_signal_event_emitted — verify StorySignalEvent on EventBus
  • [ ] test_detection_interval — verify detection runs every 30 ticks
  • [ ] test_atomic_buffer_swap — verify no events lost during detect()
  • [ ] test_resource_conflict_detection — verify competing NPCs trigger RESOURCE_SCARCITY
  • [ ] test_signal_expiry — verify signals have expires_at and are ephemeral

3.4 Content Pack Story Registration

Package: maid-stdlib | Priority: P0 | Dependencies: 3.3

  • [ ] Update packages/maid-stdlib/src/maid_stdlib/pack.py:
  • [ ] Add StorySignalSystem(world) (priority=130) to get_systems() return list

Phase 4: Polish & Optimization (Weeks 9–10)

4.1 Background NPC Catch-Up

Package: maid-stdlib | Priority: P0 | Dependencies: 1.7

  • [ ] Add to packages/maid-stdlib/src/maid_stdlib/systems/npc/autonomy.py:
  • [ ] MAX_CATCH_UP_HOURS = 168 constant (1 game week)
  • [ ] async catch_up_npc(npc_id: uuid.UUID, elapsed_game_hours: float, schedule: ScheduleComponent, needs: NeedsComponent, goals: GoalsComponent) -> None:
    • [ ] Cap elapsed hours to MAX_CATCH_UP_HOURS
    • [ ] Analytic need advancement: compute_daily_need_delta(schedule, needs) cached per archetype, scale by full days, simulate fractional remainder
    • [ ] Batched goal advancement: expire past-deadline goals, advance innate/duty goals at 2%/hour
    • [ ] Teleport NPC to schedule-predicted location
  • [ ] compute_daily_need_delta(schedule: ScheduleComponent, needs: NeedsComponent) -> dict[NeedCategory, float] — pre-compute net effect of one 24-hour cycle
  • [ ] Two-phase catch-up: (1) immediate teleport to schedule location, (2) full state catch-up at 5 NPCs/tick budget
  • [ ] Write unit tests in packages/maid-stdlib/tests/test_catchup.py:
  • [ ] test_catchup_need_advancement — verify analytic need progression
  • [ ] test_catchup_max_hours_cap — verify capped at 168 hours
  • [ ] test_catchup_goal_expiry — verify past-deadline goals failed
  • [ ] test_catchup_teleport_to_schedule — verify NPC at expected location
  • [ ] test_catchup_budget_limit — verify max 5 NPCs per tick
  • [ ] test_daily_need_delta_cached — verify per-archetype caching

4.2 Off-Tick LLM Queue and Circuit Breaker

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

  • [ ] Create packages/maid-stdlib/src/maid_stdlib/systems/npc/llm_queue.py with:
  • [ ] OffTickLLMQueue class with constants: MAX_QUEUED = 50, MAX_QPS = 10.0, PER_CALL_TIMEOUT = 2.0:
    • [ ] Bounded async queue processed outside tick loop
    • [ ] Priority ordering: player-witnessed > story-signal-relevant > routine flavor
    • [ ] Backpressure: drop new requests when full, log warning
    • [ ] Dedicated asyncio task drains queue respecting QPS limits
    • [ ] Results delivered via events on next tick
  • [ ] LLMCircuitBreaker — thin configuration wrapper around existing CircuitBreaker from packages/maid-engine/src/maid_engine/ai/circuit_breaker.py (do not reimplement):
    • [ ] Configure with FAILURE_THRESHOLD = 5, RECOVERY_WINDOW = 60.0
    • [ ] Delegate allow_call(), record_success(), record_failure() to underlying CircuitBreaker
  • [ ] Degradation path: when budget exhausted, social interactions resolve deterministically, narrative text skipped (generic description shown), goal generation falls back to rule-based
  • [ ] Narrative delivery guard: verify player still in room before emitting NarrativeReadyEvent
  • [ ] Wire SocialInteractionResolver._enqueue_narrative_request() for player-witnessed interactions (moved from Phase 2 §2.4)
  • [ ] Write unit tests in packages/maid-stdlib/tests/test_llm_queue.py:
  • [ ] test_queue_bounded — verify MAX_QUEUED limit
  • [ ] test_queue_priority_ordering — verify player-witnessed processed first
  • [ ] test_circuit_breaker_opens — verify open after 5 consecutive failures
  • [ ] test_circuit_breaker_half_open — verify probe after recovery window
  • [ ] test_degradation_path — verify deterministic fallback
  • [ ] test_narrative_delivery_guard — verify stale narratives discarded

4.3 Persistence Integration

Package: maid-stdlib | Priority: P0 | Dependencies: 1.1, 1.7

Uses Doc 01's dirty-tracking and persistence pipeline. Components register with the DirtyTracker and are saved through the standard EntityPersistenceManager. All enum types are str enums for clean JSON serialization. Uses actual GameTime type for human-readable timestamps and float for elapsed durations — do not mix with raw integer tick counts.

  • [ ] Define Pydantic persistence schemas and register them:
  • [ ] NPCNeedsDocument(BaseModel) — serializes NeedsComponent state
  • [ ] NPCGoalsDocument(BaseModel) — serializes GoalsComponent with predicate discriminator
  • [ ] NPCScheduleOverrideDocument(BaseModel) — serializes schedule overrides
  • [ ] NPCDemotionDocument(BaseModel) — serializes demotion state and failure counts
  • [ ] Register all schemas via register_stdlib_schemas() in packages/maid-stdlib/src/maid_stdlib/models/__init__.py
  • [ ] Add persistence logic using actual DocumentStore collection-based async API:
  • [ ] NeedsComponent persistence: store.register_schema("npc_needs", NPCNeedsDocument), then collection = store.get_collection("npc_needs"), await collection.update(npc_id, document) — save on significant change or every 5 minutes
  • [ ] GoalsComponent persistence: register schema, use await collection.create(npc_id, document) on creation, await collection.update(npc_id, document) on change, serialize GoalPredicate via predicate_type discriminator
  • [ ] ScheduleComponent override persistence: register schema, save on change via await collection.update(npc_id, document)
  • [ ] Demotion state persistence: register schema, use a dedicated singleton UUID constant for the demotion state document, save via await collection.update(singleton_id, document)
  • [ ] All enum types serialize as string values via use_enum_values=True; timestamps use GameTime type (not integer tick counts)
  • [ ] Write unit tests in packages/maid-stdlib/tests/test_autonomy_persistence.py:
  • [ ] test_needs_serialization — verify round-trip serialize/deserialize
  • [ ] test_goals_serialization — verify predicate serialization with discriminator
  • [ ] test_demotion_state_persisted — verify demotions survive restart
  • [ ] test_schedule_override_persistence — verify override saved on change
  • [ ] test_goal_predicate_registry — verify custom predicates register serializer/deserializer
  • [ ] test_needs_save_frequency — verify save on significant change or every 5 minutes
  • [ ] test_gametime_serialization — verify GameTime type used (not integer tick count)
  • [ ] test_enum_string_serialization — verify NeedCategory, GoalCategory serialize as strings
  • [ ] test_persistence_schema_registration — verify all schemas registered via register_stdlib_schemas()

4.4 Debug Brain Admin Command

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

The @debug_brain command is a critical development and operations tool that allows admins and builders to inspect the full autonomy state of any NPC in real time. Output includes ASCII bar graphs for needs, ranked utility scores, goal progress, and error/ demotion status. This is essential for tuning archetype parameters and diagnosing unexpected NPC behavior.

  • [ ] Create packages/maid-stdlib/src/maid_stdlib/commands/building/debug_brain.py with:
  • [ ] @debug_brain <npc_name> command (BUILDER access level):
    • [ ] Display NPC tier (Active/Nearby/Background)
    • [ ] Display mood with descriptor
    • [ ] Display all needs with bar graph, decay rate, weight, URGENT flag
    • [ ] Display current action with utility score
    • [ ] Display current schedule block with adherence
    • [ ] Display top 4 utility scores with action types
    • [ ] Display active goals with progress and priority
    • [ ] Display error count and demotion status
  • [ ] Register command in StdlibContentPack.register_commands()
  • [ ] Write unit tests in packages/maid-stdlib/tests/test_debug_brain.py:
  • [ ] test_debug_brain_output_format — verify output structure
  • [ ] test_debug_brain_nonexistent_npc — verify error message
  • [ ] test_debug_brain_access_level — verify BUILDER required

4.5 Performance Benchmarks

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

Performance targets from design doc §7.1: tier assignment 0.1ms, need decay 0.5ms, schedule checks 0.4ms, action selection 2.0ms, action execution 1.0ms, story signals 0.5ms (amortized over 30 ticks), social interactions 0.5ms. Total: <5ms/tick. Benchmarks use percentile-based tolerance thresholds to avoid CI flakiness.

  • [ ] Write benchmark tests in packages/maid-stdlib/tests/test_autonomy_benchmarks.py:
  • [ ] test_200_npc_tick_budget — verify 200 NPCs (20 Active, 50 Nearby, 130 Background) p95 within 5ms tick budget (mark as benchmark, not hard assert)
  • [ ] test_utility_scoring_throughput — verify 20 NPCs scored p95 within 2ms
  • [ ] test_catchup_24h_performance — verify 24h catch-up completes p95 in <50ms per NPC
  • [ ] test_memory_per_npc — verify <2KB per NPC with full state, <200B for archetype-shared NPCs

4.6 Integration Tests

Package: maid-stdlib | Priority: P0 | Dependencies: All previous

  • [ ] Write integration tests in packages/maid-stdlib/tests/test_autonomy_integration.py:
  • [ ] test_full_tick_cycle — needs decay → action selection → execution end-to-end
  • [ ] test_schedule_day_night_transition — NPC follows schedule across day/night boundary
  • [ ] test_gossip_to_behavior_to_signal_chain — gossip propagation → behavioral change → story signal
  • [ ] test_multi_npc_social_interaction — multiple NPCs interact socially in same tick
  • [ ] test_background_to_active_catchup — Background → Active tier transition with state catch-up
  • [ ] test_combat_suspends_autonomy — verify AutonomySystem defers to BehaviorSystem during combat
  • [ ] test_autonomy_with_existing_systems — verify coexistence with BehaviorSystem (priority=100), GameTimeSystem (priority=5), WeatherSystem (priority=10)
  • [ ] test_archetype_initialization — verify NPC spawned with archetype gets correct NeedsComponent, GoalsComponent, ScheduleComponent
  • [ ] test_need_decay_to_goal_generation — verify low need triggers goal creation via GoalGenerator
  • [ ] test_llm_degradation_path — verify system functions without LLM (deterministic fallback)
  • [ ] test_multi_player_proximity_tier — verify two players in different rooms near same NPC, both leaving at different times, with hysteresis interaction
  • [ ] test_persistence_round_trip — verify full save/load cycle preserves NPC autonomy state

Dependencies Summary

Cross-document dependency flow:

Doc 03 (Memory & Relationships) — designed but NOT implemented; null stubs used
  ├── GoalGenerator reads episodic memories for reactive goals (NullMemoryProvider stub)
  ├── GossipReactionProcessor evaluates gossip via relationship trust (NullGossipProcessor stub)
  └── SocialFabricSystem queries friends/rivals/allies from relationship state (NullRelationshipProvider stub)

Doc 01 (Durable Persistence)
  ├── DocumentStore: collection-based async API with Pydantic schemas (npc_needs, npc_goals, npc_schedule_overrides, npc_demotions)
  └── EntityPersistenceManager: dirty-tracking for save scheduling

Existing Systems (no changes required):
  ├── GameTimeSystem (priority=5) → emits TimeChangeEvent (current_time: GameTime)
  ├── WeatherSystem (priority=10) → emits WeatherChangeEvent
  ├── BehaviorSystem (priority=100) → combat takes precedence
  ├── SpawnerSystem (priority=50) → spawns NPCs (modified to attach autonomy components)
  ├── GridManager (via world.grid) → A* pathfinding for NPC movement (synchronous)
  └── CircuitBreaker → wraps LLM provider calls (reused, not reimplemented)

Internal task dependencies:

Phase 1 (internal dependencies):
  1.1 Models/Enums (no deps)
  1.9 Events (no deps)
  1.2 Archetypes → 1.1
  1.2a Spawner Integration → 1.2
  1.2b Doc 03 Null Stubs (no deps)
  1.3 Need Decay → 1.1
  1.4 Schedule System → 1.1
  1.8 Bark System → 1.1
  1.5 Utility Scorer → 1.1, 1.3
  1.6 Action Executor → 1.5
  1.7 Autonomy System → 1.3, 1.4, 1.5, 1.6
  1.10 Pack Registration → 1.7, 1.9

Phase 2 (depends on Phase 1):
  2.1 Social Models → 1.1
  2.2 SocialFabricSystem → 2.1, 1.2b
  2.3 Gossip Reactions → 2.1 (stubbed; blocked on Doc 03)
  2.4 Social Resolver → 2.2, 2.3
  2.5 Pack Social Registration → 2.2

Phase 3 (depends on Phase 1):
  3.1 Goal Generator → 1.1
  3.2 Goal Lifecycle → 3.1
  3.3 Story Signals → 1.9
  3.4 Pack Story Registration → 3.3

Phase 4 (depends on Phases 1–3):
  4.1 Catch-Up → 1.7
  4.2 LLM Queue → 2.4
  4.3 Persistence → 1.1, 1.7
  4.4 Debug Brain → 1.7
  4.5 Benchmarks → 1.7
  4.6 Integration Tests → All


Resource Allocation

Role FTE Primary Focus
Systems Developer 1.0 NPC autonomy core: needs, goals, utility scorer, autonomy system, tick budget
AI/Social Developer 0.5 Social fabric, gossip reactions, LLM queue, narrative generation
Content Developer 0.5 Archetype YAML, bark YAML, schedule definitions, story signal patterns
QA Engineer 0.5 Test authoring, performance benchmarks, integration tests

Success Criteria

  • [ ] NPCs follow daily routines that vary by time of day, weather, and personal state
  • [ ] NPCs pursue personal goals that create observable world changes
  • [ ] NPC-to-NPC interactions generate emergent social dynamics visible to players
  • [ ] Gossip meaningfully changes NPC behavior and creates narrative hooks
  • [ ] Players observe NPCs "living their lives" without direct interaction
  • [ ] 200 NPCs run within <5ms total tick budget (20 Active, 50 Nearby, 130 Background)
  • [ ] NPC actions are deterministic given the same world state (for debugging)
  • [ ] Story signals are automatically detected and emitted for quest generation (Doc 08)
  • [ ] Autonomy coexists with existing BehaviorSystem (combat takes precedence at priority=100)
  • [ ] All enum types serialize as strings; timestamps use GameTime type (not integer tick counts)
  • [ ] Per-NPC error isolation prevents single buggy NPC from crashing autonomy loop
  • [ ] Archetype-level circuit breaker demotes entire archetypes after repeated failures
  • [ ] Demotion state and failure counts persist across server restarts
  • [ ] @debug_brain command provides full NPC autonomy inspection
  • [ ] Test coverage >80% for all autonomy modules
  • [ ] MyPy strict mode passes for all autonomy modules

Prerequisites / Blockers from Other Design Docs

  • Doc 03 (NPC Memory & Relationships): The SocialFabricSystem queries Doc 03's relationship data for friends/rivals/allies. The GoalGenerator uses Doc 03's episodic memories for reactive goal generation. The GossipReactionProcessor uses Doc 03's gossip messages. Doc 03 is designed but not implemented. Null-object stubs (NullMemoryProvider, NullRelationshipProvider, NullGossipProcessor) are created in §1.2b and wired as defaults. When Doc 03 lands, these stubs are replaced via dependency injection. The core autonomy loop (needs, schedules, utility scoring) functions independently.
  • Doc 01 (Durable Persistence): DocumentStore is used for persisting needs, goals, schedule overrides, and demotion state via collection-based async API with registered Pydantic schemas. The EntityPersistenceManager from Doc 01 handles dirty-tracking and save scheduling. If Doc 01 is not fully implemented, in-memory-only operation is acceptable for initial development.
  • Doc 08 (Quest Generation): The StorySignalSystem emits StorySignalEvent events that Doc 08's quest generation system consumes. This is a one-way dependency — the autonomy system does not require Doc 08 to be implemented. Story signals are ephemeral events that are simply not consumed if Doc 08 is absent.
  • Existing systems: GameTimeSystem (priority=5) must emit TimeChangeEvent (with current_time: GameTime) for the ScheduleSystem. WeatherSystem (priority=10) must emit WeatherChangeEvent for schedule condition evaluation. Both already exist in packages/maid-classic-rpg/src/maid_classic_rpg/systems/world/. SpawnerSystem (priority=50) is modified to attach autonomy components at spawn time.

Deferred to v2+

The following are explicitly out of scope for v1:

  • NPC-to-NPC full LLM conversations (beyond single narrative generation)
  • Player suggesting goals to NPCs via dialogue (requires dialogue system extension)
  • NPC death/respawn goal retention policy (retain goals is the recommendation but not enforced yet)
  • Visual NPC schedule editor for admin UI
  • Per-NPC schedule customization UI (individual YAML only for now)
  • NPC faction politics system (beyond basic faction_standing)
  • Seasonal schedule variations (beyond weather conditions)
  • NPC skill/ability learning through training activities

Files Created (Summary)

packages/maid-stdlib/src/maid_stdlib/
    models/npc/
        autonomy.py           # NeedsComponent, GoalsComponent, ScheduleComponent,
                              # SocialComponent, NeedCategory, GoalCategory, GoalSource,
                              # ActivityType, ActionType, ActionState, Need, Goal,
                              # GoalCondition, GoalPredicate, built-in predicates,
                              # ScheduleBlock, ScheduleCondition, Action, ScoredAction,
                              # ActiveAction, ActionCost, NavigationIntent, SocialIntent,
                              # SocialInteractionType, GossipIntentType, GossipIntent,
                              # BehaviorModification, InteractionOutcome
        archetypes.py         # NPCArchetype, NPCPersonality, GoalTemplate,
                              # ArchetypeRegistry
    systems/npc/
        needs.py              # NeedDecaySystem (priority=110), calculate_mood()
        schedule.py           # ScheduleSystem (priority=105)
        scoring.py            # UtilityScorer, NPCDecisionContext, WorldDecisionContext
        executor.py           # ActionExecutor, NPCMovementHandler (System, priority=116),
                              # ActionResult, MovementResult
        autonomy.py           # AutonomySystem (priority=115), TickBudgetGovernor,
                              # catch_up_npc(), compute_daily_need_delta()
        social.py             # SocialFabricSystem (priority=120),
                              # SocialInteractionResolver
        gossip.py             # GossipReactionProcessor (protocol), NullGossipProcessor
        signals.py            # StorySignalSystem (priority=130), StorySignalDetector,
                              # StorySignal, StorySignalType
        barks.py              # BarkTemplate, BarkLibrary, BarkSystem (priority=125),
                              # load_barks_from_yaml()
        goals.py              # GoalGenerator, GoalLifecycleManager,
                              # GoalPredicateRegistry
        llm_queue.py          # OffTickLLMQueue, LLMCircuitBreaker (thin wrapper)
        stubs.py              # NullMemoryProvider, NullRelationshipProvider,
                              # MemoryProvider (Protocol), RelationshipProvider (Protocol)
    commands/building/
        debug_brain.py        # @debug_brain admin command
    events/
        npc_autonomy.py       # NPCActivityChangedEvent, NPCGoalCreatedEvent,
                               # NPCGoalCompletedEvent, NPCGoalFailedEvent,
                               # NPCSocialInteractionEvent, StorySignalEvent,
                               # NPCMoodChangedEvent, ArchetypeCircuitBreakerEvent,
                               # IntentCancelledEvent, NarrativeReadyEvent,
                               # TickBudgetExceededEvent, ActionCompletedEvent,
                               # ActionFailedEvent, NPCActionEvent

packages/maid-stdlib/data/npcs/
    archetypes.yaml       # Archetype definitions (tradesperson, blacksmith,
                           # merchant, guard, innkeeper, farmer)
    barks.yaml            # Bark templates per archetype

packages/maid-stdlib/tests/
    test_autonomy_models.py
    test_archetypes.py
    test_need_decay.py
    test_schedule_system.py
    test_utility_scorer.py
    test_action_executor.py
    test_autonomy_system.py
    test_barks.py
    test_autonomy_events.py
    test_social_fabric.py
    test_gossip_reactions.py
    test_social_resolver.py
    test_goal_generator.py
    test_goal_lifecycle.py
    test_story_signals.py
    test_llm_queue.py
    test_catchup.py
    test_autonomy_persistence.py
    test_debug_brain.py
    test_autonomy_benchmarks.py
    test_autonomy_integration.py

Files Modified (Summary)

packages/maid-stdlib/src/maid_stdlib/pack.py            # Add 7 new systems to get_systems(),
                                                                   # 14 new events to get_events(),
                                                                   # archetype/bark loading in on_load(),
                                                                   # @debug_brain command registration
packages/maid-stdlib/src/maid_stdlib/systems/__init__.py # Add new systems to
                                                                   # get_stdlib_systems()
packages/maid-stdlib/src/maid_stdlib/events/__init__.py  # Include autonomy events in
                                                                   # get_stdlib_events()
packages/maid-stdlib/src/maid_stdlib/models/__init__.py  # Register persistence schemas in
                                                                   # register_stdlib_schemas()