Srishti280992 commited on
Commit
e461acd
·
verified ·
1 Parent(s): d60cf63

Update factory/world_factory.py

Browse files
Files changed (1) hide show
  1. factory/world_factory.py +317 -169
factory/world_factory.py CHANGED
@@ -18,23 +18,13 @@ Typical pipeline:
18
  -> WorldFactory
19
  -> World
20
 
21
- Example:
22
- from dsl.parser import parse_world_file
23
- from factory.world_factory import WorldFactory
24
-
25
- spec = parse_world_file("examples/research.json")
26
- world = WorldFactory().create_world(spec)
27
-
28
- for _ in range(spec.simulation.steps):
29
- world.step()
30
-
31
- Future extensibility:
32
- - Add plugin package discovery for external behavior registries.
33
- - Add metric object instantiation once metrics registries are available.
34
- - Add migration-aware factory profiles for schema versions.
35
- - Add object pooling for large simulations.
36
- - Add dependency injection hooks for testing.
37
- - Add richer event adapters and world-level event buses.
38
  """
39
 
40
  from __future__ import annotations
@@ -42,13 +32,14 @@ from __future__ import annotations
42
  import copy
43
  import inspect
44
  import logging
45
- import numpy as np
46
- from collections.abc import Mapping, MutableMapping, Sequence
47
  from dataclasses import dataclass, field
48
  from importlib import import_module
49
  from types import MappingProxyType
50
  from typing import Any
51
 
 
 
52
  from core.agent import Agent
53
  from core.behavior import Behavior
54
  from core.event import Event
@@ -72,8 +63,10 @@ from dsl.validator import (
72
  )
73
  from policies.base_policy import BasePolicy
74
 
 
75
  logger = logging.getLogger(__name__)
76
 
 
77
  DEFAULT_BEHAVIOR_MODULES: tuple[str, ...] = (
78
  "behaviors.movement",
79
  "behaviors.consume",
@@ -96,6 +89,7 @@ DEFAULT_POLICY_MODULES: tuple[str, ...] = (
96
  )
97
 
98
  BEHAVIOR_REGISTRY_ATTRIBUTES: tuple[str, ...] = ("BEHAVIOR_REGISTRY",)
 
99
  POLICY_REGISTRY_ATTRIBUTES: tuple[str, ...] = (
100
  "POLICY_REGISTRY",
101
  "RULE_POLICY_REGISTRY",
@@ -126,6 +120,7 @@ _CORE_EVENT_ALIASES: Mapping[str, tuple[str, ...]] = MappingProxyType(
126
  )
127
 
128
  _CORE_WORLD_ALIASES: Mapping[str, tuple[str, ...]] = MappingProxyType({})
 
129
  _CORE_SCHEDULER_ALIASES: Mapping[str, tuple[str, ...]] = MappingProxyType({})
130
 
131
 
@@ -229,24 +224,7 @@ class WorldBuildResult:
229
 
230
  @dataclass
231
  class WorldFactoryConfig:
232
- """Configuration for ``WorldFactory``.
233
-
234
- Attributes:
235
- validate_before_build: Run semantic DSL validation before construction.
236
- strict_unknown_behaviors: Raise if a behavior name is not registered.
237
- strict_unknown_policies: Raise if a policy type is not registered.
238
- strict_constructor_params: Raise on constructor parameters not accepted
239
- by the runtime class.
240
- include_disabled_behaviors: Instantiate behaviors marked disabled.
241
- include_disabled_events: Instantiate events marked disabled.
242
- attach_default_policy: Attach a default policy to agents that define
243
- behaviors but omit a policy.
244
- default_policy_type: Default policy registry key.
245
- attach_scheduler: Attach a scheduler instance to the world when
246
- possible.
247
- attach_dsl_spec_to_world: Store the validated ``WorldSpec`` on the
248
- runtime world for diagnostics and UI use.
249
- """
250
 
251
  validate_before_build: bool = True
252
 
@@ -280,12 +258,7 @@ class WorldFactoryConfig:
280
 
281
  @dataclass
282
  class GenericScheduledEvent:
283
- """Fallback event object used when ``core.event.Event`` is not instantiable.
284
-
285
- The adapter keeps event records executable and inspectable without imposing
286
- domain-specific event semantics. It returns its payload when executed.
287
- Future event-system modules can replace this with richer event dispatch.
288
- """
289
 
290
  name: str
291
  trigger_step: int
@@ -298,12 +271,7 @@ class GenericScheduledEvent:
298
  metadata: Mapping[str, Any] = field(default_factory=dict)
299
 
300
  def execute(self, world: World) -> dict[str, Any]:
301
- """Execute the event in a domain-agnostic way.
302
-
303
- The fallback adapter does not mutate world state by default. It returns
304
- a structured event outcome that schedulers, logs, metrics, or narrators
305
- can consume.
306
- """
307
 
308
  return {
309
  "event": self.name,
@@ -319,12 +287,7 @@ class GenericScheduledEvent:
319
 
320
  @dataclass
321
  class WorldFactory:
322
- """Factory that converts ``WorldSpec`` into a runtime ``World``.
323
-
324
- The factory owns behavior and policy registries. Registries may come from
325
- default behavior/policy modules or from custom mappings supplied through
326
- ``WorldFactoryConfig``.
327
- """
328
 
329
  config: WorldFactoryConfig = field(default_factory=WorldFactoryConfig)
330
 
@@ -355,69 +318,9 @@ class WorldFactory:
355
  """Return registry import errors collected during initialization."""
356
 
357
  return MappingProxyType(self._registry_import_errors)
358
-
359
- @staticmethod
360
- def _behavior_runtime_id(
361
- spec: BehaviorSpec,
362
- *,
363
- owner_agent_id: str | None,
364
- behavior_index: int | None,
365
- ) -> str:
366
- """Return a deterministic runtime id for a behavior spec."""
367
-
368
- metadata_id = None
369
- if isinstance(spec.metadata, Mapping):
370
- metadata_id = spec.metadata.get("id") or spec.metadata.get("behavior_id")
371
-
372
- if metadata_id is not None and str(metadata_id).strip():
373
- return str(metadata_id).strip()
374
-
375
- owner = owner_agent_id or "world"
376
- index = 0 if behavior_index is None else int(behavior_index)
377
- return f"{owner}:behavior:{index}:{spec.name}"
378
-
379
- @staticmethod
380
- def _agent_runtime_position(position: Any) -> np.ndarray:
381
- """Return a NumPy position vector compatible with core.agent.Agent.
382
-
383
- The current Agent implementation expects position to behave like a NumPy
384
- array. In particular, Agent.observe() and Agent.snapshot() use array-style
385
- methods such as copy() and tolist().
386
- """
387
-
388
- if position is None:
389
- return np.zeros(2, dtype=np.float64)
390
-
391
- try:
392
- array = np.asarray(position, dtype=np.float64).reshape(-1)
393
- except (TypeError, ValueError) as exc:
394
- raise RuntimeObjectBuildError(
395
- f"Agent position must be numeric, got {position!r}"
396
- ) from exc
397
-
398
- if array.size == 0:
399
- return np.zeros(2, dtype=np.float64)
400
-
401
- if not np.all(np.isfinite(array)):
402
- raise RuntimeObjectBuildError(
403
- f"Agent position must contain only finite values, got {position!r}"
404
- )
405
-
406
- return array
407
 
408
  def create_world(self, spec: WorldSpec | Mapping[str, Any]) -> World:
409
- """Create a runtime world from a validated or mappable world spec.
410
-
411
- Args:
412
- spec: ``WorldSpec`` or mapping that can validate into ``WorldSpec``.
413
-
414
- Returns:
415
- Runtime ``World`` object.
416
-
417
- Raises:
418
- WorldFactoryValidationError: If semantic validation fails.
419
- WorldFactoryError: If runtime construction fails.
420
- """
421
 
422
  return self.create_world_result(spec).world
423
 
@@ -427,14 +330,32 @@ class WorldFactory:
427
  world_spec = self._coerce_world_spec(spec)
428
  validation_report = self._validate_before_build(world_spec)
429
 
430
- resources = tuple(self.create_resource(resource_spec) for resource_spec in world_spec.resources)
 
 
 
 
 
 
 
 
 
 
431
  events = tuple(
432
  self.create_event(event_spec)
433
  for event_spec in world_spec.events
434
  if event_spec.enabled or self.config.include_disabled_events
435
  )
436
- agents = tuple(self.create_agent(agent_spec) for agent_spec in world_spec.agents)
437
- metrics = tuple(self.create_metric_config(metric_spec) for metric_spec in world_spec.metrics)
 
 
 
 
 
 
 
 
438
 
439
  world = self._create_world_object(
440
  spec=world_spec,
@@ -475,32 +396,59 @@ class WorldFactory:
475
 
476
  return WorldBuildResult(world=world, report=report)
477
 
478
- def create_agent(self, spec: AgentSpec) -> Agent:
 
 
 
 
 
479
  """Create a runtime ``Agent`` from an ``AgentSpec``."""
480
 
481
- behaviors = tuple(
482
- self.create_behavior(
 
 
 
 
 
483
  behavior_spec,
484
  owner_agent_id=spec.id,
485
  behavior_index=behavior_index,
486
  )
487
- for behavior_index, behavior_spec in enumerate(spec.behaviors)
488
- if behavior_spec.enabled or self.config.include_disabled_behaviors
489
- )
 
 
 
 
 
 
 
 
 
 
 
 
490
  policy = self.create_policy_for_agent(spec)
491
 
492
- runtime_position = self._agent_runtime_position(spec.position)
 
 
 
493
 
494
  params = {
495
  "id": spec.id,
496
  "type": spec.type,
497
- "position": runtime_position,
498
  "state": copy.deepcopy(spec.state),
499
  "memory": copy.deepcopy(spec.memory),
500
  "goals": copy.deepcopy(spec.goals),
501
- "behaviors": behaviors,
502
  "policy": policy,
503
  "alive": bool(spec.alive),
 
 
504
  }
505
 
506
  agent = self._instantiate_runtime_object(
@@ -514,16 +462,17 @@ class WorldFactory:
514
 
515
  self._safe_setattr(agent, "id", spec.id)
516
  self._safe_setattr(agent, "type", spec.type)
517
- self._safe_setattr(agent, "position", runtime_position.copy())
518
  self._safe_setattr(agent, "state", copy.deepcopy(spec.state))
519
  self._safe_setattr(agent, "memory", copy.deepcopy(spec.memory))
520
  self._safe_setattr(agent, "goals", copy.deepcopy(spec.goals))
521
- self._safe_setattr(agent, "behaviors", behaviors)
522
  self._safe_setattr(agent, "policy", policy)
523
  self._safe_setattr(agent, "alive", bool(spec.alive))
524
  self._safe_setattr(agent, "metadata", copy.deepcopy(spec.metadata))
525
  self._safe_setattr(agent, "dsl_spec", spec)
526
 
 
527
  return agent
528
 
529
  def create_behavior(
@@ -533,12 +482,7 @@ class WorldFactory:
533
  owner_agent_id: str | None = None,
534
  behavior_index: int | None = None,
535
  ) -> Behavior:
536
- """Create a runtime behavior object from a ``BehaviorSpec``.
537
-
538
- The DSL does not require behavior ids because behavior specs are declarative.
539
- Runtime ids are injected here so behavior base classes that require ``id``
540
- can still be instantiated safely and deterministically.
541
- """
542
 
543
  target = self._resolve_registry_item(
544
  registry=self._behavior_registry,
@@ -555,9 +499,6 @@ class WorldFactory:
555
  )
556
 
557
  params = copy.deepcopy(spec.params)
558
-
559
- # Runtime-injected fields. These are filtered out automatically if the
560
- # concrete behavior constructor does not accept them.
561
  params.setdefault("id", runtime_behavior_id)
562
  params.setdefault("behavior_id", runtime_behavior_id)
563
  params.setdefault("name", spec.name)
@@ -587,11 +528,7 @@ class WorldFactory:
587
  return behavior
588
 
589
  def create_policy_for_agent(self, agent_spec: AgentSpec) -> BasePolicy | None:
590
- """Create the policy configured for an agent.
591
-
592
- If the agent has behaviors but omits a policy, the factory may attach a
593
- default policy depending on configuration.
594
- """
595
 
596
  policy_spec = agent_spec.policy
597
 
@@ -616,6 +553,7 @@ class WorldFactory:
616
 
617
  params = copy.deepcopy(spec.params)
618
  params.setdefault("enabled", bool(spec.enabled))
 
619
  if spec.metadata:
620
  params.setdefault("metadata", copy.deepcopy(spec.metadata))
621
 
@@ -639,21 +577,32 @@ class WorldFactory:
639
 
640
  return policy
641
 
642
- def create_resource(self, spec: ResourceSpec) -> Resource:
 
 
 
 
 
643
  """Create a runtime ``Resource`` from a ``ResourceSpec``."""
644
 
645
  metadata = copy.deepcopy(spec.metadata)
646
  metadata.setdefault("regeneration_rate", spec.regeneration_rate)
647
  metadata.setdefault("max_amount", spec.max_amount)
648
 
 
 
 
 
 
649
  params = {
650
  "id": spec.id,
651
  "type": spec.type,
652
  "amount": float(spec.amount),
653
- "position": None if spec.position is None else tuple(spec.position),
654
  "metadata": metadata,
655
  "regeneration_rate": float(spec.regeneration_rate),
656
  "max_amount": spec.max_amount,
 
657
  }
658
 
659
  resource = self._instantiate_runtime_object(
@@ -668,12 +617,13 @@ class WorldFactory:
668
  self._safe_setattr(resource, "id", spec.id)
669
  self._safe_setattr(resource, "type", spec.type)
670
  self._safe_setattr(resource, "amount", float(spec.amount))
671
- self._safe_setattr(resource, "position", None if spec.position is None else tuple(spec.position))
672
  self._safe_setattr(resource, "metadata", metadata)
673
  self._safe_setattr(resource, "regeneration_rate", float(spec.regeneration_rate))
674
  self._safe_setattr(resource, "max_amount", spec.max_amount)
675
  self._safe_setattr(resource, "dsl_spec", spec)
676
 
 
677
  return resource
678
 
679
  def create_event(self, spec: EventSpec) -> Event | GenericScheduledEvent:
@@ -728,12 +678,7 @@ class WorldFactory:
728
  return event
729
 
730
  def create_metric_config(self, spec: MetricSpec) -> dict[str, Any]:
731
- """Create a metric configuration record.
732
-
733
- Metric runtime classes are introduced later in the implementation
734
- order. Until then, metric specs are preserved as serializable records on
735
- the world.
736
- """
737
 
738
  return {
739
  "name": spec.name,
@@ -753,12 +698,13 @@ class WorldFactory:
753
  ) -> World:
754
  """Instantiate and populate the runtime world object."""
755
 
756
- agent_collection: Mapping[str, Agent] | tuple[Agent, ...]
757
- resource_collection: Mapping[str, Resource] | tuple[Resource, ...]
758
-
759
  if self.config.world_uses_mapping_collections:
760
- agent_collection = {str(getattr(agent, "id")): agent for agent in agents}
761
- resource_collection = {str(getattr(resource, "id")): resource for resource in resources}
 
 
 
 
762
  else:
763
  agent_collection = tuple(agents)
764
  resource_collection = tuple(resources)
@@ -775,6 +721,7 @@ class WorldFactory:
775
  "events": tuple(events),
776
  "metrics": metric_collection,
777
  "step_count": 0,
 
778
  }
779
 
780
  try:
@@ -790,9 +737,14 @@ class WorldFactory:
790
  logger.debug("Falling back to no-argument World constructor", exc_info=True)
791
  world = self.config.world_class()
792
 
 
 
 
 
793
  self._install_agents(world, agents)
794
  self._install_resources(world, resources)
795
  self._install_events(world, events)
 
796
  self._safe_setattr(world, "metrics", metric_collection)
797
  self._safe_setattr(world, "step_count", 0)
798
 
@@ -807,14 +759,24 @@ class WorldFactory:
807
  self._safe_setattr(world, "agents", [])
808
 
809
  add_agent = getattr(world, "add_agent", None)
 
810
  if callable(add_agent):
811
  for agent in agents:
 
812
  add_agent(agent)
813
  return
814
 
815
  if self.config.world_uses_mapping_collections:
816
- self._safe_setattr(world, "agents", {str(getattr(agent, "id")): agent for agent in agents})
 
 
 
 
 
 
817
  else:
 
 
818
  self._safe_setattr(world, "agents", list(agents))
819
 
820
  def _install_resources(self, world: World, resources: Sequence[Resource]) -> None:
@@ -826,18 +788,24 @@ class WorldFactory:
826
  self._safe_setattr(world, "resources", [])
827
 
828
  add_resource = getattr(world, "add_resource", None)
 
829
  if callable(add_resource):
830
  for resource in resources:
 
831
  add_resource(resource)
832
  return
833
 
834
  if self.config.world_uses_mapping_collections:
 
 
835
  self._safe_setattr(
836
  world,
837
  "resources",
838
  {str(getattr(resource, "id")): resource for resource in resources},
839
  )
840
  else:
 
 
841
  self._safe_setattr(world, "resources", list(resources))
842
 
843
  def _install_events(
@@ -850,6 +818,7 @@ class WorldFactory:
850
  self._safe_setattr(world, "events", [])
851
 
852
  add_event = getattr(world, "add_event", None)
 
853
  if callable(add_event):
854
  for event in events:
855
  add_event(event)
@@ -861,6 +830,7 @@ class WorldFactory:
861
  """Attach a scheduler to the world when a scheduler class is configured."""
862
 
863
  scheduler_class = self.config.scheduler_class
 
864
  if scheduler_class is None:
865
  return
866
 
@@ -903,6 +873,11 @@ class WorldFactory:
903
  self._safe_setattr(world, "space_config", None if spec.space is None else spec.space.to_dict())
904
  self._safe_setattr(world, "schema_version", spec.schema_version)
905
 
 
 
 
 
 
906
  if self.config.attach_dsl_spec_to_world:
907
  self._safe_setattr(world, "dsl_spec", spec)
908
 
@@ -925,6 +900,7 @@ class WorldFactory:
925
  if self.config.strict_constructor_params
926
  else ValidationSeverity.WARNING,
927
  )
 
928
  report = validate_world_spec(spec, config=validation_config)
929
 
930
  if not report.is_valid:
@@ -957,7 +933,10 @@ class WorldFactory:
957
  self._registry_import_errors.update(module_report.import_errors)
958
 
959
  if self.config.behavior_registry is not None:
960
- registry.update({str(key): value for key, value in self.config.behavior_registry.items()})
 
 
 
961
 
962
  return self._with_class_aliases(registry)
963
 
@@ -975,7 +954,10 @@ class WorldFactory:
975
  self._registry_import_errors.update(module_report.import_errors)
976
 
977
  if self.config.policy_registry is not None:
978
- registry.update({str(key): value for key, value in self.config.policy_registry.items()})
 
 
 
979
 
980
  return self._with_class_aliases(registry)
981
 
@@ -994,16 +976,21 @@ class WorldFactory:
994
  for module_name in module_names:
995
  try:
996
  module = import_module(module_name)
997
- except Exception as exc: # pragma: no cover - defensive for user modules
998
  import_errors[str(module_name)] = f"{exc.__class__.__name__}: {exc}"
999
  continue
1000
 
1001
  for attribute_name in registry_attributes:
1002
  raw_registry = getattr(module, attribute_name, None)
1003
  if isinstance(raw_registry, Mapping):
1004
- registry.update({str(key): value for key, value in raw_registry.items()})
 
 
 
1005
 
1006
- registry.update(self._discover_named_subclasses(module, base_class=base_class))
 
 
1007
 
1008
  return RegistryLoadReport(registry=registry, import_errors=import_errors)
1009
 
@@ -1084,7 +1071,11 @@ class WorldFactory:
1084
  raise RegistryResolutionError(message)
1085
 
1086
  logger.warning(message)
1087
- return self._noop_behavior_factory(name) if item_kind == "behavior" else self._noop_policy_factory(name)
 
 
 
 
1088
 
1089
  def _instantiate_runtime_object(
1090
  self,
@@ -1162,6 +1153,7 @@ class WorldFactory:
1162
  break
1163
 
1164
  unknown_params = set(working.keys()) - allowed_params
 
1165
  if unknown_params and strict:
1166
  raise ConstructorBindingError(
1167
  f"Unknown constructor parameter(s) for {item_kind} at {path}: "
@@ -1175,6 +1167,7 @@ class WorldFactory:
1175
  }
1176
 
1177
  missing_required = required_params - set(filtered.keys())
 
1178
  if missing_required:
1179
  raise ConstructorBindingError(
1180
  f"Missing required constructor parameter(s) for {item_kind} at {path}: "
@@ -1221,13 +1214,150 @@ class WorldFactory:
1221
  return allowed_params, required_params, accepts_var_keyword
1222
 
1223
  @staticmethod
1224
- def _safe_setattr(instance: Any, name: str, value: Any) -> bool:
1225
- """Set an attribute defensively.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1226
 
