@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
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import time
3
4
  from unittest.mock import AsyncMock, MagicMock
4
5
 
5
6
  import pytest
@@ -11,9 +12,10 @@ from elizaos.bootstrap.autonomy import (
11
12
  autonomy_status_provider,
12
13
  send_to_admin_action,
13
14
  )
15
+ from elizaos.bootstrap.autonomy.service import AUTONOMY_TASK_TAGS
14
16
  from elizaos.bootstrap.autonomy.types import AutonomyStatus
15
17
  from elizaos.types.memory import Memory
16
- from elizaos.types.primitives import Content, as_uuid
18
+ from elizaos.types.primitives import Content, as_uuid, string_to_uuid
17
19
 
18
20
  TEST_AGENT_ID = "00000000-0000-0000-0000-000000000001"
19
21
  TEST_ROOM_ID = "00000000-0000-0000-0000-000000000002"
@@ -75,6 +77,9 @@ class TestAutonomyService:
75
77
  assert AutonomyService.service_type == AUTONOMY_SERVICE_TYPE
76
78
  assert AutonomyService.service_type == "AUTONOMY"
77
79
 
80
+ def test_task_tags_match_typescript(self):
81
+ assert AUTONOMY_TASK_TAGS == ["queue", "repeat", "autonomy"]
82
+
78
83
  @pytest.mark.asyncio
79
84
  async def test_start_creates_service(self, test_runtime):
80
85
  service = await AutonomyService.start(test_runtime)
@@ -85,6 +90,12 @@ class TestAutonomyService:
85
90
  assert service.get_loop_interval() == 30000
86
91
  assert service.get_autonomous_room_id() is not None
87
92
 
93
+ @pytest.mark.asyncio
94
+ async def test_autonomous_room_id_is_deterministic(self, test_runtime):
95
+ service = await AutonomyService.start(test_runtime)
96
+ expected_room_id = as_uuid(string_to_uuid(f"autonomy-room-{test_runtime.agent_id}"))
97
+ assert service.get_autonomous_room_id() == expected_room_id
98
+
88
99
  @pytest.mark.asyncio
89
100
  async def test_auto_start_when_enabled(self, test_runtime):
90
101
  test_runtime.enable_autonomy = True
@@ -172,13 +183,14 @@ class TestAutonomyService:
172
183
  test_runtime.get_setting = MagicMock(return_value=str(target_room_id))
173
184
 
174
185
  dup_id = as_uuid(TEST_MESSAGE_ID)
186
+ now_ms = int(time.time() * 1000)
175
187
  older = Memory(
176
188
  id=dup_id,
177
189
  room_id=target_room_id,
178
190
  entity_id=as_uuid(TEST_ENTITY_ID),
179
191
  agent_id=as_uuid(TEST_AGENT_ID),
180
192
  content=Content(text="old"),
181
- created_at=10,
193
+ created_at=now_ms - 60_000, # 1 minute ago (within 1-hour window)
182
194
  )
183
195
  newer = Memory(
184
196
  id=dup_id,
@@ -186,7 +198,7 @@ class TestAutonomyService:
186
198
  entity_id=as_uuid(TEST_ENTITY_ID),
187
199
  agent_id=as_uuid(TEST_AGENT_ID),
188
200
  content=Content(text="new"),
189
- created_at=20,
201
+ created_at=now_ms - 30_000, # 30 seconds ago (within 1-hour window)
190
202
  )
191
203
 
192
204
  async def get_memories(params):
@@ -435,11 +447,20 @@ class TestAutonomyIntegration:
435
447
  AutonomyService,
436
448
  admin_chat_provider,
437
449
  autonomy_status_provider,
450
+ disable_autonomy_action,
451
+ enable_autonomy_action,
452
+ post_action_evaluator,
438
453
  send_to_admin_action,
439
454
  )
440
455
 
441
456
  assert AutonomyService is not None
442
457
  assert AUTONOMY_SERVICE_TYPE == "AUTONOMY"
443
458
  assert send_to_admin_action is not None
