Srishti280992 commited on
Commit
566a38d
·
verified ·
1 Parent(s): f52ca12

Update llm/world_generator.py

Browse files
Files changed (1) hide show
  1. llm/world_generator.py +1066 -1183
llm/world_generator.py CHANGED
@@ -1,1419 +1,1302 @@
1
  """
2
- Natural language to WorldSmithAI DSL generation.
3
-
4
- This module converts user prompts into ``WorldSpec`` JSON. It is designed for
5
- root-level Hugging Face Spaces ``app.py`` usage and does not assume any
6
- ``app/`` package or folder structure.
7
-
8
- The SLM's responsibility is strictly:
9
- Natural language -> JSON DSL
10
-
11
- The SLM must not generate Python code. The deterministic Python engine consumes
12
- only validated ``WorldSpec`` objects.
13
-
14
- Example root-level app.py usage:
15
- from llm.world_generator import generate_world_spec
 
 
 
 
 
 
 
16
  from factory.world_factory import WorldFactory
17
 
18
- def run(prompt: str):
19
- spec = generate_world_spec(prompt)
20
- world = WorldFactory().create_world(spec)
21
- return spec.to_json_string(), world
22
 
23
- Using a custom SLM client:
24
- generator = WorldGenerator(client=my_client)
25
- result = generator.generate("A research ecosystem with competing labs")
26
- print(result.json_text)
27
 
28
  Future extensibility:
29
- - Add first-class Hugging Face transformers adapters.
30
- - Add constrained decoding when available.
31
- - Add schema-version migrations.
32
- - Add model-specific prompt profiles.
33
- - Add automatic JSON repair loops.
34
- - Add world-quality self-critique before factory construction.
35
  """
36
 
37
  from __future__ import annotations
38
 
39
  import copy
40
  import inspect
41
- import json
42
  import logging
43
- import re
44
- from collections.abc import Callable, Mapping, Sequence
45
  from dataclasses import dataclass, field
46
- from enum import Enum
47
- from typing import Any, Protocol, runtime_checkable
48
-
49
- from dsl.parser import DSLParseError, ParseResult, WorldDSLParser
 
 
 
 
 
 
50
  from dsl.schema import (
51
  AgentSpec,
52
  BehaviorSpec,
53
  EventSpec,
 
54
  PolicySpec,
55
  ResourceSpec,
56
- SimulationSpec,
57
- SpaceSpec,
58
  WorldSpec,
59
  )
60
- from dsl.validator import ValidationConfig, ValidationReport, ValidationSeverity, validate_world_spec
 
 
 
 
 
 
61
 
62
  logger = logging.getLogger(__name__)
63
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
64
 
65
- DEFAULT_SYSTEM_PROMPT = """
66
- You are WorldSmithAI's world DSL generator.
67
-
68
- Your only job is to convert a user's natural language description into a
69
- WorldSmithAI WorldSpec JSON object.
70
-
71
- Critical rules:
72
- - Output JSON only.
73
- - Do not include Markdown fences.
74
- - Do not include explanations.
75
- - Do not generate Python code.
76
- - Do not invent classes such as Sheep, Wolf, Scientist, Dragon, City, or Truck.
77
- - Use generic DSL entities: agents, resources, events, behaviors, policies.
78
- - Agents may have any imaginative type string, but the runtime object is still Agent.
79
- - Behaviors must be referenced by registry name.
80
- - Policies must be referenced by registry type.
81
- - Keep the world small enough for a lightweight Gradio demo.
82
- - Prefer 4 to 8 agents unless the user asks for more.
83
- - Prefer 2D positions when possible.
84
- - Include state, memory, goals, behaviors, and policy for each agent.
85
- - Use deterministic values and avoid randomness in the DSL.
86
- """.strip()
87
-
88
-
89
- WORLD_DSL_FORMAT_GUIDE = """
90
- Return one JSON object with this structure:
91
-
92
- {
93
- "schema_version": "1.0",
94
- "id": "short_world_id",
95
- "name": "Human readable world name",
96
- "description": "Short description",
97
- "simulation": {
98
- "steps": 50,
99
- "seed": 0,
100
- "scheduler": "sequential",
101
- "activation": "sequential",
102
- "collect_history": true
103
- },
104
- "space": {
105
- "dimensions": 2,
106
- "bounds": [[0, 10], [0, 10]],
107
- "toroidal": false,
108
- "enforce_bounds": true
109
- },
110
- "agents": [
111
  {
112
- "id": "agent_1",
113
- "type": "generic_type",
114
- "position": [1, 1],
115
- "state": {"energy": 10, "credits": 5},
116
- "memory": {"known_options": {"cooperate": 1.0}},
117
- "goals": [{"id": "survive", "importance": 1.0, "score": 1.0}],
118
- "behaviors": [
119
- {"name": "remember", "params": {"category": "observation", "content": {"note": "initial memory"}}},
120
- {"name": "prioritize", "params": {}},
121
- {"name": "choose_goal", "params": {}},
122
- {"name": "communicate", "params": {"message": "status_update", "max_recipients": 1}},
123
- {"name": "cooperate", "params": {"effort": 1.0}}
124
- ],
125
- "policy": {
126
- "type": "rule_policy",
127
- "params": {
128
- "rules": [
129
- {"behavior_name": "choose_goal", "score_delta": 3.0},
130
- {"behavior_name": "communicate", "score_delta": 1.0}
131
- ]
132
- }
133
- },
134
- "alive": true,
135
- "metadata": {}
136
  }
137
- ],
138
- "resources": [
 
139
  {
140
- "id": "resource_1",
141
- "type": "generic_resource",
142
- "amount": 10,
143
- "position": [5, 5],
144
- "regeneration_rate": 0,
145
- "max_amount": 20,
146
- "metadata": {}
147
  }
148
- ],
149
- "events": [],
150
- "metrics": [
151
- {"name": "diversity", "params": {"collection": "agents", "group_by_path": "type"}}
152
- ],
153
- "metadata": {}
154
- }
155
-
156
- Known behavior names available in this project include:
157
- move, wander, seek, flee,
158
- eat, harvest, mine, collect,
159
- buy, sell, exchange,
160
- attack, defend, raid,
161
- study, publish, learn,
162
- cooperate, communicate, share, recommend, negotiate,
163
- route, deliver, ship, queue, charge,
164
- build, repair, expand, demolish,
165
- regulate, tax, subsidize, litigate, approve, enforce,
166
- bid, ask, price, discount, forecast, rebalance,
167
- adopt, switch, imitate, abandon, evaluate,
168
- prioritize, schedule, allocate_time, choose_goal,
169
- remember, forget, reinforce, update_belief.
170
-
171
- Known policy types include:
172
- rule_policy, contextual_bandit.
173
- """.strip()
174
-
175
-
176
- DEFAULT_MINIMAL_EXAMPLE = {
177
- "schema_version": "1.0",
178
- "id": "tiny_research_world",
179
- "name": "Tiny Research World",
180
- "description": "A small generic research ecosystem.",
181
- "simulation": {
182
- "steps": 40,
183
- "seed": 0,
184
- "scheduler": "sequential",
185
- "activation": "sequential",
186
- "collect_history": True,
187
- },
188
- "space": {
189
- "dimensions": 2,
190
- "bounds": [[0, 10], [0, 10]],
191
- "toroidal": False,
192
- "enforce_bounds": True,
193
- },
194
- "agents": [
195
- {
196
- "id": "scientist_1",
197
- "type": "scientist",
198
- "position": [1, 1],
199
- "state": {"energy": 10, "credits": 5, "knowledge": 1},
200
- "memory": {"known_options": {"study": 1.0, "communicate": 0.5}},
201
- "goals": [{"id": "discover", "importance": 2.0, "score": 2.0}],
202
- "behaviors": [
203
- {
204
- "name": "remember",
205
- "params": {
206
- "category": "observation",
207
- "content": {"note": "initial research context"},
208
- },
209
- },
210
- {"name": "prioritize", "params": {}},
211
- {"name": "choose_goal", "params": {}},
212
- {"name": "communicate", "params": {"message": "research_update"}},
213
- {"name": "cooperate", "params": {"goal": "discover", "effort": 1.0}},
214
- ],
215
- "policy": {
216
- "type": "rule_policy",
217
- "params": {
218
- "rules": [
219
- {"behavior_name": "choose_goal", "score_delta": 3.0},
220
- {"behavior_name": "communicate", "score_delta": 2.0},
221
- {"behavior_name": "cooperate", "score_delta": 1.0},
222
- ]
223
- },
224
- },
225
- "alive": True,
226
- "metadata": {},
227
- }
228
- ],
229
- "resources": [
230
- {
231
- "id": "knowledge_pool",
232
- "type": "knowledge",
233
- "amount": 12,
234
- "position": [5, 5],
235
- "regeneration_rate": 0.2,
236
- "max_amount": 20,
237
- "metadata": {},
238
- }
239
- ],
240
- "events": [],
241
- "metrics": [
242
- {"name": "diversity", "params": {"collection": "agents", "group_by_path": "type"}},
243
- {"name": "entropy", "params": {"collection": "agents", "value_path": "type"}},
244
- {"name": "interestingness", "params": {"collection": "agents", "group_by_path": "type"}},
245
- ],
246
- "metadata": {"generated_by": "worldsmithai_example"},
247
- }
248
 
 
 
 
 
 
 
 
 
249
 
250
- class GenerationMode(str, Enum):
251
- """Supported generation modes."""
252
 
253
- MODEL = "Qwen/Qwen2.5-3B"
254
- FALLBACK = "fallback"
255
- AUTO = "auto"
256
 
 
 
257
 
258
- class ResponseFormat(str, Enum):
259
- """Expected model response format."""
260
 
261
- JSON_OBJECT = "json_object"
262
- TEXT_WITH_JSON = "text_with_json"
263
 
264
 
265
- class WorldGenerationError(RuntimeError):
266
- """Raised when natural-language-to-DSL generation fails."""
267
 
268
 
269
- class ModelClientError(WorldGenerationError):
270
- """Raised when an SLM client cannot be called successfully."""
271
 
272
 
273
- class WorldGenerationValidationError(WorldGenerationError):
274
- """Raised when generated DSL fails strict validation."""
275
 
276
  def __init__(self, report: ValidationReport) -> None:
277
- """Initialize the exception with a semantic validation report."""
278
 
279
  self.report = report
280
  super().__init__(report.summary())
281
 
282
 
283
- @runtime_checkable
284
- class SupportsGenerate(Protocol):
285
- """Protocol for simple text generation clients."""
286
-
287
- def generate(self, *args: Any, **kwargs: Any) -> Any:
288
- """Generate text from prompt-like arguments."""
289
-
290
-
291
  @dataclass(frozen=True)
292
- class PromptBundle:
293
- """Prompt payload sent to an SLM client.
294
 
295
- The bundle includes both chat-style messages and flattened prompt text so
296
- many different client APIs can be supported from root-level ``app.py``.
297
- """
298
-
299
- system_prompt: str
300
- user_prompt: str
301
- messages: tuple[Mapping[str, str], ...]
302
- metadata: Mapping[str, Any] = field(default_factory=dict)
303
 
304
  @property
305
- def text(self) -> str:
306
- """Return a flattened prompt for completion-style models."""
307
 
308
- return f"{self.system_prompt}\n\nUSER REQUEST:\n{self.user_prompt}"
309
 
310
  def to_dict(self) -> dict[str, Any]:
311
- """Return a JSON-friendly prompt bundle."""
312
 
313
  return {
314
- "system_prompt": self.system_prompt,
315
- "user_prompt": self.user_prompt,
316
- "messages": [dict(message) for message in self.messages],
317
- "metadata": dict(self.metadata),
318
  }
319
 
320
 
321
  @dataclass(frozen=True)
322
- class WorldGenerationResult:
323
- """Result returned by ``WorldGenerator.generate``.
324
-
325
- Attributes:
326
- spec: Validated world specification.
327
- raw_response: Raw text returned by the model, or fallback JSON text.
328
- parse_result: Parser result when available.
329
- validation_report: Optional semantic validation report.
330
- mode: Whether model or deterministic fallback produced the final spec.
331
- prompt_bundle: Prompt used for model generation when available.
332
- metadata: Additional diagnostics useful for Gradio display.
333
- """
334
-
335
- spec: WorldSpec
336
- raw_response: str
337
- parse_result: ParseResult | None = None
338
  validation_report: ValidationReport | None = None
339
- mode: GenerationMode = GenerationMode.MODEL
340
- prompt_bundle: PromptBundle | None = None
341
  metadata: Mapping[str, Any] = field(default_factory=dict)
342
 
343
  @property
344
- def json_text(self) -> str:
345
- """Return the generated world spec as formatted JSON."""
346
-
347
- return self.spec.to_json_string(indent=2, exclude_none=True)
348
-
349
- @property
350
- def is_semantically_valid(self) -> bool:
351
- """Return whether semantic validation passed or was not run."""
352
 
353
  return self.validation_report is None or self.validation_report.is_valid
354
 
355
  def to_dict(self) -> dict[str, Any]:
356
- """Return a JSON-friendly result summary."""
357
 