1227
- Returns:
1228
- ``True`` if the attribute was set, otherwise ``False``.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1229
  """
1230
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1231
  try:
1232
  setattr(instance, name, value)
1233
  return True
@@ -1240,6 +1370,24 @@ class WorldFactory:
1240
  )
1241
  return False
1242
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1243
  @staticmethod
1244
  def _noop_behavior_factory(name: str) -> type[Behavior]:
1245
  """Create a no-op behavior class for permissive unknown behavior mode."""
 
18
  -> WorldFactory
19
  -> World
20
 
21
+ Important runtime compatibility notes:
22
+ - DSL positions are JSON arrays, but runtime World/Agent/Resource code may
23
+ expect NumPy arrays with attributes such as ``.size``.
24
+ - This factory converts all runtime agent/resource positions to NumPy arrays.
25
+ - Behavior ids are runtime details and are injected automatically.
26
+ - Agent behavior storage is provided as a mapping keyed by deterministic
27
+ behavior ids.
 
 
 
 
 
 
 
 
 
 
28
  """
29
 
30
  from __future__ import annotations
 
32
  import copy
33
  import inspect
34
  import logging
35
+ from collections.abc import Mapping, Sequence
 
36
  from dataclasses import dataclass, field
37
  from importlib import import_module
38
  from types import MappingProxyType