459
+ assert enable_autonomy_action is not None
460
+ assert enable_autonomy_action.name == "ENABLE_AUTONOMY"
461
+ assert disable_autonomy_action is not None
462
+ assert disable_autonomy_action.name == "DISABLE_AUTONOMY"
463
+ assert post_action_evaluator is not None
464
+ assert post_action_evaluator.name == "POST_ACTION_EVALUATOR"
444
465
  assert admin_chat_provider is not None
445
466
  assert autonomy_status_provider is not None
@@ -59,17 +59,15 @@ class TestAgentRuntimeSettings:
59
59
  runtime.set_setting("test_key", "test_value")
60
60
  assert runtime.get_setting("test_key") == "test_value"
61
61
 
62
- @pytest.mark.skip(reason="CharacterSettings proto doesn't support arbitrary fields")
63
62
  def test_get_setting_from_character(self) -> None:
64
63
  character = Character(
65
64
  name="Test",
66
65
  bio=["Test"],
67
- settings={"char_setting": "char_value"},
66
+ settings={"extra": {"char_setting": "char_value"}},
68
67
  )
69
68
  runtime = AgentRuntime(character=character)
70
69
  assert runtime.get_setting("char_setting") == "char_value"
71
70
 
72
- @pytest.mark.skip(reason="Runtime get_setting from secrets not yet implemented")
73
71
  def test_get_setting_from_secrets(self) -> None:
74
72
  character = Character(
75
73
  name="Test",
@@ -305,42 +303,38 @@ class TestAgentRuntimeLLMMode:
305
303
  runtime = AgentRuntime(character=character, llm_mode=LLMMode.LARGE)
306
304
  assert runtime.get_llm_mode() == LLMMode.LARGE
307
305
 
308
- @pytest.mark.skip(reason="CharacterSettings proto doesn't have LLM_MODE field")
309
306
  def test_character_setting_small(self) -> None:
310
307
  character = Character(
311
308
  name="Test",
312
309
  bio=["Test"],
313
- settings={"LLM_MODE": "SMALL"},
310
+ settings={"extra": {"LLM_MODE": "SMALL"}},
314
311
  )
315
312
  runtime = AgentRuntime(character=character)
316
313
  assert runtime.get_llm_mode() == LLMMode.SMALL
317
314
 
318
- @pytest.mark.skip(reason="CharacterSettings proto doesn't have LLM_MODE field")
319
315
  def test_constructor_option_takes_precedence(self) -> None:
320
316
  character = Character(
321
317
  name="Test",
322
318
  bio=["Test"],
323
- settings={"LLM_MODE": "SMALL"},
319
+ settings={"extra": {"LLM_MODE": "SMALL"}},
324
320
  )
325
321
  runtime = AgentRuntime(character=character, llm_mode=LLMMode.LARGE)
326
322
  assert runtime.get_llm_mode() == LLMMode.LARGE
327
323
 
328
- @pytest.mark.skip(reason="CharacterSettings proto doesn't have LLM_MODE field")
329
324
  def test_case_insensitive_character_setting(self) -> None:
330
325
  character = Character(
331
326
  name="Test",
332
327
  bio=["Test"],
333
- settings={"LLM_MODE": "small"},
328
+ settings={"extra": {"LLM_MODE": "small"}},
334
329
  )
335
330
  runtime = AgentRuntime(character=character)
336
331
  assert runtime.get_llm_mode() == LLMMode.SMALL
337
332
 
338
- @pytest.mark.skip(reason="CharacterSettings proto doesn't have LLM_MODE field")
339
333
  def test_invalid_setting_defaults_to_default(self) -> None:
340
334
  character = Character(
341
335
  name="Test",
342
336
  bio=["Test"],
343
- settings={"LLM_MODE": "invalid"},
337
+ settings={"extra": {"LLM_MODE": "invalid"}},
344
338
  )
345
339
  runtime = AgentRuntime(character=character)
346
340
  assert runtime.get_llm_mode() == LLMMode.DEFAULT
@@ -391,32 +385,29 @@ class TestAgentRuntimeCheckShouldRespond:
391
385
  runtime = AgentRuntime(character=character, check_should_respond=True)
392
386
  assert runtime.is_check_should_respond_enabled() is True
393
387
 
394
- @pytest.mark.skip(reason="CharacterSettings proto doesn't have CHECK_SHOULD_RESPOND field")
395
388
  def test_character_setting_false(self) -> None:
396
389
  character = Character(
397
390
  name="Test",
398
391
  bio=["Test"],
399
- settings={"CHECK_SHOULD_RESPOND": "false"},
392
+ settings={"extra": {"CHECK_SHOULD_RESPOND": "false"}},
400
393
  )
401
394
  runtime = AgentRuntime(character=character)
402
395
  assert runtime.is_check_should_respond_enabled() is False
403
396
 
404
- @pytest.mark.skip(reason="CharacterSettings proto doesn't have CHECK_SHOULD_RESPOND field")
405
397
  def test_constructor_option_takes_precedence(self) -> None:
406
398
  character = Character(
407
399
  name="Test",
408
400
  bio=["Test"],
409
- settings={"CHECK_SHOULD_RESPOND": "false"},
401
+ settings={"extra": {"CHECK_SHOULD_RESPOND": "false"}},
410
402
  )
411
403
  runtime = AgentRuntime(character=character, check_should_respond=True)
412
404
  assert runtime.is_check_should_respond_enabled() is True
413
405
 
414
- @pytest.mark.skip(reason="CharacterSettings proto doesn't have CHECK_SHOULD_RESPOND field")
415
406
  def test_non_false_string_defaults_to_true(self) -> None:
416
407
  character = Character(
417
408
  name="Test",
418
409
  bio=["Test"],
419
- settings={"CHECK_SHOULD_RESPOND": "yes"},
410
+ settings={"extra": {"CHECK_SHOULD_RESPOND": "yes"}},
420
411
  )
421
412
  runtime = AgentRuntime(character=character)
422
413
  assert runtime.is_check_should_respond_enabled() is True
@@ -0,0 +1,260 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from types import SimpleNamespace
5
+ from typing import Protocol
6
+
7
+ import pytest
8
+
9
+ from elizaos.advanced_capabilities.actions.schedule_follow_up import (
10
+ schedule_follow_up_action as advanced_schedule_follow_up_action,
11
+ )
12
+ from elizaos.bootstrap.actions.schedule_follow_up import (
13
+ schedule_follow_up_action as bootstrap_schedule_follow_up_action,
14
+ )
15
+ from elizaos.bootstrap.services.follow_up import FollowUpService
16
+ from elizaos.bootstrap.services.rolodex import RolodexService
17
+ from elizaos.types import Content, Memory, as_uuid
18
+
19
+
20
+ class RolodexLike(Protocol):
21
+ async def search_contacts(
22
+ self,
23
+ categories: list[str] | None = None,
24
+ tags: list[str] | None = None,
25
+ search_term: str | None = None,
26
+ ) -> list[SimpleNamespace]: ...
27
+
28
+ async def get_contact(self, entity_id: object) -> SimpleNamespace | None: ...
29
+
30
+
31
+ class ActionLike(Protocol):
32
+ async def handler(
33
+ self,
34
+ runtime: object,
35
+ message: Memory,
36
+ state: object | None,
37
+ options: object | None,
38
+ callback: object | None,
39
+ responses: list[Memory] | None,
40
+ ) -> object: ...
41
+
42
+
43
+ @dataclass
44
+ class FakeFollowUpCall:
45
+ entity_id: object
46
+ priority: str
47
+
48
+
49
+ class FakeFollowUpService(FollowUpService):
50
+ def __init__(self) -> None:
51
+ self.calls: list[FakeFollowUpCall] = []
52
+
53
+ async def schedule_follow_up(
54
+ self,
55
+ entity_id: object,
56
+ scheduled_at: object,
57
+ reason: str,
58
+ priority: str = "medium",
59
+ message: str | None = None,
60
+ ) -> SimpleNamespace:
61
+ self.calls.append(FakeFollowUpCall(entity_id=entity_id, priority=priority))
62
+ return SimpleNamespace(
63
+ entity_id=entity_id,
64
+ scheduled_at=str(scheduled_at),
65
+ reason=reason,
66
+ priority=priority,
67
+ message=message,
68
+ )
69
+
70
+
71
+ class FakeRolodexService(RolodexService):
72
+ def __init__(self, contacts: list[SimpleNamespace]) -> None:
73
+ self._contacts = contacts
74
+
75
+ async def search_contacts(
76
+ self,
77
+ categories: list[str] | None = None,
78
+ tags: list[str] | None = None,
79
+ search_term: str | None = None,
80
+ ) -> list[SimpleNamespace]:
81
+ if not search_term:
82
+ return self._contacts
83
+ lowered = search_term.lower()
84
+ return [
85
+ contact
86
+ for contact in self._contacts
87
+ if lowered in str(contact.entity_id).lower()
88
+ or lowered in str(getattr(contact, "name", "")).lower()
89
+ ]
90
+
91
+ async def get_contact(self, entity_id: object) -> SimpleNamespace | None:
92
+ for contact in self._contacts:
93
+ if contact.entity_id == entity_id:
94
+ return contact
95
+ return None
96
+
97
+
98
+ class FakeRuntime:
99
+ def __init__(self, rolodex: RolodexLike, follow_up: FakeFollowUpService, response: str) -> None:
100
+ self._rolodex = rolodex
101
+ self._follow_up = follow_up
102
+ self._response = response
103
+
104
+ def get_service(self, name: str) -> object | None:
105
+ if name == "rolodex":
106
+ return self._rolodex
107
+ if name == "follow_up":
108
+ return self._follow_up
109
+ return None
110
+
111
+ async def compose_state(self, _message: Memory, _providers: list[str]) -> SimpleNamespace:
112
+ return SimpleNamespace(values={}, data={}, text="")
113
+
114
+ def get_setting(self, _key: str) -> object | None:
115
+ return None
116
+
117
+ def compose_prompt_from_state(self, state: object, template: str) -> str:
118
+ return f"{template}\n{state}"
119
+
120
+ async def use_model(self, _model_type: object, _params: dict[str, object]) -> str:
121
+ return self._response
122
+
123
+
124
+ @pytest.mark.asyncio
125
+ @pytest.mark.parametrize(
126
+ "action_under_test",
127
+ [bootstrap_schedule_follow_up_action, advanced_schedule_follow_up_action],
128
+ )
129
+ async def test_schedule_follow_up_fails_when_contact_unresolved(
130
+ action_under_test: ActionLike,
131
+ ) -> None:
132
+ follow_up_service = FakeFollowUpService()
133
+ rolodex_service = FakeRolodexService(contacts=[])
134
+ runtime = FakeRuntime(
135
+ rolodex=rolodex_service,
136
+ follow_up=follow_up_service,
137
+ response=(
138
+ "<response>"
139
+ "<contactName>missing-contact</contactName>"
140
+ "<scheduledAt>2026-01-02T12:00:00Z</scheduledAt>"
141
+ "<reason>Check-in</reason>"
142
+ "<priority>high</priority>"
143
+ "</response>"
144
+ ),
145
+ )
146
+
147
+ message = Memory(
148
+ id=as_uuid("70000000-0000-0000-0000-000000000001"),
149
+ room_id=as_uuid("70000000-0000-0000-0000-000000000002"),
150
+ entity_id=None,
151
+ content=Content(text="follow up with missing-contact next week"),
152
+ )
153
+
154
+ result = await action_under_test.handler(
155
+ runtime,
156
+ message,
157
+ None,
158
+ None,
159
+ None,
160
+ None,
161
+ )
162
+
163
+ assert result is not None
164
+ assert result.success is False
165
+ assert len(follow_up_service.calls) == 0
166
+
167
+
168
+ @pytest.mark.asyncio
169
+ @pytest.mark.parametrize(
170
+ "action_under_test",
171
+ [bootstrap_schedule_follow_up_action, advanced_schedule_follow_up_action],
172
+ )
173
+ async def test_schedule_follow_up_normalizes_priority_and_schedules(
174
+ action_under_test: ActionLike,
175
+ ) -> None:
176
+ follow_up_service = FakeFollowUpService()
177
+ contact_id = as_uuid("80000000-0000-0000-0000-000000000001")
178
+ rolodex_service = FakeRolodexService(
179
+ contacts=[SimpleNamespace(entity_id=contact_id, name="known-contact")]
180
+ )
181
+ runtime = FakeRuntime(
182
+ rolodex=rolodex_service,
183
+ follow_up=follow_up_service,
184
+ response=(
185
+ "<response>"
186
+ "<contactName>known-contact</contactName>"
187
+ "<scheduledAt>2026-01-03T09:30:00Z</scheduledAt>"
188
+ "<reason>Status update</reason>"
189
+ "<priority>urgent</priority>"
190
+ "</response>"
191
+ ),
192
+ )
193
+
194
+ message = Memory(
195
+ id=as_uuid("80000000-0000-0000-0000-000000000002"),
196
+ room_id=as_uuid("80000000-0000-0000-0000-000000000003"),
197
+ entity_id=None,
198
+ content=Content(text="schedule follow-up with known-contact"),
199
+ )
200
+
201
+ result = await action_under_test.handler(
202
+ runtime,
203
+ message,
204
+ None,
205
+ None,
206
+ None,
207
+ None,
208
+ )
209
+
210
+ assert result is not None
211
+ assert result.success is True
212
+ assert len(follow_up_service.calls) == 1
213
+ assert follow_up_service.calls[0].priority == "medium"
214
+
215
+
216
+ @pytest.mark.asyncio
217
+ @pytest.mark.parametrize(
218
+ "action_under_test",
219
+ [bootstrap_schedule_follow_up_action, advanced_schedule_follow_up_action],
220
+ )
221
+ async def test_schedule_follow_up_rejects_invalid_scheduled_at(
222
+ action_under_test: ActionLike,
223
+ ) -> None:
224
+ follow_up_service = FakeFollowUpService()
225
+ contact_id = as_uuid("90000000-0000-0000-0000-000000000001")
226
+ rolodex_service = FakeRolodexService(
227
+ contacts=[SimpleNamespace(entity_id=contact_id, name="known-contact")]
228
+ )
229
+ runtime = FakeRuntime(
230
+ rolodex=rolodex_service,
231
+ follow_up=follow_up_service,
232
+ response=(
233
+ "<response>"
234
+ "<contactName>known-contact</contactName>"
235
+ "<scheduledAt>not-a-date</scheduledAt>"
236
+ "<reason>Status update</reason>"
237
+ "<priority>high</priority>"
238
+ "</response>"
239
+ ),
240
+ )
241
+
242
+ message = Memory(
243
+ id=as_uuid("90000000-0000-0000-0000-000000000002"),
244
+ room_id=as_uuid("90000000-0000-0000-0000-000000000003"),
245
+ entity_id=None,
246
+ content=Content(text="schedule follow-up with known-contact"),
247
+ )
248
+
249
+ result = await action_under_test.handler(
250
+ runtime,
251
+ message,
252
+ None,
253
+ None,
254
+ None,
255
+ None,
256
+ )
257
+
258
+ assert result is not None
259
+ assert result.success is False
260
+ assert len(follow_up_service.calls) == 0
@@ -0,0 +1,114 @@
1
+ from __future__ import annotations
2
+
3
+ from types import SimpleNamespace
4
+ from unittest.mock import AsyncMock, MagicMock
5
+
6
+ import pytest
7
+
8
+ from elizaos.advanced_capabilities.actions.send_message import (
9
+ send_message_action as advanced_send_message_action,
10
+ )
11
+ from elizaos.bootstrap.actions.send_message import (
12
+ send_message_action as bootstrap_send_message_action,
13
+ )
14
+ from elizaos.types.memory import Memory
15
+ from elizaos.types.primitives import Content, as_uuid
16
+
17
+
18
+ def _make_runtime() -> MagicMock:
19
+ runtime = MagicMock()
20
+ runtime.agent_id = as_uuid("42345678-1234-1234-1234-123456789001")
21
+ runtime.create_memory = AsyncMock()
22
+ runtime.emit_event = AsyncMock()
23
+ runtime.send_message_to_target = AsyncMock()
24
+ runtime.get_room = AsyncMock(return_value=None)
25
+ runtime.get_rooms = AsyncMock(return_value=[])
26
+ runtime.get_entities_for_room = AsyncMock(return_value=[])
27
+ return runtime
28
+
29
+
30
+ def _make_message() -> Memory:
31
+ return Memory(
32
+ id=as_uuid("42345678-1234-1234-1234-123456789010"),
33
+ entity_id=as_uuid("42345678-1234-1234-1234-123456789011"),
34
+ room_id=as_uuid("42345678-1234-1234-1234-123456789012"),
35
+ content=Content(text="fallback", source="telegram"),
36
+ )
37
+
38
+
39
+ @pytest.mark.asyncio
40
+ @pytest.mark.parametrize(
41
+ "action_under_test",
42
+ [bootstrap_send_message_action, advanced_send_message_action],
43
+ )
44
+ async def test_send_message_uses_room_target_parameters(action_under_test: object) -> None:
45
+ runtime = _make_runtime()
46
+ message = _make_message()
47
+ target_room_id = "42345678-1234-1234-1234-123456789099"
48
+
49
+ result = await action_under_test.handler(
50
+ runtime,
51
+ message,
52
+ None,
53
+ SimpleNamespace(
54
+ parameters={
55
+ "targetType": "room",
56
+ "target": target_room_id,
57
+ "source": "discord",
58
+ "text": "ship it",
59
+ }
60
+ ),
61
+ None,
62
+ None,
63
+ )
64
+
65
+ assert result.success is True
66
+ assert result.values["targetType"] == "room"
67
+ assert result.values["targetRoomId"] == target_room_id
68
+ runtime.create_memory.assert_awaited_once()
69
+ create_kwargs = runtime.create_memory.await_args.kwargs
70
+ assert create_kwargs["room_id"] == as_uuid(target_room_id)
71
+
72
+ runtime.send_message_to_target.assert_awaited_once()
73
+ send_target = runtime.send_message_to_target.await_args.args[0]
74
+ assert str(send_target.room_id) == target_room_id
75
+ assert send_target.source == "discord"
76
+
77
+
78
+ @pytest.mark.asyncio
79
+ @pytest.mark.parametrize(
80
+ "action_under_test",
81
+ [bootstrap_send_message_action, advanced_send_message_action],
82
+ )
83
+ async def test_send_message_resolves_user_target_from_room_entities(
84
+ action_under_test: object,
85
+ ) -> None:
86
+ runtime = _make_runtime()
87
+ message = _make_message()
88
+ target_entity_id = as_uuid("42345678-1234-1234-1234-123456789088")
89
+ runtime.get_entities_for_room = AsyncMock(
90
+ return_value=[SimpleNamespace(id=target_entity_id, names=["Alice"])]
91
+ )
92
+
93
+ result = await action_under_test.handler(
94
+ runtime,
95
+ message,
96
+ None,
97
+ SimpleNamespace(
98
+ parameters={
99
+ "targetType": "user",
100
+ "target": "alice",
101
+ "source": "discord",
102
+ "text": "hello",
103
+ }
104
+ ),
105
+ None,
106
+ None,
107
+ )
108
+
109
+ assert result.success is True
110
+ assert result.values["targetType"] == "user"
111
+ assert result.values["targetEntityId"] == target_entity_id
112
+ runtime.send_message_to_target.assert_awaited_once()
113
+ send_target = runtime.send_message_to_target.await_args.args[0]
114
+ assert str(send_target.entity_id) == target_entity_id
@@ -78,7 +78,6 @@ class TestSettingsCrypto:
78
78
  assert migrated.startswith("v2:")
79
79
  assert decrypt_string_value(migrated, salt) == plaintext
80
80
 
81
- @pytest.mark.skip(reason="Runtime get_setting from secrets not yet implemented")
82
81
  def test_runtime_get_setting_decrypts_secret_strings(
83
82
  self, monkeypatch: pytest.MonkeyPatch
84
83
  ) -> None:
@@ -96,7 +95,6 @@ class TestSettingsCrypto:
96
95
  runtime = AgentRuntime(character=character)
97
96
  assert runtime.get_setting("API_KEY") == "super-secret"
98
97
 
99
- @pytest.mark.skip(reason="Runtime get_setting from secrets not yet implemented")
100
98
  def test_runtime_get_setting_coerces_true_false_strings(
101
99
  self, monkeypatch: pytest.MonkeyPatch
102
100
  ) -> None: