@elizaos/python 2.0.0-alpha.10 → 2.0.0-alpha.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. package/elizaos/__init__.py +0 -1
  2. package/elizaos/advanced_capabilities/__init__.py +6 -41
  3. package/elizaos/advanced_capabilities/actions/__init__.py +1 -21
  4. package/elizaos/advanced_capabilities/actions/add_contact.py +21 -11
  5. package/elizaos/advanced_capabilities/actions/follow_room.py +28 -28
  6. package/elizaos/advanced_capabilities/actions/image_generation.py +13 -26
  7. package/elizaos/advanced_capabilities/actions/mute_room.py +13 -26
  8. package/elizaos/advanced_capabilities/actions/remove_contact.py +16 -2
  9. package/elizaos/advanced_capabilities/actions/roles.py +13 -27
  10. package/elizaos/advanced_capabilities/actions/schedule_follow_up.py +70 -15
  11. package/elizaos/advanced_capabilities/actions/search_contacts.py +17 -3
  12. package/elizaos/advanced_capabilities/actions/send_message.py +183 -50
  13. package/elizaos/advanced_capabilities/actions/settings.py +16 -2
  14. package/elizaos/advanced_capabilities/actions/unfollow_room.py +13 -26
  15. package/elizaos/advanced_capabilities/actions/unmute_room.py +13 -26
  16. package/elizaos/advanced_capabilities/actions/update_contact.py +16 -2
  17. package/elizaos/advanced_capabilities/actions/update_entity.py +16 -2
  18. package/elizaos/advanced_capabilities/evaluators/__init__.py +2 -9
  19. package/elizaos/advanced_capabilities/evaluators/reflection.py +3 -132
  20. package/elizaos/advanced_capabilities/evaluators/relationship_extraction.py +5 -201
  21. package/elizaos/advanced_capabilities/providers/__init__.py +1 -12
  22. package/elizaos/advanced_capabilities/providers/knowledge.py +24 -3
  23. package/elizaos/advanced_capabilities/services/__init__.py +2 -9
  24. package/elizaos/advanced_memory/actions/reset_session.py +11 -0
  25. package/elizaos/advanced_memory/evaluators/reflection.py +134 -0
  26. package/elizaos/advanced_memory/evaluators/relationship_extraction.py +203 -0
  27. package/elizaos/advanced_memory/memory_service.py +15 -17
  28. package/elizaos/advanced_memory/test_advanced_memory.py +357 -0
  29. package/elizaos/advanced_planning/actions/schedule_follow_up.py +222 -0
  30. package/elizaos/advanced_planning/planning_service.py +26 -14
  31. package/elizaos/basic_capabilities/__init__.py +0 -2
  32. package/elizaos/basic_capabilities/providers/__init__.py +0 -3
  33. package/elizaos/basic_capabilities/providers/actions.py +118 -29
  34. package/elizaos/basic_capabilities/providers/agent_settings.py +64 -0
  35. package/elizaos/basic_capabilities/providers/character.py +19 -21
  36. package/elizaos/basic_capabilities/providers/contacts.py +79 -0
  37. package/elizaos/basic_capabilities/providers/current_time.py +7 -4
  38. package/elizaos/basic_capabilities/providers/facts.py +87 -0
  39. package/elizaos/basic_capabilities/providers/follow_ups.py +117 -0
  40. package/elizaos/basic_capabilities/providers/knowledge.py +97 -0
  41. package/elizaos/basic_capabilities/providers/relationships.py +107 -0
  42. package/elizaos/basic_capabilities/providers/roles.py +96 -0
  43. package/elizaos/basic_capabilities/providers/settings.py +56 -0
  44. package/elizaos/basic_capabilities/providers/time.py +7 -4
  45. package/elizaos/bootstrap/__init__.py +21 -2
  46. package/elizaos/bootstrap/actions/schedule_follow_up.py +65 -7
  47. package/elizaos/bootstrap/actions/send_message.py +162 -15
  48. package/elizaos/bootstrap/autonomy/__init__.py +5 -1
  49. package/elizaos/bootstrap/autonomy/action.py +161 -0
  50. package/elizaos/bootstrap/autonomy/evaluators.py +217 -0
  51. package/elizaos/bootstrap/autonomy/service.py +238 -28
  52. package/elizaos/bootstrap/plugin.py +7 -0
  53. package/elizaos/bootstrap/providers/actions.py +118 -27
  54. package/elizaos/bootstrap/providers/agent_settings.py +1 -0
  55. package/elizaos/bootstrap/providers/attachments.py +1 -0
  56. package/elizaos/bootstrap/providers/capabilities.py +1 -0
  57. package/elizaos/bootstrap/providers/character.py +1 -0
  58. package/elizaos/bootstrap/providers/choice.py +1 -0
  59. package/elizaos/bootstrap/providers/contacts.py +1 -0
  60. package/elizaos/bootstrap/providers/current_time.py +8 -2
  61. package/elizaos/bootstrap/providers/entities.py +1 -0
  62. package/elizaos/bootstrap/providers/evaluators.py +1 -0
  63. package/elizaos/bootstrap/providers/facts.py +1 -0
  64. package/elizaos/bootstrap/providers/follow_ups.py +1 -0
  65. package/elizaos/bootstrap/providers/knowledge.py +27 -3
  66. package/elizaos/bootstrap/providers/providers_list.py +1 -0
  67. package/elizaos/bootstrap/providers/relationships.py +1 -0
  68. package/elizaos/bootstrap/providers/roles.py +1 -0
  69. package/elizaos/bootstrap/providers/settings.py +1 -0
  70. package/elizaos/bootstrap/providers/time.py +8 -4
  71. package/elizaos/bootstrap/providers/world.py +1 -0
  72. package/elizaos/bootstrap/services/embedding.py +156 -1
  73. package/elizaos/deterministic.py +193 -0
  74. package/elizaos/generated/__init__.py +1 -0
  75. package/elizaos/generated/action_docs.py +3181 -0
  76. package/elizaos/generated/spec_helpers.py +175 -0
  77. package/elizaos/media/mime.py +2 -2
  78. package/elizaos/media/search.py +23 -23
  79. package/elizaos/runtime.py +215 -57
  80. package/elizaos/services/message_service.py +175 -29
  81. package/elizaos/types/components.py +2 -2
  82. package/elizaos/types/generated/__init__.py +12 -0
  83. package/elizaos/types/generated/eliza/v1/agent_pb2.py +63 -0
  84. package/elizaos/types/generated/eliza/v1/agent_pb2.pyi +159 -0
  85. package/elizaos/types/generated/eliza/v1/components_pb2.py +65 -0
  86. package/elizaos/types/generated/eliza/v1/components_pb2.pyi +160 -0
  87. package/elizaos/types/generated/eliza/v1/database_pb2.py +78 -0
  88. package/elizaos/types/generated/eliza/v1/database_pb2.pyi +305 -0
  89. package/elizaos/types/generated/eliza/v1/environment_pb2.py +58 -0
  90. package/elizaos/types/generated/eliza/v1/environment_pb2.pyi +135 -0
  91. package/elizaos/types/generated/eliza/v1/events_pb2.py +82 -0
  92. package/elizaos/types/generated/eliza/v1/events_pb2.pyi +322 -0
  93. package/elizaos/types/generated/eliza/v1/ipc_pb2.py +113 -0
  94. package/elizaos/types/generated/eliza/v1/ipc_pb2.pyi +367 -0
  95. package/elizaos/types/generated/eliza/v1/knowledge_pb2.py +41 -0
  96. package/elizaos/types/generated/eliza/v1/knowledge_pb2.pyi +26 -0
  97. package/elizaos/types/generated/eliza/v1/memory_pb2.py +55 -0
  98. package/elizaos/types/generated/eliza/v1/memory_pb2.pyi +111 -0
  99. package/elizaos/types/generated/eliza/v1/message_service_pb2.py +48 -0
  100. package/elizaos/types/generated/eliza/v1/message_service_pb2.pyi +69 -0
  101. package/elizaos/types/generated/eliza/v1/messaging_pb2.py +51 -0
  102. package/elizaos/types/generated/eliza/v1/messaging_pb2.pyi +97 -0
  103. package/elizaos/types/generated/eliza/v1/model_pb2.py +84 -0
  104. package/elizaos/types/generated/eliza/v1/model_pb2.pyi +280 -0
  105. package/elizaos/types/generated/eliza/v1/payment_pb2.py +44 -0
  106. package/elizaos/types/generated/eliza/v1/payment_pb2.pyi +70 -0
  107. package/elizaos/types/generated/eliza/v1/plugin_pb2.py +68 -0
  108. package/elizaos/types/generated/eliza/v1/plugin_pb2.pyi +145 -0
  109. package/elizaos/types/generated/eliza/v1/primitives_pb2.py +48 -0
  110. package/elizaos/types/generated/eliza/v1/primitives_pb2.pyi +92 -0
  111. package/elizaos/types/generated/eliza/v1/prompts_pb2.py +52 -0
  112. package/elizaos/types/generated/eliza/v1/prompts_pb2.pyi +74 -0
  113. package/elizaos/types/generated/eliza/v1/service_interfaces_pb2.py +211 -0
  114. package/elizaos/types/generated/eliza/v1/service_interfaces_pb2.pyi +1296 -0
  115. package/elizaos/types/generated/eliza/v1/service_pb2.py +42 -0
  116. package/elizaos/types/generated/eliza/v1/service_pb2.pyi +69 -0
  117. package/elizaos/types/generated/eliza/v1/settings_pb2.py +58 -0
  118. package/elizaos/types/generated/eliza/v1/settings_pb2.pyi +85 -0
  119. package/elizaos/types/generated/eliza/v1/state_pb2.py +60 -0
  120. package/elizaos/types/generated/eliza/v1/state_pb2.pyi +114 -0
  121. package/elizaos/types/generated/eliza/v1/task_pb2.py +42 -0
  122. package/elizaos/types/generated/eliza/v1/task_pb2.pyi +58 -0
  123. package/elizaos/types/generated/eliza/v1/tee_pb2.py +52 -0
  124. package/elizaos/types/generated/eliza/v1/tee_pb2.pyi +90 -0
  125. package/elizaos/types/generated/eliza/v1/testing_pb2.py +39 -0
  126. package/elizaos/types/generated/eliza/v1/testing_pb2.pyi +23 -0
  127. package/elizaos/types/model.py +30 -0
  128. package/elizaos/types/runtime.py +6 -2
  129. package/elizaos/utils/validation.py +76 -0
  130. package/package.json +3 -2
  131. package/tests/test_action_parameters.py +2 -3
  132. package/tests/test_actions_provider_examples.py +58 -1
  133. package/tests/test_advanced_memory_behavior.py +0 -2
  134. package/tests/test_advanced_memory_flag.py +0 -2
  135. package/tests/test_advanced_planning_behavior.py +11 -5
  136. package/tests/test_async_embedding.py +124 -0
  137. package/tests/test_autonomy.py +24 -3
  138. package/tests/test_runtime.py +8 -17
  139. package/tests/test_schedule_follow_up_action.py +260 -0
  140. package/tests/test_send_message_action_targets.py +114 -0
  141. package/tests/test_settings_crypto.py +0 -2
  142. package/tests/test_validation.py +141 -0
  143. package/tests/verify_memory_architecture.py +192 -0
  144. package/uv.lock +1565 -0
  145. package/elizaos/basic_capabilities/providers/capabilities.py +0 -62
