@elizaos/python 2.0.0-alpha.10

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 (197) hide show
  1. package/LICENSE +26 -0
  2. package/README.md +239 -0
  3. package/elizaos/__init__.py +280 -0
  4. package/elizaos/action_docs.py +149 -0
  5. package/elizaos/advanced_capabilities/__init__.py +85 -0
  6. package/elizaos/advanced_capabilities/actions/__init__.py +54 -0
  7. package/elizaos/advanced_capabilities/actions/add_contact.py +139 -0
  8. package/elizaos/advanced_capabilities/actions/follow_room.py +151 -0
  9. package/elizaos/advanced_capabilities/actions/image_generation.py +148 -0
  10. package/elizaos/advanced_capabilities/actions/mute_room.py +164 -0
  11. package/elizaos/advanced_capabilities/actions/remove_contact.py +145 -0
  12. package/elizaos/advanced_capabilities/actions/roles.py +207 -0
  13. package/elizaos/advanced_capabilities/actions/schedule_follow_up.py +154 -0
  14. package/elizaos/advanced_capabilities/actions/search_contacts.py +145 -0
  15. package/elizaos/advanced_capabilities/actions/send_message.py +187 -0
  16. package/elizaos/advanced_capabilities/actions/settings.py +151 -0
  17. package/elizaos/advanced_capabilities/actions/unfollow_room.py +164 -0
  18. package/elizaos/advanced_capabilities/actions/unmute_room.py +164 -0
  19. package/elizaos/advanced_capabilities/actions/update_contact.py +164 -0
  20. package/elizaos/advanced_capabilities/actions/update_entity.py +161 -0
  21. package/elizaos/advanced_capabilities/evaluators/__init__.py +18 -0
  22. package/elizaos/advanced_capabilities/evaluators/reflection.py +134 -0
  23. package/elizaos/advanced_capabilities/evaluators/relationship_extraction.py +203 -0
  24. package/elizaos/advanced_capabilities/providers/__init__.py +36 -0
  25. package/elizaos/advanced_capabilities/providers/agent_settings.py +60 -0
  26. package/elizaos/advanced_capabilities/providers/contacts.py +77 -0
  27. package/elizaos/advanced_capabilities/providers/facts.py +82 -0
  28. package/elizaos/advanced_capabilities/providers/follow_ups.py +113 -0
  29. package/elizaos/advanced_capabilities/providers/knowledge.py +83 -0
  30. package/elizaos/advanced_capabilities/providers/relationships.py +112 -0
  31. package/elizaos/advanced_capabilities/providers/roles.py +97 -0
  32. package/elizaos/advanced_capabilities/providers/settings.py +51 -0
  33. package/elizaos/advanced_capabilities/services/__init__.py +18 -0
  34. package/elizaos/advanced_capabilities/services/follow_up.py +138 -0
  35. package/elizaos/advanced_capabilities/services/rolodex.py +244 -0
  36. package/elizaos/advanced_memory/__init__.py +3 -0
  37. package/elizaos/advanced_memory/evaluators.py +97 -0
  38. package/elizaos/advanced_memory/memory_service.py +556 -0
  39. package/elizaos/advanced_memory/plugin.py +30 -0
  40. package/elizaos/advanced_memory/prompts.py +12 -0
  41. package/elizaos/advanced_memory/providers.py +90 -0
  42. package/elizaos/advanced_memory/types.py +65 -0
  43. package/elizaos/advanced_planning/__init__.py +10 -0
  44. package/elizaos/advanced_planning/actions.py +145 -0
  45. package/elizaos/advanced_planning/message_classifier.py +127 -0
  46. package/elizaos/advanced_planning/planning_service.py +712 -0
  47. package/elizaos/advanced_planning/plugin.py +40 -0
  48. package/elizaos/advanced_planning/prompts.py +4 -0
  49. package/elizaos/basic_capabilities/__init__.py +66 -0
  50. package/elizaos/basic_capabilities/actions/__init__.py +24 -0
  51. package/elizaos/basic_capabilities/actions/choice.py +140 -0
  52. package/elizaos/basic_capabilities/actions/ignore.py +66 -0
  53. package/elizaos/basic_capabilities/actions/none.py +56 -0
  54. package/elizaos/basic_capabilities/actions/reply.py +120 -0
  55. package/elizaos/basic_capabilities/providers/__init__.py +54 -0
  56. package/elizaos/basic_capabilities/providers/action_state.py +113 -0
  57. package/elizaos/basic_capabilities/providers/actions.py +263 -0
  58. package/elizaos/basic_capabilities/providers/attachments.py +76 -0
  59. package/elizaos/basic_capabilities/providers/capabilities.py +62 -0
  60. package/elizaos/basic_capabilities/providers/character.py +113 -0
  61. package/elizaos/basic_capabilities/providers/choice.py +73 -0
  62. package/elizaos/basic_capabilities/providers/context_bench.py +44 -0
  63. package/elizaos/basic_capabilities/providers/current_time.py +58 -0
  64. package/elizaos/basic_capabilities/providers/entities.py +99 -0
  65. package/elizaos/basic_capabilities/providers/evaluators.py +54 -0
  66. package/elizaos/basic_capabilities/providers/providers_list.py +55 -0
  67. package/elizaos/basic_capabilities/providers/recent_messages.py +85 -0
  68. package/elizaos/basic_capabilities/providers/time.py +45 -0
  69. package/elizaos/basic_capabilities/providers/world.py +93 -0
  70. package/elizaos/basic_capabilities/services/__init__.py +18 -0
  71. package/elizaos/basic_capabilities/services/embedding.py +122 -0
  72. package/elizaos/basic_capabilities/services/task.py +178 -0
  73. package/elizaos/bootstrap/__init__.py +12 -0
  74. package/elizaos/bootstrap/actions/__init__.py +68 -0
  75. package/elizaos/bootstrap/actions/add_contact.py +149 -0
  76. package/elizaos/bootstrap/actions/choice.py +147 -0
  77. package/elizaos/bootstrap/actions/follow_room.py +151 -0
  78. package/elizaos/bootstrap/actions/ignore.py +80 -0
  79. package/elizaos/bootstrap/actions/image_generation.py +135 -0
  80. package/elizaos/bootstrap/actions/mute_room.py +151 -0
  81. package/elizaos/bootstrap/actions/none.py +71 -0
  82. package/elizaos/bootstrap/actions/remove_contact.py +159 -0
  83. package/elizaos/bootstrap/actions/reply.py +140 -0
  84. package/elizaos/bootstrap/actions/roles.py +193 -0
  85. package/elizaos/bootstrap/actions/schedule_follow_up.py +164 -0
  86. package/elizaos/bootstrap/actions/search_contacts.py +159 -0
  87. package/elizaos/bootstrap/actions/send_message.py +173 -0
  88. package/elizaos/bootstrap/actions/settings.py +165 -0
  89. package/elizaos/bootstrap/actions/unfollow_room.py +151 -0
  90. package/elizaos/bootstrap/actions/unmute_room.py +151 -0
  91. package/elizaos/bootstrap/actions/update_contact.py +178 -0
  92. package/elizaos/bootstrap/actions/update_entity.py +175 -0
  93. package/elizaos/bootstrap/autonomy/__init__.py +18 -0
  94. package/elizaos/bootstrap/autonomy/action.py +197 -0
  95. package/elizaos/bootstrap/autonomy/providers.py +165 -0
  96. package/elizaos/bootstrap/autonomy/routes.py +171 -0
  97. package/elizaos/bootstrap/autonomy/service.py +562 -0
  98. package/elizaos/bootstrap/autonomy/types.py +18 -0
  99. package/elizaos/bootstrap/evaluators/__init__.py +19 -0
  100. package/elizaos/bootstrap/evaluators/reflection.py +118 -0
  101. package/elizaos/bootstrap/evaluators/relationship_extraction.py +192 -0
  102. package/elizaos/bootstrap/plugin.py +140 -0
  103. package/elizaos/bootstrap/providers/__init__.py +80 -0
  104. package/elizaos/bootstrap/providers/action_state.py +71 -0
  105. package/elizaos/bootstrap/providers/actions.py +256 -0
  106. package/elizaos/bootstrap/providers/agent_settings.py +63 -0
  107. package/elizaos/bootstrap/providers/attachments.py +76 -0
  108. package/elizaos/bootstrap/providers/capabilities.py +66 -0
  109. package/elizaos/bootstrap/providers/character.py +128 -0
  110. package/elizaos/bootstrap/providers/choice.py +77 -0
  111. package/elizaos/bootstrap/providers/contacts.py +78 -0
  112. package/elizaos/bootstrap/providers/context_bench.py +49 -0
  113. package/elizaos/bootstrap/providers/current_time.py +56 -0
  114. package/elizaos/bootstrap/providers/entities.py +99 -0
  115. package/elizaos/bootstrap/providers/evaluators.py +58 -0
  116. package/elizaos/bootstrap/providers/facts.py +86 -0
  117. package/elizaos/bootstrap/providers/follow_ups.py +116 -0
  118. package/elizaos/bootstrap/providers/knowledge.py +73 -0
  119. package/elizaos/bootstrap/providers/providers_list.py +59 -0
  120. package/elizaos/bootstrap/providers/recent_messages.py +85 -0
  121. package/elizaos/bootstrap/providers/relationships.py +106 -0
  122. package/elizaos/bootstrap/providers/roles.py +95 -0
  123. package/elizaos/bootstrap/providers/settings.py +55 -0
  124. package/elizaos/bootstrap/providers/time.py +45 -0
  125. package/elizaos/bootstrap/providers/world.py +97 -0
  126. package/elizaos/bootstrap/services/__init__.py +26 -0
  127. package/elizaos/bootstrap/services/embedding.py +122 -0
  128. package/elizaos/bootstrap/services/follow_up.py +138 -0
  129. package/elizaos/bootstrap/services/rolodex.py +244 -0
  130. package/elizaos/bootstrap/services/task.py +585 -0
  131. package/elizaos/bootstrap/types.py +54 -0
  132. package/elizaos/bootstrap/utils/__init__.py +7 -0
  133. package/elizaos/bootstrap/utils/xml.py +69 -0
  134. package/elizaos/character.py +149 -0
  135. package/elizaos/logger.py +179 -0
  136. package/elizaos/media/__init__.py +45 -0
  137. package/elizaos/media/mime.py +315 -0
  138. package/elizaos/media/search.py +161 -0
  139. package/elizaos/media/tests/__init__.py +1 -0
  140. package/elizaos/media/tests/test_mime.py +117 -0
  141. package/elizaos/media/tests/test_search.py +156 -0
  142. package/elizaos/plugin.py +191 -0
  143. package/elizaos/prompts.py +1071 -0
  144. package/elizaos/py.typed +0 -0
  145. package/elizaos/runtime.py +2572 -0
  146. package/elizaos/services/__init__.py +49 -0
  147. package/elizaos/services/hook_service.py +511 -0
  148. package/elizaos/services/message_service.py +1248 -0
  149. package/elizaos/settings.py +182 -0
  150. package/elizaos/streaming_context.py +159 -0
  151. package/elizaos/trajectory_context.py +18 -0
  152. package/elizaos/types/__init__.py +512 -0
  153. package/elizaos/types/agent.py +31 -0
  154. package/elizaos/types/components.py +208 -0
  155. package/elizaos/types/database.py +64 -0
  156. package/elizaos/types/environment.py +46 -0
  157. package/elizaos/types/events.py +47 -0
  158. package/elizaos/types/memory.py +45 -0
  159. package/elizaos/types/model.py +393 -0
  160. package/elizaos/types/plugin.py +188 -0
  161. package/elizaos/types/primitives.py +100 -0
  162. package/elizaos/types/runtime.py +460 -0
  163. package/elizaos/types/service.py +113 -0
  164. package/elizaos/types/service_interfaces.py +244 -0
  165. package/elizaos/types/state.py +188 -0
  166. package/elizaos/types/task.py +29 -0
  167. package/elizaos/utils/__init__.py +108 -0
  168. package/elizaos/utils/spec_examples.py +48 -0
  169. package/elizaos/utils/streaming.py +426 -0
  170. package/elizaos_atropos_shared/__init__.py +1 -0
  171. package/elizaos_atropos_shared/canonical_eliza.py +282 -0
  172. package/package.json +19 -0
  173. package/pyproject.toml +143 -0
  174. package/requirements-dev.in +11 -0
  175. package/requirements-dev.lock +134 -0
  176. package/requirements.in +9 -0
  177. package/requirements.lock +64 -0
  178. package/tests/__init__.py +0 -0
  179. package/tests/test_action_parameters.py +154 -0
  180. package/tests/test_actions_provider_examples.py +39 -0
  181. package/tests/test_advanced_memory_behavior.py +96 -0
  182. package/tests/test_advanced_memory_flag.py +30 -0
  183. package/tests/test_advanced_planning_behavior.py +225 -0
  184. package/tests/test_advanced_planning_flag.py +26 -0
  185. package/tests/test_autonomy.py +445 -0
  186. package/tests/test_bootstrap_initialize.py +37 -0
  187. package/tests/test_character.py +163 -0
  188. package/tests/test_character_provider.py +231 -0
  189. package/tests/test_dynamic_prompt_exec.py +561 -0
  190. package/tests/test_logger_redaction.py +43 -0
  191. package/tests/test_plugin.py +117 -0
  192. package/tests/test_runtime.py +422 -0
  193. package/tests/test_salt_production_enforcement.py +22 -0
  194. package/tests/test_settings_crypto.py +118 -0
  195. package/tests/test_streaming.py +295 -0
  196. package/tests/test_types.py +221 -0
  197. package/tests/test_uuid_parity.py +46 -0
