@elizaos/python 2.0.0-alpha.3 → 2.0.0-alpha.31

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 (167) 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 +24 -13
  5. package/elizaos/advanced_capabilities/actions/follow_room.py +29 -29
  6. package/elizaos/advanced_capabilities/actions/image_generation.py +15 -28
  7. package/elizaos/advanced_capabilities/actions/mute_room.py +15 -28
  8. package/elizaos/advanced_capabilities/actions/remove_contact.py +17 -3
  9. package/elizaos/advanced_capabilities/actions/roles.py +17 -30
  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 +184 -51
  13. package/elizaos/advanced_capabilities/actions/settings.py +17 -3
  14. package/elizaos/advanced_capabilities/actions/unfollow_room.py +15 -28
  15. package/elizaos/advanced_capabilities/actions/unmute_room.py +15 -28
  16. package/elizaos/advanced_capabilities/actions/update_contact.py +17 -3
  17. package/elizaos/advanced_capabilities/actions/update_entity.py +17 -3
  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 +23 -3
  23. package/elizaos/advanced_capabilities/services/__init__.py +2 -9
  24. package/elizaos/advanced_capabilities/services/rolodex.py +2 -2
  25. package/elizaos/advanced_memory/actions/reset_session.py +143 -0
  26. package/elizaos/advanced_memory/evaluators/reflection.py +134 -0
  27. package/elizaos/advanced_memory/evaluators/relationship_extraction.py +203 -0
  28. package/elizaos/advanced_memory/memory_service.py +69 -27
  29. package/elizaos/advanced_memory/plugin.py +2 -1
  30. package/elizaos/advanced_memory/test_advanced_memory.py +357 -0
  31. package/elizaos/advanced_memory/types.py +2 -2
  32. package/elizaos/advanced_planning/actions/schedule_follow_up.py +222 -0
  33. package/elizaos/advanced_planning/planning_service.py +26 -14
  34. package/elizaos/basic_capabilities/__init__.py +0 -2
  35. package/elizaos/basic_capabilities/providers/__init__.py +0 -3
  36. package/elizaos/basic_capabilities/providers/actions.py +118 -29
  37. package/elizaos/basic_capabilities/providers/agent_settings.py +64 -0
  38. package/elizaos/basic_capabilities/providers/character.py +19 -21
  39. package/elizaos/basic_capabilities/providers/contacts.py +79 -0
  40. package/elizaos/basic_capabilities/providers/current_time.py +7 -4
  41. package/elizaos/basic_capabilities/providers/facts.py +87 -0
  42. package/elizaos/basic_capabilities/providers/follow_ups.py +117 -0
  43. package/elizaos/basic_capabilities/providers/knowledge.py +96 -0
  44. package/elizaos/basic_capabilities/providers/recent_messages.py +5 -0
  45. package/elizaos/basic_capabilities/providers/relationships.py +113 -0
  46. package/elizaos/basic_capabilities/providers/roles.py +96 -0
  47. package/elizaos/basic_capabilities/providers/settings.py +56 -0
  48. package/elizaos/basic_capabilities/providers/time.py +7 -4
  49. package/elizaos/basic_capabilities/services/embedding.py +10 -7
  50. package/elizaos/basic_capabilities/services/task.py +3 -3
  51. package/elizaos/bootstrap/__init__.py +21 -2
  52. package/elizaos/bootstrap/actions/__init__.py +3 -0
  53. package/elizaos/bootstrap/actions/reset_session.py +3 -0
  54. package/elizaos/bootstrap/actions/roles.py +5 -4
  55. package/elizaos/bootstrap/actions/schedule_follow_up.py +65 -7
  56. package/elizaos/bootstrap/actions/send_message.py +162 -15
  57. package/elizaos/bootstrap/autonomy/__init__.py +5 -1
  58. package/elizaos/bootstrap/autonomy/action.py +161 -0
  59. package/elizaos/bootstrap/autonomy/evaluators.py +217 -0
  60. package/elizaos/bootstrap/autonomy/service.py +238 -28
  61. package/elizaos/bootstrap/plugin.py +7 -0
  62. package/elizaos/bootstrap/providers/actions.py +118 -27
  63. package/elizaos/bootstrap/providers/agent_settings.py +1 -0
  64. package/elizaos/bootstrap/providers/attachments.py +1 -0
  65. package/elizaos/bootstrap/providers/capabilities.py +1 -0
  66. package/elizaos/bootstrap/providers/character.py +1 -0
  67. package/elizaos/bootstrap/providers/choice.py +1 -0
  68. package/elizaos/bootstrap/providers/contacts.py +1 -0
  69. package/elizaos/bootstrap/providers/current_time.py +8 -2
  70. package/elizaos/bootstrap/providers/entities.py +1 -0
  71. package/elizaos/bootstrap/providers/evaluators.py +1 -0
  72. package/elizaos/bootstrap/providers/facts.py +1 -0
  73. package/elizaos/bootstrap/providers/follow_ups.py +1 -0
  74. package/elizaos/bootstrap/providers/knowledge.py +26 -3
  75. package/elizaos/bootstrap/providers/providers_list.py +1 -0
  76. package/elizaos/bootstrap/providers/recent_messages.py +5 -0
  77. package/elizaos/bootstrap/providers/relationships.py +20 -13
  78. package/elizaos/bootstrap/providers/roles.py +1 -0
  79. package/elizaos/bootstrap/providers/settings.py +1 -0
  80. package/elizaos/bootstrap/providers/time.py +8 -4
  81. package/elizaos/bootstrap/providers/world.py +1 -0
  82. package/elizaos/bootstrap/services/embedding.py +206 -8
  83. package/elizaos/bootstrap/services/rolodex.py +2 -2
  84. package/elizaos/bootstrap/services/task.py +3 -3
  85. package/elizaos/deterministic.py +193 -0
  86. package/elizaos/generated/__init__.py +1 -0
  87. package/elizaos/generated/action_docs.py +3181 -0
  88. package/elizaos/generated/spec_helpers.py +175 -0
  89. package/elizaos/media/mime.py +4 -4
  90. package/elizaos/media/search.py +23 -23
  91. package/elizaos/runtime.py +223 -64
  92. package/elizaos/services/hook_service.py +3 -3
  93. package/elizaos/services/message_service.py +175 -29
  94. package/elizaos/types/components.py +2 -2
  95. package/elizaos/types/generated/__init__.py +12 -0
  96. package/elizaos/types/generated/eliza/v1/agent_pb2.py +63 -0
  97. package/elizaos/types/generated/eliza/v1/agent_pb2.pyi +159 -0
  98. package/elizaos/types/generated/eliza/v1/components_pb2.py +65 -0
  99. package/elizaos/types/generated/eliza/v1/components_pb2.pyi +160 -0
  100. package/elizaos/types/generated/eliza/v1/database_pb2.py +78 -0
  101. package/elizaos/types/generated/eliza/v1/database_pb2.pyi +305 -0
  102. package/elizaos/types/generated/eliza/v1/environment_pb2.py +58 -0
  103. package/elizaos/types/generated/eliza/v1/environment_pb2.pyi +135 -0
  104. package/elizaos/types/generated/eliza/v1/events_pb2.py +82 -0
  105. package/elizaos/types/generated/eliza/v1/events_pb2.pyi +322 -0
  106. package/elizaos/types/generated/eliza/v1/ipc_pb2.py +113 -0
  107. package/elizaos/types/generated/eliza/v1/ipc_pb2.pyi +367 -0
  108. package/elizaos/types/generated/eliza/v1/knowledge_pb2.py +41 -0
  109. package/elizaos/types/generated/eliza/v1/knowledge_pb2.pyi +26 -0
  110. package/elizaos/types/generated/eliza/v1/memory_pb2.py +55 -0
  111. package/elizaos/types/generated/eliza/v1/memory_pb2.pyi +111 -0
  112. package/elizaos/types/generated/eliza/v1/message_service_pb2.py +48 -0
  113. package/elizaos/types/generated/eliza/v1/message_service_pb2.pyi +69 -0
  114. package/elizaos/types/generated/eliza/v1/messaging_pb2.py +51 -0
  115. package/elizaos/types/generated/eliza/v1/messaging_pb2.pyi +97 -0
  116. package/elizaos/types/generated/eliza/v1/model_pb2.py +84 -0
  117. package/elizaos/types/generated/eliza/v1/model_pb2.pyi +280 -0
  118. package/elizaos/types/generated/eliza/v1/payment_pb2.py +44 -0
  119. package/elizaos/types/generated/eliza/v1/payment_pb2.pyi +70 -0
  120. package/elizaos/types/generated/eliza/v1/plugin_pb2.py +68 -0
  121. package/elizaos/types/generated/eliza/v1/plugin_pb2.pyi +145 -0
  122. package/elizaos/types/generated/eliza/v1/primitives_pb2.py +48 -0
  123. package/elizaos/types/generated/eliza/v1/primitives_pb2.pyi +92 -0
  124. package/elizaos/types/generated/eliza/v1/prompts_pb2.py +52 -0
  125. package/elizaos/types/generated/eliza/v1/prompts_pb2.pyi +74 -0
  126. package/elizaos/types/generated/eliza/v1/service_interfaces_pb2.py +211 -0
  127. package/elizaos/types/generated/eliza/v1/service_interfaces_pb2.pyi +1296 -0
  128. package/elizaos/types/generated/eliza/v1/service_pb2.py +42 -0
  129. package/elizaos/types/generated/eliza/v1/service_pb2.pyi +69 -0
  130. package/elizaos/types/generated/eliza/v1/settings_pb2.py +58 -0
  131. package/elizaos/types/generated/eliza/v1/settings_pb2.pyi +85 -0
  132. package/elizaos/types/generated/eliza/v1/state_pb2.py +60 -0
  133. package/elizaos/types/generated/eliza/v1/state_pb2.pyi +114 -0
  134. package/elizaos/types/generated/eliza/v1/task_pb2.py +42 -0
  135. package/elizaos/types/generated/eliza/v1/task_pb2.pyi +58 -0
  136. package/elizaos/types/generated/eliza/v1/tee_pb2.py +52 -0
  137. package/elizaos/types/generated/eliza/v1/tee_pb2.pyi +90 -0
  138. package/elizaos/types/generated/eliza/v1/testing_pb2.py +39 -0
  139. package/elizaos/types/generated/eliza/v1/testing_pb2.pyi +23 -0
  140. package/elizaos/types/model.py +33 -3
  141. package/elizaos/types/primitives.py +3 -3
  142. package/elizaos/types/runtime.py +17 -3
  143. package/elizaos/types/state.py +2 -2
  144. package/elizaos/utils/streaming.py +3 -3
  145. package/elizaos/utils/validation.py +76 -0
  146. package/package.json +4 -3
  147. package/pyproject.toml +1 -2
  148. package/requirements-dev.lock +2 -2
  149. package/requirements.in +1 -2
  150. package/requirements.lock +2 -2
  151. package/tests/test_action_parameters.py +2 -3
  152. package/tests/test_actions_provider_examples.py +58 -1
  153. package/tests/test_advanced_memory_behavior.py +0 -2
  154. package/tests/test_advanced_memory_flag.py +0 -2
  155. package/tests/test_advanced_planning_behavior.py +11 -5
  156. package/tests/test_async_embedding.py +124 -0
  157. package/tests/test_autonomy.py +24 -3
  158. package/tests/test_history_compaction.py +104 -0
  159. package/tests/test_memory_bounds.py +115 -0
  160. package/tests/test_runtime.py +8 -17
  161. package/tests/test_schedule_follow_up_action.py +260 -0
  162. package/tests/test_send_message_action_targets.py +114 -0
  163. package/tests/test_settings_crypto.py +0 -2
  164. package/tests/test_validation.py +141 -0
  165. package/tests/verify_memory_architecture.py +192 -0
  166. package/uv.lock +1565 -0
  167. package/elizaos/basic_capabilities/providers/capabilities.py +0 -62