@@ -11,6 +11,7 @@ import contextlib
11
11
  import logging
12
12
  import time
13
13
  import uuid
14
+ from collections.abc import Iterable
14
15
  from typing import TYPE_CHECKING, Any
15
16
 
16
17
  from elizaos.bootstrap.services.task import Task
@@ -23,7 +24,7 @@ from elizaos.prompts import (
23
24
  from elizaos.types.environment import Room, World
24
25
  from elizaos.types.events import EventType
25
26
  from elizaos.types.memory import Memory
26
- from elizaos.types.primitives import UUID, Content, as_uuid
27
+ from elizaos.types.primitives import UUID, Content, as_uuid, string_to_uuid
27
28
  from elizaos.types.service import Service
28
29
 
29
30
  from .types import AutonomyStatus
@@ -40,8 +41,7 @@ AUTONOMY_SERVICE_TYPE = "AUTONOMY"
40
41
  AUTONOMY_TASK_NAME = "AUTONOMY_THINK"
41
42
 
42
43
  # Tags used for autonomy tasks (parity with TypeScript).
43
- # Note: TypeScript uses ["repeat", "autonomy", "internal"] without "queue".
44
- AUTONOMY_TASK_TAGS = ["repeat", "autonomy", "internal"]
44
+ AUTONOMY_TASK_TAGS = ["queue", "repeat", "autonomy"]
45
45
 
46
46
  # Default interval in milliseconds
47
47
  DEFAULT_INTERVAL_MS = 30_000
@@ -112,8 +112,10 @@ class AutonomyService(Service):
112
112
  self._interval_ms = DEFAULT_INTERVAL_MS
113
113
  self._task_registered = False
114
114
  self._settings_monitor_task: asyncio.Task[None] | None = None
115
- self._autonomous_room_id = as_uuid(str(uuid.uuid4()))
115
+ # Placeholder; replaced with a deterministic ID during _initialize().
116
+ self._autonomous_room_id = as_uuid("00000000-0000-0000-0000-000000000000")
116
117
  self._autonomous_world_id = as_uuid("00000000-0000-0000-0000-000000000001")
118
+ self._autonomy_entity_id = as_uuid("00000000-0000-0000-0000-000000000002")
117
119
 
118
120
  def _log(self, level: str, msg: str) -> None:
119
121
  if self._runtime:
@@ -135,6 +137,9 @@ class AutonomyService(Service):
135
137
  if not self._runtime:
136
138
  return
137
139
 
140
+ self._autonomous_room_id = as_uuid(
141
+ string_to_uuid(f"autonomy-room-{self._runtime.agent_id}")
142
+ )
138
143
  self._log("info", f"Using autonomous room ID: {self._autonomous_room_id}")
139
144
 
140
145
  # Ensure autonomous context exists
@@ -289,35 +294,231 @@ class AutonomyService(Service):
289
294
  except Exception:
290
295
  return None
291
296
 
297
+ @staticmethod
298
+ def _coerce_name(entity: object) -> str | None:
299
+ if isinstance(entity, dict):
300
+ names = entity.get("names")
301
+ if isinstance(names, list):
302
+ for name in names:
303
+ if isinstance(name, str) and name.strip():
304
+ return name.strip()
305
+ name = entity.get("name")
306
+ if isinstance(name, str) and name.strip():
307
+ return name.strip()
308
+ return None
309
+
310
+ names = getattr(entity, "names", None)
311
+ if isinstance(names, list):
312
+ for name in names:
313
+ if isinstance(name, str) and name.strip():
314
+ return name.strip()
315
+
316
+ name = getattr(entity, "name", None)
317
+ if isinstance(name, str) and name.strip():
318
+ return name.strip()
319
+
320
+ return None
321
+
322
+ @staticmethod
323
+ def _memory_text(memory: Memory) -> str:
324
+ if memory.content and isinstance(memory.content.text, str):
325
+ return memory.content.text.strip()
326
+ return ""
327
+
328
+ async def _build_entity_name_lookup(self, entity_ids: Iterable[UUID]) -> dict[UUID, str]:
329
+ if not self._runtime:
330
+ return {}
331
+
332
+ ids = list({entity_id for entity_id in entity_ids if entity_id})
333
+ if not ids:
334
+ return {}
335
+
336
+ getter = getattr(self._runtime, "get_entities_by_ids", None)
337
+ if not callable(getter):
338
+ return {}
339
+
340
+ try:
341
+ entities = await getter(ids)
342
+ except Exception:
343
+ return {}
344
+
345
+ name_by_id: dict[UUID, str] = {}
346
+ for entity in entities or []:
347
+ entity_id = None
348
+ if isinstance(entity, dict):
349
+ raw_id = entity.get("id")
350
+ if isinstance(raw_id, str):
351
+ with contextlib.suppress(Exception):
352
+ entity_id = as_uuid(raw_id)
353
+ else:
354
+ raw_id = getattr(entity, "id", None)
355
+ if raw_id is not None:
356
+ with contextlib.suppress(Exception):
357
+ entity_id = as_uuid(str(raw_id))
358
+
359
+ if not entity_id:
360
+ continue
361
+
362
+ entity_name = self._coerce_name(entity)
363
+ if entity_name:
364
+ name_by_id[entity_id] = entity_name
365
+
366
+ return name_by_id
367
+
368
+ @staticmethod
369
+ def _dedupe_memories_by_id_keep_earliest(memories: list[Memory]) -> list[Memory]:
370
+ by_id: dict[str, Memory] = {}
371
+ without_id: list[Memory] = []
372
+
373
+ for memory in memories:
374
+ mem_id = str(memory.id) if memory.id else ""
375
+ if not mem_id:
376
+ without_id.append(memory)
377
+ continue
378
+
379
+ existing = by_id.get(mem_id)
380
+ if existing is None or (memory.created_at or 0) < (existing.created_at or 0):
381
+ by_id[mem_id] = memory
382
+
383
+ return [*without_id, *by_id.values()]
384
+
292
385
  async def _get_target_room_context_text(self) -> str:
293
386
  if not self._runtime:
294
- return "(no target room configured)"
387
+ return "(no rooms configured)"
388
+
295
389
  target_room_id = self._get_target_room_id()
296
- if not target_room_id:
297
- return "(no target room configured)"
298
- memories_table = await self._runtime.get_memories(
299
- {"roomId": target_room_id, "count": 15, "tableName": "memories"}
390
+
391
+ ordered_room_ids: list[UUID] = []
392
+ if target_room_id:
393
+ ordered_room_ids.append(target_room_id)
394
+
395
+ get_participant_rooms = getattr(self._runtime, "get_rooms_for_participant", None)
396
+ if callable(get_participant_rooms):
397
+ with contextlib.suppress(Exception):
398
+ participant_rooms = await get_participant_rooms(self._runtime.agent_id)
399
+ for room_id in participant_rooms or []:
400
+ if room_id not in ordered_room_ids:
401
+ ordered_room_ids.append(room_id)
402
+
403
+ if not ordered_room_ids:
404
+ return "(no rooms configured)"
405
+
406
+ room_name_by_id: dict[UUID, str] = {}
407
+ get_rooms_by_ids = getattr(self._runtime, "get_rooms_by_ids", None)
408
+ if callable(get_rooms_by_ids):
409
+ with contextlib.suppress(Exception):
410
+ rooms = await get_rooms_by_ids(ordered_room_ids)
411
+ for room in rooms or []:
412
+ if room and room.id:
413
+ room_name_by_id[room.id] = (
414
+ room.name if isinstance(room.name, str) and room.name else str(room.id)
415
+ )
416
+
417
+ message_room_ids = [rid for rid in ordered_room_ids if rid != self._autonomous_room_id]
418
+ per_room_limit = 10
419
+
420
+ fetched_messages: list[Memory] = []
421
+ if message_room_ids:
422
+ get_memories_by_room_ids = getattr(self._runtime, "get_memories_by_room_ids", None)
423
+ if callable(get_memories_by_room_ids):
424
+ with contextlib.suppress(Exception):
425
+ fetched_messages = await get_memories_by_room_ids(
426
+ {
427
+ "roomIds": message_room_ids,
428
+ "limit": per_room_limit * len(message_room_ids),
429
+ "tableName": "messages",
430
+ }
431
+ )
432
+ if not fetched_messages:
433
+ for room_id in message_room_ids:
434
+ with contextlib.suppress(Exception):
435
+ fetched_messages.extend(
436
+ await self._runtime.get_memories(
437
+ {
438
+ "roomId": room_id,
439
+ "count": per_room_limit,
440
+ "tableName": "messages",
441
+ }
442
+ )
443
+ )
444
+
445
+ autonomy_memories = await self._runtime.get_memories(
446
+ {"roomId": self._autonomous_room_id, "count": per_room_limit, "tableName": "memories"}
300
447
  )
301
- messages_table = await self._runtime.get_memories(
302
- {"roomId": target_room_id, "count": 15, "tableName": "messages"}
448
+
449
+ # ── Recent-context cutoff: ignore messages older than 1 hour ──
450
+ one_hour_ms = 3_600_000
451
+ now_ms = int(time.time() * 1000)
452
+ cutoff_ms = now_ms - one_hour_ms
453
+
454
+ fetched_messages = [m for m in fetched_messages if (m.created_at or 0) >= cutoff_ms]
455
+ autonomy_memories = [m for m in autonomy_memories if (m.created_at or 0) >= cutoff_ms]
456
+
457
+ external_messages = [
458
+ m
459
+ for m in fetched_messages
460
+ if m and m.entity_id and m.entity_id != self._runtime.agent_id
461
+ ]
462
+ entity_name_by_id = await self._build_entity_name_lookup(
463
+ memory.entity_id for memory in external_messages
303
464
  )
304
- by_id: dict[str, Memory] = {}
305
- for m in [*memories_table, *messages_table]:
306
- mem_id = m.id or ""
307
- if not mem_id:
465
+
466
+ messages_by_room: dict[UUID, list[Memory]] = {}
467
+ sorted_messages = sorted(
468
+ self._dedupe_memories_by_id_keep_earliest(external_messages),
469
+ key=lambda m: m.created_at or 0,
470
+ reverse=True,
471
+ )
472
+ for memory in sorted_messages:
473
+ bucket = messages_by_room.setdefault(memory.room_id, [])
474
+ if len(bucket) >= per_room_limit:
308
475
  continue
309
- created_at = m.created_at or 0
310
- existing = by_id.get(mem_id)
311
- if existing is None or created_at < (existing.created_at or 0):
312
- by_id[mem_id] = m
313
- ordered = sorted(by_id.values(), key=lambda m: m.created_at or 0)
314
- lines: list[str] = []
315
- for m in ordered:
316
- role = "Agent" if m.entity_id == self._runtime.agent_id else "User"
317
- text = m.content.text if m.content and isinstance(m.content.text, str) else ""
318
- if text.strip():
319
- lines.append(f"{role}: {text}")
320
- return "\n".join(lines) if lines else "(no recent messages)"
476
+ bucket.append(memory)
477
+
478
+ room_sections: list[str] = []
479
+ for room_id in message_room_ids:
480
+ room_name = room_name_by_id.get(room_id, str(room_id))
481
+ room_messages = list(reversed(messages_by_room.get(room_id, [])))
482
+ if not room_messages:
483
+ room_sections.append(f"Room: {room_name}\n(no recent messages)")
484
+ continue
485
+
486
+ lines: list[str] = []
487
+ for memory in room_messages:
488
+ text = self._memory_text(memory)
489
+ if not text:
490
+ continue
491
+ author = entity_name_by_id.get(memory.entity_id, str(memory.entity_id))
492
+ lines.append(f"{author}: {text}")
493
+
494
+ if lines:
495
+ room_sections.append(f"Room: {room_name}\n" + "\n".join(lines))
496
+ else:
497
+ room_sections.append(f"Room: {room_name}\n(no recent messages)")
498
+
499
+ autonomy_entries: list[str] = []
500
+ for memory in autonomy_memories:
501
+ text = self._memory_text(memory)
502
+ if not text:
503
+ continue
504
+
505
+ metadata_obj = memory.content.data if memory.content else None
506
+ metadata: dict[str, object] = metadata_obj if isinstance(metadata_obj, dict) else {}
507
+ entry_type = metadata.get("type")
508
+
509
+ if memory.entity_id == self._runtime.agent_id and entry_type == "autonomous-response":
510
+ autonomy_entries.append(f"Thought: {text}")
511
+ elif (
512
+ memory.entity_id == self._autonomy_entity_id and entry_type == "autonomous-trigger"
513
+ ):
514
+ autonomy_entries.append(f"Trigger: {text}")
515
+
516
+ if autonomy_entries:
517
+ autonomy_section = "Autonomous context:\n" + "\n".join(autonomy_entries)
518
+ else:
519
+ autonomy_section = "Autonomous context: (none)"
520
+
521
+ return "\n\n".join([*room_sections, autonomy_section])
321
522
 
322
523
  async def _settings_monitoring(self) -> None:
323
524
  while not self._is_stopped:
@@ -412,7 +613,7 @@ class AutonomyService(Service):
412
613
  else self._create_continuous_prompt(last_thought, is_first_thought, target_context)
413
614
  )
414
615
 
415
- entity_id = agent_entity.id if agent_entity.id else self._runtime.agent_id
616
+ entity_id = self._autonomy_entity_id
416
617
  current_time_ms = int(time.time() * 1000)
417
618
  autonomous_message = Memory(
418
619
  id=as_uuid(str(uuid.uuid4())),
@@ -420,6 +621,15 @@ class AutonomyService(Service):
420
621
  content=Content(
421
622
  text=autonomy_prompt,
422
623
  source="autonomy-service",
624
+ data={
625
+ "type": "autonomous-prompt",
626
+ "isAutonomous": True,
627
+ "isInternalThought": True,
628
+ "autonomyMode": mode,
629
+ "channelId": "autonomous",
630
+ "timestamp": current_time_ms,
631
+ "isContinuation": not is_first_thought,
632
+ },
423
633
  ),
424
634
  room_id=self._autonomous_room_id,
425
635
  agent_id=self._runtime.agent_id,
@@ -21,6 +21,9 @@ from .autonomy import (
21
21
  AutonomyService,
22
22
  admin_chat_provider,
23
23
  autonomy_status_provider,
24
+ disable_autonomy_action,
25
+ enable_autonomy_action,
26
+ post_action_evaluator,
24
27
  send_to_admin_action,
25
28
  )
26
29
  from .types import CapabilityConfig
@@ -63,6 +66,8 @@ def _get_actions(config: CapabilityConfig) -> list:
63
66
  result.extend(EXTENDED_ACTIONS)
64
67
  if config.enable_autonomy:
65
68
  result.append(send_to_admin_action)
69
+ result.append(enable_autonomy_action)
70
+ result.append(disable_autonomy_action)
66
71
  return result
67
72
 
68
73
 
@@ -73,6 +78,8 @@ def _get_evaluators(config: CapabilityConfig) -> list:
73
78
  result.extend(BASIC_EVALUATORS)
74
79
  if config.enable_extended:
75
80
  result.extend(EXTENDED_EVALUATORS)
81
+ if config.enable_autonomy:
82
+ result.append(post_action_evaluator)
76
83
  return result
77
84
 
78
85
 
@@ -1,8 +1,14 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import TYPE_CHECKING
3
+ import contextlib
4
+ from typing import TYPE_CHECKING, TypeVar
4
5
 
5
6
  from elizaos.action_docs import get_canonical_action_example_calls
7
+ from elizaos.deterministic import (
8
+ build_conversation_seed,
9
+ build_deterministic_seed,
10
+ deterministic_int,
11
+ )
6
12
  from elizaos.generated.spec_helpers import require_provider_spec
7
13
  from elizaos.types import Provider, ProviderResult
8
14
  from elizaos.types.components import ActionExample
@@ -21,10 +27,6 @@ if TYPE_CHECKING:
21
27
  _spec = require_provider_spec("ACTIONS")
22
28
 
23
29
 
24
- def format_action_names(actions: list[Action]) -> str:
25
- return ", ".join(action.name for action in actions)
26
-
27
-
28
30
  def _format_parameter_type(schema: ActionParameterSchema) -> str:
29
31
  if schema.type == "number" and (schema.minimum is not None or schema.maximum is not None):
30
32
  min_val = schema.minimum if schema.minimum is not None else "∞"
@@ -62,9 +64,23 @@ def _format_action_parameters(parameters: list[ActionParameter]) -> str:
62
64
  return "\n".join(lines)
63
65
 
64
66
 
65
- def format_actions(actions: list[Action]) -> str:
67
+ T = TypeVar("T")
68
+
69
+
70
+ def _deterministic_shuffle(items: list[T], seed: str, surface: str = "shuffle") -> list[T]:
71
+ shuffled = list(items)
72
+ for i in range(len(shuffled) - 1, 0, -1):
73
+ j = deterministic_int(seed, f"{surface}:{i}", i + 1)
74
+ shuffled[i], shuffled[j] = shuffled[j], shuffled[i]
75
+ return shuffled
76
+
77
+
78
+ def format_actions(actions: list[Action], seed: str | None = None) -> str:
79
+ deterministic_seed = seed or build_deterministic_seed(
80
+ ["actions-format", ",".join(action.name for action in actions)]
81
+ )
66
82
  lines: list[str] = []
67
- for action in actions:
83
+ for action in _deterministic_shuffle(actions, deterministic_seed, "actions"):
68
84
  line = f"- **{action.name}**: {action.description or 'No description'}"
69
85
  if action.parameters:
70
86
  params_text = _format_action_parameters(action.parameters)
@@ -81,7 +97,24 @@ def _replace_name_placeholders(text: str) -> str:
81
97
  return text
82
98
 
83
99
 
84
- def format_action_examples(actions: list[Action], max_examples: int = 10) -> str:
100
+ def _replace_name_placeholders_seeded(text: str, seed: str, example_index: int) -> str:
101
+ names = ["Alex", "Jordan", "Sam", "Taylor", "Riley"]
102
+ output = text
103
+ for placeholder_index in range(1, 6):
104
+ name_index = deterministic_int(
105
+ seed,
106
+ f"example:{example_index}:name:{placeholder_index}",
107
+ len(names),
108
+ )
109
+ output = output.replace(f"{{{{name{placeholder_index}}}}}", names[name_index])
110
+ return output
111
+
112
+
113
+ def format_action_examples(
114
+ actions: list[Action],
115
+ max_examples: int = 10,
116
+ seed: str | None = None,
117
+ ) -> str:
85
118
  """
86
119
  Format a deterministic subset of action examples for prompt context.
87
120
 
@@ -90,27 +123,60 @@ def format_action_examples(actions: list[Action], max_examples: int = 10) -> str
90
123
  if max_examples <= 0:
91
124
  return ""
92
125
 
93
- examples: list[list[ActionExample]] = []
94
- for action in sorted(actions, key=lambda a: a.name):
95
- if not action.examples:
96
- continue
97
- for ex in action.examples:
98
- if isinstance(ex, list) and ex:
99
- examples.append(ex)
100
- if len(examples) >= max_examples:
101
- break
102
- if len(examples) >= max_examples:
103
- break
104
-
105
- if not examples:
126
+ actions_with_examples = [
127
+ action
128
+ for action in actions
129
+ if action.examples and isinstance(action.examples, list) and len(action.examples) > 0
130
+ ]
131
+ if not actions_with_examples:
106
132
  return ""
107
133
 
134
+ examples_copy: list[list[list[ActionExample]]] = [
135
+ [example for example in (action.examples or []) if isinstance(example, list) and example]
136
+ for action in actions_with_examples
137
+ ]
138
+ available_action_indices = [
139
+ idx for idx, action_examples in enumerate(examples_copy) if action_examples
140
+ ]
141
+
142
+ selection_seed = seed or build_deterministic_seed(
143
+ [
144
+ "action-examples",
145
+ ",".join(action.name for action in actions_with_examples),
146
+ max_examples,
147
+ ]
148
+ )
149
+
150
+ selected_examples: list[list[ActionExample]] = []
151
+ iteration = 0
152
+ while len(selected_examples) < max_examples and available_action_indices:
153
+ random_index = deterministic_int(
154
+ selection_seed,
155
+ f"action-index:{iteration}",
156
+ len(available_action_indices),
157
+ )
158
+ action_index = available_action_indices[random_index]
159
+ action_examples = examples_copy[action_index]
160
+
161
+ example_index = deterministic_int(
162
+ selection_seed,
163
+ f"example-index:{iteration}",
164
+ len(action_examples),
165
+ )
166
+ selected_examples.append(action_examples.pop(example_index))
167
+ iteration += 1
168
+
169
+ if not action_examples:
170
+ available_action_indices.pop(random_index)
171
+
108
172
  blocks: list[str] = []
109
- for ex in examples:
173
+ for example_index, ex in enumerate(selected_examples):
110
174
  lines: list[str] = []
111
175
  for msg in ex:
112
176
  msg_text = msg.content.text if msg.content and msg.content.text else ""
113
- lines.append(f"{msg.name}: {_replace_name_placeholders(msg_text)}")
177
+ lines.append(
178
+ f"{msg.name}: {_replace_name_placeholders_seeded(msg_text, selection_seed, example_index)}"
179
+ )
114
180
  blocks.append("\n".join(lines))
115
181
 
116
182
  return "\n\n".join(blocks)
@@ -180,6 +246,17 @@ def format_action_call_examples(actions: list[Action], max_examples: int = 5) ->
180
246
  return "\n\n".join(blocks)
181
247
 
182
248
 
249
+ def format_action_names(actions: list[Action], seed: str | None = None) -> str:
250
+ if not actions:
251
+ return ""
252
+
253
+ deterministic_seed = seed or build_deterministic_seed(
254
+ ["action-names", ",".join(action.name for action in actions)]
255
+ )
256
+ shuffled = _deterministic_shuffle(actions, deterministic_seed, "actions")
257
+ return ", ".join(action.name for action in shuffled)
258
+
259
+
183
260
  async def get_actions(
184
261
  runtime: IAgentRuntime,
185
262
  message: Memory,
@@ -189,13 +266,27 @@ async def get_actions(
189
266
 
190
267
  for action in runtime.actions:
191
268
  validate_fn = getattr(action, "validate", None) or getattr(action, "validate_fn", None)
192
- is_valid = await validate_fn(runtime, message, state) if validate_fn else True
269
+ if not validate_fn:
270
+ is_valid = True
271
+ else:
272
+ try:
273
+ is_valid = await validate_fn(runtime, message, state)
274
+ except Exception:
275
+ if hasattr(runtime, "logger"):
276
+ with contextlib.suppress(Exception):
277
+ runtime.logger.warning(
278
+ f"Action validation failed for {action.name}; excluding from prompt"
279
+ )
280
+ is_valid = False
193
281
  if is_valid:
194
282
  validated_actions.append(action)
195
283
 
196
- action_names = format_action_names(validated_actions)
197
- actions_text = format_actions(validated_actions)
198
- examples_text = format_action_examples(validated_actions, max_examples=10)
284
+ action_seed = build_conversation_seed(runtime, message, state, "provider:actions")
285
+ action_names = format_action_names(validated_actions, seed=f"{action_seed}:names")
286
+ actions_text = format_actions(validated_actions, seed=f"{action_seed}:descriptions")
287
+ examples_text = format_action_examples(
288
+ validated_actions, max_examples=10, seed=f"{action_seed}:examples"
289
+ )
199
290
  call_examples_text = format_action_call_examples(validated_actions, max_examples=5)
200
291
 
201
292
  text_parts: list[str] = [f"Possible response actions: {action_names}"]
@@ -60,4 +60,5 @@ agent_settings_provider = Provider(
60
60
  description=_spec["description"],
61
61
  get=get_agent_settings_context,
62
62
  dynamic=_spec.get("dynamic", True),
63
+ position=_spec.get("position"),
63
64
  )
@@ -73,4 +73,5 @@ attachments_provider = Provider(
73
73
  description=_spec["description"],
74
74
  get=get_attachments,
75
75
  dynamic=_spec.get("dynamic", True),
76
+ position=_spec.get("position"),
76
77
  )
@@ -63,4 +63,5 @@ capabilities_provider = Provider(
63
63
  description=_spec["description"],
64
64
  get=get_capabilities,
65
65
  dynamic=_spec.get("dynamic", False),
66
+ position=_spec.get("position"),
66
67
  )
@@ -125,4 +125,5 @@ character_provider = Provider(
125
125
  description=_spec["description"],
126
126
  get=get_character_context,
127
127
  dynamic=_spec.get("dynamic", False),
128
+ position=_spec.get("position"),
128
129
  )
@@ -74,4 +74,5 @@ choice_provider = Provider(
74
74
  description=_spec["description"],
75
75
  get=get_choice_options,
76
76
  dynamic=_spec.get("dynamic", True),
77
+ position=_spec.get("position"),
77
78
  )
@@ -75,4 +75,5 @@ contacts_provider = Provider(
75
75
  description=_spec["description"],
76
76
  get=get_contacts_context,
77
77
  dynamic=_spec.get("dynamic", True),
78
+ position=_spec.get("position"),
78
79
  )
@@ -1,8 +1,8 @@
1
1
  from __future__ import annotations
2
2
 
3
- from datetime import UTC, datetime
4
3
  from typing import TYPE_CHECKING
5
4
 
5
+ from elizaos.deterministic import get_prompt_reference_datetime
6
6
  from elizaos.generated.spec_helpers import require_provider_spec
7
7
  from elizaos.types import Provider, ProviderResult
8
8
 
@@ -18,7 +18,12 @@ async def get_current_time_context(
18
18
  message: Memory,
19
19
  state: State | None = None,
20
20
  ) -> ProviderResult:
21
- now = datetime.now(UTC)
21
+ now = get_prompt_reference_datetime(
22
+ runtime,
23
+ message,
24
+ state,
25
+ "provider:current_time",
26
+ )
22
27
 
23
28
  iso_timestamp = now.isoformat()
24
29
  human_readable = now.strftime("%A, %B %d, %Y at %H:%M:%S UTC")
@@ -53,4 +58,5 @@ current_time_provider = Provider(
53
58
  description=_spec["description"],
54
59
  get=get_current_time_context,
55
60
  dynamic=_spec.get("dynamic", True),
61
+ position=_spec.get("position"),
56
62
  )
@@ -96,4 +96,5 @@ entities_provider = Provider(
96
96
  description=_spec["description"],
97
97
  get=get_entities_context,
98
98
  dynamic=_spec.get("dynamic", True),
99
+ position=_spec.get("position"),
99
100
  )
@@ -55,4 +55,5 @@ evaluators_provider = Provider(
55
55
  description=_spec["description"],
56
56
  get=get_evaluators,
57
57
  dynamic=_spec.get("dynamic", False),
58
+ position=_spec.get("position"),
58
59
  )
@@ -83,4 +83,5 @@ facts_provider = Provider(
83
83
  description=_spec["description"],
84
84
  get=get_facts_context,
85
85
  dynamic=_spec.get("dynamic", True),
86
+ position=_spec.get("position"),
86
87
  )
@@ -113,4 +113,5 @@ follow_ups_provider = Provider(
113
113
  description=_spec["description"],
114
114
  get=get_follow_ups_context,
115
115
  dynamic=_spec.get("dynamic", True),
116
+ position=_spec.get("position"),
116
117
  )