@@ -0,0 +1,445 @@
1
+ from __future__ import annotations
2
+
3
+ from unittest.mock import AsyncMock, MagicMock
4
+
5
+ import pytest
6
+
7
+ from elizaos.bootstrap.autonomy import (
8
+ AUTONOMY_SERVICE_TYPE,
9
+ AutonomyService,
10
+ admin_chat_provider,
11
+ autonomy_status_provider,
12
+ send_to_admin_action,
13
+ )
14
+ from elizaos.bootstrap.autonomy.types import AutonomyStatus
15
+ from elizaos.types.memory import Memory
16
+ from elizaos.types.primitives import Content, as_uuid
17
+
18
+ TEST_AGENT_ID = "00000000-0000-0000-0000-000000000001"
19
+ TEST_ROOM_ID = "00000000-0000-0000-0000-000000000002"
20
+ TEST_ENTITY_ID = "00000000-0000-0000-0000-000000000003"
21
+ TEST_MESSAGE_ID = "00000000-0000-0000-0000-000000000004"
22
+ OTHER_ROOM_ID = "00000000-0000-0000-0000-000000000005"
23
+ AUTONOMOUS_ROOM_ID = "00000000-0000-0000-0000-000000000006"
24
+
25
+
26
+ @pytest.fixture
27
+ def test_runtime():
28
+ runtime = MagicMock()
29
+ runtime.agent_id = as_uuid(TEST_AGENT_ID)
30
+ runtime.character = MagicMock()
31
+ runtime.character.name = "Test Agent"
32
+
33
+ runtime.ensure_world_exists = AsyncMock()
34
+ runtime.ensure_room_exists = AsyncMock()
35
+ runtime.add_participant = AsyncMock()
36
+ runtime.get_entity_by_id = AsyncMock(return_value=MagicMock(id=TEST_AGENT_ID))
37
+ runtime.get_memories = AsyncMock(return_value=[])
38
+ runtime.emit_event = AsyncMock()
39
+ runtime.create_memory = AsyncMock(return_value="memory-id")
40
+
41
+ runtime.get_setting = MagicMock(return_value=None)
42
+ runtime.set_setting = MagicMock()
43
+ runtime.enable_autonomy = False
44
+
45
+ runtime.logger = MagicMock()
46
+ runtime.logger.info = MagicMock()
47
+ runtime.logger.debug = MagicMock()
48
+ runtime.logger.error = MagicMock()
49
+ runtime.logger.warn = MagicMock()
50
+ runtime.logger.warning = MagicMock()
51
+
52
+ # Task system mocks (used by _create_autonomy_task / _remove_autonomy_task)
53
+ runtime.create_task = AsyncMock()
54
+ runtime.get_tasks = AsyncMock(return_value=[])
55
+ runtime.delete_task = AsyncMock()
56
+ runtime.register_task_worker = MagicMock()
57
+
58
+ return runtime
59
+
60
+
61
+ @pytest.fixture
62
+ def test_memory():
63
+ return Memory(
64
+ id=as_uuid(TEST_MESSAGE_ID),
65
+ room_id=as_uuid(TEST_ROOM_ID),
66
+ entity_id=as_uuid(TEST_ENTITY_ID),
67
+ agent_id=as_uuid(TEST_AGENT_ID),
68
+ content=Content(text="Test message"),
69
+ created_at=1234567890,
70
+ )
71
+
72
+
73
+ class TestAutonomyService:
74
+ def test_service_type(self):
75
+ assert AutonomyService.service_type == AUTONOMY_SERVICE_TYPE
76
+ assert AutonomyService.service_type == "AUTONOMY"
77
+
78
+ @pytest.mark.asyncio
79
+ async def test_start_creates_service(self, test_runtime):
80
+ service = await AutonomyService.start(test_runtime)
81
+
82
+ assert service is not None
83
+ assert isinstance(service, AutonomyService)
84
+ assert service.is_loop_running() is False
85
+ assert service.get_loop_interval() == 30000
86
+ assert service.get_autonomous_room_id() is not None
87
+
88
+ @pytest.mark.asyncio
89
+ async def test_auto_start_when_enabled(self, test_runtime):
90
+ test_runtime.enable_autonomy = True
91
+
92
+ service = await AutonomyService.start(test_runtime)
93
+
94
+ assert service.is_loop_running() is True
95
+
96
+ await service.disable_autonomy()
97
+
98
+ @pytest.mark.asyncio
99
+ async def test_ensure_context_on_initialization(self, test_runtime):
100
+ _ = await AutonomyService.start(test_runtime)
101
+
102
+ test_runtime.ensure_world_exists.assert_called_once()
103
+ test_runtime.ensure_room_exists.assert_called_once()
104
+ test_runtime.add_participant.assert_called_once()
105
+
106
+ world_call = test_runtime.ensure_world_exists.call_args[0][0]
107
+ assert world_call.name == "Autonomy World"
108
+
109
+ room_call = test_runtime.ensure_room_exists.call_args[0][0]
110
+ assert room_call.name == "Autonomous Thoughts"
111
+ assert room_call.source == "autonomy-service"
112
+
113
+ @pytest.mark.asyncio
114
+ async def test_start_stop_loop(self, test_runtime):
115
+ service = await AutonomyService.start(test_runtime)
116
+
117
+ assert service.is_loop_running() is False
118
+
119
+ await service.enable_autonomy()
120
+ assert service.is_loop_running() is True
121
+ assert test_runtime.enable_autonomy is True
122
+
123
+ await service.disable_autonomy()
124
+ assert service.is_loop_running() is False
125
+ assert test_runtime.enable_autonomy is False
126
+
127
+ @pytest.mark.asyncio
128
+ async def test_no_double_start(self, test_runtime):
129
+ service = await AutonomyService.start(test_runtime)
130
+
131
+ await service.enable_autonomy()
132
+ call_count = test_runtime.set_setting.call_count
133
+
134
+ await service.enable_autonomy()
135
+ assert test_runtime.set_setting.call_count == call_count
136
+
137
+ await service.disable_autonomy()
138
+
139
+ @pytest.mark.asyncio
140
+ async def test_no_double_stop(self, test_runtime):
141
+ service = await AutonomyService.start(test_runtime)
142
+
143
+ call_count = test_runtime.set_setting.call_count
144
+ await service.disable_autonomy()
145
+ assert test_runtime.set_setting.call_count == call_count
146
+
147
+ @pytest.mark.asyncio
148
+ async def test_interval_configuration(self, test_runtime):
149
+ service = await AutonomyService.start(test_runtime)
150
+
151
+ await service.set_loop_interval(60000)
152
+ assert service.get_loop_interval() == 60000
153
+
154
+ @pytest.mark.asyncio
155
+ async def test_interval_minimum_enforced(self, test_runtime):
156
+ service = await AutonomyService.start(test_runtime)
157
+
158
+ await service.set_loop_interval(1000)
159
+ assert service.get_loop_interval() == 5000
160
+
161
+ @pytest.mark.asyncio
162
+ async def test_interval_maximum_enforced(self, test_runtime):
163
+ service = await AutonomyService.start(test_runtime)
164
+
165
+ await service.set_loop_interval(1000000)
166
+ assert service.get_loop_interval() == 600000
167
+
168
+ @pytest.mark.asyncio
169
+ async def test_target_room_context_dedupes_by_earliest_created_at(self, test_runtime):
170
+ service = await AutonomyService.start(test_runtime)
171
+ target_room_id = as_uuid(OTHER_ROOM_ID)
172
+ test_runtime.get_setting = MagicMock(return_value=str(target_room_id))
173
+
174
+ dup_id = as_uuid(TEST_MESSAGE_ID)
175
+ older = Memory(
176
+ id=dup_id,
177
+ room_id=target_room_id,
178
+ entity_id=as_uuid(TEST_ENTITY_ID),
179
+ agent_id=as_uuid(TEST_AGENT_ID),
180
+ content=Content(text="old"),
181
+ created_at=10,
182
+ )
183
+ newer = Memory(
184
+ id=dup_id,
185
+ room_id=target_room_id,
186
+ entity_id=as_uuid(TEST_ENTITY_ID),
187
+ agent_id=as_uuid(TEST_AGENT_ID),
188
+ content=Content(text="new"),
189
+ created_at=20,
190
+ )
191
+
192
+ async def get_memories(params):
193
+ if params["tableName"] == "memories":
194
+ return [newer]
195
+ return [older]
196
+
197
+ test_runtime.get_memories = AsyncMock(side_effect=get_memories)
198
+
199
+ context = await service._get_target_room_context_text()
200
+ assert "old" in context
201
+ assert "new" not in context
202
+
203
+ @pytest.mark.asyncio
204
+ async def test_enable_autonomy(self, test_runtime):
205
+ service = await AutonomyService.start(test_runtime)
206
+
207
+ await service.enable_autonomy()
208
+
209
+ assert test_runtime.enable_autonomy is True
210
+ assert service.is_loop_running() is True
211
+
212
+ await service.disable_autonomy()
213
+
214
+ @pytest.mark.asyncio
215
+ async def test_disable_autonomy(self, test_runtime):
216
+ service = await AutonomyService.start(test_runtime)
217
+
218
+ await service.enable_autonomy()
219
+ await service.disable_autonomy()
220
+
221
+ assert test_runtime.enable_autonomy is False
222
+ assert service.is_loop_running() is False
223
+
224
+ @pytest.mark.asyncio
225
+ async def test_get_status(self, test_runtime):
226
+ test_runtime.enable_autonomy = True
227
+ service = await AutonomyService.start(test_runtime)
228
+
229
+ status = service.get_status()
230
+
231
+ assert isinstance(status, AutonomyStatus)
232
+ assert status.enabled is True
233
+ assert status.running is True
234
+ assert status.thinking is False
235
+ assert status.interval == 30000
236
+ assert status.autonomous_room_id is not None
237
+
238
+ @pytest.mark.asyncio
239
+ async def test_last_autonomous_thought_uses_latest_created_at(self, test_runtime):
240
+ service = await AutonomyService.start(test_runtime)
241
+ test_runtime.enable_autonomy = True
242
+ test_runtime.get_setting = MagicMock(return_value=None)
243
+
244
+ older = Memory(
245
+ id=as_uuid("12345678-1234-1234-1234-123456789010"),
246
+ room_id=service.get_autonomous_room_id(),
247
+ entity_id=as_uuid(TEST_AGENT_ID),
248
+ agent_id=as_uuid(TEST_AGENT_ID),
249
+ content=Content(text="older"),
250
+ created_at=10,
251
+ )
252
+ newer = Memory(
253
+ id=as_uuid("12345678-1234-1234-1234-123456789011"),
254
+ room_id=service.get_autonomous_room_id(),
255
+ entity_id=as_uuid(TEST_AGENT_ID),
256
+ agent_id=as_uuid(TEST_AGENT_ID),
257
+ content=Content(text="newer"),
258
+ created_at=20,
259
+ )
260
+
261
+ test_runtime.get_memories = AsyncMock(return_value=[older, newer])
262
+
263
+ await service.perform_autonomous_think()
264
+
265
+ assert test_runtime.emit_event.called is True
266
+ payload = test_runtime.emit_event.call_args[0][1]
267
+ msg = payload.get("message")
268
+ assert msg is not None
269
+ assert "newer" in (msg.content.text or "")
270
+
271
+ await service.disable_autonomy()
272
+
273
+ @pytest.mark.asyncio
274
+ async def test_thinking_guard_initial_state(self, test_runtime):
275
+ service = await AutonomyService.start(test_runtime)
276
+
277
+ assert service.is_thinking_in_progress() is False
278
+ assert service.get_status().thinking is False
279
+
280
+ @pytest.mark.asyncio
281
+ async def test_thinking_guard_prevents_overlap(self, test_runtime):
282
+ service = await AutonomyService.start(test_runtime)
283
+
284
+ service._is_thinking = True
285
+
286
+ assert service.is_thinking_in_progress() is True
287
+ assert service.get_status().thinking is True
288
+
289
+ service._is_thinking = False
290
+ assert service.is_thinking_in_progress() is False
291
+
292
+
293
+ class TestSendToAdminAction:
294
+ def test_action_metadata(self):
295
+ assert send_to_admin_action.name == "SEND_TO_ADMIN"
296
+ assert send_to_admin_action.description is not None
297
+ assert send_to_admin_action.examples is not None
298
+ assert len(send_to_admin_action.examples) > 0
299
+
300
+ @pytest.mark.asyncio
301
+ async def test_validate_in_autonomous_room(self, test_runtime):
302
+ room_id = as_uuid(TEST_ROOM_ID)
303
+ mock_service = MagicMock(spec=AutonomyService)
304
+ mock_service.get_autonomous_room_id = MagicMock(return_value=room_id)
305
+ test_runtime.get_service = MagicMock(return_value=mock_service)
306
+ test_runtime.get_setting = MagicMock(return_value="admin-user-id")
307
+
308
+ message = Memory(
309
+ id=as_uuid(TEST_MESSAGE_ID),
310
+ room_id=room_id,
311
+ entity_id=as_uuid(TEST_ENTITY_ID),
312
+ agent_id=as_uuid(TEST_AGENT_ID),
313
+ content=Content(text="Tell admin about this update"),
314
+ created_at=1234567890,
315
+ )
316
+
317
+ is_valid = await send_to_admin_action.validate(test_runtime, message)
318
+ assert is_valid is True
319
+
320
+ @pytest.mark.asyncio
321
+ async def test_validate_not_in_autonomous_room(self, test_runtime, test_memory):
322
+ mock_service = MagicMock(spec=AutonomyService)
323
+ mock_service.get_autonomous_room_id = MagicMock(return_value=as_uuid(OTHER_ROOM_ID))
324
+ test_runtime.get_service = MagicMock(return_value=mock_service)
325
+ test_runtime.get_setting = MagicMock(return_value="admin-user-id")
326
+
327
+ is_valid = await send_to_admin_action.validate(test_runtime, test_memory)
328
+ assert is_valid is False
329
+
330
+ @pytest.mark.asyncio
331
+ async def test_validate_no_admin_configured(self, test_runtime, test_memory):
332
+ mock_service = MagicMock(spec=AutonomyService)
333
+ mock_service.get_autonomous_room_id = MagicMock(return_value=test_memory.room_id)
334
+ test_runtime.get_service = MagicMock(return_value=mock_service)
335
+ test_runtime.get_setting = MagicMock(return_value=None)
336
+
337
+ is_valid = await send_to_admin_action.validate(test_runtime, test_memory)
338
+ assert is_valid is False
339
+
340
+
341
+ class TestAdminChatProvider:
342
+ def test_provider_metadata(self):
343
+ assert admin_chat_provider.name == "ADMIN_CHAT_HISTORY"
344
+ assert admin_chat_provider.description is not None
345
+
346
+ @pytest.mark.asyncio
347
+ async def test_returns_empty_when_no_service(self, test_runtime, test_memory):
348
+ test_runtime.get_service = MagicMock(return_value=None)
349
+
350
+ result = await admin_chat_provider.get(test_runtime, test_memory, {})
351
+
352
+ assert result.text == ""
353
+
354
+ @pytest.mark.asyncio
355
+ async def test_returns_empty_when_not_in_autonomous_room(self, test_runtime, test_memory):
356
+ mock_service = MagicMock(spec=AutonomyService)
357
+ mock_service.get_autonomous_room_id = MagicMock(return_value=as_uuid(OTHER_ROOM_ID))
358
+ test_runtime.get_service = MagicMock(return_value=mock_service)
359
+
360
+ result = await admin_chat_provider.get(test_runtime, test_memory, {})
361
+
362
+ assert result.text == ""
363
+
364
+ @pytest.mark.asyncio
365
+ async def test_indicates_no_admin_configured(self, test_runtime, test_memory):
366
+ mock_service = MagicMock(spec=AutonomyService)
367
+ mock_service.get_autonomous_room_id = MagicMock(return_value=test_memory.room_id)
368
+ test_runtime.get_service = MagicMock(return_value=mock_service)
369
+ test_runtime.get_setting = MagicMock(return_value=None)
370
+
371
+ result = await admin_chat_provider.get(test_runtime, test_memory, {})
372
+
373
+ assert "No admin user configured" in result.text
374
+ assert result.data == {"adminConfigured": False}
375
+
376
+
377
+ class TestAutonomyStatusProvider:
378
+ def test_provider_metadata(self):
379
+ assert autonomy_status_provider.name == "AUTONOMY_STATUS"
380
+ assert autonomy_status_provider.description is not None
381
+
382
+ @pytest.mark.asyncio
383
+ async def test_returns_empty_when_no_service(self, test_runtime, test_memory):
384
+ test_runtime.get_service = MagicMock(return_value=None)
385
+
386
+ result = await autonomy_status_provider.get(test_runtime, test_memory, {})
387
+
388
+ assert result.text == ""
389
+
390
+ @pytest.mark.asyncio
391
+ async def test_returns_empty_in_autonomous_room(self, test_runtime, test_memory):
392
+ mock_service = MagicMock(spec=AutonomyService)
393
+ mock_service.get_autonomous_room_id = MagicMock(return_value=test_memory.room_id)
394
+ test_runtime.get_service = MagicMock(return_value=mock_service)
395
+
396
+ result = await autonomy_status_provider.get(test_runtime, test_memory, {})
397
+
398
+ assert result.text == ""
399
+
400
+ @pytest.mark.asyncio
401
+ async def test_shows_running_status(self, test_runtime, test_memory):
402
+ mock_service = MagicMock(spec=AutonomyService)
403
+ mock_service.get_autonomous_room_id = MagicMock(return_value=as_uuid(AUTONOMOUS_ROOM_ID))
404
+ mock_service.is_loop_running = MagicMock(return_value=True)
405
+ mock_service.get_loop_interval = MagicMock(return_value=30000)
406
+ test_runtime.get_service = MagicMock(return_value=mock_service)
407
+ test_runtime.get_setting = MagicMock(return_value=True)
408
+
409
+ result = await autonomy_status_provider.get(test_runtime, test_memory, {})
410
+
411
+ assert "AUTONOMY_STATUS" in result.text
412
+ assert "running autonomously" in result.text
413
+ assert result.data["serviceRunning"] is True
414
+ assert result.data["status"] == "running"
415
+
416
+ @pytest.mark.asyncio
417
+ async def test_shows_disabled_status(self, test_runtime, test_memory):
418
+ mock_service = MagicMock(spec=AutonomyService)
419
+ mock_service.get_autonomous_room_id = MagicMock(return_value=as_uuid(AUTONOMOUS_ROOM_ID))
420
+ mock_service.is_loop_running = MagicMock(return_value=False)
421
+ mock_service.get_loop_interval = MagicMock(return_value=30000)
422
+ test_runtime.get_service = MagicMock(return_value=mock_service)
423
+ test_runtime.get_setting = MagicMock(return_value=False)
424
+
425
+ result = await autonomy_status_provider.get(test_runtime, test_memory, {})
426
+
427
+ assert "autonomy disabled" in result.text
428
+ assert result.data["status"] == "disabled"
429
+
430
+
431
+ class TestAutonomyIntegration:
432
+ def test_exports_all_components(self):
433
+ from elizaos.bootstrap.autonomy import (
434
+ AUTONOMY_SERVICE_TYPE,
435
+ AutonomyService,
436
+ admin_chat_provider,
437
+ autonomy_status_provider,
438
+ send_to_admin_action,
439
+ )
440
+
441
+ assert AutonomyService is not None
442
+ assert AUTONOMY_SERVICE_TYPE == "AUTONOMY"
443
+ assert send_to_admin_action is not None
444
+ assert admin_chat_provider is not None
445
+ assert autonomy_status_provider is not None
@@ -0,0 +1,37 @@
1
+ import pytest
2
+
3
+ from elizaos.runtime import AgentRuntime
4
+ from elizaos.types import Character
5
+ from elizaos.types.service import ServiceType
6
+
7
+
8
+ @pytest.mark.asyncio
9
+ async def test_runtime_initialize_registers_bootstrap_services() -> None:
10
+ """
11
+ Ensure AgentRuntime.initialize() loads the bootstrap plugin and registers services.
12
+
13
+ This guards against regressions where bootstrap service registration silently
14
+ stores None or fails to start services properly.
15
+ """
16
+ runtime = AgentRuntime(
17
+ character=Character(name="BootstrapTest", bio="bootstrap init test"),
18
+ log_level="ERROR",
19
+ )
20
+
21
+ await runtime.initialize()
22
+
23
+ assert any(p.name == "bootstrap" for p in runtime.plugins)
24
+
25
+ # Bootstrap should register at least one service of type TASK.
26
+ assert runtime.has_service(ServiceType.TASK)
27
+
28
+ task_service = runtime.get_service(ServiceType.TASK)
29
+ assert task_service is not None
30
+
31
+ task_services = runtime.get_services_by_type(ServiceType.TASK)
32
+ assert task_services
33
+ assert all(s is not None for s in task_services)
34
+
35
+ # Bootstrap should also register some core actions/providers/evaluators.
36
+ assert runtime.actions
37
+ assert runtime.providers
@@ -0,0 +1,163 @@
1
+ import json
2
+ import os
3
+ import tempfile
4
+
5
+ import pytest
6
+
7
+ from elizaos.character import (
8
+ CharacterLoadError,
9
+ CharacterValidationError,
10
+ build_character_plugins,
11
+ load_character_from_file,
12
+ merge_character_defaults,
13
+ parse_character,
14
+ validate_character_config,
15
+ )
16
+ from elizaos.types import Character
17
+
18
+
19
+ class TestParseCharacter:
20
+ def test_parse_character_object(self) -> None:
21
+ character = Character(name="Test", bio=["A test agent"])
22
+ result = parse_character(character)
23
+ assert result.name == "Test"
24
+ assert list(result.bio) == ["A test agent"]
25
+
26
+ def test_parse_character_dict(self) -> None:
27
+ data = {"name": "Test", "bio": ["A test agent"]}
28
+ result = parse_character(data)
29
+ assert result.name == "Test"
30
+ assert list(result.bio) == ["A test agent"]
31
+
32
+ def test_parse_character_invalid_dict(self) -> None:
33
+ data = {"bio": "Missing name"}
34
+ with pytest.raises(CharacterValidationError):
35
+ parse_character(data)
36
+
37
+ def test_parse_character_file_path(self) -> None:
38
+ with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f:
39
+ json.dump({"name": "FileAgent", "bio": ["From file"]}, f)
40
+ f.flush()
41
+
42
+ try:
43
+ result = parse_character(f.name)
44
+ assert result.name == "FileAgent"
45
+ finally:
46
+ os.unlink(f.name)
47
+
48
+
49
+ class TestLoadCharacterFromFile:
50
+ def test_load_valid_file(self) -> None:
51
+ with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f:
52
+ json.dump(
53
+ {
54
+ "name": "FileAgent",
55
+ "bio": ["A file-based agent"],
56
+ "topics": ["testing"],
57
+ },
58
+ f,
59
+ )
60
+ f.flush()
61
+
62
+ try:
63
+ result = load_character_from_file(f.name)
64
+ assert result.name == "FileAgent"
65
+ assert list(result.topics) == ["testing"]
66
+ finally:
67
+ os.unlink(f.name)
68
+
69
+ def test_load_nonexistent_file(self) -> None:
70
+ with pytest.raises(CharacterLoadError, match="not found"):
71
+ load_character_from_file("/nonexistent/path/character.json")
72
+
73
+ def test_load_invalid_json(self) -> None:
74
+ with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f:
75
+ f.write("not valid json {")
76
+ f.flush()
77
+
78
+ try:
79
+ with pytest.raises(CharacterLoadError, match="Invalid JSON"):
80
+ load_character_from_file(f.name)
81
+ finally:
82
+ os.unlink(f.name)
83
+
84
+
85
+ class TestValidateCharacterConfig:
86
+ def test_valid_character(self) -> None:
87
+ character = Character(name="Test", bio=["A test agent"])
88
+ result = validate_character_config(character)
89
+ assert result["isValid"] is True
90
+ assert result["errors"] == []
91
+
92
+ def test_character_with_all_fields(self) -> None:
93
+ character = Character(
94
+ name="CompleteAgent",
95
+ username="complete",
96
+ bio=["Line 1", "Line 2"],
97
+ system="You are a complete agent.",
98
+ topics=["all", "topics"],
99
+ adjectives=["thorough", "complete"],
100
+ plugins=["@elizaos/plugin-sql"],
101
+ )
102
+ result = validate_character_config(character)
103
+ assert result["isValid"] is True
104
+
105
+
106
+ class TestMergeCharacterDefaults:
107
+ def test_merge_empty(self) -> None:
108
+ result = merge_character_defaults({})
109
+ assert result.name == "Unnamed Character"
110
+ assert list(result.plugins) == []
111
+
112
+ def test_merge_partial(self) -> None:
113
+ result = merge_character_defaults({"name": "CustomAgent", "bio": ["Custom bio"]})
114
+ assert result.name == "CustomAgent"
115
+ assert list(result.bio) == ["Custom bio"]
116
+
117
+ def test_merge_preserves_values(self) -> None:
118
+ result = merge_character_defaults(
119
+ {
120
+ "name": "Agent",
121
+ "bio": ["Bio"],
122
+ "plugins": ["plugin-1"],
123
+ }
124
+ )
125
+ assert list(result.plugins) == ["plugin-1"]
126
+
127
+
128
+ class TestBuildCharacterPlugins:
129
+ def test_default_plugins(self) -> None:
130
+ plugins = build_character_plugins({})
131
+ assert "@elizaos/plugin-sql" in plugins
132
+ assert "@elizaos/plugin-ollama" in plugins
133
+
134
+ def test_with_openai(self) -> None:
135
+ plugins = build_character_plugins({"OPENAI_API_KEY": "test-key"})
136
+ assert "@elizaos/plugin-openai" in plugins
137
+ assert "@elizaos/plugin-ollama" not in plugins
138
+
139
+ def test_with_anthropic(self) -> None:
140
+ plugins = build_character_plugins({"ANTHROPIC_API_KEY": "test-key"})
141
+ assert "@elizaos/plugin-anthropic" in plugins
142
+ assert "@elizaos/plugin-ollama" not in plugins
143
+
144
+ def test_with_discord(self) -> None:
145
+ plugins = build_character_plugins(
146
+ {
147
+ "DISCORD_API_TOKEN": "test-token",
148
+ "OPENAI_API_KEY": "test-key",
149
+ }
150
+ )
151
+ assert "@elizaos/plugin-discord" in plugins
152
+
153
+ def test_plugin_order(self) -> None:
154
+ plugins = build_character_plugins(
155
+ {
156
+ "OPENAI_API_KEY": "key1",
157
+ "ANTHROPIC_API_KEY": "key2",
158
+ "DISCORD_API_TOKEN": "token",
159
+ }
160
+ )
161
+ assert plugins.index("@elizaos/plugin-sql") == 0
162
+ assert plugins.index("@elizaos/plugin-anthropic") < plugins.index("@elizaos/plugin-openai")
163
+ assert plugins.index("@elizaos/plugin-discord") > plugins.index("@elizaos/plugin-openai")