358
  return {
359
- "world_id": self.spec.id,
360
- "world_name": self.spec.name,
361
- "mode": self.mode.value,
362
- "agent_count": len(self.spec.agents),
363
- "resource_count": len(self.spec.resources),
364
- "event_count": len(self.spec.events),
365
- "behavior_count": len(self.spec.behavior_names),
366
- "is_semantically_valid": self.is_semantically_valid,
 
 
367
  "validation_report": None
368
  if self.validation_report is None
369
  else self.validation_report.to_dict(),
370
- "parse_result": None if self.parse_result is None else self.parse_result.to_dict(),
371
  "metadata": copy.deepcopy(dict(self.metadata)),
372
  }
373
 
374
 
 
 
 
 
 
 
 
 
375
  @dataclass
376
- class WorldGenerationConfig:
377
- """Configuration for ``WorldGenerator``.
378
-
379
- Defaults are designed for a Gradio demo:
380
- - model output is parsed strictly,
381
- - semantic validation runs but does not raise by default,
382
- - deterministic fallback is enabled,
383
- - generated worlds stay small enough for interactive simulation.
 
 
 
 
 
 
 
 
 
 
384
  """
385
 
386
- mode: GenerationMode | str = GenerationMode.AUTO
387
- response_format: ResponseFormat | str = ResponseFormat.JSON_OBJECT
388
 
389
- system_prompt: str = DEFAULT_SYSTEM_PROMPT
390
- include_format_guide: bool = True
391
- include_example: bool = True
392
- include_json_schema_excerpt: bool = False
393
 
394
- default_steps: int = 60
395
- default_seed: int = 0
396
- max_agents_hint: int = 8
397
- max_resources_hint: int = 6
398
 
399
- parser: WorldDSLParser = field(default_factory=WorldDSLParser)
 
400
 
401
- semantic_validation: bool = True
402
- strict_semantic_validation: bool = False
403
- semantic_validation_config: ValidationConfig | None = None
404
 
405
- fallback_on_model_error: bool = True
406
- fallback_on_parse_error: bool = True
407
- fallback_on_semantic_error: bool = False
408
 
409
- repair_attempts: int = 1
410
- model_kwargs: Mapping[str, Any] = field(default_factory=dict)
411
 
412
- metadata: Mapping[str, Any] = field(default_factory=dict)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
413
 
414
- def resolved_mode(self) -> GenerationMode:
415
- """Return normalized generation mode."""
 
 
 
 
 
 
 
416
 
417
- if isinstance(self.mode, GenerationMode):
418
- return self.mode
419
- return GenerationMode(str(self.mode))
420
 
421
- def resolved_response_format(self) -> ResponseFormat:
422
- """Return normalized response format."""
 
 
423
 
424
- if isinstance(self.response_format, ResponseFormat):
425
- return self.response_format
426
- return ResponseFormat(str(self.response_format))
 
 
 
 
 
 
 
427
 
428
 
429
  @dataclass
430
- class DeterministicWorldSpecBuilder:
431
- """Build a valid generic fallback ``WorldSpec`` without an SLM.
432
 
433
- This fallback exists so the Gradio demo can still run when no SLM client is
434
- configured or when the model returns invalid JSON. It does not replace the
435
- SLM; it provides a safe baseline.
436
  """
437
 
438
- default_steps: int = 60
439
- default_seed: int = 0
440
- max_agents: int = 6
441
 
442
- def build(
443
- self,
444
- prompt: str,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
445
  *,
446
- constraints: Mapping[str, Any] | str | None = None,
447
- ) -> WorldSpec:
448
- """Build a deterministic world spec from prompt keywords."""
449
-
450
- domain = self._infer_domain(prompt)
451
- world_id = _slugify(f"{domain}_world")
452
- roles = self._roles_for_domain(domain)[: max(1, int(self.max_agents))]
453
- resources = self._resources_for_domain(domain)
454
- goals = self._goals_for_domain(domain)
455
-
456
- agents = tuple(
457
- self._agent_spec(
458
- index=index,
459
- role=role,
460
- domain=domain,
461
- goals=goals,
462
- total_agents=len(roles),
463
- )
464
- for index, role in enumerate(roles)
465
- )
466
 
467
- resource_specs = tuple(
468
- ResourceSpec(
469
- id=f"{resource_type}_{index + 1}",
470
- type=resource_type,
471
- amount=float(10 + index * 4),
472
- position=(float(2 + index * 2), float(7 - index % 4)),
473
- regeneration_rate=0.1 if index == 0 else 0.0,
474
- max_amount=float(30 + index * 5),
475
- metadata={"domain": domain},
476
- )
477
- for index, resource_type in enumerate(resources)
478
- )
 
 
 
 
 
 
 
479
 
