@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,585 @@
1
+ """Task service implementation with scheduling capabilities.
2
+
3
+ This module provides parity with the TypeScript TaskService, including:
4
+ - Timer-based task checking (tick loop)
5
+ - Task workers with execute/validate callbacks
6
+ - Tag-based filtering ("queue", "repeat")
7
+ - Blocking mechanism to prevent overlapping executions
8
+ - Automatic deletion of non-repeating tasks after execution
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import asyncio
14
+ import contextlib
15
+ import time
16
+ from dataclasses import dataclass
17
+ from enum import Enum
18
+ from typing import TYPE_CHECKING, Any, Protocol, runtime_checkable
19
+ from uuid import UUID, uuid4
20
+
21
+ from elizaos.types import Service, ServiceType
22
+
23
+ if TYPE_CHECKING:
24
+ from elizaos.types import IAgentRuntime, Memory, State
25
+
26
+ # Interval in milliseconds to check for tasks (parity with TypeScript TICK_INTERVAL)
27
+ TICK_INTERVAL_MS = 1000
28
+
29
+
30
+ class TaskStatus(str, Enum):
31
+ """Task status enum."""
32
+
33
+ PENDING = "pending"
34
+ IN_PROGRESS = "in_progress"
35
+ COMPLETED = "completed"
36
+ FAILED = "failed"
37
+ CANCELLED = "cancelled"
38
+
39
+
40
+ class TaskPriority(str, Enum):
41
+ """Task priority enum."""
42
+
43
+ LOW = "low"
44
+ MEDIUM = "medium"
45
+ HIGH = "high"
46
+ URGENT = "urgent"
47
+
48
+
49
+ @dataclass
50
+ class TaskMetadata:
51
+ """Task metadata containing scheduling and configuration information.
52
+
53
+ Provides parity with TypeScript's TaskMetadata interface.
54
+ """
55
+
56
+ target_entity_id: str | None = None
57
+ reason: str | None = None
58
+ priority: str | None = None
59
+ message: str | None = None
60
+ status: str | None = None
61
+ scheduled_at: str | None = None
62
+ snoozed_at: str | None = None
63
+ original_scheduled_at: Any | None = None
64
+ created_at: str | None = None
65
+ completed_at: str | None = None
66
+ completion_notes: str | None = None
67
+ last_executed: str | None = None
68
+ updated_at: int | None = None
69
+ update_interval: int | None = None
70
+ """Interval in milliseconds between updates or executions for recurring tasks."""
71
+ blocking: bool | None = None
72
+ """If true (default), the task will block the next scheduled execution while running.
73
+ Set to false to allow overlapping executions."""
74
+ options: list[dict[str, str]] | None = None
75
+ values: dict[str, Any] | None = None
76
+
77
+
78
+ @dataclass
79
+ class Task:
80
+ """Represents a task to be performed, often in the background or at a later time.
81
+
82
+ Tasks are managed by the TaskService and processed by registered TaskWorkers.
83
+ """
84
+
85
+ name: str
86
+ id: UUID | None = None
87
+ description: str | None = None
88
+ status: TaskStatus | None = TaskStatus.PENDING
89
+ room_id: UUID | None = None
90
+ world_id: UUID | None = None
91
+ entity_id: UUID | None = None
92
+ tags: list[str] | None = None
93
+ metadata: TaskMetadata | None = None
94
+ created_at: int | None = None
95
+ updated_at: int | None = None
96
+ scheduled_at: int | None = None
97
+ repeat_interval: int | None = None
98
+ data: Any | None = None
99
+
100
+ @classmethod
101
+ def create(cls, name: str) -> Task:
102
+ """Create a new task with defaults."""
103
+ now = _current_timestamp()
104
+ return cls(
105
+ id=uuid4(),
106
+ name=name,
107
+ status=TaskStatus.PENDING,
108
+ created_at=now,
109
+ updated_at=now,
110
+ metadata=TaskMetadata(
111
+ updated_at=now,
112
+ created_at=str(now),
113
+ ),
114
+ )
115
+
116
+ @classmethod
117
+ def scheduled(cls, name: str, scheduled_at: int) -> Task:
118
+ """Create a scheduled task."""
119
+ task = cls.create(name)
120
+ task.scheduled_at = scheduled_at
121
+ return task
122
+
123
+ @classmethod
124
+ def repeating(cls, name: str, interval_ms: int) -> Task:
125
+ """Create a repeating task with the given interval."""
126
+ task = cls.create(name)
127
+ task.tags = ["queue", "repeat"]
128
+ if task.metadata:
129
+ task.metadata.update_interval = interval_ms
130
+ task.metadata.blocking = True # Default to blocking
131
+ else:
132
+ task.metadata = TaskMetadata(
133
+ update_interval=interval_ms,
134
+ blocking=True,
135
+ )
136
+ return task
137
+
138
+ @classmethod
139
+ def repeating_with_blocking(cls, name: str, interval_ms: int, blocking: bool) -> Task:
140
+ """Create a repeating task with blocking configuration."""
141
+ task = cls.repeating(name, interval_ms)
142
+ if task.metadata:
143
+ task.metadata.blocking = blocking
144
+ return task
145
+
146
+ def is_repeating(self) -> bool:
147
+ """Check if this task is a repeating task."""
148
+ return self.tags is not None and "repeat" in self.tags
149
+
150
+ def is_blocking(self) -> bool:
151
+ """Check if this task should block overlapping executions."""
152
+ if self.metadata and self.metadata.blocking is not None:
153
+ return self.metadata.blocking
154
+ return True # Default to blocking
155
+
156
+ def get_update_interval(self) -> int | None:
157
+ """Get the update interval in milliseconds."""
158
+ if self.metadata:
159
+ return self.metadata.update_interval
160
+ return None
161
+
162
+
163
+ @runtime_checkable
164
+ class TaskWorker(Protocol):
165
+ """Task worker protocol - defines the contract for executing tasks.
166
+
167
+ Parity with TypeScript's TaskWorker interface.
168
+ """
169
+
170
+ @property
171
+ def name(self) -> str:
172
+ """The unique name of the task type this worker handles."""
173
+ ...
174
+
175
+ async def execute(
176
+ self,
177
+ runtime: IAgentRuntime,
178
+ options: dict[str, Any],
179
+ task: Task,
180
+ ) -> None:
181
+ """Execute the task."""
182
+ ...
183
+
184
+ async def validate(
185
+ self,
186
+ runtime: IAgentRuntime,
187
+ message: Memory,
188
+ state: State,
189
+ ) -> bool:
190
+ """Optional validation function."""
191
+ ...
192
+
193
+
194
+ class TaskService(Service):
195
+ """Service for managing and scheduling tasks.
196
+
197
+ Provides parity with TypeScript's TaskService.
198
+ """
199
+
200
+ name = "task"
201
+ service_type = ServiceType.TASK
202
+
203
+ @property
204
+ def capability_description(self) -> str:
205
+ """Capability description for the service."""
206
+ return "Task management service with scheduling and worker execution."
207
+
208
+ def __init__(self) -> None:
209
+ """Initialize the task service."""
210
+ self._workers: dict[str, TaskWorker] = {}
211
+ self._tasks: dict[str, Task] = {}
212
+ self._executing_tasks: set[str] = set()
213
+ self._runtime: IAgentRuntime | None = None
214
+ self._stop_flag = False
215
+ self._loop_task: asyncio.Task[None] | None = None
216
+
217
+ @classmethod
218
+ async def start(cls, runtime: IAgentRuntime) -> TaskService:
219
+ """Start the task service."""
220
+ service = cls()
221
+ service._runtime = runtime
222
+ service._stop_flag = False
223
+ runtime.logger.info(
224
+ "Task service started",
225
+ src="service:task",
226
+ agentId=str(runtime.agent_id),
227
+ )
228
+
229
+ # Start the timer loop
230
+ service._loop_task = asyncio.create_task(service._run_timer())
231
+
232
+ return service
233
+
234
+ async def stop(self) -> None:
235
+ """Stop the task service."""
236
+ self._stop_flag = True
237
+
238
+ if self._loop_task:
239
+ self._loop_task.cancel()
240
+ with contextlib.suppress(asyncio.CancelledError):
241
+ await self._loop_task
242
+ self._loop_task = None
243
+
244
+ if self._runtime:
245
+ self._runtime.logger.info(
246
+ "Task service stopped",
247
+ src="service:task",
248
+ agentId=str(self._runtime.agent_id),
249
+ )
250
+
251
+ self._tasks.clear()
252
+ self._executing_tasks.clear()
253
+ self._runtime = None
254
+
255
+ async def register_worker(self, worker: TaskWorker) -> None:
256
+ """Register a task worker."""
257
+ self._workers[worker.name] = worker
258
+ if self._runtime:
259
+ self._runtime.logger.debug(
260
+ f"Registered task worker: {worker.name}",
261
+ src="service:task",
262
+ )
263
+
264
+ def has_worker(self, name: str) -> bool:
265
+ """Check if a worker exists for the given task name."""
266
+ return name in self._workers
267
+
268
+ async def create_task(self, task: Task) -> Task:
269
+ """Create a new task."""
270
+ now = _current_timestamp()
271
+
272
+ # Ensure task has an ID
273
+ if task.id is None:
274
+ task.id = uuid4()
275
+
276
+ # Set timestamps
277
+ task.created_at = now
278
+ task.updated_at = now
279
+
280
+ # Ensure metadata exists with timestamps
281
+ if task.metadata is None:
282
+ task.metadata = TaskMetadata()
283
+ if task.metadata.updated_at is None:
284
+ task.metadata.updated_at = now
285
+ if task.metadata.created_at is None:
286
+ task.metadata.created_at = str(now)
287
+
288
+ task_id = str(task.id)
289
+
290
+ if self._runtime:
291
+ self._runtime.logger.debug(
292
+ f"Task created: {task_id}",
293
+ src="service:task",
294
+ taskId=task_id,
295
+ taskName=task.name,
296
+ )
297
+
298
+ self._tasks[task_id] = task
299
+ return task
300
+
301
+ async def get_task(self, task_id: str) -> Task | None:
302
+ """Get a task by ID."""
303
+ return self._tasks.get(task_id)
304
+
305
+ async def get_tasks_by_name(self, name: str) -> list[Task]:
306
+ """Get tasks by name."""
307
+ return [t for t in self._tasks.values() if t.name == name]
308
+
309
+ async def get_tasks_by_tags(self, tags: list[str]) -> list[Task]:
310
+ """Get tasks with specific tags."""
311
+ return [t for t in self._tasks.values() if t.tags and all(tag in t.tags for tag in tags)]
312
+
313
+ async def update_task(
314
+ self, task_id: str, metadata: TaskMetadata | None = None, tags: list[str] | None = None
315
+ ) -> Task | None:
316
+ """Update a task."""
317
+ task = self._tasks.get(task_id)
318
+ if task is None:
319
+ return None
320
+
321
+ task.updated_at = _current_timestamp()
322
+
323
+ if metadata:
324
+ if task.metadata is None:
325
+ task.metadata = metadata
326
+ else:
327
+ if metadata.updated_at is not None:
328
+ task.metadata.updated_at = metadata.updated_at
329
+ if metadata.update_interval is not None:
330
+ task.metadata.update_interval = metadata.update_interval
331
+ if metadata.blocking is not None:
332
+ task.metadata.blocking = metadata.blocking
333
+ if metadata.values is not None:
334
+ if task.metadata.values is None:
335
+ task.metadata.values = metadata.values
336
+ else:
337
+ task.metadata.values.update(metadata.values)
338
+
339
+ if tags is not None:
340
+ task.tags = tags
341
+
342
+ if self._runtime:
343
+ self._runtime.logger.debug(
344
+ f"Task updated: {task_id}",
345
+ src="service:task",
346
+ taskId=task_id,
347
+ )
348
+
349
+ return task
350
+
351
+ async def delete_task(self, task_id: str) -> bool:
352
+ """Delete a task."""
353
+ if task_id in self._tasks:
354
+ del self._tasks[task_id]
355
+ self._executing_tasks.discard(task_id)
356
+
357
+ if self._runtime:
358
+ self._runtime.logger.debug(
359
+ f"Task deleted: {task_id}",
360
+ src="service:task",
361
+ taskId=task_id,
362
+ )
363
+ return True
364
+ return False
365
+
366
+ async def _validate_tasks(self, tasks: list[Task]) -> list[Task]:
367
+ """Validate an array of Task objects.
368
+
369
+ Skips tasks without IDs or if no worker is found for the task.
370
+ If a worker has a `validate` function, it will run validation.
371
+ Parity with TypeScript's validateTasks.
372
+
373
+ Args:
374
+ tasks: An array of Task objects to validate.
375
+
376
+ Returns:
377
+ An array of validated Task objects.
378
+ """
379
+ if not self._runtime:
380
+ return []
381
+
382
+ validated_tasks: list[Task] = []
383
+
384
+ for task in tasks:
385
+ # Skip tasks without IDs
386
+ if task.id is None:
387
+ continue
388
+
389
+ worker = self._workers.get(task.name)
390
+
391
+ # Skip if no worker found for task
392
+ if worker is None:
393
+ continue
394
+
395
+ # If worker has validate function, run validation
396
+ # Pass empty dict/object for message and state since validation is time-based
397
+ if hasattr(worker, "validate"):
398
+ is_valid = await worker.validate(
399
+ self._runtime,
400
+ {}, # Empty message (dict representation)
401
+ {}, # Empty state (dict representation)
402
+ )
403
+ if not is_valid:
404
+ continue
405
+
406
+ validated_tasks.append(task)
407
+
408
+ return validated_tasks
409
+
410
+ async def _run_timer(self) -> None:
411
+ """Run the timer loop for checking tasks."""
412
+ interval_seconds = TICK_INTERVAL_MS / 1000
413
+
414
+ while not self._stop_flag:
415
+ try:
416
+ await asyncio.sleep(interval_seconds)
417
+
418
+ if self._stop_flag:
419
+ break
420
+
421
+ await self._check_tasks()
422
+ except asyncio.CancelledError:
423
+ break
424
+ except Exception as e:
425
+ if self._runtime:
426
+ self._runtime.logger.warning(
427
+ f"Error checking tasks: {e}",
428
+ src="service:task",
429
+ )
430
+
431
+ if self._runtime:
432
+ self._runtime.logger.info(
433
+ "Task service timer loop stopped",
434
+ src="service:task",
435
+ )
436
+
437
+ async def _check_tasks(self) -> None:
438
+ """Check tasks and execute those that are due."""
439
+ now = _current_timestamp()
440
+
441
+ # Get all tasks with "queue" tag
442
+ queue_tasks = [t for t in self._tasks.values() if t.tags and "queue" in t.tags]
443
+
444
+ # Validate the tasks (parity with TypeScript)
445
+ tasks = await self._validate_tasks(queue_tasks)
446
+
447
+ for task in tasks:
448
+ if task.id is None:
449
+ continue
450
+
451
+ task_id = str(task.id)
452
+
453
+ # Check if worker exists for this task
454
+ worker = self._workers.get(task.name)
455
+ if worker is None:
456
+ continue
457
+
458
+ # For non-repeating tasks, execute immediately
459
+ if not task.is_repeating():
460
+ await self._execute_task(task, task_id, worker)
461
+ continue
462
+
463
+ # For repeating tasks, check if interval has elapsed
464
+ task_start_time = (
465
+ task.updated_at or (task.metadata.updated_at if task.metadata else None) or 0
466
+ )
467
+
468
+ update_interval = task.get_update_interval() or 0
469
+
470
+ # Check for immediate execution on first run
471
+ metadata_updated_at = task.metadata.updated_at if task.metadata else None
472
+ metadata_created_at = None
473
+ if task.metadata and task.metadata.created_at:
474
+ with contextlib.suppress(ValueError):
475
+ metadata_created_at = int(task.metadata.created_at)
476
+
477
+ if metadata_updated_at == metadata_created_at:
478
+ if task.tags and "immediate" in task.tags:
479
+ if self._runtime:
480
+ self._runtime.logger.debug(
481
+ f"Immediately running task: {task.name}",
482
+ src="service:task",
483
+ )
484
+ await self._execute_task(task, task_id, worker)
485
+ continue
486
+
487
+ # Check if enough time has passed
488
+ if now - task_start_time >= update_interval:
489
+ # Check blocking
490
+ is_blocking = task.is_blocking()
491
+ if is_blocking and task_id in self._executing_tasks:
492
+ if self._runtime:
493
+ self._runtime.logger.debug(
494
+ f"Skipping task {task.name} - already executing (blocking enabled)",
495
+ src="service:task",
496
+ taskId=task_id,
497
+ )
498
+ continue
499
+
500
+ if self._runtime:
501
+ self._runtime.logger.debug(
502
+ f"Executing task {task.name} - interval elapsed",
503
+ src="service:task",
504
+ intervalMs=update_interval,
505
+ )
506
+
507
+ await self._execute_task(task, task_id, worker)
508
+
509
+ async def _execute_task(self, task: Task, task_id: str, worker: TaskWorker) -> None:
510
+ """Execute a single task."""
511
+ if self._runtime is None:
512
+ return
513
+
514
+ # Mark task as executing
515
+ self._executing_tasks.add(task_id)
516
+ start_time = _current_timestamp()
517
+
518
+ # For repeating tasks, update the timestamp before execution
519
+ if task.is_repeating():
520
+ task.updated_at = _current_timestamp()
521
+ if task.metadata:
522
+ task.metadata.updated_at = _current_timestamp()
523
+ if self._runtime:
524
+ self._runtime.logger.debug(
525
+ f"Updated repeating task timestamp: {task.name}",
526
+ src="service:task",
527
+ taskId=task_id,
528
+ )
529
+
530
+ # Execute the task
531
+ options = task.metadata.values if task.metadata and task.metadata.values else {}
532
+
533
+ if self._runtime:
534
+ self._runtime.logger.debug(
535
+ f"Executing task: {task.name}",
536
+ src="service:task",
537
+ taskId=task_id,
538
+ )
539
+
540
+ try:
541
+ await worker.execute(self._runtime, options, task)
542
+ except Exception as e:
543
+ if self._runtime:
544
+ self._runtime.logger.warning(
545
+ f"Task execution failed: {task.name} - {e}",
546
+ src="service:task",
547
+ taskId=task_id,
548
+ )
549
+
550
+ # For non-repeating tasks, delete after execution
551
+ if not task.is_repeating():
552
+ self._tasks.pop(task_id, None)
553
+ if self._runtime:
554
+ self._runtime.logger.debug(
555
+ f"Deleted non-repeating task after execution: {task.name}",
556
+ src="service:task",
557
+ taskId=task_id,
558
+ )
559
+
560
+ # Always remove from executing set
561
+ self._executing_tasks.discard(task_id)
562
+
563
+ duration_ms = _current_timestamp() - start_time
564
+ if self._runtime:
565
+ self._runtime.logger.debug(
566
+ f"Task execution completed: {task.name}",
567
+ src="service:task",
568
+ taskId=task_id,
569
+ durationMs=duration_ms,
570
+ )
571
+
572
+
573
+ def _current_timestamp() -> int:
574
+ """Get the current timestamp in milliseconds."""
575
+ return int(time.time() * 1000)
576
+
577
+
578
+ __all__ = [
579
+ "Task",
580
+ "TaskMetadata",
581
+ "TaskPriority",
582
+ "TaskService",
583
+ "TaskStatus",
584
+ "TaskWorker",
585
+ ]
@@ -0,0 +1,54 @@
1
+ """Local type definitions for the elizaOS Bootstrap Plugin."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Any
7
+
8
+ from pydantic import BaseModel, Field
9
+
10
+
11
+ @dataclass
12
+ class CapabilityConfig:
13
+ """Configuration for bootstrap capabilities.
14
+
15
+ Attributes:
16
+ disable_basic: If True, disables basic capabilities (reply, ignore, none, choice).
17
+ enable_extended: If True, enables extended/advanced capabilities.
18
+ advanced_capabilities: Alias for enable_extended (for consistency with TypeScript).
19
+ skip_character_provider: If True, excludes the CHARACTER provider.
20
+ enable_autonomy: If True, enables autonomy capabilities.
21
+ """
22
+
23
+ disable_basic: bool = False
24
+ enable_extended: bool = False
25
+ advanced_capabilities: bool = False # Alias for enable_extended
26
+ skip_character_provider: bool = False
27
+ enable_autonomy: bool = False
28
+
29
+ def __post_init__(self) -> None:
30
+ """Post-initialization to handle aliasing."""
31
+ # Support both enable_extended and advanced_capabilities
32
+ if self.advanced_capabilities and not self.enable_extended:
33
+ self.enable_extended = True
34
+
35
+
36
+ class EvaluatorResult(BaseModel):
37
+ """Result from an evaluator."""
38
+
39
+ score: int = Field(..., description="Numeric score 0-100")
40
+ passed: bool = Field(..., description="Whether evaluation passed")
41
+ reason: str = Field(..., description="Reason for the result")
42
+ details: dict[str, Any] = Field(default_factory=dict, description="Additional details")
43
+
44
+ model_config = {"populate_by_name": True}
45
+
46
+ @classmethod
47
+ def pass_result(cls, score: int, reason: str) -> EvaluatorResult:
48
+ """Create a passing evaluation result."""
49
+ return cls(score=score, passed=True, reason=reason)
50
+
51
+ @classmethod
52
+ def fail_result(cls, score: int, reason: str) -> EvaluatorResult:
53
+ """Create a failing evaluation result."""
54
+ return cls(score=score, passed=False, reason=reason)
@@ -0,0 +1,7 @@
1
+ """
2
+ Utilities for the elizaOS Bootstrap Plugin.
3
+ """
4
+
5
+ from .xml import parse_key_value_xml
6
+
7
+ __all__ = ["parse_key_value_xml"]