@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,562 @@
1
+ """Autonomy service using the Task system (TypeScript parity).
2
+
3
+ This service registers an `AUTONOMY_THINK` task worker and creates a recurring
4
+ task that triggers autonomous thinking at a configurable interval.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import asyncio
10
+ import contextlib
11
+ import logging
12
+ import time
13
+ import uuid
14
+ from typing import TYPE_CHECKING, Any
15
+
16
+ from elizaos.bootstrap.services.task import Task
17
+ from elizaos.prompts import (
18
+ AUTONOMY_CONTINUOUS_CONTINUE_TEMPLATE,
19
+ AUTONOMY_CONTINUOUS_FIRST_TEMPLATE,
20
+ AUTONOMY_TASK_CONTINUE_TEMPLATE,
21
+ AUTONOMY_TASK_FIRST_TEMPLATE,
22
+ )
23
+ from elizaos.types.environment import Room, World
24
+ from elizaos.types.events import EventType
25
+ from elizaos.types.memory import Memory
26
+ from elizaos.types.primitives import UUID, Content, as_uuid
27
+ from elizaos.types.service import Service
28
+
29
+ from .types import AutonomyStatus
30
+
31
+ if TYPE_CHECKING:
32
+ from elizaos.types.runtime import IAgentRuntime
33
+
34
+ _logger = logging.getLogger(__name__)
35
+
36
+ # Service type constant for autonomy (parity with TypeScript)
37
+ AUTONOMY_SERVICE_TYPE = "AUTONOMY"
38
+
39
+ # Task name for autonomy thinking (parity with TypeScript)
40
+ AUTONOMY_TASK_NAME = "AUTONOMY_THINK"
41
+
42
+ # Tags used for autonomy tasks (parity with TypeScript).
43
+ # Note: TypeScript uses ["repeat", "autonomy", "internal"] without "queue".
44
+ AUTONOMY_TASK_TAGS = ["repeat", "autonomy", "internal"]
45
+
46
+ # Default interval in milliseconds
47
+ DEFAULT_INTERVAL_MS = 30_000
48
+
49
+
50
+ class AutonomyTaskWorker:
51
+ """Task worker for autonomous thinking."""
52
+
53
+ def __init__(self, service: AutonomyService) -> None:
54
+ self._service = service
55
+
56
+ @property
57
+ def name(self) -> str:
58
+ return AUTONOMY_TASK_NAME
59
+
60
+ async def execute(
61
+ self,
62
+ runtime: IAgentRuntime,
63
+ options: dict[str, Any],
64
+ task: Task,
65
+ ) -> None:
66
+ """Execute the autonomy task."""
67
+ start_time = time.time()
68
+
69
+ self._service._log(
70
+ "debug",
71
+ f"Executing autonomy task (task_id={task.id})",
72
+ )
73
+
74
+ try:
75
+ await self._service.perform_autonomous_think()
76
+ duration_ms = int((time.time() - start_time) * 1000)
77
+ self._service._log(
78
+ "debug",
79
+ f"Autonomy task completed successfully (duration={duration_ms}ms)",
80
+ )
81
+ except Exception as e:
82
+ duration_ms = int((time.time() - start_time) * 1000)
83
+ self._service._log(
84
+ "error",
85
+ f"Autonomy task failed: {e} (duration={duration_ms}ms)",
86
+ )
87
+ raise
88
+
89
+ async def validate(
90
+ self,
91
+ runtime: IAgentRuntime,
92
+ message: Memory,
93
+ state: Any,
94
+ ) -> bool:
95
+ """Validate the task (always returns True)."""
96
+ return True
97
+
98
+
99
+ class AutonomyService(Service):
100
+ """Autonomy service using the Task system.
101
+
102
+ Provides parity with TypeScript's AutonomyService.
103
+ """
104
+
105
+ service_type = AUTONOMY_SERVICE_TYPE
106
+
107
+ def __init__(self) -> None:
108
+ self._runtime: IAgentRuntime | None = None
109
+ self._is_running = False
110
+ self._is_thinking = False
111
+ self._is_stopped = False
112
+ self._interval_ms = DEFAULT_INTERVAL_MS
113
+ self._task_registered = False
114
+ self._settings_monitor_task: asyncio.Task[None] | None = None
115
+ self._autonomous_room_id = as_uuid(str(uuid.uuid4()))
116
+ self._autonomous_world_id = as_uuid("00000000-0000-0000-0000-000000000001")
117
+
118
+ def _log(self, level: str, msg: str) -> None:
119
+ if self._runtime:
120
+ agent_id = str(self._runtime.agent_id)
121
+ full_msg = f"[autonomy] {msg} (agent={agent_id})"
122
+ getattr(self._runtime.logger, level)(full_msg)
123
+ else:
124
+ getattr(_logger, level)(f"[autonomy] {msg}")
125
+
126
+ @classmethod
127
+ async def start(cls, runtime: IAgentRuntime) -> AutonomyService:
128
+ """Start the autonomy service."""
129
+ service = cls()
130
+ service._runtime = runtime
131
+ await service._initialize()
132
+ return service
133
+
134
+ async def _initialize(self) -> None:
135
+ if not self._runtime:
136
+ return
137
+
138
+ self._log("info", f"Using autonomous room ID: {self._autonomous_room_id}")
139
+
140
+ # Ensure autonomous context exists
141
+ await self._ensure_autonomous_context()
142
+
143
+ # Register the task worker
144
+ await self._register_autonomy_task_worker()
145
+
146
+ autonomy_enabled = self._is_autonomy_enabled()
147
+
148
+ self._log(
149
+ "debug",
150
+ f"Autonomy enabled (setting or runtime): {autonomy_enabled}",
151
+ )
152
+
153
+ # Check if autonomy should auto-start based on runtime configuration
154
+ if autonomy_enabled:
155
+ self._log(
156
+ "info",
157
+ "Autonomy enabled (enable_autonomy: True), creating autonomy task...",
158
+ )
159
+ await self._create_autonomy_task()
160
+ else:
161
+ self._log(
162
+ "info",
163
+ "Autonomy not enabled (enable_autonomy: False or not set). "
164
+ "Set enable_autonomy=True in runtime options to auto-start.",
165
+ )
166
+
167
+ # Start settings monitoring
168
+ self._settings_monitor_task = asyncio.create_task(self._settings_monitoring())
169
+
170
+ async def _register_autonomy_task_worker(self) -> None:
171
+ """Register the task worker for autonomous thinking."""
172
+ if self._task_registered or not self._runtime:
173
+ return
174
+
175
+ worker = AutonomyTaskWorker(self)
176
+ self._runtime.register_task_worker(worker)
177
+ self._task_registered = True
178
+
179
+ self._log("debug", "Registered autonomy task worker")
180
+
181
+ async def _create_autonomy_task(self) -> None:
182
+ """Create the recurring autonomy task."""
183
+ if not self._runtime:
184
+ return
185
+
186
+ # Remove any existing autonomy tasks
187
+ await self._remove_autonomy_task()
188
+
189
+ # Create the recurring task
190
+ task = Task.repeating(AUTONOMY_TASK_NAME, self._interval_ms)
191
+ task.description = f"Autonomous thinking for agent {self._runtime.agent_id}"
192
+ task.world_id = self._autonomous_world_id
193
+ task.room_id = self._autonomous_room_id
194
+ task.tags = AUTONOMY_TASK_TAGS.copy()
195
+
196
+ # Ensure metadata has blocking = true
197
+ if task.metadata:
198
+ task.metadata.blocking = True
199
+
200
+ await self._runtime.create_task(task)
201
+
202
+ self._is_running = True
203
+ self._runtime.enable_autonomy = True
204
+
205
+ self._log(
206
+ "info",
207
+ f"Created autonomy task (interval={self._interval_ms}ms)",
208
+ )
209
+
210
+ async def _remove_autonomy_task(self) -> None:
211
+ """Remove existing autonomy tasks.
212
+
213
+ Uses full AUTONOMY_TASK_TAGS for filtering (parity with TypeScript).
214
+ """
215
+ if not self._runtime:
216
+ return
217
+
218
+ try:
219
+ existing_tasks = await self._runtime.get_tasks(
220
+ {
221
+ "tags": list(AUTONOMY_TASK_TAGS),
222
+ }
223
+ )
224
+
225
+ for task in existing_tasks:
226
+ task_name = (
227
+ getattr(task, "name", None) or task.get("name")
228
+ if isinstance(task, dict)
229
+ else None
230
+ )
231
+ task_id = (
232
+ getattr(task, "id", None) or task.get("id") if isinstance(task, dict) else None
233
+ )
234
+
235
+ if task_name == AUTONOMY_TASK_NAME and task_id:
236
+ await self._runtime.delete_task(task_id)
237
+ self._log("debug", f"Removed existing autonomy task (id={task_id})")
238
+ except Exception as e:
239
+ self._log("debug", f"Error removing autonomy tasks: {e}")
240
+
241
+ async def _ensure_autonomous_context(self) -> None:
242
+ if not self._runtime:
243
+ return
244
+
245
+ try:
246
+ world = World(
247
+ id=self._autonomous_world_id,
248
+ name="Autonomy World",
249
+ agent_id=self._runtime.agent_id,
250
+ message_server_id=as_uuid("00000000-0000-0000-0000-000000000000"),
251
+ )
252
+ await self._runtime.ensure_world_exists(world)
253
+
254
+ room = Room(
255
+ id=self._autonomous_room_id,
256
+ name="Autonomous Thoughts",
257
+ world_id=self._autonomous_world_id,
258
+ source="autonomy-service",
259
+ type="SELF",
260
+ )
261
+ await self._runtime.ensure_room_exists(room)
262
+
263
+ await self._runtime.add_participant(self._runtime.agent_id, self._autonomous_room_id)
264
+
265
+ self._log(
266
+ "debug",
267
+ f"Ensured autonomous room exists with world ID: {self._autonomous_world_id}",
268
+ )
269
+ except Exception as e:
270
+ self._log("error", f"Failed to ensure autonomous context: {e}")
271
+ raise
272
+
273
+ def _get_autonomy_mode(self) -> str:
274
+ if not self._runtime:
275
+ return "continuous"
276
+ raw = self._runtime.get_setting("AUTONOMY_MODE")
277
+ if isinstance(raw, str) and raw.strip().lower() == "task":
278
+ return "task"
279
+ return "continuous"
280
+
281
+ def _get_target_room_id(self) -> UUID | None:
282
+ if not self._runtime:
283
+ return None
284
+ raw = self._runtime.get_setting("AUTONOMY_TARGET_ROOM_ID")
285
+ if not isinstance(raw, str) or raw.strip() == "":
286
+ return None
287
+ try:
288
+ return as_uuid(raw.strip())
289
+ except Exception:
290
+ return None
291
+
292
+ async def _get_target_room_context_text(self) -> str:
293
+ if not self._runtime:
294
+ return "(no target room configured)"
295
+ target_room_id = self._get_target_room_id()
296
+ if not target_room_id:
297
+ return "(no target room configured)"
298
+ memories_table = await self._runtime.get_memories(
299
+ {"roomId": target_room_id, "count": 15, "tableName": "memories"}
300
+ )
301
+ messages_table = await self._runtime.get_memories(
302
+ {"roomId": target_room_id, "count": 15, "tableName": "messages"}
303
+ )
304
+ by_id: dict[str, Memory] = {}
305
+ for m in [*memories_table, *messages_table]:
306
+ mem_id = m.id or ""
307
+ if not mem_id:
308
+ continue
309
+ created_at = m.created_at or 0
310
+ existing = by_id.get(mem_id)
311
+ if existing is None or created_at < (existing.created_at or 0):
312
+ by_id[mem_id] = m
313
+ ordered = sorted(by_id.values(), key=lambda m: m.created_at or 0)
314
+ lines: list[str] = []
315
+ for m in ordered:
316
+ role = "Agent" if m.entity_id == self._runtime.agent_id else "User"
317
+ text = m.content.text if m.content and isinstance(m.content.text, str) else ""
318
+ if text.strip():
319
+ lines.append(f"{role}: {text}")
320
+ return "\n".join(lines) if lines else "(no recent messages)"
321
+
322
+ async def _settings_monitoring(self) -> None:
323
+ while not self._is_stopped:
324
+ await asyncio.sleep(10)
325
+
326
+ if not self._runtime or self._is_stopped:
327
+ break
328
+
329
+ try:
330
+ should_be_running = self._is_autonomy_enabled()
331
+
332
+ if should_be_running and not self._is_running:
333
+ self._log(
334
+ "info", "Runtime indicates autonomy should be enabled, creating task..."
335
+ )
336
+ await self._create_autonomy_task()
337
+ elif not should_be_running and self._is_running:
338
+ self._log(
339
+ "info", "Runtime indicates autonomy should be disabled, removing task..."
340
+ )
341
+ await self._remove_autonomy_task()
342
+ self._is_running = False
343
+ except Exception as e:
344
+ self._log("error", f"Error in settings monitoring: {e}")
345
+
346
+ async def perform_autonomous_think(self) -> None:
347
+ """Perform one iteration of autonomous thinking.
348
+
349
+ This is called by the task worker when the task executes.
350
+ """
351
+ if not self._runtime:
352
+ return
353
+
354
+ # Guard against overlapping think cycles
355
+ if self._is_thinking:
356
+ self._log(
357
+ "debug",
358
+ "Previous autonomous think still in progress, skipping this iteration",
359
+ )
360
+ return
361
+
362
+ self._is_thinking = True
363
+ try:
364
+ await self._do_think()
365
+ except Exception as e:
366
+ self._log("error", f"Error in autonomous think: {e}")
367
+ finally:
368
+ self._is_thinking = False
369
+
370
+ async def _do_think(self) -> None:
371
+ """Execute the actual thinking logic."""
372
+ if not self._runtime:
373
+ return
374
+
375
+ self._log("debug", "Performing autonomous thinking...")
376
+
377
+ agent_entity = await self._runtime.get_entity_by_id(self._runtime.agent_id)
378
+ if not agent_entity:
379
+ self._log("error", "Failed to get agent entity, skipping autonomous thought")
380
+ return
381
+
382
+ last_thought: str | None = None
383
+ is_first_thought = False
384
+
385
+ recent_memories = await self._runtime.get_memories(
386
+ {
387
+ "roomId": self._autonomous_room_id,
388
+ "count": 3,
389
+ "tableName": "memories",
390
+ }
391
+ )
392
+
393
+ last_agent_thought = None
394
+ last_created_at = None
395
+ for m in recent_memories:
396
+ if m.entity_id == agent_entity.id and m.content and m.content.text:
397
+ created_at = m.created_at or 0
398
+ if last_created_at is None or created_at > last_created_at:
399
+ last_created_at = created_at
400
+ last_agent_thought = m
401
+
402
+ if last_agent_thought and last_agent_thought.content and last_agent_thought.content.text:
403
+ last_thought = last_agent_thought.content.text
404
+ else:
405
+ is_first_thought = True
406
+
407
+ mode = self._get_autonomy_mode()
408
+ target_context = await self._get_target_room_context_text()
409
+ autonomy_prompt = (
410
+ self._create_task_prompt(last_thought, is_first_thought, target_context)
411
+ if mode == "task"
412
+ else self._create_continuous_prompt(last_thought, is_first_thought, target_context)
413
+ )
414
+
415
+ entity_id = agent_entity.id if agent_entity.id else self._runtime.agent_id
416
+ current_time_ms = int(time.time() * 1000)
417
+ autonomous_message = Memory(
418
+ id=as_uuid(str(uuid.uuid4())),
419
+ entity_id=as_uuid(str(entity_id)),
420
+ content=Content(
421
+ text=autonomy_prompt,
422
+ source="autonomy-service",
423
+ ),
424
+ room_id=self._autonomous_room_id,
425
+ agent_id=self._runtime.agent_id,
426
+ created_at=current_time_ms,
427
+ )
428
+
429
+ self._log(
430
+ "debug",
431
+ "Processing through Eliza agent pipeline (providers, actions, evaluators)...",
432
+ )
433
+
434
+ async def callback(content: Content) -> None:
435
+ if self._runtime:
436
+ self._log("debug", f"Response generated: {(content.text or '')[:100]}...")
437
+
438
+ await self._runtime.emit_event(
439
+ EventType.EVENT_TYPE_MESSAGE_RECEIVED,
440
+ {
441
+ "runtime": self._runtime,
442
+ "message": autonomous_message,
443
+ "callback": callback,
444
+ "source": "autonomy-service",
445
+ },
446
+ )
447
+
448
+ self._log("debug", "Autonomous message event emitted to agent pipeline")
449
+
450
+ def _create_continuous_prompt(
451
+ self, last_thought: str | None, is_first_thought: bool, target_context: str
452
+ ) -> str:
453
+ template = (
454
+ AUTONOMY_CONTINUOUS_FIRST_TEMPLATE
455
+ if is_first_thought
456
+ else AUTONOMY_CONTINUOUS_CONTINUE_TEMPLATE
457
+ )
458
+ return self._fill_autonomy_template(template, target_context, last_thought)
459
+
460
+ def _create_task_prompt(
461
+ self, last_thought: str | None, is_first_thought: bool, target_context: str
462
+ ) -> str:
463
+ template = (
464
+ AUTONOMY_TASK_FIRST_TEMPLATE if is_first_thought else AUTONOMY_TASK_CONTINUE_TEMPLATE
465
+ )
466
+ return self._fill_autonomy_template(template, target_context, last_thought)
467
+
468
+ def _fill_autonomy_template(
469
+ self, template: str, target_context: str, last_thought: str | None
470
+ ) -> str:
471
+ output = template.replace("{{targetRoomContext}}", target_context)
472
+ output = output.replace("{{lastThought}}", last_thought or "")
473
+ return output
474
+
475
+ def is_thinking_in_progress(self) -> bool:
476
+ return self._is_thinking
477
+
478
+ def is_loop_running(self) -> bool:
479
+ return self._is_running
480
+
481
+ def get_loop_interval(self) -> int:
482
+ return self._interval_ms
483
+
484
+ async def set_loop_interval(self, ms: int) -> None:
485
+ """Set loop interval (recreates the task with new interval if running).
486
+
487
+ Parity with TypeScript's setLoopInterval.
488
+ """
489
+ MIN_INTERVAL = 5000
490
+ MAX_INTERVAL = 600000
491
+
492
+ if ms < MIN_INTERVAL:
493
+ self._log("warning", f"Interval too short, minimum is {MIN_INTERVAL}ms")
494
+ ms = MIN_INTERVAL
495
+ if ms > MAX_INTERVAL:
496
+ self._log("warning", f"Interval too long, maximum is {MAX_INTERVAL}ms")
497
+ ms = MAX_INTERVAL
498
+
499
+ self._interval_ms = ms
500
+ self._log("info", f"Loop interval set to {ms}ms")
501
+
502
+ # Recreate the task if running (parity with TypeScript)
503
+ if self._is_running:
504
+ await self._create_autonomy_task()
505
+
506
+ def get_autonomous_room_id(self) -> UUID:
507
+ return self._autonomous_room_id
508
+
509
+ async def enable_autonomy(self) -> None:
510
+ """Enable autonomy by creating the task."""
511
+ if self._runtime:
512
+ self._runtime.enable_autonomy = True
513
+ await self._create_autonomy_task()
514
+
515
+ async def disable_autonomy(self) -> None:
516
+ """Disable autonomy by removing the task."""
517
+ if self._runtime:
518
+ self._runtime.enable_autonomy = False
519
+ await self._remove_autonomy_task()
520
+ self._is_running = False
521
+
522
+ def get_status(self) -> AutonomyStatus:
523
+ enabled = self._is_autonomy_enabled()
524
+
525
+ return AutonomyStatus(
526
+ enabled=enabled,
527
+ running=self._is_running,
528
+ thinking=self._is_thinking,
529
+ interval=self._interval_ms,
530
+ autonomous_room_id=str(self._autonomous_room_id),
531
+ )
532
+
533
+ async def stop(self) -> None:
534
+ """Stop the autonomy service."""
535
+ self._is_stopped = True
536
+
537
+ # Remove the autonomy task
538
+ await self._remove_autonomy_task()
539
+ self._is_running = False
540
+
541
+ if self._settings_monitor_task:
542
+ self._settings_monitor_task.cancel()
543
+ with contextlib.suppress(asyncio.CancelledError):
544
+ await self._settings_monitor_task
545
+ self._settings_monitor_task = None
546
+
547
+ self._log("info", "Autonomy service stopped")
548
+
549
+ @property
550
+ def capability_description(self) -> str:
551
+ return (
552
+ "Autonomous operation using the Task system for continuous agent thinking and actions"
553
+ )
554
+
555
+ def _is_autonomy_enabled(self) -> bool:
556
+ if not self._runtime:
557
+ return False
558
+ setting_value = self._runtime.get_setting("AUTONOMY_ENABLED")
559
+ setting_enabled = setting_value is True or (
560
+ isinstance(setting_value, str) and setting_value.strip().lower() == "true"
561
+ )
562
+ return setting_enabled or self._runtime.enable_autonomy is True
@@ -0,0 +1,18 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+
5
+
6
+ @dataclass
7
+ class AutonomyStatus:
8
+ enabled: bool
9
+ running: bool
10
+ thinking: bool
11
+ interval: int
12
+ autonomous_room_id: str
13
+
14
+
15
+ @dataclass
16
+ class AutonomyConfig:
17
+ interval_ms: int = 30000
18
+ auto_start: bool = False
@@ -0,0 +1,19 @@
1
+ from .reflection import reflection_evaluator
2
+ from .relationship_extraction import relationship_extraction_evaluator
3
+
4
+ __all__ = [
5
+ "reflection_evaluator",
6
+ "relationship_extraction_evaluator",
7
+ "BASIC_EVALUATORS",
8
+ "EXTENDED_EVALUATORS",
9
+ "ALL_EVALUATORS",
10
+ ]
11
+
12
+ BASIC_EVALUATORS: list = []
13
+
14
+ EXTENDED_EVALUATORS = [
15
+ reflection_evaluator,
16
+ relationship_extraction_evaluator,
17
+ ]
18
+
19
+ ALL_EVALUATORS = BASIC_EVALUATORS + EXTENDED_EVALUATORS