480
- events = (
481
- EventSpec(
482
- id="midpoint_pressure",
483
- name="midpoint_pressure",
484
- trigger_step=max(1, self.default_steps // 2),
485
- payload={
486
- "description": "A generic pressure event that can be narrated later.",
487
- "domain": domain,
488
- },
489
- enabled=True,
490
- metadata={"generated_by": "fallback_builder"},
491
- ),
 
 
 
 
 
 
492
  )
 
 
493
 
494
- description = self._description(prompt, domain, constraints)
495
-
496
- return WorldSpec(
497
- schema_version="1.0",
498
- id=world_id,
499
- name=_title_from_domain(domain),
500
- description=description,
501
- simulation=SimulationSpec(
502
- steps=max(1, int(self.default_steps)),
503
- seed=self.default_seed,
504
- scheduler="sequential",
505
- activation="sequential",
506
- collect_history=True,
507
- ),
508
- space=SpaceSpec(
509
- dimensions=2,
510
- bounds=((0.0, 10.0), (0.0, 10.0)),
511
- toroidal=False,
512
- enforce_bounds=True,
513
- ),
514
  agents=agents,
515
- resources=resource_specs,
516
  events=events,
517
- metrics=(
518
- {"name": "diversity", "params": {"collection": "agents", "group_by_path": "type"}},
519
- {"name": "entropy", "params": {"collection": "agents", "value_path": "type"}},
520
- {"name": "stability", "params": {"collection": "agents", "group_by_path": "type"}},
521
- {"name": "interestingness", "params": {"collection": "agents", "group_by_path": "type"}},
522
- ),
523
- metadata={
524
- "generated_by": "deterministic_fallback",
525
- "domain": domain,
526
- "source_prompt": prompt,
527
- },
528
  )
529
 
530
- def _agent_spec(
531
- self,
532
- *,
533
- index: int,
534
- role: str,
535
- domain: str,
536
- goals: Sequence[str],
537
- total_agents: int,
538
- ) -> AgentSpec:
539
- """Create one generic fallback agent spec."""
540
-
541
- x = 1.0 + (index % 3) * 3.0
542
- y = 1.0 + (index // 3) * 3.0
543
- agent_id = f"{role}_{index + 1}"
544
-
545
- target_agent_id = None
546
- if total_agents > 1:
547
- target_agent_id = f"{self._roles_for_domain(domain)[(index + 1) % total_agents]}_{((index + 1) % total_agents) + 1}"
548
-
549
- goal_records = [
550
- {
551
- "id": goal,
552
- "importance": float(len(goals) - goal_index),
553
- "score": float(len(goals) - goal_index),
554
- "description": f"{role} works toward {goal}",
555
- }
556
- for goal_index, goal in enumerate(goals)
557
- ]
558
-
559
- behaviors = [
560
- BehaviorSpec(
561
- name="remember",
562
- params={
563
- "category": "initial_context",
564
- "content": {
565
- "role": role,
566
- "domain": domain,
567
- "note": "Seed memory created by fallback generator.",
568
- },
569
- "tags": ["initial", domain],
570
- },
571
- ),
572
- BehaviorSpec(name="evaluate", params={"category": "strategy"}),
573
- BehaviorSpec(name="prioritize", params={"source_path": "memory.goals"}),
574
- BehaviorSpec(name="choose_goal", params={}),
575
- BehaviorSpec(
576
- name="communicate",
577
- params={
578
- "target_agent_id": target_agent_id,
579
- "message": f"{role}_status_update",
580
- "max_recipients": 1,
581
- }
582
- if target_agent_id is not None
583
- else {"message": f"{role}_status_update", "max_recipients": 1},
584
- ),
585
- BehaviorSpec(
586
- name="cooperate",
587
- params={
588
- "target_agent_id": target_agent_id,
589
- "goal": goals[0],
590
- "effort": 1.0,
591
- }
592
- if target_agent_id is not None
593
- else {"goal": goals[0], "effort": 1.0},
594
- ),
595
- ]
596
-
597
- policy = PolicySpec(
598
- type="rule_policy",
599
- params={
600
- "selection_strategy": "highest_score",
601
- "rules": [
602
- {"behavior_name": "choose_goal", "score_delta": 4.0, "priority_delta": 1.0},
603
- {"behavior_name": "prioritize", "score_delta": 3.0},
604
- {"behavior_name": "communicate", "score_delta": 2.0},
605
- {"behavior_name": "cooperate", "score_delta": 1.5},
606
- {"behavior_name": "remember", "score_delta": 0.5},
607
- {"behavior_name": "evaluate", "score_delta": 0.25},
608
- ],
609
- },
610
- )
611
 
612
- return AgentSpec(
613
- id=agent_id,
614
- type=role,
615
- position=(x, y),
616
- state={
617
- "energy": float(10 - index),
618
- "credits": float(5 + index),
619
- "influence": float(index + 1),
620
- "domain_pressure": 0.2,
621
- },
622
- memory={
623
- "goals": goal_records,
624
- "known_options": {
625
- "cooperate": 1.0,
626
- "communicate": 0.8,
627
- "evaluate": 0.6,
628
- },
629
- "relationships": {},
630
- },
631
- goals=goal_records,
632
- behaviors=tuple(behaviors),
633
- policy=policy,
634
- alive=True,
635
  metadata={
636
- "domain": domain,
637
- "generated_by": "deterministic_fallback",
 
638
  },
639
  )
640
 
641
- @staticmethod
642
- def _infer_domain(prompt: str) -> str:
643
- """Infer a broad fallback domain from prompt keywords."""
644
-
645
- text = prompt.lower()
646
-
647
- keyword_domains = (
648
- ("farm", ("farm", "crop", "soil", "harvest", "animal", "pasture")),
649
- ("medieval", ("medieval", "kingdom", "castle", "guild", "village", "empire")),
650
- ("research", ("research", "scientist", "paper", "lab", "study", "knowledge")),
651
- ("startup", ("startup", "founder", "investor", "customer", "market", "product")),
652
- ("social", ("social", "network", "influencer", "community", "friend", "viral")),
653
- ("transport", ("transport", "route", "traffic", "delivery", "ship", "vehicle")),
654
- ("power", ("power", "grid", "energy", "battery", "generator", "electric")),
655
- ("space", ("space", "colony", "planet", "terraform", "asteroid", "orbital")),
656
- ("fantasy", ("fantasy", "dragon", "mana", "mage", "kingdom", "quest")),
657
  )
658
 
659
- for domain, keywords in keyword_domains:
660
- if any(keyword in text for keyword in keywords):
661
- return domain
662
 
663
- return "generic"
 
664
 
665
- @staticmethod
666
- def _roles_for_domain(domain: str) -> tuple[str, ...]:
667
- """Return fallback agent roles for a domain."""
668
-
669
- roles_by_domain = {
670
- "farm": ("farmer", "pollinator", "soil_guardian", "market_vendor"),
671
- "medieval": ("ruler", "merchant", "artisan", "scholar", "guard"),
672
- "research": ("scientist", "reviewer", "engineer", "curator", "funding_agent"),
673
- "startup": ("founder", "customer", "investor", "operator", "competitor"),
674
- "social": ("creator", "moderator", "member", "recommender", "critic"),
675
- "transport": ("hub", "carrier", "dispatcher", "charger", "passenger"),
676
- "power": ("generator", "storage_node", "consumer", "regulator", "maintainer"),
677
- "space": ("colonist", "engineer", "botanist", "navigator", "miner"),
678
- "fantasy": ("mage", "merchant", "guardian", "dragon", "healer"),
679
- "generic": ("explorer", "builder", "trader", "learner", "coordinator"),
 
 
 
 
 
 
680
  }
681
- return roles_by_domain.get(domain, roles_by_domain["generic"])
682
 
683
- @staticmethod
684
- def _resources_for_domain(domain: str) -> tuple[str, ...]:
685
- """Return fallback resource types for a domain."""
686
-
687
- resources_by_domain = {
688
- "farm": ("food", "water", "soil_health"),
689
- "medieval": ("grain", "gold", "craft_materials"),
690
- "research": ("knowledge", "funding", "attention"),
691
- "startup": ("capital", "users", "compute"),
692
- "social": ("attention", "trust", "content"),
693
- "transport": ("capacity", "fuel", "demand"),
694
- "power": ("energy", "storage", "load"),
695
- "space": ("oxygen", "minerals", "habitat_capacity"),
696
- "fantasy": ("mana", "relics", "influence"),
697
- "generic": ("energy", "materials", "knowledge"),
698
- }
699
- return resources_by_domain.get(domain, resources_by_domain["generic"])
700
 
701
- @staticmethod
702
- def _goals_for_domain(domain: str) -> tuple[str, ...]:
703
- """Return fallback goal ids for a domain."""
704
-
705
- goals_by_domain = {
706
- "farm": ("sustain_harvest", "share_resources", "adapt_to_weather"),
707
- "medieval": ("maintain_order", "grow_trade", "protect_settlement"),
708
- "research": ("discover", "publish", "collaborate"),
709
- "startup": ("find_users", "improve_product", "raise_capital"),
710
- "social": ("grow_trust", "spread_useful_content", "moderate_conflict"),
711
- "transport": ("deliver_reliably", "reduce_congestion", "balance_capacity"),
712
- "power": ("meet_demand", "prevent_outage", "rebalance_energy"),
713
- "space": ("survive_colony", "expand_habitat", "extract_resources"),
714
- "fantasy": ("protect_realm", "gather_mana", "negotiate_alliances"),
715
- "generic": ("survive", "learn", "cooperate"),
716
- }
717
- return goals_by_domain.get(domain, goals_by_domain["generic"])
 
 
 
 
 
 
 
 
 
 
718
 
719
- @staticmethod
720
- def _description(
721
- prompt: str,
722
- domain: str,
723
- constraints: Mapping[str, Any] | str | None,
724
- ) -> str:
725
- """Build fallback world description."""
726
 
727
- constraint_text = ""
728
- if constraints:
729
- constraint_text = f" Constraints: {constraints}"
 
 
730
 
731
- return (
732
- f"A deterministic fallback {domain} world generated from the prompt: "
733
- f"{prompt.strip()[:240]}{constraint_text}"
 
 
 
 
 
 
 
 
 
 
 
 
734
  )
735
 
 
 
 
 
736
 
737
- @dataclass
738
- class WorldGenerator:
739
- """Generate ``WorldSpec`` objects from natural language prompts.
 
 
 
 
740
 
741
- The generator is intentionally adapter-neutral. A client can be any object
742
- exposing ``generate``, ``complete``, ``chat``, or ``__call__``. The module
743
- does not import a specific model SDK.
744
- """
745
 
746
- client: Any | None = None
747
- config: WorldGenerationConfig = field(default_factory=WorldGenerationConfig)
748
 
749
- def generate(
750
- self,
751
- prompt: str,
752
- *,
753
- constraints: Mapping[str, Any] | str | None = None,
754
- examples: Sequence[WorldSpec | Mapping[str, Any] | str] | None = None,
755
- model_kwargs: Mapping[str, Any] | None = None,
756
- ) -> WorldGenerationResult:
757
- """Generate a validated ``WorldSpec`` from natural language.
758
 
759
- Args:
760
- prompt: Natural language world description.
761
- constraints: Optional extra constraints from the UI.
762
- examples: Optional few-shot examples as specs, mappings, or strings.
763
- model_kwargs: Optional generation kwargs for the model client.
764
 
765
- Returns:
766
- ``WorldGenerationResult``.
767
 
768
- Raises:
769
- WorldGenerationError: If generation fails and fallback is disabled.
770
- """
771
 
772
- normalized_prompt = _require_prompt(prompt)
773
- mode = self.config.resolved_mode()
774
 
775
- if mode is GenerationMode.FALLBACK or self.client is None:
776
- return self._fallback_result(
777
- normalized_prompt,
778
- constraints=constraints,
779
- reason="fallback_mode" if mode is GenerationMode.FALLBACK else "client_not_configured",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
780
  )
781
 
782
- prompt_bundle = self.build_prompt(
783
- normalized_prompt,
784
- constraints=constraints,
785
- examples=examples,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
786
  )
787
 
788
- try:
789
- raw_response = self._call_client(prompt_bundle, model_kwargs=model_kwargs)
790
- except Exception as exc:
791
- if self.config.fallback_on_model_error and mode is GenerationMode.AUTO:
792
- logger.warning("Model client failed; using deterministic fallback: %s", exc)
793
- return self._fallback_result(
794
- normalized_prompt,
795
- constraints=constraints,
796
- reason="model_client_error",
797
- error=exc,
798
- prompt_bundle=prompt_bundle,
799
- )
800
- raise ModelClientError(f"Model client failed: {exc}") from exc
 
 
 
 
 
 
 
 
 
 
 
 
801
 
802
- parse_result: ParseResult | None = None
803
- parse_error: BaseException | None = None
804
- current_response = raw_response
805
 
806
- for attempt in range(max(1, int(self.config.repair_attempts) + 1)):
 
 
807
  try:
808
- parse_result = self.config.parser.parse_result(current_response)
809
- break
810
- except Exception as exc:
811
- parse_error = exc
812
- if attempt >= int(self.config.repair_attempts):
813
- break
814
- current_response = self._repair_response(
815
- prompt_bundle=prompt_bundle,
816
- bad_response=current_response,
817
- error=exc,
818
- model_kwargs=model_kwargs,
819
  )
820
-
821
- if parse_result is None:
822
- if self.config.fallback_on_parse_error and mode is GenerationMode.AUTO:
823
- logger.warning("Generated DSL could not be parsed; using fallback: %s", parse_error)
824
- return self._fallback_result(
825
- normalized_prompt,
826
- constraints=constraints,
827
- reason="parse_error",
828
- error=parse_error,
829
- raw_response=raw_response,
830
- prompt_bundle=prompt_bundle,
831
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
832
 
833
- raise WorldGenerationError(f"Generated DSL could not be parsed: {parse_error}") from parse_error
834
-
835
- validation_report = self._semantic_validation(parse_result.spec)
836
-
837
- if validation_report is not None and not validation_report.is_valid:
838
- if self.config.strict_semantic_validation:
839
- if self.config.fallback_on_semantic_error and mode is GenerationMode.AUTO:
840
- return self._fallback_result(
841
- normalized_prompt,
842
- constraints=constraints,
843
- reason="semantic_validation_error",
844
- raw_response=raw_response,
845
- prompt_bundle=prompt_bundle,
846
- )
847
- raise WorldGenerationValidationError(validation_report)
848
-
849
- return WorldGenerationResult(
850
- spec=parse_result.spec,
851
- raw_response=raw_response,
852
- parse_result=parse_result,
853
- validation_report=validation_report,
854
- mode=GenerationMode.MODEL,
855
- prompt_bundle=prompt_bundle,
856
- metadata={
857
- **copy.deepcopy(dict(self.config.metadata)),
858
- "repair_attempts_used": 0 if current_response == raw_response else 1,
859
- },
860
- )
861
 
862
- def generate_spec(
863
- self,
864
- prompt: str,
865
- *,
866
- constraints: Mapping[str, Any] | str | None = None,
867
- examples: Sequence[WorldSpec | Mapping[str, Any] | str] | None = None,
868
- model_kwargs: Mapping[str, Any] | None = None,
869
- ) -> WorldSpec:
870
- """Generate and return only the ``WorldSpec``."""
871
-
872
- return self.generate(
873
- prompt,
874
- constraints=constraints,
875
- examples=examples,
876
- model_kwargs=model_kwargs,
877
- ).spec
878
-
879
- def generate_json(
880
  self,
881
- prompt: str,
882
  *,
883
- constraints: Mapping[str, Any] | str | None = None,
884
- examples: Sequence[WorldSpec | Mapping[str, Any] | str] | None = None,
885
- model_kwargs: Mapping[str, Any] | None = None,
886
- ) -> str:
887
- """Generate and return formatted WorldSpec JSON."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
888
 
889
- return self.generate(
890
- prompt,
891
- constraints=constraints,
892
- examples=examples,
893
- model_kwargs=model_kwargs,
894
- ).json_text
 
895
 
896
- def build_prompt(
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
897
  self,
898
- prompt: str,
899
- *,
900
- constraints: Mapping[str, Any] | str | None = None,
901
- examples: Sequence[WorldSpec | Mapping[str, Any] | str] | None = None,
902
- ) -> PromptBundle:
903
- """Build the prompt bundle sent to the model client."""
904
 
905
- sections: list[str] = []
906
- sections.append(f"World request:\n{prompt.strip()}")
907
 
908
- if constraints:
909
- sections.append(f"Additional constraints:\n{_serialize_constraints(constraints)}")
 
 
 
910
 
911
- sections.append(
912
- "Return a compact but complete WorldSmithAI WorldSpec JSON object. "
913
- "The JSON must be parseable by Python json.loads."
914
- )
915
 
916
- if self.config.include_format_guide:
917
- sections.append(f"WorldSpec format guide:\n{WORLD_DSL_FORMAT_GUIDE}")
918
 
919
- if self.config.include_json_schema_excerpt:
920
- sections.append(f"Schema excerpt:\n{world_spec_schema_excerpt()}")
 
921
 
922
- example_payloads = self._example_payloads(examples)
923
- if self.config.include_example and example_payloads:
924
- sections.append("Example valid WorldSpec JSON:\n" + example_payloads[0])
 
 
 
925
 
926
- sections.append(
927
- "Final answer requirements:\n"
928
- "- JSON object only.\n"
929
- "- No markdown.\n"
930
- "- No comments.\n"
931
- "- No Python code.\n"
932
- "- No prose before or after the JSON."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
933
  )
 
934
 
935
- user_prompt = "\n\n".join(sections)
936
- system_prompt = self.config.system_prompt
937
 
938
- return PromptBundle(
939
- system_prompt=system_prompt,
940
- user_prompt=user_prompt,
941
- messages=(
942
- {"role": "system", "content": system_prompt},
943
- {"role": "user", "content": user_prompt},
944
- ),
945
- metadata={
946
- "response_format": self.config.resolved_response_format().value,
947
- "default_steps": self.config.default_steps,
948
- "max_agents_hint": self.config.max_agents_hint,
949
- "max_resources_hint": self.config.max_resources_hint,
950
- },
 
 
 
 
 
 
 
 
 
951
  )
 
 
952
 
953
- def _example_payloads(
954
- self,
955
- examples: Sequence[WorldSpec | Mapping[str, Any] | str] | None,
956
- ) -> tuple[str, ...]:
957
- """Return serialized few-shot examples."""
958
 
959
- if examples is None:
960
- return (json.dumps(DEFAULT_MINIMAL_EXAMPLE, indent=2),)
961
 
962
- payloads: list[str] = []
 
 
 
 
 
 
 
 
963
 
964
- for example in examples:
965
- if isinstance(example, WorldSpec):
966
- payloads.append(example.to_json_string(indent=2, exclude_none=True))
967
- elif isinstance(example, Mapping):
968
- payloads.append(json.dumps(example, indent=2))
969
- elif isinstance(example, str):
970
- payloads.append(example.strip())
971
 
972
- return tuple(payload for payload in payloads if payload)
973
 
974
- def _call_client(
975
  self,
976
- prompt_bundle: PromptBundle,
 
977
  *,
978
- model_kwargs: Mapping[str, Any] | None = None,
979
- ) -> str:
980
- """Call the configured model client and normalize its response to text."""
981
 
982
- if self.client is None:
983
- raise ModelClientError("No model client is configured")
984
 
985
- kwargs = {
986
- **copy.deepcopy(dict(self.config.model_kwargs)),
987
- **copy.deepcopy(dict(model_kwargs or {})),
988
- }
 
 
989
 
990
- callable_obj, call_style = self._resolve_client_callable(self.client)
 
 
 
991
 
992
- call_kwargs = {
993
- "prompt": prompt_bundle.text,
994
- "system_prompt": prompt_bundle.system_prompt,
995
- "user_prompt": prompt_bundle.user_prompt,
996
- "messages": [dict(message) for message in prompt_bundle.messages],
997
- "response_format": {"type": "json_object"},
998
- **kwargs,
999
- }
1000
 
1001
- if call_style == "chat":
1002
- call_kwargs.setdefault("prompt", prompt_bundle.user_prompt)
1003
 
1004
- response = _call_with_supported_kwargs(callable_obj, call_kwargs)
1005
- return normalize_model_response(response)
 
1006
 
1007
- def _repair_response(
1008
- self,
1009
- *,
1010
- prompt_bundle: PromptBundle,
1011
- bad_response: str,
1012
- error: BaseException,
1013
- model_kwargs: Mapping[str, Any] | None,
1014
- ) -> str:
1015
- """Ask the model client to repair malformed JSON."""
1016
-
1017
- repair_user_prompt = (
1018
- "The previous response was not valid WorldSmithAI JSON.\n\n"
1019
- f"Parser error:\n{error}\n\n"
1020
- "Previous response:\n"
1021
- f"{bad_response[:8000]}\n\n"
1022
- "Return only a corrected WorldSpec JSON object. No markdown. No prose."
1023
- )
1024
- repair_bundle = PromptBundle(
1025
- system_prompt=prompt_bundle.system_prompt,
1026
- user_prompt=repair_user_prompt,
1027
- messages=(
1028
- {"role": "system", "content": prompt_bundle.system_prompt},
1029
- {"role": "user", "content": repair_user_prompt},
1030
- ),
1031
- metadata={"repair": True},
1032
- )
1033
- return self._call_client(repair_bundle, model_kwargs=model_kwargs)
1034
 
1035
  @staticmethod
1036
- def _resolve_client_callable(client: Any) -> tuple[Callable[..., Any], str]:
1037
- """Resolve a callable generation method from a model client."""
1038
-
1039
- for method_name, style in (
1040
- ("generate", "generate"),
1041
- ("complete", "complete"),
1042
- ("chat", "chat"),
1043
- ):
1044
- method = getattr(client, method_name, None)
1045
- if callable(method):
1046
- return method, style
1047
-
1048
- if callable(client):
1049
- return client, "callable"
1050
-
1051
- raise ModelClientError(
1052
- "Model client must expose generate, complete, chat, or be callable"
1053
- )
1054
 
1055
- def _semantic_validation(self, spec: WorldSpec) -> ValidationReport | None:
1056
- """Run optional semantic validation."""
1057
 
1058
- if not self.config.semantic_validation:
1059
- return None
 
 
1060
 
1061
- validation_config = self.config.semantic_validation_config
1062
- if validation_config is None:
1063
- validation_config = ValidationConfig(
1064
- require_known_behaviors=True,
1065
- require_known_policies=True,
1066
- unknown_registry_item_severity=ValidationSeverity.WARNING,
1067
- constructor_param_severity=ValidationSeverity.WARNING,
1068
- unresolved_reference_severity=ValidationSeverity.WARNING,
1069
- )
1070
 
1071
- return validate_world_spec(spec, config=validation_config)
1072
 
1073
- def _fallback_result(
1074
  self,
1075
- prompt: str,
1076
  *,
1077
- constraints: Mapping[str, Any] | str | None,
1078
- reason: str,
1079
- error: BaseException | None = None,
1080
- raw_response: str | None = None,
1081
- prompt_bundle: PromptBundle | None = None,
1082
- ) -> WorldGenerationResult:
1083
- """Build a deterministic fallback result."""
1084
-
1085
- builder = DeterministicWorldSpecBuilder(
1086
- default_steps=self.config.default_steps,
1087
- default_seed=self.config.default_seed,
1088
- max_agents=min(max(1, int(self.config.max_agents_hint)), 6),
 
 
 
 
 
 
 
 
 
 
1089
  )
1090
- spec = builder.build(prompt, constraints=constraints)
1091
- validation_report = self._semantic_validation(spec)
1092
- raw = raw_response or spec.to_json_string(indent=2, exclude_none=True)
1093
-
1094
- return WorldGenerationResult(
1095
- spec=spec,
1096
- raw_response=raw,
1097
- parse_result=None,
1098
- validation_report=validation_report,
1099
- mode=GenerationMode.FALLBACK,
1100
- prompt_bundle=prompt_bundle,
1101
- metadata={
1102
- **copy.deepcopy(dict(self.config.metadata)),
1103
- "fallback_reason": reason,
1104
- "error": None if error is None else f"{error.__class__.__name__}: {error}",
1105
- },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1106
  )
1107
 
 
 
 
 
 
 
 
 
 
 
1108
 
1109
- def generate_world_spec(
1110
- prompt: str,
1111
- *,
1112
- client: Any | None = None,
1113
- constraints: Mapping[str, Any] | str | None = None,
1114
- config: WorldGenerationConfig | None = None,
1115
- examples: Sequence[WorldSpec | Mapping[str, Any] | str] | None = None,
1116
- model_kwargs: Mapping[str, Any] | None = None,
1117
- ) -> WorldSpec:
1118
- """Convenience function that returns a generated ``WorldSpec``.
1119
-
1120
- This is the simplest function to call from root-level ``app.py``.
1121
- """
1122
 
1123
- generator = WorldGenerator(client=client, config=config or WorldGenerationConfig())
1124
- return generator.generate_spec(
1125
- prompt,
1126
- constraints=constraints,
1127
- examples=examples,
1128
- model_kwargs=model_kwargs,
1129
- )
1130
 
 
 
1131
 
1132
- def generate_world_json(
1133
- prompt: str,
1134
- *,
1135
- client: Any | None = None,
1136
- constraints: Mapping[str, Any] | str | None = None,
1137
- config: WorldGenerationConfig | None = None,
1138
- examples: Sequence[WorldSpec | Mapping[str, Any] | str] | None = None,
1139
- model_kwargs: Mapping[str, Any] | None = None,
1140
- ) -> str:
1141
- """Convenience function that returns generated WorldSpec JSON."""
1142
-
1143
- generator = WorldGenerator(client=client, config=config or WorldGenerationConfig())
1144
- return generator.generate_json(
1145
- prompt,
1146
- constraints=constraints,
1147
- examples=examples,
1148
- model_kwargs=model_kwargs,
1149
- )
1150
-
1151
-
1152
- def generate_world_result(
1153
- prompt: str,
1154
- *,
1155
- client: Any | None = None,
1156
- constraints: Mapping[str, Any] | str | None = None,
1157
- config: WorldGenerationConfig | None = None,
1158
- examples: Sequence[WorldSpec | Mapping[str, Any] | str] | None = None,
1159
- model_kwargs: Mapping[str, Any] | None = None,
1160
- ) -> WorldGenerationResult:
1161
- """Convenience function that returns the full generation result."""
1162
-
1163
- generator = WorldGenerator(client=client, config=config or WorldGenerationConfig())
1164
- return generator.generate(
1165
- prompt,
1166
- constraints=constraints,
1167
- examples=examples,
1168
- model_kwargs=model_kwargs,
1169
- )
1170
-
1171
-
1172
- def build_world_generation_prompt(
1173
- prompt: str,
1174
- *,
1175
- constraints: Mapping[str, Any] | str | None = None,
1176
- config: WorldGenerationConfig | None = None,
1177
- ) -> PromptBundle:
1178
- """Build the prompt bundle without calling a model."""
1179
 
1180
- generator = WorldGenerator(client=None, config=config or WorldGenerationConfig())
1181
- return generator.build_prompt(prompt, constraints=constraints)
1182
 
 
 
 
1183
 
1184
- def normalize_model_response(response: Any) -> str:
1185
- """Normalize common model-client response shapes into text.
1186
 
1187
- Supported shapes:
1188
- - plain string
1189
- - bytes
1190
- - mapping with text/content/generated_text/output
1191
- - OpenAI-like mapping with choices[0].message.content
1192
- - object with text/content/generated_text attributes
1193
- """
1194
 
1195
- if response is None:
1196
- raise ModelClientError("Model client returned None")
 
 
 
 
1197
 
1198
- if isinstance(response, str):
1199
- return response.strip()
 
 
 
1200
 
1201
- if isinstance(response, bytes):
1202
- return response.decode("utf-8").strip()
 
 
 
 
1203
 
1204
- if isinstance(response, Mapping):
1205
- direct = _first_present(
1206
- response,
1207
- ("text", "content", "generated_text", "output", "response"),
1208
- )
1209
- if direct is not None:
1210
- return normalize_model_response(direct)
1211
-
1212
- choices = response.get("choices")
1213
- if isinstance(choices, Sequence) and choices:
1214
- first_choice = choices[0]
1215
- if isinstance(first_choice, Mapping):
1216
- message = first_choice.get("message")
1217
- if isinstance(message, Mapping) and message.get("content") is not None:
1218
- return normalize_model_response(message["content"])
1219
- if first_choice.get("text") is not None:
1220
- return normalize_model_response(first_choice["text"])
1221
-
1222
- if response.get("data") is not None:
1223
- return normalize_model_response(response["data"])
1224
-
1225
- for attribute in ("text", "content", "generated_text", "output", "response"):
1226
- value = getattr(response, attribute, None)
1227
- if value is not None:
1228
- return normalize_model_response(value)
1229
-
1230
- raise ModelClientError(
1231
- f"Could not extract text from model response of type {response.__class__.__name__}"
1232
- )
1233
-
1234
-
1235
- def world_spec_schema_excerpt() -> str:
1236
- """Return a compact schema guide suitable for small-model prompts."""
1237
-
1238
- excerpt = {
1239
- "WorldSpec": {
1240
- "required": ["id", "name", "agents", "resources", "events"],
1241
- "fields": {
1242
- "schema_version": "string",
1243
- "id": "string",
1244
- "name": "string",
1245
- "description": "string|null",
1246
- "simulation": "SimulationSpec",
1247
- "space": "SpaceSpec|null",
1248
- "agents": "AgentSpec[]",
1249
- "resources": "ResourceSpec[]",
1250
- "events": "EventSpec[]",
1251
- "metrics": "MetricSpec[]",
1252
- "metadata": "object",
1253
- },
1254
- },
1255
- "AgentSpec": {
1256
- "fields": {
1257
- "id": "string",
1258
- "type": "string",
1259
- "position": "number[]|null",
1260
- "state": "object",
1261
- "memory": "object",
1262
- "goals": "JSON",
1263
- "behaviors": "BehaviorSpec[]",
1264
- "policy": "PolicySpec|null",
1265
- "alive": "boolean",
1266
- "metadata": "object",
1267
- }
1268
- },
1269
- "BehaviorSpec": {"fields": {"name": "string", "params": "object"}},
1270
- "PolicySpec": {"fields": {"type": "string", "params": "object"}},
1271
- "ResourceSpec": {
1272
- "fields": {
1273
- "id": "string",
1274
- "type": "string",
1275
- "amount": "number",
1276
- "position": "number[]|null",
1277
- "regeneration_rate": "number",
1278
- "max_amount": "number|null",
1279
- "metadata": "object",
1280
- }
1281
- },
1282
- "EventSpec": {
1283
- "fields": {
1284
- "id": "string|null",
1285
- "name": "string",
1286
- "trigger_step": "integer",
1287
- "payload": "object",
1288
- "enabled": "boolean",
1289
- }
1290
- },
1291
- }
1292
- return json.dumps(excerpt, indent=2)
1293
 
 
 
 
1294
 
1295
- def extract_json_object_text(text: str) -> str:
1296
- """Extract the first balanced JSON object from text.
 
 
1297
 
1298
- This is exposed as a utility for app-level debugging. The parser already
1299
- uses equivalent extraction internally.
1300
- """
1301
 
1302
- parser = WorldDSLParser()
1303
- return parser.extract_json_text(text)
 
1304
 
 
 
 
1305
 
1306
- def _call_with_supported_kwargs(callable_obj: Callable[..., Any], kwargs: Mapping[str, Any]) -> Any:
1307
- """Call a function using only kwargs supported by its signature.
1308
 
1309
- If the callable accepts ``**kwargs``, all supplied kwargs are passed.
1310
- Otherwise unsupported kwargs are filtered out. If no matching keyword is
1311
- accepted but a positional prompt appears possible, the flattened prompt is
1312
- passed positionally.
1313
- """
1314
 
1315
- try:
1316
- signature = inspect.signature(callable_obj)
1317
- except (TypeError, ValueError):
1318
- return callable_obj(**dict(kwargs))
1319
 
1320
- parameters = signature.parameters
1321
- accepts_var_keyword = any(
1322
- parameter.kind is inspect.Parameter.VAR_KEYWORD
1323
- for parameter in parameters.values()
1324
- )
1325
 
1326
- if accepts_var_keyword:
1327
- return callable_obj(**dict(kwargs))
1328
 
1329
- accepted = {
1330
- key: value
1331
- for key, value in kwargs.items()
1332
- if key in parameters
1333
- }
1334
 
1335
- if accepted:
1336
- return callable_obj(**accepted)
 
1337
 
1338
- positional_params = [
1339
- parameter
1340
- for parameter in parameters.values()
1341
- if parameter.kind
1342
- in {
1343
- inspect.Parameter.POSITIONAL_ONLY,
1344
- inspect.Parameter.POSITIONAL_OR_KEYWORD,
1345
- }
1346
- ]
 
 
1347
 
1348
- if positional_params and "prompt" in kwargs:
1349
- return callable_obj(kwargs["prompt"])
 
1350
 
1351
- return callable_obj()
 
1352
 
 
1353
 
1354
- def _first_present(mapping: Mapping[str, Any], keys: Sequence[str]) -> Any:
1355
- """Return first present non-null mapping value."""
1356
 
1357
- for key in keys:
1358
- if key in mapping and mapping[key] is not None:
1359
- return mapping[key]
1360
- return None
1361
 
 
 
1362
 
1363
- def _serialize_constraints(constraints: Mapping[str, Any] | str) -> str:
1364
- """Serialize UI constraints for prompt inclusion."""
 
 
 
 
 
1365
 
1366
- if isinstance(constraints, str):
1367
- return constraints.strip()
1368
 
1369
- return json.dumps(constraints, indent=2, sort_keys=True)
 
 
1370
 
 
 
1371
 
1372
- def _require_prompt(prompt: str) -> str:
1373
- """Validate and normalize a user prompt."""
1374
 
1375
- normalized = str(prompt).strip()
1376
- if not normalized:
1377
- raise WorldGenerationError("World generation prompt must not be empty")
1378
- return normalized
 
 
 
1379
 
 
 
1380
 
1381
- def _slugify(value: str) -> str:
1382
- """Return a stable lowercase identifier."""
1383
 
1384
- lowered = value.lower().strip()
1385
- slug = re.sub(r"[^a-z0-9]+", "_", lowered)
1386
- slug = re.sub(r"_+", "_", slug).strip("_")
1387
- return slug or "world"
1388
 
 
 
 
 
 
 
1389
 
1390
- def _title_from_domain(domain: str) -> str:
1391
- """Return a human-readable fallback world title."""
 
 
 
 
 
 
 
1392
 
1393
- words = domain.replace("_", " ").split()
1394
- return " ".join(word.capitalize() for word in words) + " World"
1395
 
1396
 
1397
  __all__ = [
1398
- "DEFAULT_MINIMAL_EXAMPLE",
1399
- "DEFAULT_SYSTEM_PROMPT",
1400
- "WORLD_DSL_FORMAT_GUIDE",
1401
- "DeterministicWorldSpecBuilder",
1402
- "GenerationMode",
1403
- "ModelClientError",
1404
- "PromptBundle",
1405
- "ResponseFormat",
1406
- "SupportsGenerate",
1407
- "WorldGenerationConfig",
1408
- "WorldGenerationError",
1409
- "WorldGenerationResult",
1410
- "WorldGenerationValidationError",
1411
- "WorldGenerator",
1412
- "build_world_generation_prompt",
1413
- "extract_json_object_text",
1414
- "generate_world_json",
1415
- "generate_world_result",
1416
- "generate_world_spec",
1417
- "normalize_model_response",
1418
- "world_spec_schema_excerpt",
1419
  ]
 
1
  """
2
+ Runtime object factory for WorldSmithAI.
3
+
4
+ This module converts validated DSL specifications into live runtime objects:
5
+ World, Agent, Resource, Event, Behavior, Policy, and Scheduler instances.
6
+
7
+ The factory is intentionally domain-agnostic. It does not know about sheep,
8
+ wolves, farms, dragons, scientists, markets, civilizations, power grids,
9
+ transport networks, or research ecosystems. It only resolves generic DSL
10
+ records through registries and runtime constructors.
11
+
12
+ Typical pipeline:
13
+ raw prompt
14
+ -> llm.world_generator
15
+ -> JSON DSL
16
+ -> dsl.parser
17
+ -> dsl.validator
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
41
 
42
  import copy
43
  import inspect
 
44
  import logging
45
+ from collections.abc import Mapping, MutableMapping, Sequence
 
46
  from dataclasses import dataclass, field
47
+ from importlib import import_module
48
+ from types import MappingProxyType
49
+ from typing import Any
50
+
51
+ from core.agent import Agent
52
+ from core.behavior import Behavior
53
+ from core.event import Event
54
+ from core.resource import Resource
55
+ from core.scheduler import Scheduler
56
+ from core.world import World
57
  from dsl.schema import (
58
  AgentSpec,
59
  BehaviorSpec,
60
  EventSpec,
61
+ MetricSpec,
62
  PolicySpec,
63
  ResourceSpec,
 
 
64
  WorldSpec,
65
  )
66
+ from dsl.validator import (
67
+ ValidationConfig,
68
+ ValidationReport,
69
+ ValidationSeverity,
70
+ validate_world_spec,
71
+ )
72
+ from policies.base_policy import BasePolicy
73
 
74
  logger = logging.getLogger(__name__)
75
 
76
+ DEFAULT_BEHAVIOR_MODULES: tuple[str, ...] = (
77
+ "behaviors.movement",
78
+ "behaviors.consume",
79
+ "behaviors.trade",
80
+ "behaviors.attack",
81
+ "behaviors.research",
82
+ "behaviors.collaboration",
83
+ "behaviors.transport",
84
+ "behaviors.construction",
85
+ "behaviors.governance",
86
+ "behaviors.market",
87
+ "behaviors.adoption",
88
+ "behaviors.planning",
89
+ "behaviors.memory",
90
+ )
91
+
92
+ DEFAULT_POLICY_MODULES: tuple[str, ...] = (
93
+ "policies.rule_policy",
94
+ "policies.contextual_bandit",
95
+ )
96
+
97
+ BEHAVIOR_REGISTRY_ATTRIBUTES: tuple[str, ...] = ("BEHAVIOR_REGISTRY",)
98
+ POLICY_REGISTRY_ATTRIBUTES: tuple[str, ...] = (
99
+ "POLICY_REGISTRY",
100
+ "RULE_POLICY_REGISTRY",
101
+ )
102
 
103
+ _CORE_AGENT_ALIASES: Mapping[str, tuple[str, ...]] = MappingProxyType(
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
104
  {
105
+ "id": ("agent_id",),
106
+ "type": ("agent_type", "kind"),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
107
  }
108
+ )
109
+
110
+ _CORE_RESOURCE_ALIASES: Mapping[str, tuple[str, ...]] = MappingProxyType(
111
  {
112
+ "id": ("resource_id",),
113
+ "type": ("resource_type", "kind"),
114
+ "amount": ("quantity",),
 
 
 
 
115
  }
116
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
117
 
118
+ _CORE_EVENT_ALIASES: Mapping[str, tuple[str, ...]] = MappingProxyType(
119
+ {
120
+ "id": ("event_id",),
121
+ "name": ("type", "event_type"),
122
+ "trigger_step": ("step", "at_step"),
123
+ "payload": ("data",),
124
+ }
125
+ )
126
 
127
+ _CORE_WORLD_ALIASES: Mapping[str, tuple[str, ...]] = MappingProxyType({})
128
+ _CORE_SCHEDULER_ALIASES: Mapping[str, tuple[str, ...]] = MappingProxyType({})
129
 
 
 
 
130
 
131
+ class WorldFactoryError(RuntimeError):
132
+ """Base exception raised by the runtime world factory."""
133
 
 
 
134
 
135
+ class RegistryResolutionError(WorldFactoryError):
136
+ """Raised when a behavior, policy, or runtime class cannot be resolved."""
137
 
138
 
139
+ class ConstructorBindingError(WorldFactoryError):
140
+ """Raised when DSL parameters cannot be bound to a constructor."""
141
 
142
 
143
+ class RuntimeObjectBuildError(WorldFactoryError):
144
+ """Raised when a runtime object cannot be built."""
145
 
146
 
147
+ class WorldFactoryValidationError(WorldFactoryError):
148
+ """Raised when semantic DSL validation fails before world construction."""
149
 
150
  def __init__(self, report: ValidationReport) -> None:
151
+ """Initialize the error from a validation report."""
152
 
153
  self.report = report
154
  super().__init__(report.summary())
155
 
156
 
 
 
 
 
 
 
 
 
157
  @dataclass(frozen=True)
158
+ class RegistryLoadReport:
159
+ """Report describing dynamically loaded registry entries."""
160
 
161
+ registry: Mapping[str, Any]
162
+ import_errors: Mapping[str, str] = field(default_factory=dict)
 
 
 
 
 
 
163
 
164
  @property
165
+ def names(self) -> tuple[str, ...]:
166
+ """Return sorted registry names."""
167
 
168
+ return tuple(sorted(str(name) for name in self.registry.keys()))
169
 
170
  def to_dict(self) -> dict[str, Any]:
171
+ """Return a JSON-friendly registry load report."""
172
 
173
  return {
174
+ "names": list(self.names),
175
+ "count": len(self.registry),
176
+ "import_errors": copy.deepcopy(dict(self.import_errors)),
 
177
  }
178
 
179
 
180
  @dataclass(frozen=True)
181
+ class WorldBuildReport:
182
+ """Structured report generated during world construction."""
183
+
184
+ world_id: str
185
+ agent_ids: tuple[str, ...]
186
+ resource_ids: tuple[str, ...]
187
+ event_keys: tuple[str, ...]
188
+ behavior_registry_count: int
189
+ policy_registry_count: int
190
+ import_errors: Mapping[str, str] = field(default_factory=dict)
 
 
 
 
 
 
191
  validation_report: ValidationReport | None = None
 
 
192
  metadata: Mapping[str, Any] = field(default_factory=dict)
193
 
194
  @property
195
+ def valid(self) -> bool:
196
+ """Return whether the optional validation report is valid."""
 
 
 
 
 
 
197
 
198
  return self.validation_report is None or self.validation_report.is_valid
199
 
200
  def to_dict(self) -> dict[str, Any]:
201
+ """Return a JSON-friendly build report."""
202
 
203
  return {
204
+ "world_id": self.world_id,
205
+ "agent_ids": list(self.agent_ids),
206
+ "resource_ids": list(self.resource_ids),
207
+ "event_keys": list(self.event_keys),
208
+ "agent_count": len(self.agent_ids),
209
+ "resource_count": len(self.resource_ids),
210
+ "event_count": len(self.event_keys),
211
+ "behavior_registry_count": self.behavior_registry_count,
212
+ "policy_registry_count": self.policy_registry_count,
213
+ "import_errors": copy.deepcopy(dict(self.import_errors)),
214
  "validation_report": None
215
  if self.validation_report is None
216
  else self.validation_report.to_dict(),
 
217
  "metadata": copy.deepcopy(dict(self.metadata)),
218
  }
219
 
220
 
221
+ @dataclass(frozen=True)
222
+ class WorldBuildResult:
223
+ """Factory result containing the runtime world and build report."""
224
+
225
+ world: World
226
+ report: WorldBuildReport
227
+
228
+
229
  @dataclass
230
+ class WorldFactoryConfig:
231
+ """Configuration for ``WorldFactory``.
232
+
233
+ Attributes:
234
+ validate_before_build: Run semantic DSL validation before construction.
235
+ strict_unknown_behaviors: Raise if a behavior name is not registered.
236
+ strict_unknown_policies: Raise if a policy type is not registered.
237
+ strict_constructor_params: Raise on constructor parameters not accepted
238
+ by the runtime class.
239
+ include_disabled_behaviors: Instantiate behaviors marked disabled.
240
+ include_disabled_events: Instantiate events marked disabled.
241
+ attach_default_policy: Attach a default policy to agents that define
242
+ behaviors but omit a policy.
243
+ default_policy_type: Default policy registry key.
244
+ attach_scheduler: Attach a scheduler instance to the world when
245
+ possible.
246
+ attach_dsl_spec_to_world: Store the validated ``WorldSpec`` on the
247
+ runtime world for diagnostics and UI use.
248
  """
249
 
250
+ validate_before_build: bool = True
 
251
 
252
+ strict_unknown_behaviors: bool = True
253
+ strict_unknown_policies: bool = True
254
+ strict_constructor_params: bool = True
 
255
 
256
+ include_disabled_behaviors: bool = False
257
+ include_disabled_events: bool = False
 
 
258
 
259
+ attach_default_policy: bool = True
260
+ default_policy_type: str = "rule_policy"
261
 
262
+ attach_scheduler: bool = True
263
+ attach_dsl_spec_to_world: bool = True
 
264
 
265
+ behavior_modules: tuple[str, ...] = DEFAULT_BEHAVIOR_MODULES
266
+ policy_modules: tuple[str, ...] = DEFAULT_POLICY_MODULES
 
267
 
268
+ behavior_registry: Mapping[str, Any] | None = None
269
+ policy_registry: Mapping[str, Any] | None = None
270
 
271
+ agent_class: type[Agent] = Agent
272
+ resource_class: type[Resource] = Resource
273
+ event_class: type[Event] = Event
274
+ world_class: type[World] = World
275
+ scheduler_class: type[Scheduler] | None = Scheduler
276
+
277
+ world_uses_mapping_collections: bool = True
278
+
279
+
280
+ @dataclass
281
+ class GenericScheduledEvent:
282
+ """Fallback event object used when ``core.event.Event`` is not instantiable.
283
+
284
+ The adapter keeps event records executable and inspectable without imposing
285
+ domain-specific event semantics. It returns its payload when executed.
286
+ Future event-system modules can replace this with richer event dispatch.
287
+ """
288
 
289
+ name: str
290
+ trigger_step: int
291
+ payload: Mapping[str, Any] = field(default_factory=dict)
292
+ id: str | None = None
293
+ enabled: bool = True
294
+ repeat_interval: int | None = None
295
+ target_agent_ids: tuple[str, ...] = ()
296
+ target_resource_ids: tuple[str, ...] = ()
297
+ metadata: Mapping[str, Any] = field(default_factory=dict)
298
 
299
+ def execute(self, world: World) -> dict[str, Any]:
300
+ """Execute the event in a domain-agnostic way.
 
301
 
302
+ The fallback adapter does not mutate world state by default. It returns
303
+ a structured event outcome that schedulers, logs, metrics, or narrators
304
+ can consume.
305
+ """
306
 
307
+ return {
308
+ "event": self.name,
309
+ "event_id": self.id,
310
+ "trigger_step": self.trigger_step,
311
+ "enabled": self.enabled,
312
+ "payload": copy.deepcopy(dict(self.payload)),
313
+ "target_agent_ids": list(self.target_agent_ids),
314
+ "target_resource_ids": list(self.target_resource_ids),
315
+ "world_step": getattr(world, "step_count", None),
316
+ }
317
 
318
 
319
  @dataclass
320
+ class WorldFactory:
321
+ """Factory that converts ``WorldSpec`` into a runtime ``World``.
322
 
323
+ The factory owns behavior and policy registries. Registries may come from
324
+ default behavior/policy modules or from custom mappings supplied through
325
+ ``WorldFactoryConfig``.
326
  """
327
 
328
+ config: WorldFactoryConfig = field(default_factory=WorldFactoryConfig)
 
 
329
 
330
+ _behavior_registry: dict[str, Any] = field(default_factory=dict, init=False, repr=False)
331
+ _policy_registry: dict[str, Any] = field(default_factory=dict, init=False, repr=False)
332
+ _registry_import_errors: dict[str, str] = field(default_factory=dict, init=False, repr=False)
333
+
334
+ def __post_init__(self) -> None:
335
+ """Load configured behavior and policy registries."""
336
+
337
+ self._behavior_registry = self._build_behavior_registry()
338
+ self._policy_registry = self._build_policy_registry()
339
+
340
+ @property
341
+ def behavior_registry(self) -> Mapping[str, Any]:
342
+ """Return a read-only view of the behavior registry."""
343
+
344
+ return MappingProxyType(self._behavior_registry)
345
+
346
+ @property
347
+ def policy_registry(self) -> Mapping[str, Any]:
348
+ """Return a read-only view of the policy registry."""
349
+
350
+ return MappingProxyType(self._policy_registry)
351
+
352
+ @property
353
+ def registry_import_errors(self) -> Mapping[str, str]:
354
+ """Return registry import errors collected during initialization."""
355
+
356
+ return MappingProxyType(self._registry_import_errors)
357
+
358
+ @staticmethod
359
+ def _behavior_runtime_id(
360
+ spec: BehaviorSpec,
361
  *,
362
+ owner_agent_id: str | None,
363
+ behavior_index: int | None,
364
+ ) -> str:
365
+ """Return a deterministic runtime id for a behavior spec."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
366
 
367
+ metadata_id = None
368
+ if isinstance(spec.metadata, Mapping):
369
+ metadata_id = spec.metadata.get("id") or spec.metadata.get("behavior_id")
370
+
371
+ if metadata_id is not None and str(metadata_id).strip():
372
+ return str(metadata_id).strip()
373
+
374
+ owner = owner_agent_id or "world"
375
+ index = 0 if behavior_index is None else int(behavior_index)
376
+ return f"{owner}:behavior:{index}:{spec.name}"
377
+
378
+ def create_world(self, spec: WorldSpec | Mapping[str, Any]) -> World:
379
+ """Create a runtime world from a validated or mappable world spec.
380
+
381
+ Args:
382
+ spec: ``WorldSpec`` or mapping that can validate into ``WorldSpec``.
383
+
384
+ Returns:
385
+ Runtime ``World`` object.
386
 
387
+ Raises:
388
+ WorldFactoryValidationError: If semantic validation fails.
389
+ WorldFactoryError: If runtime construction fails.
390
+ """
391
+
392
+ return self.create_world_result(spec).world
393
+
394
+ def create_world_result(self, spec: WorldSpec | Mapping[str, Any]) -> WorldBuildResult:
395
+ """Create a world and return both runtime object and build report."""
396
+
397
+ world_spec = self._coerce_world_spec(spec)
398
+ validation_report = self._validate_before_build(world_spec)
399
+
400
+ resources = tuple(self.create_resource(resource_spec) for resource_spec in world_spec.resources)
401
+ events = tuple(
402
+ self.create_event(event_spec)
403
+ for event_spec in world_spec.events
404
+ if event_spec.enabled or self.config.include_disabled_events
405
  )
406
+ agents = tuple(self.create_agent(agent_spec) for agent_spec in world_spec.agents)
407
+ metrics = tuple(self.create_metric_config(metric_spec) for metric_spec in world_spec.metrics)
408
 
409
+ world = self._create_world_object(
410
+ spec=world_spec,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
411
  agents=agents,
412
+ resources=resources,
413
  events=events,
414
+ metrics=metrics,
 
 
 
 
 
 
 
 
 
 
415
  )
416
 
417
+ if self.config.attach_scheduler:
418
+ self._attach_scheduler(world, world_spec)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
419
 
420
+ self._attach_world_metadata(world, world_spec)
421
+
422
+ report = WorldBuildReport(
423
+ world_id=world_spec.id,
424
+ agent_ids=tuple(agent.id for agent in world_spec.agents),
425
+ resource_ids=tuple(resource.id for resource in world_spec.resources),
426
+ event_keys=tuple(event.event_key for event in world_spec.events),
427
+ behavior_registry_count=len(self._behavior_registry),
428
+ policy_registry_count=len(self._policy_registry),
429
+ import_errors=copy.deepcopy(self._registry_import_errors),
430
+ validation_report=validation_report,
 
 
 
 
 
 
 
 
 
 
 
 
431
  metadata={
432
+ "world_name": world_spec.name,
433
+ "schema_version": world_spec.schema_version,
434
+ "simulation_steps": world_spec.simulation.steps,
435
  },
436
  )
437
 
438
+ logger.info(
439
+ "Created world %s with %s agent(s), %s resource(s), and %s event(s)",
440
+ world_spec.id,
441
+ len(agents),
442
+ len(resources),
443
+ len(events),
 
 
 
 
 
 
 
 
 
 
444
  )
445
 
446
+ return WorldBuildResult(world=world, report=report)
 
 
447
 
448
+ def create_agent(self, spec: AgentSpec) -> Agent:
449
+ """Create a runtime ``Agent`` from an ``AgentSpec``."""
450
 
451
+ behaviors = tuple(
452
+ self.create_behavior(
453
+ behavior_spec,
454
+ owner_agent_id=spec.id,
455
+ behavior_index=behavior_index,
456
+ )
457
+ for behavior_index, behavior_spec in enumerate(spec.behaviors)
458
+ if behavior_spec.enabled or self.config.include_disabled_behaviors
459
+ )
460
+ policy = self.create_policy_for_agent(spec)
461
+
462
+ params = {
463
+ "id": spec.id,
464
+ "type": spec.type,
465
+ "position": None if spec.position is None else tuple(spec.position),
466
+ "state": copy.deepcopy(spec.state),
467
+ "memory": copy.deepcopy(spec.memory),
468
+ "goals": copy.deepcopy(spec.goals),
469
+ "behaviors": behaviors,
470
+ "policy": policy,
471
+ "alive": bool(spec.alive),
472
  }
 
473
 
474
+ agent = self._instantiate_runtime_object(
475
+ self.config.agent_class,
476
+ params,
477
+ aliases=_CORE_AGENT_ALIASES,
478
+ strict=False,
479
+ path=f"agents.{spec.id}",
480
+ item_kind="agent",
481
+ )
 
 
 
 
 
 
 
 
 
482
 
483
+ self._safe_setattr(agent, "id", spec.id)
484
+ self._safe_setattr(agent, "type", spec.type)
485
+ self._safe_setattr(agent, "position", None if spec.position is None else tuple(spec.position))
486
+ self._safe_setattr(agent, "state", copy.deepcopy(spec.state))
487
+ self._safe_setattr(agent, "memory", copy.deepcopy(spec.memory))
488
+ self._safe_setattr(agent, "goals", copy.deepcopy(spec.goals))
489
+ self._safe_setattr(agent, "behaviors", behaviors)
490
+ self._safe_setattr(agent, "policy", policy)
491
+ self._safe_setattr(agent, "alive", bool(spec.alive))
492
+ self._safe_setattr(agent, "metadata", copy.deepcopy(spec.metadata))
493
+ self._safe_setattr(agent, "dsl_spec", spec)
494
+
495
+ return agent
496
+
497
+ def create_behavior(
498
+ self,
499
+ spec: BehaviorSpec,
500
+ *,
501
+ owner_agent_id: str | None = None,
502
+ behavior_index: int | None = None,
503
+ ) -> Behavior:
504
+ """Create a runtime behavior object from a ``BehaviorSpec``.
505
+
506
+ The DSL does not require behavior ids because behavior specs are declarative.
507
+ Runtime ids are injected here so behavior base classes that require ``id``
508
+ can still be instantiated safely and deterministically.
509
+ """
510
 
511
+ target = self._resolve_registry_item(
512
+ registry=self._behavior_registry,
513
+ name=spec.name,
514
+ strict=self.config.strict_unknown_behaviors,
515
+ item_kind="behavior",
516
+ path=f"behavior:{spec.name}",
517
+ )
518
 
519
+ runtime_behavior_id = self._behavior_runtime_id(
520
+ spec,
521
+ owner_agent_id=owner_agent_id,
522
+ behavior_index=behavior_index,
523
+ )
524
 
525
+ params = copy.deepcopy(spec.params)
526
+
527
+ # Runtime-injected fields. These are filtered out automatically if the
528
+ # concrete behavior constructor does not accept them.
529
+ params.setdefault("id", runtime_behavior_id)
530
+ params.setdefault("behavior_id", runtime_behavior_id)
531
+ params.setdefault("name", spec.name)
532
+
533
+ behavior = self._instantiate_runtime_object(
534
+ target,
535
+ params,
536
+ aliases={},
537
+ strict=self.config.strict_constructor_params,
538
+ path=f"behavior:{spec.name}.params",
539
+ item_kind="behavior",
540
  )
541
 
542
+ if not isinstance(behavior, Behavior) and not callable(getattr(behavior, "execute", None)):
543
+ raise RuntimeObjectBuildError(
544
+ f"Behavior {spec.name!r} did not produce an executable behavior object"
545
+ )
546
 
547
+ self._safe_setattr(behavior, "id", runtime_behavior_id)
548
+ self._safe_setattr(behavior, "name", spec.name)
549
+ self._safe_setattr(behavior, "enabled", bool(spec.enabled))
550
+ self._safe_setattr(behavior, "priority", float(spec.priority))
551
+ self._safe_setattr(behavior, "tags", tuple(spec.tags))
552
+ self._safe_setattr(behavior, "metadata", copy.deepcopy(spec.metadata))
553
+ self._safe_setattr(behavior, "dsl_spec", spec)
554
 
555
+ return behavior
 
 
 
556
 
557
+ def create_policy_for_agent(self, agent_spec: AgentSpec) -> BasePolicy | None:
558
+ """Create the policy configured for an agent.
559
 
560
+ If the agent has behaviors but omits a policy, the factory may attach a
561
+ default policy depending on configuration.
562
+ """
 
 
 
 
 
 
563
 
564
+ policy_spec = agent_spec.policy
 
 
 
 
565
 
566
+ if policy_spec is None and self.config.attach_default_policy and agent_spec.behaviors:
567
+ policy_spec = PolicySpec(type=self.config.default_policy_type, params={})
568
 
569
+ if policy_spec is None:
570
+ return None
 
571
 
572
+ return self.create_policy(policy_spec)
 
573
 
574
+ def create_policy(self, spec: PolicySpec) -> BasePolicy:
575
+ """Create a runtime policy object from a ``PolicySpec``."""
576
+
577
+ target = self._resolve_registry_item(
578
+ registry=self._policy_registry,
579
+ name=spec.type,
580
+ strict=self.config.strict_unknown_policies,
581
+ item_kind="policy",
582
+ path=f"policy:{spec.type}",
583
+ )
584
+
585
+ params = copy.deepcopy(spec.params)
586
+ params.setdefault("enabled", bool(spec.enabled))
587
+ if spec.metadata:
588
+ params.setdefault("metadata", copy.deepcopy(spec.metadata))
589
+
590
+ policy = self._instantiate_runtime_object(
591
+ target,
592
+ params,
593
+ aliases={},
594
+ strict=self.config.strict_constructor_params,
595
+ path=f"policy:{spec.type}.params",
596
+ item_kind="policy",
597
+ )
598
+
599
+ if not isinstance(policy, BasePolicy) and not callable(getattr(policy, "choose_action", None)):
600
+ raise RuntimeObjectBuildError(
601
+ f"Policy {spec.type!r} did not produce a valid policy object"
602
  )
603
 
604
+ self._safe_setattr(policy, "enabled", bool(spec.enabled))
605
+ self._safe_setattr(policy, "metadata", copy.deepcopy(spec.metadata))
606
+ self._safe_setattr(policy, "dsl_spec", spec)
607
+
608
+ return policy
609
+
610
+ def create_resource(self, spec: ResourceSpec) -> Resource:
611
+ """Create a runtime ``Resource`` from a ``ResourceSpec``."""
612
+
613
+ metadata = copy.deepcopy(spec.metadata)
614
+ metadata.setdefault("regeneration_rate", spec.regeneration_rate)
615
+ metadata.setdefault("max_amount", spec.max_amount)
616
+
617
+ params = {
618
+ "id": spec.id,
619
+ "type": spec.type,
620
+ "amount": float(spec.amount),
621
+ "position": None if spec.position is None else tuple(spec.position),
622
+ "metadata": metadata,
623
+ "regeneration_rate": float(spec.regeneration_rate),
624
+ "max_amount": spec.max_amount,
625
+ }
626
+
627
+ resource = self._instantiate_runtime_object(
628
+ self.config.resource_class,
629
+ params,
630
+ aliases=_CORE_RESOURCE_ALIASES,
631
+ strict=False,
632
+ path=f"resources.{spec.id}",
633
+ item_kind="resource",
634
  )
635
 
636
+ self._safe_setattr(resource, "id", spec.id)
637
+ self._safe_setattr(resource, "type", spec.type)
638
+ self._safe_setattr(resource, "amount", float(spec.amount))
639
+ self._safe_setattr(resource, "position", None if spec.position is None else tuple(spec.position))
640
+ self._safe_setattr(resource, "metadata", metadata)
641
+ self._safe_setattr(resource, "regeneration_rate", float(spec.regeneration_rate))
642
+ self._safe_setattr(resource, "max_amount", spec.max_amount)
643
+ self._safe_setattr(resource, "dsl_spec", spec)
644
+
645
+ return resource
646
+
647
+ def create_event(self, spec: EventSpec) -> Event | GenericScheduledEvent:
648
+ """Create a runtime event object from an ``EventSpec``."""
649
+
650
+ params = {
651
+ "id": spec.id,
652
+ "name": spec.name,
653
+ "trigger_step": int(spec.trigger_step),
654
+ "payload": copy.deepcopy(spec.payload),
655
+ "enabled": bool(spec.enabled),
656
+ "repeat_interval": spec.repeat_interval,
657
+ "target_agent_ids": tuple(spec.target_agent_ids),
658
+ "target_resource_ids": tuple(spec.target_resource_ids),
659
+ "metadata": copy.deepcopy(spec.metadata),
660
+ }
661
 
662
+ event_class = self.config.event_class
663
+ use_fallback = inspect.isabstract(event_class)
 
664
 
665
+ if use_fallback:
666
+ event: Event | GenericScheduledEvent = GenericScheduledEvent(**params)
667
+ else:
668
  try:
669
+ event = self._instantiate_runtime_object(
670
+ event_class,
671
+ params,
672
+ aliases=_CORE_EVENT_ALIASES,
673
+ strict=False,
674
+ path=f"events.{spec.event_key}",
675
+ item_kind="event",
 
 
 
 
676
  )
677
+ except WorldFactoryError:
678
+ logger.debug(
679
+ "Falling back to GenericScheduledEvent for event %s",
680
+ spec.event_key,
681
+ exc_info=True,
 
 
 
 
 
 
682
  )
683
+ event = GenericScheduledEvent(**params)
684
+
685
+ self._safe_setattr(event, "id", spec.id)
686
+ self._safe_setattr(event, "name", spec.name)
687
+ self._safe_setattr(event, "trigger_step", int(spec.trigger_step))
688
+ self._safe_setattr(event, "payload", copy.deepcopy(spec.payload))
689
+ self._safe_setattr(event, "enabled", bool(spec.enabled))
690
+ self._safe_setattr(event, "repeat_interval", spec.repeat_interval)
691
+ self._safe_setattr(event, "target_agent_ids", tuple(spec.target_agent_ids))
692
+ self._safe_setattr(event, "target_resource_ids", tuple(spec.target_resource_ids))
693
+ self._safe_setattr(event, "metadata", copy.deepcopy(spec.metadata))
694
+ self._safe_setattr(event, "dsl_spec", spec)
695
+
696
+ return event
697
+
698
+ def create_metric_config(self, spec: MetricSpec) -> dict[str, Any]:
699
+ """Create a metric configuration record.
700
+
701
+ Metric runtime classes are introduced later in the implementation
702
+ order. Until then, metric specs are preserved as serializable records on
703
+ the world.
704
+ """
705
 
706
+ return {
707
+ "name": spec.name,
708
+ "params": copy.deepcopy(spec.params),
709
+ "enabled": bool(spec.enabled),
710
+ "metadata": copy.deepcopy(spec.metadata),
711
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
712
 
713
+ def _create_world_object(
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
714
  self,
 
715
  *,
716
+ spec: WorldSpec,
717
+ agents: Sequence[Agent],
718
+ resources: Sequence[Resource],
719
+ events: Sequence[Event | GenericScheduledEvent],
720
+ metrics: Sequence[Mapping[str, Any]],
721
+ ) -> World:
722
+ """Instantiate and populate the runtime world object."""
723
+
724
+ agent_collection: Mapping[str, Agent] | tuple[Agent, ...]
725
+ resource_collection: Mapping[str, Resource] | tuple[Resource, ...]
726
+
727
+ if self.config.world_uses_mapping_collections:
728
+ agent_collection = {str(getattr(agent, "id")): agent for agent in agents}
729
+ resource_collection = {str(getattr(resource, "id")): resource for resource in resources}
730
+ else:
731
+ agent_collection = tuple(agents)
732
+ resource_collection = tuple(resources)
733
+
734
+ metric_collection = {
735
+ str(metric["name"]): copy.deepcopy(dict(metric))
736
+ for metric in metrics
737
+ if "name" in metric
738
+ }
739
 
740
+ params = {
741
+ "agents": agent_collection,
742
+ "resources": resource_collection,
743
+ "events": tuple(events),
744
+ "metrics": metric_collection,
745
+ "step_count": 0,
746
+ }
747
 
748
+ try:
749
+ world = self._instantiate_runtime_object(
750
+ self.config.world_class,
751
+ params,
752
+ aliases=_CORE_WORLD_ALIASES,
753
+ strict=False,
754
+ path=f"world:{spec.id}",
755
+ item_kind="world",
756
+ )
757
+ except WorldFactoryError:
758
+ logger.debug("Falling back to no-argument World constructor", exc_info=True)
759
+ world = self.config.world_class()
760
+
761
+ self._install_agents(world, agents)
762
+ self._install_resources(world, resources)
763
+ self._install_events(world, events)
764
+ self._safe_setattr(world, "metrics", metric_collection)
765
+ self._safe_setattr(world, "step_count", 0)
766
+
767
+ return world
768
+
769
+ def _install_agents(self, world: World, agents: Sequence[Agent]) -> None:
770
+ """Install agents into the world using adders when available."""
771
+
772
+ if self.config.world_uses_mapping_collections:
773
+ self._safe_setattr(world, "agents", {})
774
+ else:
775
+ self._safe_setattr(world, "agents", [])
776
+
777
+ add_agent = getattr(world, "add_agent", None)
778
+ if callable(add_agent):
779
+ for agent in agents:
780
+ add_agent(agent)
781
+ return
782
+
783
+ if self.config.world_uses_mapping_collections:
784
+ self._safe_setattr(world, "agents", {str(getattr(agent, "id")): agent for agent in agents})
785
+ else:
786
+ self._safe_setattr(world, "agents", list(agents))
787
+
788
+ def _install_resources(self, world: World, resources: Sequence[Resource]) -> None:
789
+ """Install resources into the world using adders when available."""
790
+
791
+ if self.config.world_uses_mapping_collections:
792
+ self._safe_setattr(world, "resources", {})
793
+ else:
794
+ self._safe_setattr(world, "resources", [])
795
+
796
+ add_resource = getattr(world, "add_resource", None)
797
+ if callable(add_resource):
798
+ for resource in resources:
799
+ add_resource(resource)
800
+ return
801
+
802
+ if self.config.world_uses_mapping_collections:
803
+ self._safe_setattr(
804
+ world,
805
+ "resources",
806
+ {str(getattr(resource, "id")): resource for resource in resources},
807
+ )
808
+ else:
809
+ self._safe_setattr(world, "resources", list(resources))
810
+
811
+ def _install_events(
812
  self,
813
+ world: World,
814
+ events: Sequence[Event | GenericScheduledEvent],
815
+ ) -> None:
816
+ """Install events into the world using adders when available."""
 
 
817
 
818
+ self._safe_setattr(world, "events", [])
 
819
 
820
+ add_event = getattr(world, "add_event", None)
821
+ if callable(add_event):
822
+ for event in events:
823
+ add_event(event)
824
+ return
825
 
826
+ self._safe_setattr(world, "events", list(events))
 
 
 
827
 
828
+ def _attach_scheduler(self, world: World, spec: WorldSpec) -> None:
829
+ """Attach a scheduler to the world when a scheduler class is configured."""
830
 
831
+ scheduler_class = self.config.scheduler_class
832
+ if scheduler_class is None:
833
+ return
834
 
835
+ params = {
836
+ "world": world,
837
+ "activation": spec.simulation.activation,
838
+ "scheduler": spec.simulation.scheduler,
839
+ "seed": spec.simulation.seed,
840
+ }
841
 
842
+ try:
843
+ scheduler = self._instantiate_runtime_object(
844
+ scheduler_class,
845
+ params,
846
+ aliases=_CORE_SCHEDULER_ALIASES,
847
+ strict=False,
848
+ path=f"world:{spec.id}.scheduler",
849
+ item_kind="scheduler",
850
+ )
851
+ except WorldFactoryError:
852
+ logger.debug("Falling back to no-argument Scheduler constructor", exc_info=True)
853
+ scheduler = scheduler_class()
854
+
855
+ self._safe_setattr(scheduler, "world", world)
856
+ self._safe_setattr(scheduler, "activation", spec.simulation.activation)
857
+ self._safe_setattr(scheduler, "scheduler", spec.simulation.scheduler)
858
+ self._safe_setattr(scheduler, "seed", spec.simulation.seed)
859
+ self._safe_setattr(world, "scheduler", scheduler)
860
+
861
+ def _attach_world_metadata(self, world: World, spec: WorldSpec) -> None:
862
+ """Attach DSL metadata and simulation configuration to the runtime world."""
863
+
864
+ self._safe_setattr(world, "id", spec.id)
865
+ self._safe_setattr(world, "name", spec.name)
866
+ self._safe_setattr(world, "description", spec.description)
867
+ self._safe_setattr(world, "metadata", copy.deepcopy(spec.metadata))
868
+ self._safe_setattr(world, "simulation", spec.simulation)
869
+ self._safe_setattr(world, "simulation_config", spec.simulation.to_dict())
870
+ self._safe_setattr(world, "space", spec.space)
871
+ self._safe_setattr(world, "space_config", None if spec.space is None else spec.space.to_dict())
872
+ self._safe_setattr(world, "schema_version", spec.schema_version)
873
+
874
+ if self.config.attach_dsl_spec_to_world:
875
+ self._safe_setattr(world, "dsl_spec", spec)
876
+
877
+ def _validate_before_build(self, spec: WorldSpec) -> ValidationReport | None:
878
+ """Optionally run semantic validation before object construction."""
879
+
880
+ if not self.config.validate_before_build:
881
+ return None
882
+
883
+ validation_config = ValidationConfig(
884
+ behavior_registry=self._behavior_registry,
885
+ policy_registry=self._policy_registry,
886
+ require_known_behaviors=self.config.strict_unknown_behaviors,
887
+ require_known_policies=self.config.strict_unknown_policies,
888
+ validate_constructor_params=self.config.strict_constructor_params,
889
+ unknown_registry_item_severity=ValidationSeverity.ERROR
890
+ if self.config.strict_unknown_behaviors or self.config.strict_unknown_policies
891
+ else ValidationSeverity.WARNING,
892
+ constructor_param_severity=ValidationSeverity.ERROR
893
+ if self.config.strict_constructor_params
894
+ else ValidationSeverity.WARNING,
895
  )
896
+ report = validate_world_spec(spec, config=validation_config)
897
 
898
+ if not report.is_valid:
899
+ raise WorldFactoryValidationError(report)
900
 
901
+ return report
902
+
903
+ def _coerce_world_spec(self, spec: WorldSpec | Mapping[str, Any]) -> WorldSpec:
904
+ """Return a ``WorldSpec`` from a spec-like object."""
905
+
906
+ if isinstance(spec, WorldSpec):
907
+ return spec
908
+
909
+ if isinstance(spec, Mapping):
910
+ return WorldSpec.model_validate(dict(spec))
911
+
912
+ raise TypeError(f"Expected WorldSpec or mapping, got {spec.__class__.__name__}")
913
+
914
+ def _build_behavior_registry(self) -> dict[str, Any]:
915
+ """Build the behavior registry from defaults and custom entries."""
916
+
917
+ registry: dict[str, Any] = {}
918
+
919
+ module_report = self._load_module_registries(
920
+ self.config.behavior_modules,
921
+ BEHAVIOR_REGISTRY_ATTRIBUTES,
922
+ base_class=Behavior,
923
  )
924
+ registry.update(module_report.registry)
925
+ self._registry_import_errors.update(module_report.import_errors)
926
 
927
+ if self.config.behavior_registry is not None:
928
+ registry.update({str(key): value for key, value in self.config.behavior_registry.items()})
929
+
930
+ return self._with_class_aliases(registry)
 
931
 
932
+ def _build_policy_registry(self) -> dict[str, Any]:
933
+ """Build the policy registry from defaults and custom entries."""
934
 
935
+ registry: dict[str, Any] = {}
936
+
937
+ module_report = self._load_module_registries(
938
+ self.config.policy_modules,
939
+ POLICY_REGISTRY_ATTRIBUTES,
940
+ base_class=BasePolicy,
941
+ )
942
+ registry.update(module_report.registry)
943
+ self._registry_import_errors.update(module_report.import_errors)
944
 
945
+ if self.config.policy_registry is not None:
946
+ registry.update({str(key): value for key, value in self.config.policy_registry.items()})
 
 
 
 
 
947
 
948
+ return self._with_class_aliases(registry)
949
 
950
+ def _load_module_registries(
951
  self,
952
+ module_names: Sequence[str],
953
+ registry_attributes: Sequence[str],
954
  *,
955
+ base_class: type[Any],
956
+ ) -> RegistryLoadReport:
957
+ """Load registry mappings from modules and discover named subclasses."""
958
 
959
+ registry: dict[str, Any] = {}
960
+ import_errors: dict[str, str] = {}
961
 
962
+ for module_name in module_names:
963
+ try:
964
+ module = import_module(module_name)
965
+ except Exception as exc: # pragma: no cover - defensive for user modules
966
+ import_errors[str(module_name)] = f"{exc.__class__.__name__}: {exc}"
967
+ continue
968
 
969
+ for attribute_name in registry_attributes:
970
+ raw_registry = getattr(module, attribute_name, None)
971
+ if isinstance(raw_registry, Mapping):
972
+ registry.update({str(key): value for key, value in raw_registry.items()})
973
 
974
+ registry.update(self._discover_named_subclasses(module, base_class=base_class))
 
 
 
 
 
 
 
975
 
976
+ return RegistryLoadReport(registry=registry, import_errors=import_errors)
 
977
 
978
+ @staticmethod
979
+ def _discover_named_subclasses(module: Any, *, base_class: type[Any]) -> dict[str, Any]:
980
+ """Discover subclasses with a string ``name`` attribute from a module."""
981
 
982
+ discovered: dict[str, Any] = {}
983
+
984
+ for attribute_value in vars(module).values():
985
+ if not inspect.isclass(attribute_value):
986
+ continue
987
+
988
+ if attribute_value is base_class:
989
+ continue
990
+
991
+ try:
992
+ is_subclass = issubclass(attribute_value, base_class)
993
+ except TypeError:
994
+ is_subclass = False
995
+
996
+ if not is_subclass:
997
+ continue
998
+
999
+ name = getattr(attribute_value, "name", None)
1000
+ if isinstance(name, str) and name:
1001
+ discovered.setdefault(name, attribute_value)
1002
+
1003
+ return discovered
 
 
 
 
 
1004
 
1005
  @staticmethod
1006
+ def _with_class_aliases(registry: Mapping[str, Any]) -> dict[str, Any]:
1007
+ """Add class-name and ``name`` aliases to registry entries."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1008
 
1009
+ expanded = {str(key): value for key, value in registry.items()}
 
1010
 
1011
+ for key, value in list(expanded.items()):
1012
+ class_name = getattr(value, "__name__", None)
1013
+ if class_name:
1014
+ expanded.setdefault(str(class_name), value)
1015
 
1016
+ runtime_name = getattr(value, "name", None)
1017
+ if isinstance(runtime_name, str) and runtime_name:
1018
+ expanded.setdefault(runtime_name, value)
1019
+
1020
+ expanded.setdefault(str(key).lower(), value)
 
 
 
 
1021
 
1022
+ return expanded
1023
 
1024
+ def _resolve_registry_item(
1025
  self,
 
1026
  *,
1027
+ registry: Mapping[str, Any],
1028
+ name: str,
1029
+ strict: bool,
1030
+ item_kind: str,
1031
+ path: str,
1032
+ ) -> Any:
1033
+ """Resolve a registry item by exact or case-insensitive name."""
1034
+
1035
+ if name in registry:
1036
+ return registry[name]
1037
+
1038
+ lower_name = name.lower()
1039
+ if lower_name in registry:
1040
+ return registry[lower_name]
1041
+
1042
+ for registered_name, item in registry.items():
1043
+ if registered_name.lower() == lower_name:
1044
+ return item
1045
+
1046
+ message = (
1047
+ f"Unknown {item_kind} {name!r} at {path}. "
1048
+ f"Known {item_kind}s: {sorted(str(key) for key in registry.keys())}"
1049
  )
1050
+
1051
+ if strict:
1052
+ raise RegistryResolutionError(message)
1053
+
1054
+ logger.warning(message)
1055
+ return self._noop_behavior_factory(name) if item_kind == "behavior" else self._noop_policy_factory(name)
1056
+
1057
+ def _instantiate_runtime_object(
1058
+ self,
1059
+ target: Any,
1060
+ params: Mapping[str, Any],
1061
+ *,
1062
+ aliases: Mapping[str, Sequence[str]],
1063
+ strict: bool,
1064
+ path: str,
1065
+ item_kind: str,
1066
+ ) -> Any:
1067
+ """Instantiate a runtime object using signature-aware keyword binding."""
1068
+
1069
+ if not callable(target):
1070
+ try:
1071
+ return copy.deepcopy(target)
1072
+ except Exception as exc:
1073
+ raise RuntimeObjectBuildError(
1074
+ f"Registered {item_kind} at {path} is not callable and could not be copied"
1075
+ ) from exc
1076
+
1077
+ kwargs = self._bind_constructor_kwargs(
1078
+ target,
1079
+ params,
1080
+ aliases=aliases,
1081
+ strict=strict,
1082
+ path=path,
1083
+ item_kind=item_kind,
1084
  )
1085
 
1086
+ try:
1087
+ return target(**kwargs)
1088
+ except TypeError as exc:
1089
+ raise ConstructorBindingError(
1090
+ f"Could not instantiate {item_kind} at {path}: {exc}"
1091
+ ) from exc
1092
+ except Exception as exc:
1093
+ raise RuntimeObjectBuildError(
1094
+ f"Constructor for {item_kind} at {path} failed: {exc}"
1095
+ ) from exc
1096
 
1097
+ def _bind_constructor_kwargs(
1098
+ self,
1099
+ target: Any,
1100
+ params: Mapping[str, Any],
1101
+ *,
1102
+ aliases: Mapping[str, Sequence[str]],
1103
+ strict: bool,
1104
+ path: str,
1105
+ item_kind: str,
1106
+ ) -> dict[str, Any]:
1107
+ """Filter and alias params according to a target constructor signature."""
 
 
1108
 
1109
+ working = copy.deepcopy(dict(params))
1110
+ signature_info = self._constructor_signature_info(target)
 
 
 
 
 
1111
 
1112
+ if signature_info is None:
1113
+ return working
1114
 
1115
+ allowed_params, required_params, accepts_var_keyword = signature_info
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1116
 
1117
+ if accepts_var_keyword:
1118
+ return working
1119
 
1120
+ for canonical_name, alias_names in aliases.items():
1121
+ if canonical_name not in working:
1122
+ continue
1123
 
1124
+ if canonical_name in allowed_params:
1125
+ continue
1126
 
1127
+ for alias_name in alias_names:
1128
+ if alias_name in allowed_params and alias_name not in working:
1129
+ working[alias_name] = working[canonical_name]
1130
+ break
 
 
 
1131
 
1132
+ unknown_params = set(working.keys()) - allowed_params
1133
+ if unknown_params and strict:
1134
+ raise ConstructorBindingError(
1135
+ f"Unknown constructor parameter(s) for {item_kind} at {path}: "
1136
+ f"{sorted(unknown_params)}. Allowed parameters: {sorted(allowed_params)}"
1137
+ )
1138
 
1139
+ filtered = {
1140
+ key: value
1141
+ for key, value in working.items()
1142
+ if key in allowed_params
1143
+ }
1144
 
1145
+ missing_required = required_params - set(filtered.keys())
1146
+ if missing_required:
1147
+ raise ConstructorBindingError(
1148
+ f"Missing required constructor parameter(s) for {item_kind} at {path}: "
1149
+ f"{sorted(missing_required)}"
1150
+ )
1151
 
1152
+ return filtered
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1153
 
1154
+ @staticmethod
1155
+ def _constructor_signature_info(target: Any) -> tuple[set[str], set[str], bool] | None:
1156
+ """Return constructor binding information for a callable target."""
1157
 
1158
+ try:
1159
+ signature = inspect.signature(target)
1160
+ except (TypeError, ValueError):
1161
+ return None
1162
 
1163
+ allowed_params: set[str] = set()
1164
+ required_params: set[str] = set()
1165
+ accepts_var_keyword = False
1166
 
1167
+ for name, parameter in signature.parameters.items():
1168
+ if name == "self":
1169
+ continue
1170
 
1171
+ if parameter.kind is inspect.Parameter.VAR_KEYWORD:
1172
+ accepts_var_keyword = True
1173
+ continue
1174
 
1175
+ if parameter.kind is inspect.Parameter.VAR_POSITIONAL:
1176
+ continue
1177
 
1178
+ if parameter.kind not in {
1179
+ inspect.Parameter.POSITIONAL_OR_KEYWORD,
1180
+ inspect.Parameter.KEYWORD_ONLY,
1181
+ }:
1182
+ continue
1183
 
1184
+ allowed_params.add(name)
 
 
 
1185
 
1186
+ if parameter.default is inspect.Parameter.empty:
1187
+ required_params.add(name)
 
 
 
1188
 
1189
+ return allowed_params, required_params, accepts_var_keyword
 
1190
 
1191
+ @staticmethod
1192
+ def _safe_setattr(instance: Any, name: str, value: Any) -> bool:
1193
+ """Set an attribute defensively.
 
 
1194
 
1195
+ Returns:
1196
+ ``True`` if the attribute was set, otherwise ``False``.
1197
+ """
1198
 
1199
+ try:
1200
+ setattr(instance, name, value)
1201
+ return True
1202
+ except Exception:
1203
+ logger.debug(
1204
+ "Could not set attribute %s on %s",
1205
+ name,
1206
+ instance.__class__.__name__,
1207
+ exc_info=True,
1208
+ )
1209
+ return False
1210
 
1211
+ @staticmethod
1212
+ def _noop_behavior_factory(name: str) -> type[Behavior]:
1213
+ """Create a no-op behavior class for permissive unknown behavior mode."""
1214
 
1215
+ class NoOpBehavior(Behavior):
1216
+ """Fallback behavior used only when unknown behaviors are permissive."""
1217
 
1218
+ behavior_name = name
1219
 
1220
+ def check_preconditions(self, agent: Agent, world: World) -> bool:
1221
+ """Return False so unknown behavior never executes."""
1222
 
1223
+ del agent, world
1224
+ return False
 
 
1225
 
1226
+ def execute(self, agent: Agent, world: World) -> dict[str, Any]:
1227
+ """Return a structured no-op result."""
1228
 
1229
+ del world
1230
+ return {
1231
+ "behavior": self.behavior_name,
1232
+ "actor_id": getattr(agent, "id", None),
1233
+ "success": False,
1234
+ "details": {"reason": "unknown_behavior_noop"},
1235
+ }
1236
 
1237
+ NoOpBehavior.name = name # type: ignore[attr-defined]
1238
+ return NoOpBehavior
1239
 
1240
+ @staticmethod
1241
+ def _noop_policy_factory(name: str) -> type[BasePolicy]:
1242
+ """Create a no-op policy class for permissive unknown policy mode."""
1243
 
1244
+ class NoOpPolicy(BasePolicy):
1245
+ """Fallback policy used only when unknown policies are permissive."""
1246
 
1247
+ policy_name_value = name
 
1248
 
1249
+ def choose_action(
1250
+ self,
1251
+ agent: Agent,
1252
+ world: World,
1253
+ behaviors: Sequence[Behavior] | Mapping[str, Behavior] | None = None,
1254
+ ) -> Behavior | None:
1255
+ """Choose no behavior."""
1256
 
1257
+ del agent, world, behaviors
1258
+ return None
1259
 
1260
+ NoOpPolicy.name = name # type: ignore[attr-defined]
1261
+ return NoOpPolicy
1262
 
 
 
 
 
1263
 
1264
+ def create_world(
1265
+ spec: WorldSpec | Mapping[str, Any],
1266
+ *,
1267
+ config: WorldFactoryConfig | None = None,
1268
+ ) -> World:
1269
+ """Convenience function that creates a runtime world from a spec."""
1270
 
1271
+ return WorldFactory(config=config or WorldFactoryConfig()).create_world(spec)
1272
+
1273
+
1274
+ def create_world_result(
1275
+ spec: WorldSpec | Mapping[str, Any],
1276
+ *,
1277
+ config: WorldFactoryConfig | None = None,
1278
+ ) -> WorldBuildResult:
1279
+ """Convenience function that creates a world and returns its build report."""
1280
 
1281
+ return WorldFactory(config=config or WorldFactoryConfig()).create_world_result(spec)
 
1282
 
1283
 
1284
  __all__ = [
1285
+ "BEHAVIOR_REGISTRY_ATTRIBUTES",
1286
+ "DEFAULT_BEHAVIOR_MODULES",
1287
+ "DEFAULT_POLICY_MODULES",
1288
+ "GenericScheduledEvent",
1289
+ "ConstructorBindingError",
1290
+ "POLICY_REGISTRY_ATTRIBUTES",
1291
+ "RegistryLoadReport",
1292
+ "RegistryResolutionError",
1293
+ "RuntimeObjectBuildError",
1294
+ "WorldBuildReport",
1295
+ "WorldBuildResult",
1296
+ "WorldFactory",
1297
+ "WorldFactoryConfig",
1298
+ "WorldFactoryError",
1299
+ "WorldFactoryValidationError",
1300
+ "create_world",
1301
+ "create_world_result",
 
 
 
 
1302
  ]