@@ -0,0 +1,357 @@
1
+ """Tests for the advanced memory module.
2
+
3
+ Covers XML parsing, extraction checkpointing, config management,
4
+ confidence sorting, and formatted memory output.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from uuid import uuid4
10
+
11
+ import pytest
12
+
13
+ from .memory_service import (
14
+ MemoryService,
15
+ _parse_memory_extraction_xml,
16
+ _parse_summary_xml,
17
+ _top_k_by_confidence,
18
+ )
19
+ from .types import (
20
+ LongTermMemory,
21
+ LongTermMemoryCategory,
22
+ )
23
+
24
+ # ============================================================================
25
+ # XML Parsing: Summary
26
+ # ============================================================================
27
+
28
+
29
+ class TestParseSummaryXml:
30
+ def test_valid_xml(self):
31
+ xml = """
32
+ <summary>
33
+ <text>The user discussed their favorite coffee.</text>
34
+ <topics>coffee, preferences, beverages</topics>
35
+ <key_points>
36
+ <point>User prefers dark roast</point>
37
+ <point>User drinks 3 cups daily</point>
38
+ </key_points>
39
+ </summary>"""
40
+ result = _parse_summary_xml(xml)
41
+ assert result.summary == "The user discussed their favorite coffee."
42
+ assert result.topics == ["coffee", "preferences", "beverages"]
43
+ assert len(result.key_points) == 2
44
+ assert result.key_points[0] == "User prefers dark roast"
45
+ assert result.key_points[1] == "User drinks 3 cups daily"
46
+
47
+ def test_malformed_xml(self):
48
+ result = _parse_summary_xml("This is not XML at all")
49
+ assert result.summary == "Summary not available"
50
+ assert result.topics == []
51
+ assert result.key_points == []
52
+
53
+ def test_partial_tags(self):
54
+ result = _parse_summary_xml("<text>Just a summary</text>")
55
+ assert result.summary == "Just a summary"
56
+ assert result.topics == []
57
+ assert result.key_points == []
58
+
59
+ def test_empty_topics(self):
60
+ result = _parse_summary_xml("<text>Summary here</text><topics></topics>")
61
+ assert result.summary == "Summary here"
62
+ assert result.topics == []
63
+
64
+
65
+ # ============================================================================
66
+ # XML Parsing: Memory Extraction
67
+ # ============================================================================
68
+
69
+
70
+ class TestParseMemoryExtractionXml:
71
+ def test_valid_multiple(self):
72
+ xml = """
73
+ <memories>
74
+ <memory>
75
+ <category>semantic</category>
76
+ <content>User works as a software engineer</content>
77
+ <confidence>0.95</confidence>
78
+ </memory>
79
+ <memory>
80
+ <category>episodic</category>
81
+ <content>User had a meeting yesterday</content>
82
+ <confidence>0.8</confidence>
83
+ </memory>
84
+ <memory>
85
+ <category>procedural</category>
86
+ <content>User prefers TypeScript for backend</content>
87
+ <confidence>0.9</confidence>
88
+ </memory>
89
+ </memories>"""
90
+ extractions = _parse_memory_extraction_xml(xml)
91
+ assert len(extractions) == 3
92
+ assert extractions[0].category == LongTermMemoryCategory.SEMANTIC
93
+ assert extractions[0].content == "User works as a software engineer"
94
+ assert abs(extractions[0].confidence - 0.95) < 0.001
95
+ assert extractions[1].category == LongTermMemoryCategory.EPISODIC
96
+ assert extractions[2].category == LongTermMemoryCategory.PROCEDURAL
97
+
98
+ def test_invalid_category_skipped(self):
99
+ xml = """
100
+ <memories>
101
+ <memory>
102
+ <category>invalid_type</category>
103
+ <content>This should be skipped</content>
104
+ <confidence>0.9</confidence>
105
+ </memory>
106
+ <memory>
107
+ <category>semantic</category>
108
+ <content>This should be kept</content>
109
+ <confidence>0.85</confidence>
110
+ </memory>
111
+ </memories>"""
112
+ extractions = _parse_memory_extraction_xml(xml)
113
+ assert len(extractions) == 1
114
+ assert extractions[0].content == "This should be kept"
115
+
116
+ def test_bad_confidence_skipped(self):
117
+ xml = """
118
+ <memory>
119
+ <category>semantic</category>
120
+ <content>Bad confidence</content>
121
+ <confidence>not_a_number</confidence>
122
+ </memory>"""
123
+ extractions = _parse_memory_extraction_xml(xml)
124
+ assert len(extractions) == 0
125
+
126
+ def test_empty_input(self):
127
+ assert _parse_memory_extraction_xml("") == []
128
+
129
+ def test_no_memories(self):
130
+ assert _parse_memory_extraction_xml("The model didn't return structured data.") == []
131
+
132
+
133
+ # ============================================================================
134
+ # top_k_by_confidence
135
+ # ============================================================================
136
+
137
+
138
+ class TestTopKByConfidence:
139
+ def test_sorts_by_confidence(self):
140
+ entity_id = uuid4()
141
+ agent_id = uuid4()
142
+ memories = [
143
+ LongTermMemory(
144
+ id=uuid4(),
145
+ agent_id=agent_id,
146
+ entity_id=entity_id,
147
+ category=LongTermMemoryCategory.SEMANTIC,
148
+ content="low",
149
+ confidence=0.5,
150
+ ),
151
+ LongTermMemory(
152
+ id=uuid4(),
153
+ agent_id=agent_id,
154
+ entity_id=entity_id,
155
+ category=LongTermMemoryCategory.SEMANTIC,
156
+ content="high",
157
+ confidence=0.95,
158
+ ),
159
+ LongTermMemory(
160
+ id=uuid4(),
161
+ agent_id=agent_id,
162
+ entity_id=entity_id,
163
+ category=LongTermMemoryCategory.SEMANTIC,
164
+ content="mid",
165
+ confidence=0.8,
166
+ ),
167
+ ]
168
+ result = _top_k_by_confidence(memories, 2)
169
+ assert len(result) == 2
170
+ assert result[0].content == "high"
171
+ assert result[1].content == "mid"
172
+
173
+ def test_zero_limit(self):
174
+ entity_id = uuid4()
175
+ agent_id = uuid4()
176
+ memories = [
177
+ LongTermMemory(
178
+ id=uuid4(),
179
+ agent_id=agent_id,
180
+ entity_id=entity_id,
181
+ category=LongTermMemoryCategory.SEMANTIC,
182
+ content="x",
183
+ confidence=0.9,
184
+ ),
185
+ ]
186
+ assert _top_k_by_confidence(memories, 0) == []
187
+
188
+ def test_empty_list(self):
189
+ assert _top_k_by_confidence([], 5) == []
190
+
191
+
192
+ # ============================================================================
193
+ # Config Management
194
+ # ============================================================================
195
+
196
+
197
+ class TestConfigManagement:
198
+ def test_defaults_are_sensible(self):
199
+ svc = MemoryService()
200
+ config = svc.get_config()
201
+ assert config.short_term_summarization_threshold > 0
202
+ assert config.long_term_extraction_threshold > 0
203
+ assert config.long_term_extraction_interval > 0
204
+ assert config.long_term_confidence_threshold > 0.0
205
+
206
+ def test_get_config_returns_copy(self):
207
+ svc = MemoryService()
208
+ c1 = svc.get_config()
209
+ c1.short_term_summarization_threshold = 99999
210
+ c2 = svc.get_config()
211
+ assert c2.short_term_summarization_threshold != 99999
212
+
213
+ def test_update_config_partial(self):
214
+ svc = MemoryService()
215
+ original = svc.get_config()
216
+ svc._config.short_term_summarization_threshold = 999
217
+ updated = svc.get_config()
218
+ assert updated.short_term_summarization_threshold == 999
219
+ # Other fields preserved
220
+ assert updated.long_term_extraction_threshold == original.long_term_extraction_threshold
221
+ assert updated.long_term_extraction_interval == original.long_term_extraction_interval
222
+
223
+
224
+ # ============================================================================
225
+ # Extraction Checkpointing
226
+ # ============================================================================
227
+
228
+
229
+ class TestExtractionCheckpointing:
230
+ @pytest.mark.asyncio
231
+ async def test_below_threshold_does_not_run(self):
232
+ svc = MemoryService()
233
+ entity = uuid4()
234
+ room = uuid4()
235
+ assert not await svc.should_run_extraction(entity, room, 1)
236
+
237
+ @pytest.mark.asyncio
238
+ async def test_at_threshold_runs_first_time(self):
239
+ svc = MemoryService()
240
+ config = svc.get_config()
241
+ entity = uuid4()
242
+ room = uuid4()
243
+ assert await svc.should_run_extraction(entity, room, config.long_term_extraction_threshold)
244
+
245
+ @pytest.mark.asyncio
246
+ async def test_checkpoint_prevents_rerun(self):
247
+ svc = MemoryService()
248
+ config = svc.get_config()
249
+ threshold = config.long_term_extraction_threshold
250
+ entity = uuid4()
251
+ room = uuid4()
252
+ await svc.set_last_extraction_checkpoint(entity, room, threshold)
253
+ assert not await svc.should_run_extraction(entity, room, threshold)
254
+
255
+ @pytest.mark.asyncio
256
+ async def test_next_interval_runs(self):
257
+ svc = MemoryService()
258
+ config = svc.get_config()
259
+ threshold = config.long_term_extraction_threshold
260
+ interval = config.long_term_extraction_interval
261
+ entity = uuid4()
262
+ room = uuid4()
263
+ await svc.set_last_extraction_checkpoint(entity, room, threshold)
264
+ assert await svc.should_run_extraction(entity, room, threshold + interval)
265
+
266
+ @pytest.mark.asyncio
267
+ async def test_independent_entity_room_pairs(self):
268
+ svc = MemoryService()
269
+ config = svc.get_config()
270
+ threshold = config.long_term_extraction_threshold
271
+ entity_a = uuid4()
272
+ entity_b = uuid4()
273
+ room = uuid4()
274
+ await svc.set_last_extraction_checkpoint(entity_a, room, threshold)
275
+ # entity_b should still be eligible
276
+ assert await svc.should_run_extraction(entity_b, room, threshold)
277
+ # entity_a should not
278
+ assert not await svc.should_run_extraction(entity_a, room, threshold)
279
+
280
+
281
+ # ============================================================================
282
+ # Formatted Memory Output
283
+ # ============================================================================
284
+
285
+
286
+ class TestFormattedLongTermMemories:
287
+ @pytest.mark.asyncio
288
+ async def test_groups_by_category(self):
289
+ svc = MemoryService()
290
+ entity_id = uuid4()
291
+ agent_id = uuid4()
292
+
293
+ # Manually inject memories into fallback storage
294
+ key = str(entity_id)
295
+ svc._long_term[key] = [
296
+ LongTermMemory(
297
+ id=uuid4(),
298
+ agent_id=agent_id,
299
+ entity_id=entity_id,
300
+ category=LongTermMemoryCategory.SEMANTIC,
301
+ content="Likes coffee",
302
+ confidence=0.9,
303
+ ),
304
+ LongTermMemory(
305
+ id=uuid4(),
306
+ agent_id=agent_id,
307
+ entity_id=entity_id,
308
+ category=LongTermMemoryCategory.EPISODIC,
309
+ content="Had meeting",
310
+ confidence=0.85,
311
+ ),
312
+ LongTermMemory(
313
+ id=uuid4(),
314
+ agent_id=agent_id,
315
+ entity_id=entity_id,
316
+ category=LongTermMemoryCategory.SEMANTIC,
317
+ content="Prefers dark mode",
318
+ confidence=0.88,
319
+ ),
320
+ ]
321
+
322
+ result = await svc.get_formatted_long_term_memories(entity_id)
323
+ assert "**Semantic**:" in result
324
+ assert "**Episodic**:" in result
325
+ assert "- Likes coffee" in result
326
+ assert "- Prefers dark mode" in result
327
+ assert "- Had meeting" in result
328
+
329
+ @pytest.mark.asyncio
330
+ async def test_empty_returns_empty_string(self):
331
+ svc = MemoryService()
332
+ entity_id = uuid4()
333
+ result = await svc.get_formatted_long_term_memories(entity_id)
334
+ assert result == ""
335
+
336
+ @pytest.mark.asyncio
337
+ async def test_single_category(self):
338
+ svc = MemoryService()
339
+ entity_id = uuid4()
340
+ agent_id = uuid4()
341
+
342
+ svc._long_term[str(entity_id)] = [
343
+ LongTermMemory(
344
+ id=uuid4(),
345
+ agent_id=agent_id,
346
+ entity_id=entity_id,
347
+ category=LongTermMemoryCategory.PROCEDURAL,
348
+ content="Knows how to ride a bike",
349
+ confidence=0.95,
350
+ ),
351
+ ]
352
+
353
+ result = await svc.get_formatted_long_term_memories(entity_id)
354
+ assert "**Procedural**:" in result
355
+ assert "- Knows how to ride a bike" in result
356
+ assert "**Semantic**:" not in result
357
+ assert "**Episodic**:" not in result
@@ -1,11 +1,11 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from dataclasses import dataclass, field
4
- from enum import Enum
4
+ from enum import StrEnum
5
5
  from uuid import UUID
6
6
 
7
7
 
8
- class LongTermMemoryCategory(str, Enum):
8
+ class LongTermMemoryCategory(StrEnum):
9
9
  EPISODIC = "episodic"
10
10
  SEMANTIC = "semantic"
11
11
  PROCEDURAL = "procedural"
@@ -0,0 +1,222 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from datetime import datetime
5
+ from typing import TYPE_CHECKING, Any, cast
6
+ from uuid import UUID as StdUUID
7
+
8
+ from elizaos.bootstrap.utils.xml import parse_key_value_xml
9
+ from elizaos.deterministic import get_prompt_reference_datetime
10
+ from elizaos.generated.spec_helpers import require_action_spec
11
+ from elizaos.prompts import SCHEDULE_FOLLOW_UP_TEMPLATE
12
+ from elizaos.types import (
13
+ Action,
14
+ ActionExample,
15
+ ActionResult,
16
+ Content,
17
+ ModelType,
18
+ )
19
+
20
+ if TYPE_CHECKING:
21
+ from elizaos.types import (
22
+ HandlerCallback,
23
+ HandlerOptions,
24
+ IAgentRuntime,
25
+ Memory,
26
+ State,
27
+ )
28
+
29
+ # Get text content from centralized specs
30
+ _spec = require_action_spec("SCHEDULE_FOLLOW_UP")
31
+
32
+
33
+ def _convert_spec_examples() -> list[list[ActionExample]]:
34
+ """Convert spec examples to ActionExample format."""
35
+ spec_examples = cast(list[list[dict[str, Any]]], _spec.get("examples", []))
36
+ if spec_examples:
37
+ return [
38
+ [
39
+ ActionExample(
40
+ name=msg.get("name", ""),
41
+ content=Content(
42
+ text=msg.get("content", {}).get("text", ""),
43
+ actions=msg.get("content", {}).get("actions"),
44
+ ),
45
+ )
46
+ for msg in example
47
+ ]
48
+ for example in spec_examples
49
+ ]
50
+ return []
51
+
52
+
53
+ def _normalize_priority(raw_priority: str) -> str:
54
+ normalized = raw_priority.strip().lower()
55
+ if normalized in {"high", "medium", "low"}:
56
+ return normalized
57
+ return "medium"
58
+
59
+
60
+ def _coerce_uuid(value: object | None) -> StdUUID | None:
61
+ if value is None:
62
+ return None
63
+ text = str(value).strip()
64
+ if not text:
65
+ return None
66
+ try:
67
+ return StdUUID(text)
68
+ except ValueError:
69
+ return None
70
+
71
+
72
+ @dataclass
73
+ class ScheduleFollowUpAction:
74
+ name: str = _spec["name"]
75
+ similes: list[str] = field(default_factory=lambda: list(_spec.get("similes", [])))
76
+ description: str = _spec["description"]
77
+
78
+ async def validate(
79
+ self, runtime: IAgentRuntime, _message: Memory, _state: State | None = None
80
+ ) -> bool:
81
+ rolodex_service = runtime.get_service("rolodex")
82
+ follow_up_service = runtime.get_service("follow_up")
83
+ return rolodex_service is not None and follow_up_service is not None
84
+
85
+ async def handler(
86
+ self,
87
+ runtime: IAgentRuntime,
88
+ message: Memory,
89
+ state: State | None = None,
90
+ options: HandlerOptions | None = None,
91
+ callback: HandlerCallback | None = None,
92
+ responses: list[Memory] | None = None,
93
+ ) -> ActionResult:
94
+ from elizaos.bootstrap.services.follow_up import FollowUpService
95
+ from elizaos.bootstrap.services.rolodex import RolodexService
96
+
97
+ rolodex_service = runtime.get_service("rolodex")
98
+ follow_up_service = runtime.get_service("follow_up")
99
+
100
+ if not rolodex_service or not isinstance(rolodex_service, RolodexService):
101
+ return ActionResult(
102
+ text="Rolodex service not available",
103
+ success=False,
104
+ values={"error": True},
105
+ data={"error": "RolodexService not available"},
106
+ )
107
+
108
+ if not follow_up_service or not isinstance(follow_up_service, FollowUpService):
109
+ return ActionResult(
110
+ text="Follow-up service not available",
111
+ success=False,
112
+ values={"error": True},
113
+ data={"error": "FollowUpService not available"},
114
+ )
115
+
116
+ state = await runtime.compose_state(message, ["RECENT_MESSAGES", "ENTITIES"])
117
+ state.values["currentDateTime"] = get_prompt_reference_datetime(
118
+ runtime,
119
+ message,
120
+ state,
121
+ "action:schedule_follow_up",
122
+ ).isoformat()
123
+
124
+ prompt = runtime.compose_prompt_from_state(
125
+ state=state,
126
+ template=SCHEDULE_FOLLOW_UP_TEMPLATE,
127
+ )
128
+
129
+ response = await runtime.use_model(ModelType.TEXT_SMALL, {"prompt": prompt})
130
+ parsed = parse_key_value_xml(response)
131
+
132
+ if not parsed or not parsed.get("contactName"):
133
+ return ActionResult(
134
+ text="Could not extract follow-up information",
135
+ success=False,
136
+ values={"error": True},
137
+ data={"error": "Failed to parse follow-up info"},
138
+ )
139
+
140
+ contact_name = str(parsed.get("contactName", ""))
141
+ scheduled_at_str = str(parsed.get("scheduledAt", ""))
142
+ reason = str(parsed.get("reason", "Follow-up"))
143
+ priority = _normalize_priority(str(parsed.get("priority", "medium")))
144
+ follow_up_message = str(parsed.get("message", ""))
145
+ parsed_entity_id = _coerce_uuid(parsed.get("entityId"))
146
+ message_entity_id = _coerce_uuid(message.entity_id)
147
+
148
+ try:
149
+ scheduled_at = datetime.fromisoformat(scheduled_at_str.replace("Z", "+00:00"))
150
+ except ValueError:
151
+ return ActionResult(
152
+ text="Could not parse the follow-up date/time",
153
+ success=False,
154
+ values={"error": True},
155
+ data={"error": "Invalid follow-up datetime"},
156
+ )
157
+
158
+ entity_id_uuid = parsed_entity_id or message_entity_id
159
+ if entity_id_uuid is None and contact_name:
160
+ contacts = await rolodex_service.search_contacts(search_term=contact_name)
161
+ if contacts:
162
+ entity_id_uuid = contacts[0].entity_id
163
+
164
+ if entity_id_uuid is None:
165
+ return ActionResult(
166
+ text=f"Could not determine which contact to schedule for ({contact_name}).",
167
+ success=False,
168
+ values={"error": True},
169
+ data={"error": "Missing contact entity id"},
170
+ )
171
+
172
+ contact = await rolodex_service.get_contact(entity_id_uuid)
173
+ if contact is None:
174
+ return ActionResult(
175
+ text=f"Contact '{contact_name}' was not found in the rolodex.",
176
+ success=False,
177
+ values={"error": True},
178
+ data={"error": "Contact not found"},
179
+ )
180
+
181
+ await follow_up_service.schedule_follow_up(
182
+ entity_id=entity_id_uuid,
183
+ scheduled_at=scheduled_at,
184
+ reason=reason,
185
+ priority=priority,
186
+ message=follow_up_message,
187
+ )
188
+
189
+ response_text = f"I've scheduled a follow-up with {contact_name} for {scheduled_at.strftime('%B %d, %Y')}. Reason: {reason}"
190
+
191
+ if callback:
192
+ await callback(Content(text=response_text, actions=["SCHEDULE_FOLLOW_UP"]))
193
+
194
+ return ActionResult(
195
+ text=response_text,
196
+ success=True,
197
+ values={
198
+ "contactId": str(entity_id_uuid),
199
+ "scheduledAt": scheduled_at.isoformat(),
200
+ },
201
+ data={
202
+ "contactId": str(entity_id_uuid),
203
+ "contactName": contact_name,
204
+ "scheduledAt": scheduled_at.isoformat(),
205
+ "reason": reason,
206
+ "priority": priority,
207
+ },
208
+ )
209
+
210
+ @property
211
+ def examples(self) -> list[list[ActionExample]]:
212
+ return _convert_spec_examples()
213
+
214
+
215
+ schedule_follow_up_action = Action(
216
+ name=ScheduleFollowUpAction.name,
217
+ similes=ScheduleFollowUpAction().similes,
218
+ description=ScheduleFollowUpAction.description,
219
+ validate=ScheduleFollowUpAction().validate,
220
+ handler=ScheduleFollowUpAction().handler,
221
+ examples=ScheduleFollowUpAction().examples,
222
+ )
@@ -10,7 +10,7 @@ from uuid import UUID, uuid4
10
10
  from google.protobuf.json_format import MessageToDict
11
11
 
12
12
  from elizaos.logger import Logger
13
- from elizaos.types.components import ActionContext, ActionResult, HandlerCallback, HandlerOptions
13
+ from elizaos.types.components import ActionContext, ActionResult, HandlerCallback
14
14
  from elizaos.types.memory import Memory
15
15
  from elizaos.types.primitives import Content
16
16
  from elizaos.types.service import Service
@@ -538,6 +538,8 @@ Focus on:
538
538
  ]
539
539
  results = await asyncio.gather(*tasks, return_exceptions=True)
540
540
  for r in results:
541
+ if isinstance(r, Exception):
542
+ raise RuntimeError(f"Plan step failed during parallel execution: {r}") from r
541
543
  if isinstance(r, ActionResult):
542
544
  execution.results.append(r)
543
545
 
@@ -586,6 +588,10 @@ Focus on:
586
588
 
587
589
  for step, r in zip(ready_batch, results, strict=False):
588
590
  completed_count += 1
591
+ if isinstance(r, Exception):
592
+ raise RuntimeError(
593
+ f"Plan step '{step.action_name}' failed during DAG execution: {r}"
594
+ ) from r
589
595
  if isinstance(r, ActionResult):
590
596
  execution.results.append(r)
591
597
 
@@ -622,13 +628,16 @@ Focus on:
622
628
  if execution.abort_event.is_set():
623
629
  raise RuntimeError("Plan execution aborted")
624
630
  try:
625
- options = HandlerOptions(
626
- action_context=action_context,
627
- parameters=step.parameters,
628
- )
629
- # Attach extra execution context (allowed by extra="allow")
630
- options.previous_results = previous_results # type: ignore[attr-defined]
631
- options.context = {"workingMemory": execution.working_memory} # type: ignore[attr-defined]
631
+ options = type(
632
+ "_PlanningHandlerOptions",
633
+ (),
634
+ {
635
+ "action_context": action_context,
636
+ "parameters": step.parameters,
637
+ "previous_results": previous_results,
638
+ "context": {"workingMemory": execution.working_memory},
639
+ },
640
+ )()
632
641
 
633
642
  validate_fn = getattr(action, "validate", None) or getattr(
634
643
  action, "validate_fn", None
@@ -641,12 +650,15 @@ Focus on:
641
650
  if result is None:
642
651
  return None
643
652
 
644
- if result.data is None:
645
- result.data = {}
646
- if isinstance(result.data, dict):
647
- result.data["stepId"] = str(step.id)
648
- result.data["actionName"] = step.action_name
649
- result.data["executedAt"] = int(time.time() * 1000)
653
+ step_metadata = {
654
+ "stepId": str(step.id),
655
+ "actionName": step.action_name,
656
+ "executedAt": int(time.time() * 1000),
657
+ }
658
+ if isinstance(result.data, dict) or hasattr(result.data, "update"):
659
+ result.data.update(step_metadata)
660
+ else:
661
+ result.data = step_metadata
650
662
  return result
651
663
  except Exception as e:
652
664
  retries += 1
@@ -18,7 +18,6 @@ from .providers import (
18
18
  actions_provider,
19
19
  attachments_provider,
20
20
  basic_providers,
21
- capabilities_provider,
22
21
  character_provider,
23
22
  choice_provider,
24
23
  context_bench_provider,
@@ -48,7 +47,6 @@ __all__ = [
48
47
  "action_state_provider",
49
48
  "actions_provider",
50
49
  "attachments_provider",
51
- "capabilities_provider",
52
50
  "character_provider",
53
51
  "choice_provider",
54
52
  "context_bench_provider",