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
GameTimeSysteminpackages/maid-classic-rpg/src/maid_classic_rpg/systems/world/time.pyemitsTimeChangeEventwithcurrent_time: GameTimefield (extract hour viaevent.current_time.hour) - [ ] Verify
WeatherSysteminpackages/maid-classic-rpg/src/maid_classic_rpg/systems/world/weather.pyemitsWeatherChangeEvent - [ ] Verify
BehaviorSysteminpackages/maid-classic-rpg/src/maid_classic_rpg/systems/npc/behavior.pyat priority=100 handles combat states and will coexist with AutonomySystem at priority=115 - [ ] Verify
GridManager.find_path()inpackages/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
DocumentStoreABC inpackages/maid-engine/src/maid_engine/storage/document_store.py(line 198) uses collection-based async API:register_schema(name, Model),get_collection(name), thenawait 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
CircuitBreakerinpackages/maid-engine/src/maid_engine/ai/circuit_breaker.py(line 53) can be wrapped for LLM autonomy calls - [ ] Verify
NPCComponentinpackages/maid-stdlib/src/maid_stdlib/components/core.py(line 408) can coexist with newNeedsComponentandGoalsComponent - [ ] Verify
Componentbase class inpackages/maid-engine/src/maid_engine/core/ecs/component.py(line 10) inherits from PydanticBaseModelwithvalidate_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=Truemeans enum fields store string values - [ ] Verify
EntityManager.with_components()inpackages/maid-engine/src/maid_engine/core/ecs/entity.py(line 278) returnsIterator[Entity], notIterator[uuid.UUID]— iteration pattern isfor entity in self.entities.with_components(X): - [ ] Verify
World.grid(notWorld.grid_manager) returnsGridManagerinstance;world.move_entity()is synchronous (not async) - [ ] Verify
SystemABC inpackages/maid-engine/src/maid_engine/core/ecs/system.py(line 29) supportsstartup()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.pywith: - [ ]
NeedCategory(str, Enum)with values:SURVIVAL,ECONOMIC,PURPOSE,COMFORT(deferSOCIALandAMBITIONto v2 — archetype weights default to 0 for unused categories) - [ ]
Needmodel (Pydantic, not dataclass — Component base is Pydantic) with fields:category: NeedCategory,value: float,decay_rate: float,last_satisfied: float(useGameTimefor timestamps orfloatfor 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— useField(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 implementingGoalPredicate - [ ]
GoalConditiondataclass with fields:predicate: GoalPredicate,description: str - [ ]
Goaldataclass 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 - [ ]
ScheduleConditiondataclass with fields:category: str,operator: str,value: str - [ ]
ScheduleBlockdataclass 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 typesBUY_ITEM,SEEK_HELP,OFFER_HELP,FLEE,CONFRONT,COMPLAIN,PRAY,TRAIN,SCHEME,CELEBRATEdeferred to v2 — ActionExecutor returnsNonefor unhandled types, falling back to idle) - [ ]
ActionState(str, Enum)with values:PENDING,RUNNING,COMPLETE,FAILED,INTERRUPTED - [ ]
ActionCostfrozen dataclass with fields:time_cost: float,risk: float,currency: int = 0,stamina: float = 0.0 - [ ]
Actiondataclass 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 - [ ]
ScoredActiondataclass with fields:action: Action,utility: float,score_breakdown: dict[str, float] | None = None - [ ]
ActiveActionclass 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— verifyis_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.pywith: - [ ]
NPCPersonalityPydantic model with fields:bravery: float = 0.5,greed: float = 0.5,sociability: float = 0.5,diligence: float = 0.5,ambition: float = 0.5(defercuriosity,loyalty,temperamentto v2 — these add little observable difference in a text MUD) - [ ]
GoalTemplatedataclass with fields:category: GoalCategory,description: str,priority: float - [ ]
NPCArchetypedataclass 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.pywith: - [ ]
ArchetypeRegistryclass 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.yamlwith archetypes: - [ ]
tradespersonbase archetype (not instantiated directly) - [ ]
blacksmith(parent: tradesperson) - [ ]
merchant(parent: tradesperson) - [ ]
guardarchetype - [ ]
innkeeper(parent: tradesperson) - [ ]
farmerarchetype - [ ] 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
SpawnerSystemor create a post-spawn hook to attachNeedsComponent,GoalsComponent, andScheduleComponentbased 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.pywith: - [ ]
NullMemoryProviderclass —get_memories(npc_id) -> listreturns[],get_important_memories(npc_id, threshold) -> listreturns[] - [ ]
NullRelationshipProviderclass —get_trust(a, b) -> floatreturns0.5(neutral),get_respect(a, b) -> floatreturns0.5,get_friends(npc_id) -> list[uuid.UUID]returns[],get_rivals(npc_id) -> list[uuid.UUID]returns[] - [ ]
MemoryProvider(Protocol)andRelationshipProvider(Protocol)— interfaces that Doc 03 implementations will satisfy - [ ] Wire null providers as defaults in
SocialFabricSystem,GoalGenerator, andGossipReactionProcessor; 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.pywith: - [ ]
calculate_mood(needs: NeedsComponent, recent_memories: list) -> float— weighted average of need satisfaction, ±20% memory modifier, clamped to [0.0, 1.0] - [ ]
NeedDecaySystem(System)withpriority: ClassVar[int] = 110:- [ ]
async update(self, delta: float) -> None— for each NPC withNeedsComponent: decay needs bydecay_rate * elapsed_game_hours, recalculate mood, emitNPCMoodChangedEventon significant change (>0.1 delta) - [ ] Background NPCs: advance needs toward schedule-predicted baseline
- [ ]
- [ ]
clamp(value: float, min_val: float, max_val: float) -> floatutility - [ ] 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.pywith: - [ ]
ScheduleSystem(System)withpriority: ClassVar[int] = 105:- [ ]
async startup(self) -> None— subscribe toTimeChangeEventandWeatherChangeEvent - [ ]
async _on_time_changed(self, event: TimeChangeEvent) -> None— extract hour viaevent.current_time.hour; for each NPC withScheduleComponent: find applicable block, check conditions, compare schedule priority vs urgent need priority, emitNPCActivityChangedEventon 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 toWANDER; on overlap (multiple matching blocks) use highestpriorityfield - [ ]
_check_conditions(block: ScheduleBlock, weather: str, season: str) -> bool— evaluate typedScheduleConditionlist - [ ]
async _transition_activity(entity_id: uuid.UUID, schedule: ScheduleComponent, new_block: ScheduleBlock) -> None— update current_activity, setNavigationIntentfor 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.pywith: - [ ]
NPCDecisionContextfrozen 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 - [ ]
WorldDecisionContextfrozen dataclass with fields:current_hour,current_weather,current_season,nearby_entities,nearby_threats,available_resources - [ ]
UtilityScorerclass 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.pywith: - [ ]
ActionResultdataclass with fields:success: bool,partial: bool = False,fallback: str | None = None - [ ]
ActionExecutorclass with method:async execute(npc_id: uuid.UUID, scored_action: ScoredAction, world: World) -> ActionResult— match onActionType, set appropriate intent components (NavigationIntent,SocialIntent), emitIntentCancelledEventbefore overwriting existing intents, updateActiveAction.stateon completion/failure, emitActionCompletedEvent/ActionFailedEvent, returnNonefor unhandled action types (falls back to idle) - [ ]
NPCMovementHandleras a registeredSystem(priority=116, after AutonomySystem) withasync update(self, delta: float)— queries all entities withNavigationIntentcomponent, moves one step per tick viaworld.grid.find_path()(synchronous call), callsworld.move_entity()(synchronous), removesNavigationIntenton arrival or failure - [ ]
MovementResultdataclass 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.pywith: - [ ]
TickBudgetGovernorclass 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)withpriority: ClassVar[int] = 115,MAX_FAILURES = 3,PER_NPC_TIMEOUT = 0.002:- [ ]
async startup(self) -> None— restore persisted demotion state and failure counts fromDocumentStorecollectionnpc_demotions(via registered Pydantic schema andawait 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):), checkTickBudgetGovernor.should_process(), skip demoted/deleted entities, buildNPCDecisionContext.snapshot()(only when NPC needs new decision) andWorldDecisionContext, checkActiveActionlifecycle, generate candidates, score viaUtilityScorer, select highest utility, execute viaActionExecutor, emitNPCActionEvent; process Nearby NPCs every 10th tick; process Background NPCs every 60th tick; callTickBudgetGovernor.end_tick() - [ ] Per-NPC error isolation with
asyncio.wait_for2ms timeout; failure counter; demotion to Background after 3 consecutive failures - [ ] Archetype-level circuit breaker:
ARCHETYPE_FAILURE_THRESHOLD = 3; emitArchetypeCircuitBreakerEventwhen 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
RoomIndexandworld.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.pywith: - [ ]
BarkTemplatedataclass 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 - [ ]
BarkLibraryclass 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)withpriority: ClassVar[int] = 125(after AutonomySystem): queries NPCs withNeedsComponentin rooms containing players, callsBarkLibrary.select_bark(), emits bark text to players in the room via EventBus - [ ] Create content data in
packages/maid-classic-rpg/data/npcs/barks.yamlwith 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__.pyto include autonomy events inget_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.pyStdlibContentPack: - [ ] Add
ScheduleSystem(world)(priority=105),NeedDecaySystem(world)(priority=110),AutonomySystem(world)(priority=115),NPCMovementHandler(world)(priority=116),BarkSystem(world)(priority=125) toget_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.pyClassicRPGContentPack: - [ ] During
on_load(), callArchetypeRegistry.load_from_yaml()usingpackages/maid-classic-rpg/data/npcs/archetypes.yaml, then register loaded archetypes - [ ] During
on_load(), load bark templates frompackages/maid-classic-rpg/data/npcs/barks.yamland wire them into the stdlib bark framework (BarkLibrary/BarkSystem) - [ ] Update
packages/maid-stdlib/src/maid_stdlib/systems/__init__.pyget_stdlib_systems()to include all new systems (this is the actual registration path, not onlypack.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 - [ ]
GossipIntentdataclass with fields:intent_type: GossipIntentType,target_npc_id: uuid.UUID | None,desired_outcome: str,source_event_id: uuid.UUID | None = None - [ ]
BehaviorModificationdataclass 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 - [ ]
InteractionOutcomedataclass 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.pywith: - [ ]
SocialFabricSystem(System)withpriority: 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 viaRelationshipProvider(usesNullRelationshipProviderstub 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
- [ ] Relationship caches:
- [ ] 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.
GossipReactionProcessorconsumesGossipMessageobjects 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.pywith: - [ ]
GossipReactionProcessorprotocol/interface with method signature:async process_gossip_reaction(listener, gossip, intent) -> list[BehaviorModification] - [ ]
NullGossipProcessorstub 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: - [ ]
SocialInteractionResolverclass:- [ ]
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
Nonefor 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) toget_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.pywith: - [ ]
GoalGeneratorclass 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 viaasyncio.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)
- [ ]
- [ ]
GoalPredicateRegistryclass — 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: - [ ]
GoalLifecycleManagerclass:- [ ]
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, emitNPCGoalCompletedEventorNPCGoalFailedEvent, 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.pywith: - [ ]
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 - [ ]
StorySignaldataclass 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 - [ ]
StorySignalDetectorclass with constants:DETECTION_INTERVAL_TICKS = 30,MAX_BUFFER_SIZE = 1000:- [ ]
async startup(self) -> None— subscribe toNPCGoalCreatedEvent,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 —StorySignalTypeenum can stay large (unused types are harmless)
- [ ]
- [ ]
StorySignalSystem(System)withpriority: ClassVar[int] = 130:- [ ]
async update(self, delta: float) -> None— every 30 ticks, calldetector.detect(), emitStorySignalEventfor 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) toget_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 = 168constant (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.pywith: - [ ]
OffTickLLMQueueclass 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 existingCircuitBreakerfrompackages/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 underlyingCircuitBreaker
- [ ] Configure with
- [ ] 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
DirtyTrackerand are saved through the standardEntityPersistenceManager. All enum types arestrenums for clean JSON serialization. Uses actualGameTimetype for human-readable timestamps andfloatfor elapsed durations — do not mix with raw integer tick counts.
- [ ] Define Pydantic persistence schemas and register them:
- [ ]
NPCNeedsDocument(BaseModel)— serializesNeedsComponentstate - [ ]
NPCGoalsDocument(BaseModel)— serializesGoalsComponentwith predicate discriminator - [ ]
NPCScheduleOverrideDocument(BaseModel)— serializes schedule overrides - [ ]
NPCDemotionDocument(BaseModel)— serializes demotion state and failure counts - [ ] Register all schemas via
register_stdlib_schemas()inpackages/maid-stdlib/src/maid_stdlib/models/__init__.py - [ ] Add persistence logic using actual
DocumentStorecollection-based async API: - [ ]
NeedsComponentpersistence:store.register_schema("npc_needs", NPCNeedsDocument), thencollection = store.get_collection("npc_needs"),await collection.update(npc_id, document)— save on significant change or every 5 minutes - [ ]
GoalsComponentpersistence: register schema, useawait collection.create(npc_id, document)on creation,await collection.update(npc_id, document)on change, serialize GoalPredicate viapredicate_typediscriminator - [ ]
ScheduleComponentoverride persistence: register schema, save on change viaawait 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 useGameTimetype (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— verifyGameTimetype used (not integer tick count) - [ ]
test_enum_string_serialization— verify NeedCategory, GoalCategory serialize as strings - [ ]
test_persistence_schema_registration— verify all schemas registered viaregister_stdlib_schemas()
4.4 Debug Brain Admin Command¶
Package: maid-stdlib | Priority: P1 | Dependencies: 1.7
The
@debug_braincommand 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.pywith: - [ ]
@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
GameTimetype (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_braincommand 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
SocialFabricSystemqueries Doc 03's relationship data for friends/rivals/allies. TheGoalGeneratoruses Doc 03's episodic memories for reactive goal generation. TheGossipReactionProcessoruses 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):
DocumentStoreis used for persisting needs, goals, schedule overrides, and demotion state via collection-based async API with registered Pydantic schemas. TheEntityPersistenceManagerfrom 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
StorySignalSystememitsStorySignalEventevents 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 emitTimeChangeEvent(withcurrent_time: GameTime) for theScheduleSystem.WeatherSystem(priority=10) must emitWeatherChangeEventfor schedule condition evaluation. Both already exist inpackages/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()