39
  from typing import Any
40
 
41
+ import numpy as np
42
+
43
  from core.agent import Agent
44
  from core.behavior import Behavior
45
  from core.event import Event
 
63
  )
64
  from policies.base_policy import BasePolicy
65
 
66
+
67
  logger = logging.getLogger(__name__)
68
 
69
+
70
  DEFAULT_BEHAVIOR_MODULES: tuple[str, ...] = (
71
  "behaviors.movement",
72
  "behaviors.consume",
 
89
  )
90
 
91
  BEHAVIOR_REGISTRY_ATTRIBUTES: tuple[str, ...] = ("BEHAVIOR_REGISTRY",)
92
+
93
  POLICY_REGISTRY_ATTRIBUTES: tuple[str, ...] = (
94
  "POLICY_REGISTRY",
95
  "RULE_POLICY_REGISTRY",
 
120
  )
121
 
122
  _CORE_WORLD_ALIASES: Mapping[str, tuple[str, ...]] = MappingProxyType({})
123
+
124
  _CORE_SCHEDULER_ALIASES: Mapping[str, tuple[str, ...]] = MappingProxyType({})
125
 
126
 
 
224
 
225
  @dataclass
226
  class WorldFactoryConfig:
227
+ """Configuration for ``WorldFactory``."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
228
 
229
  validate_before_build: bool = True
230
 
 
258
 
259
  @dataclass
260
  class GenericScheduledEvent:
261
+ """Fallback event object used when ``core.event.Event`` is not instantiable."""
 
 
 
 
 
262
 
263
  name: str
264
  trigger_step: int
 
271
  metadata: Mapping[str, Any] = field(default_factory=dict)
272
 
273
  def execute(self, world: World) -> dict[str, Any]:
274
+ """Execute the event in a domain-agnostic way."""
 
 
 
 
 
275
 
276
  return {
277
  "event": self.name,
 
287
 
288
  @dataclass
289
  class WorldFactory:
290
+ """Factory that converts ``WorldSpec`` into a runtime ``World``."""
 
 
 
 
 
291
 
292
  config: WorldFactoryConfig = field(default_factory=WorldFactoryConfig)
293
 
 
318
  """Return registry import errors collected during initialization."""
319
 
320
  return MappingProxyType(self._registry_import_errors)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
321
 
322
  def create_world(self, spec: WorldSpec | Mapping[str, Any]) -> World:
323
+ """Create a runtime world from a validated or mappable world spec."""
 
 
 
 
 
 
 
 
 
 
 
324
 
325
  return self.create_world_result(spec).world
326
 
 
330
  world_spec = self._coerce_world_spec(spec)
331
  validation_report = self._validate_before_build(world_spec)
332
 
333
+ space_dimensions = (
334
+ int(world_spec.space.dimensions)
335
+ if world_spec.space is not None
336
+ else None
337
+ )
338
+
339
+ resources = tuple(
340
+ self.create_resource(resource_spec, dimensions=space_dimensions)
341
+ for resource_spec in world_spec.resources
342
+ )
343
+
344
  events = tuple(
345
  self.create_event(event_spec)
346
  for event_spec in world_spec.events
347
  if event_spec.enabled or self.config.include_disabled_events
348
  )
349
+
350
+ agents = tuple(
351
+ self.create_agent(agent_spec, dimensions=space_dimensions)
352
+ for agent_spec in world_spec.agents
353
+ )
354
+
355
+ metrics = tuple(
356
+ self.create_metric_config(metric_spec)
357
+ for metric_spec in world_spec.metrics
358
+ )
359
 
360
  world = self._create_world_object(
361
  spec=world_spec,
 
396
 
397
  return WorldBuildResult(world=world, report=report)
398
 
399
+ def create_agent(
400
+ self,
401
+ spec: AgentSpec,
402
+ *,
403
+ dimensions: int | None = None,
404
+ ) -> Agent:
405
  """Create a runtime ``Agent`` from an ``AgentSpec``."""
406
 
407
+ behavior_map: dict[str, Behavior] = {}
408
+
409
+ for behavior_index, behavior_spec in enumerate(spec.behaviors):
410
+ if not behavior_spec.enabled and not self.config.include_disabled_behaviors:
411
+ continue
412
+
413
+ behavior = self.create_behavior(
414
  behavior_spec,
415
  owner_agent_id=spec.id,
416
  behavior_index=behavior_index,
417
  )
418
+
419
+ behavior_id = str(
420
+ getattr(behavior, "id", "")
421
+ or self._behavior_runtime_id(
422
+ behavior_spec,
423
+ owner_agent_id=spec.id,
424
+ behavior_index=behavior_index,
425
+ )
426
+ )
427
+
428
+ if behavior_id in behavior_map:
429
+ behavior_id = f"{behavior_id}#{behavior_index}"
430
+
431
+ behavior_map[behavior_id] = behavior
432
+
433
  policy = self.create_policy_for_agent(spec)
434
 
435
+ runtime_position = self._agent_runtime_position(
436
+ spec.position,
437
+ dimensions=dimensions,
438
+ )
439
 
440
  params = {
441
  "id": spec.id,
442
  "type": spec.type,
443
+ "position": runtime_position.copy(),
444
  "state": copy.deepcopy(spec.state),
445
  "memory": copy.deepcopy(spec.memory),
446
  "goals": copy.deepcopy(spec.goals),
447
+ "behaviors": behavior_map,
448
  "policy": policy,
449
  "alive": bool(spec.alive),
450
+ "metadata": copy.deepcopy(spec.metadata),
451
+ "dsl_spec": spec,
452
  }
453
 
454
  agent = self._instantiate_runtime_object(
 
462
 
463
  self._safe_setattr(agent, "id", spec.id)
464
  self._safe_setattr(agent, "type", spec.type)
465
+ self._force_setattr(agent, "position", runtime_position.copy())
466
  self._safe_setattr(agent, "state", copy.deepcopy(spec.state))
467
  self._safe_setattr(agent, "memory", copy.deepcopy(spec.memory))
468
  self._safe_setattr(agent, "goals", copy.deepcopy(spec.goals))
469
+ self._safe_setattr(agent, "behaviors", behavior_map)
470
  self._safe_setattr(agent, "policy", policy)
471
  self._safe_setattr(agent, "alive", bool(spec.alive))
472
  self._safe_setattr(agent, "metadata", copy.deepcopy(spec.metadata))
473
  self._safe_setattr(agent, "dsl_spec", spec)
474
 
475
+ self._ensure_agent_position_array(agent, dimensions=dimensions)
476
  return agent
477
 
478
  def create_behavior(
 
482
  owner_agent_id: str | None = None,
483
  behavior_index: int | None = None,
484
  ) -> Behavior:
485
+ """Create a runtime behavior object from a ``BehaviorSpec``."""
 
 
 
 
 
486
 
487
  target = self._resolve_registry_item(
488
  registry=self._behavior_registry,
 
499
  )
500
 
501
  params = copy.deepcopy(spec.params)
 
 
 
502
  params.setdefault("id", runtime_behavior_id)
503
  params.setdefault("behavior_id", runtime_behavior_id)
504
  params.setdefault("name", spec.name)
 
528
  return behavior
529
 
530
  def create_policy_for_agent(self, agent_spec: AgentSpec) -> BasePolicy | None:
531
+ """Create the policy configured for an agent."""
 
 
 
 
532
 
533
  policy_spec = agent_spec.policy
534
 
 
553
 
554
  params = copy.deepcopy(spec.params)
555
  params.setdefault("enabled", bool(spec.enabled))
556
+
557
  if spec.metadata:
558
  params.setdefault("metadata", copy.deepcopy(spec.metadata))
559
 
 
577
 
578
  return policy
579
 
580
+ def create_resource(
581
+ self,
582
+ spec: ResourceSpec,
583
+ *,
584
+ dimensions: int | None = None,
585
+ ) -> Resource:
586
  """Create a runtime ``Resource`` from a ``ResourceSpec``."""
587
 
588
  metadata = copy.deepcopy(spec.metadata)
589
  metadata.setdefault("regeneration_rate", spec.regeneration_rate)
590
  metadata.setdefault("max_amount", spec.max_amount)
591
 
592
+ runtime_position = self._resource_runtime_position(
593
+ spec.position,
594
+ dimensions=dimensions,
595
+ )
596
+
597
  params = {
598
  "id": spec.id,
599
  "type": spec.type,
600
  "amount": float(spec.amount),
601
+ "position": runtime_position.copy(),
602
  "metadata": metadata,
603
  "regeneration_rate": float(spec.regeneration_rate),
604
  "max_amount": spec.max_amount,
605
+ "dsl_spec": spec,
606
  }
607
 
608
  resource = self._instantiate_runtime_object(
 
617
  self._safe_setattr(resource, "id", spec.id)
618
  self._safe_setattr(resource, "type", spec.type)
619
  self._safe_setattr(resource, "amount", float(spec.amount))
620
+ self._force_setattr(resource, "position", runtime_position.copy())
621
  self._safe_setattr(resource, "metadata", metadata)
622
  self._safe_setattr(resource, "regeneration_rate", float(spec.regeneration_rate))
623
  self._safe_setattr(resource, "max_amount", spec.max_amount)
624
  self._safe_setattr(resource, "dsl_spec", spec)
625
 
626
+ self._ensure_resource_position_array(resource, dimensions=dimensions)
627
  return resource
628
 
629
  def create_event(self, spec: EventSpec) -> Event | GenericScheduledEvent:
 
678
  return event
679
 
680
  def create_metric_config(self, spec: MetricSpec) -> dict[str, Any]:
681
+ """Create a metric configuration record."""
 
 
 
 
 
682
 
683
  return {
684
  "name": spec.name,
 
698
  ) -> World:
699
  """Instantiate and populate the runtime world object."""
700
 
 
 
 
701
  if self.config.world_uses_mapping_collections:
702
+ agent_collection: Mapping[str, Agent] | tuple[Agent, ...] = {
703
+ str(getattr(agent, "id")): agent for agent in agents
704
+ }
705
+ resource_collection: Mapping[str, Resource] | tuple[Resource, ...] = {
706
+ str(getattr(resource, "id")): resource for resource in resources
707
+ }
708
  else:
709
  agent_collection = tuple(agents)
710
  resource_collection = tuple(resources)
 
721
  "events": tuple(events),
722
  "metrics": metric_collection,
723
  "step_count": 0,
724
+ "metadata": copy.deepcopy(spec.metadata),
725
  }
726
 
727
  try:
 
737
  logger.debug("Falling back to no-argument World constructor", exc_info=True)
738
  world = self.config.world_class()
739
 
740
+ # Attach metadata before installation so World.add_agent/add_resource can
741
+ # see space/dimension configuration if the World implementation uses it.
742
+ self._attach_world_metadata(world, spec)
743
+
744
  self._install_agents(world, agents)
745
  self._install_resources(world, resources)
746
  self._install_events(world, events)
747
+
748
  self._safe_setattr(world, "metrics", metric_collection)
749
  self._safe_setattr(world, "step_count", 0)
750
 
 
759
  self._safe_setattr(world, "agents", [])
760
 
761
  add_agent = getattr(world, "add_agent", None)
762
+
763
  if callable(add_agent):
764
  for agent in agents:
765
+ self._ensure_agent_position_array(agent)
766
  add_agent(agent)
767
  return
768
 
769
  if self.config.world_uses_mapping_collections:
770
+ for agent in agents:
771
+ self._ensure_agent_position_array(agent)
772
+ self._safe_setattr(
773
+ world,
774
+ "agents",
775
+ {str(getattr(agent, "id")): agent for agent in agents},
776
+ )
777
  else:
778
+ for agent in agents:
779
+ self._ensure_agent_position_array(agent)
780
  self._safe_setattr(world, "agents", list(agents))
781
 
782
  def _install_resources(self, world: World, resources: Sequence[Resource]) -> None:
 
788
  self._safe_setattr(world, "resources", [])
789
 
790
  add_resource = getattr(world, "add_resource", None)
791
+
792
  if callable(add_resource):
793
  for resource in resources:
794
+ self._ensure_resource_position_array(resource)
795
  add_resource(resource)
796
  return
797
 
798
  if self.config.world_uses_mapping_collections:
799
+ for resource in resources:
800
+ self._ensure_resource_position_array(resource)
801
  self._safe_setattr(
802
  world,
803
  "resources",
804
  {str(getattr(resource, "id")): resource for resource in resources},
805
  )
806
  else:
807
+ for resource in resources:
808
+ self._ensure_resource_position_array(resource)
809
  self._safe_setattr(world, "resources", list(resources))
810
 
811
  def _install_events(
 
818
  self._safe_setattr(world, "events", [])
819
 
820
  add_event = getattr(world, "add_event", None)
821
+
822
  if callable(add_event):
823
  for event in events:
824
  add_event(event)
 
830
  """Attach a scheduler to the world when a scheduler class is configured."""
831
 
832
  scheduler_class = self.config.scheduler_class
833
+
834
  if scheduler_class is None:
835
  return
836
 
 
873
  self._safe_setattr(world, "space_config", None if spec.space is None else spec.space.to_dict())
874
  self._safe_setattr(world, "schema_version", spec.schema_version)
875
 
876
+ if spec.space is not None:
877
+ self._safe_setattr(world, "dimensions", int(spec.space.dimensions))
878
+ self._safe_setattr(world, "dimension", int(spec.space.dimensions))
879
+ self._safe_setattr(world, "space_dimensions", int(spec.space.dimensions))
880
+
881
  if self.config.attach_dsl_spec_to_world:
882
  self._safe_setattr(world, "dsl_spec", spec)
883
 
 
900
  if self.config.strict_constructor_params
901
  else ValidationSeverity.WARNING,
902
  )
903
+
904
  report = validate_world_spec(spec, config=validation_config)
905
 
906
  if not report.is_valid:
 
933
  self._registry_import_errors.update(module_report.import_errors)
934
 
935
  if self.config.behavior_registry is not None:
936
+ registry.update({
937
+ str(key): value
938
+ for key, value in self.config.behavior_registry.items()
939
+ })
940
 
941
  return self._with_class_aliases(registry)
942
 
 
954
  self._registry_import_errors.update(module_report.import_errors)
955
 
956
  if self.config.policy_registry is not None:
957
+ registry.update({
958
+ str(key): value
959
+ for key, value in self.config.policy_registry.items()
960
+ })
961
 
962
  return self._with_class_aliases(registry)
963
 
 
976
  for module_name in module_names:
977
  try:
978
  module = import_module(module_name)
979
+ except Exception as exc:
980
  import_errors[str(module_name)] = f"{exc.__class__.__name__}: {exc}"
981
  continue
982
 
983
  for attribute_name in registry_attributes:
984
  raw_registry = getattr(module, attribute_name, None)
985
  if isinstance(raw_registry, Mapping):
986
+ registry.update({
987
+ str(key): value
988
+ for key, value in raw_registry.items()
989
+ })
990
 
991
+ registry.update(
992
+ self._discover_named_subclasses(module, base_class=base_class)
993
+ )
994
 
995
  return RegistryLoadReport(registry=registry, import_errors=import_errors)
996
 
 
1071
  raise RegistryResolutionError(message)
1072
 
1073
  logger.warning(message)
1074
+
1075
+ if item_kind == "behavior":
1076
+ return self._noop_behavior_factory(name)
1077
+
1078
+ return self._noop_policy_factory(name)
1079
 
1080
  def _instantiate_runtime_object(
1081
  self,
 
1153
  break
1154
 
1155
  unknown_params = set(working.keys()) - allowed_params
1156
+
1157
  if unknown_params and strict:
1158
  raise ConstructorBindingError(
1159
  f"Unknown constructor parameter(s) for {item_kind} at {path}: "
 
1167
  }
1168
 
1169
  missing_required = required_params - set(filtered.keys())
1170
+
1171
  if missing_required:
1172
  raise ConstructorBindingError(
1173
  f"Missing required constructor parameter(s) for {item_kind} at {path}: "
 
1214
  return allowed_params, required_params, accepts_var_keyword
1215
 
1216
  @staticmethod
1217
+ def _behavior_runtime_id(
1218
+ spec: BehaviorSpec,
1219
+ *,
1220
+ owner_agent_id: str | None,
1221
+ behavior_index: int | None,
1222
+ ) -> str:
1223
+ """Return a deterministic runtime id for a behavior spec."""
1224
+
1225
+ metadata_id = None
1226
+
1227
+ if isinstance(spec.metadata, Mapping):
1228
+ metadata_id = spec.metadata.get("id") or spec.metadata.get("behavior_id")
1229
+
1230
+ if metadata_id is not None and str(metadata_id).strip():
1231
+ return str(metadata_id).strip()
1232
+
1233
+ owner = owner_agent_id or "world"
1234
+ index = 0 if behavior_index is None else int(behavior_index)
1235
+ return f"{owner}:behavior:{index}:{spec.name}"
1236
+
1237
+ @staticmethod
1238
+ def _agent_runtime_position(
1239
+ position: Any,
1240
+ *,
1241
+ dimensions: int | None = None,
1242
+ ) -> np.ndarray:
1243
+ """Return a NumPy position vector compatible with runtime agents."""
1244
+
1245
+ return WorldFactory._runtime_position(
1246
+ position,
1247
+ dimensions=dimensions,
1248
+ object_label="Agent",
1249
+ )
1250
 
1251
+ @staticmethod
1252
+ def _resource_runtime_position(
1253
+ position: Any,
1254
+ *,
1255
+ dimensions: int | None = None,
1256
+ ) -> np.ndarray:
1257
+ """Return a NumPy position vector compatible with runtime resources."""
1258
+
1259
+ return WorldFactory._runtime_position(
1260
+ position,
1261
+ dimensions=dimensions,
1262
+ object_label="Resource",
1263
+ )
1264
+
1265
+ @staticmethod
1266
+ def _runtime_position(
1267
+ position: Any,
1268
+ *,
1269
+ dimensions: int | None = None,
1270
+ object_label: str = "Object",
1271
+ ) -> np.ndarray:
1272
+ """Return a NumPy runtime position vector.
1273
+
1274
+ Runtime World implementations may validate positions with NumPy-style
1275
+ attributes such as ``position.size``. Therefore positions must not be
1276
+ tuples by the time objects are installed into the World.
1277
  """
1278
 
1279
+ target_dimensions = (
1280
+ int(dimensions)
1281
+ if dimensions is not None and int(dimensions) > 0
1282
+ else 2
1283
+ )
1284
+
1285
+ if position is None:
1286
+ return np.zeros(target_dimensions, dtype=np.float64)
1287
+
1288
+ try:
1289
+ array = np.asarray(position, dtype=np.float64).reshape(-1)
1290
+ except (TypeError, ValueError) as exc:
1291
+ raise RuntimeObjectBuildError(
1292
+ f"{object_label} position must be numeric, got {position!r}"
1293
+ ) from exc
1294
+
1295
+ if array.size == 0:
1296
+ return np.zeros(target_dimensions, dtype=np.float64)
1297
+
1298
+ if not np.all(np.isfinite(array)):
1299
+ raise RuntimeObjectBuildError(
1300
+ f"{object_label} position must contain only finite values, got {position!r}"
1301
+ )
1302
+
1303
+ if int(array.size) != target_dimensions:
1304
+ raise RuntimeObjectBuildError(
1305
+ f"{object_label} position has {int(array.size)} dimension(s), "
1306
+ f"but world space expects {target_dimensions}"
1307
+ )
1308
+
1309
+ return array.astype(np.float64, copy=True)
1310
+
1311
+ def _ensure_agent_position_array(
1312
+ self,
1313
+ agent: Agent,
1314
+ *,
1315
+ dimensions: int | None = None,
1316
+ ) -> None:
1317
+ """Ensure agent.position is a NumPy array before adding it to World."""
1318
+
1319
+ raw_position = getattr(agent, "position", None)
1320
+ runtime_position = self._agent_runtime_position(
1321
+ raw_position,
1322
+ dimensions=dimensions,
1323
+ )
1324
+ self._force_setattr(agent, "position", runtime_position.copy())
1325
+
1326
+ repaired_position = getattr(agent, "position", None)
1327
+ if not hasattr(repaired_position, "size"):
1328
+ raise RuntimeObjectBuildError(
1329
+ "Agent.position could not be converted to a NumPy array before "
1330
+ f"world installation. Agent={getattr(agent, 'id', None)!r}, "
1331
+ f"position_type={type(repaired_position).__name__}"
1332
+ )
1333
+
1334
+ def _ensure_resource_position_array(
1335
+ self,
1336
+ resource: Resource,
1337
+ *,
1338
+ dimensions: int | None = None,
1339
+ ) -> None:
1340
+ """Ensure resource.position is a NumPy array before adding it to World."""
1341
+
1342
+ raw_position = getattr(resource, "position", None)
1343
+ runtime_position = self._resource_runtime_position(
1344
+ raw_position,
1345
+ dimensions=dimensions,
1346
+ )
1347
+ self._force_setattr(resource, "position", runtime_position.copy())
1348
+
1349
+ repaired_position = getattr(resource, "position", None)
1350
+ if not hasattr(repaired_position, "size"):
1351
+ raise RuntimeObjectBuildError(
1352
+ "Resource.position could not be converted to a NumPy array before "
1353
+ f"world installation. Resource={getattr(resource, 'id', None)!r}, "
1354
+ f"position_type={type(repaired_position).__name__}"
1355
+ )
1356
+
1357
+ @staticmethod
1358
+ def _safe_setattr(instance: Any, name: str, value: Any) -> bool:
1359
+ """Set an attribute defensively."""
1360
+
1361
  try:
1362
  setattr(instance, name, value)
1363
  return True
 
1370
  )
1371
  return False
1372
 
1373
+ @staticmethod
1374
+ def _force_setattr(instance: Any, name: str, value: Any) -> None:
1375
+ """Set an attribute, using object.__setattr__ as a fallback."""
1376
+
1377
+ try:
1378
+ setattr(instance, name, value)
1379
+ return
1380
+ except Exception:
1381
+ pass
1382
+
1383
+ try:
1384
+ object.__setattr__(instance, name, value)
1385
+ return
1386
+ except Exception as exc:
1387
+ raise RuntimeObjectBuildError(
1388
+ f"Could not set attribute {name!r} on {instance.__class__.__name__}"
1389
+ ) from exc
1390
+
1391
  @staticmethod
1392
  def _noop_behavior_factory(name: str) -> type[Behavior]:
1393
  """Create a no-op behavior class for permissive unknown behavior mode."""