@elizaos/python 2.0.0-alpha.10 → 2.0.0-alpha.11

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 (99) hide show
  1. package/elizaos/__init__.py +0 -1
  2. package/elizaos/advanced_capabilities/actions/schedule_follow_up.py +70 -15
  3. package/elizaos/advanced_capabilities/actions/send_message.py +9 -184
  4. package/elizaos/advanced_memory/memory_service.py +15 -17
  5. package/elizaos/advanced_planning/planning_service.py +26 -14
  6. package/elizaos/basic_capabilities/providers/actions.py +118 -29
  7. package/elizaos/basic_capabilities/providers/character.py +19 -21
  8. package/elizaos/basic_capabilities/providers/current_time.py +7 -4
  9. package/elizaos/basic_capabilities/providers/time.py +7 -4
  10. package/elizaos/bootstrap/__init__.py +21 -2
  11. package/elizaos/bootstrap/actions/schedule_follow_up.py +65 -7
  12. package/elizaos/bootstrap/actions/send_message.py +162 -15
  13. package/elizaos/bootstrap/autonomy/service.py +230 -28
  14. package/elizaos/bootstrap/providers/actions.py +118 -27
  15. package/elizaos/bootstrap/providers/agent_settings.py +1 -0
  16. package/elizaos/bootstrap/providers/attachments.py +1 -0
  17. package/elizaos/bootstrap/providers/capabilities.py +1 -0
  18. package/elizaos/bootstrap/providers/character.py +1 -0
  19. package/elizaos/bootstrap/providers/choice.py +1 -0
  20. package/elizaos/bootstrap/providers/contacts.py +1 -0
  21. package/elizaos/bootstrap/providers/current_time.py +8 -2
  22. package/elizaos/bootstrap/providers/entities.py +1 -0
  23. package/elizaos/bootstrap/providers/evaluators.py +1 -0
  24. package/elizaos/bootstrap/providers/facts.py +1 -0
  25. package/elizaos/bootstrap/providers/follow_ups.py +1 -0
  26. package/elizaos/bootstrap/providers/knowledge.py +1 -0
  27. package/elizaos/bootstrap/providers/providers_list.py +1 -0
  28. package/elizaos/bootstrap/providers/relationships.py +1 -0
  29. package/elizaos/bootstrap/providers/roles.py +1 -0
  30. package/elizaos/bootstrap/providers/settings.py +1 -0
  31. package/elizaos/bootstrap/providers/time.py +8 -4
  32. package/elizaos/bootstrap/providers/world.py +1 -0
  33. package/elizaos/deterministic.py +193 -0
  34. package/elizaos/generated/__init__.py +1 -0
  35. package/elizaos/generated/action_docs.py +3181 -0
  36. package/elizaos/generated/spec_helpers.py +175 -0
  37. package/elizaos/media/mime.py +2 -2
  38. package/elizaos/media/search.py +23 -23
  39. package/elizaos/runtime.py +152 -39
  40. package/elizaos/services/message_service.py +2 -6
  41. package/elizaos/types/components.py +2 -2
  42. package/elizaos/types/generated/__init__.py +12 -0
  43. package/elizaos/types/generated/eliza/v1/agent_pb2.py +63 -0
  44. package/elizaos/types/generated/eliza/v1/agent_pb2.pyi +161 -0
  45. package/elizaos/types/generated/eliza/v1/components_pb2.py +65 -0
  46. package/elizaos/types/generated/eliza/v1/components_pb2.pyi +160 -0
  47. package/elizaos/types/generated/eliza/v1/database_pb2.py +78 -0
  48. package/elizaos/types/generated/eliza/v1/database_pb2.pyi +305 -0
  49. package/elizaos/types/generated/eliza/v1/environment_pb2.py +58 -0
  50. package/elizaos/types/generated/eliza/v1/environment_pb2.pyi +135 -0
  51. package/elizaos/types/generated/eliza/v1/events_pb2.py +82 -0
  52. package/elizaos/types/generated/eliza/v1/events_pb2.pyi +322 -0
  53. package/elizaos/types/generated/eliza/v1/ipc_pb2.py +113 -0
  54. package/elizaos/types/generated/eliza/v1/ipc_pb2.pyi +367 -0
  55. package/elizaos/types/generated/eliza/v1/knowledge_pb2.py +41 -0
  56. package/elizaos/types/generated/eliza/v1/knowledge_pb2.pyi +26 -0
  57. package/elizaos/types/generated/eliza/v1/memory_pb2.py +55 -0
  58. package/elizaos/types/generated/eliza/v1/memory_pb2.pyi +111 -0
  59. package/elizaos/types/generated/eliza/v1/message_service_pb2.py +48 -0
  60. package/elizaos/types/generated/eliza/v1/message_service_pb2.pyi +69 -0
  61. package/elizaos/types/generated/eliza/v1/messaging_pb2.py +51 -0
  62. package/elizaos/types/generated/eliza/v1/messaging_pb2.pyi +97 -0
  63. package/elizaos/types/generated/eliza/v1/model_pb2.py +84 -0
  64. package/elizaos/types/generated/eliza/v1/model_pb2.pyi +280 -0
  65. package/elizaos/types/generated/eliza/v1/payment_pb2.py +44 -0
  66. package/elizaos/types/generated/eliza/v1/payment_pb2.pyi +70 -0
  67. package/elizaos/types/generated/eliza/v1/plugin_pb2.py +68 -0
  68. package/elizaos/types/generated/eliza/v1/plugin_pb2.pyi +145 -0
  69. package/elizaos/types/generated/eliza/v1/primitives_pb2.py +48 -0
  70. package/elizaos/types/generated/eliza/v1/primitives_pb2.pyi +92 -0
  71. package/elizaos/types/generated/eliza/v1/prompts_pb2.py +52 -0
  72. package/elizaos/types/generated/eliza/v1/prompts_pb2.pyi +74 -0
  73. package/elizaos/types/generated/eliza/v1/service_interfaces_pb2.py +211 -0
  74. package/elizaos/types/generated/eliza/v1/service_interfaces_pb2.pyi +1296 -0
  75. package/elizaos/types/generated/eliza/v1/service_pb2.py +42 -0
  76. package/elizaos/types/generated/eliza/v1/service_pb2.pyi +69 -0
  77. package/elizaos/types/generated/eliza/v1/settings_pb2.py +58 -0
  78. package/elizaos/types/generated/eliza/v1/settings_pb2.pyi +85 -0
  79. package/elizaos/types/generated/eliza/v1/state_pb2.py +60 -0
  80. package/elizaos/types/generated/eliza/v1/state_pb2.pyi +114 -0
  81. package/elizaos/types/generated/eliza/v1/task_pb2.py +42 -0
  82. package/elizaos/types/generated/eliza/v1/task_pb2.pyi +58 -0
  83. package/elizaos/types/generated/eliza/v1/tee_pb2.py +52 -0
  84. package/elizaos/types/generated/eliza/v1/tee_pb2.pyi +90 -0
  85. package/elizaos/types/generated/eliza/v1/testing_pb2.py +39 -0
  86. package/elizaos/types/generated/eliza/v1/testing_pb2.pyi +23 -0
  87. package/elizaos/types/model.py +3 -0
  88. package/elizaos/types/runtime.py +1 -1
  89. package/package.json +3 -2
  90. package/tests/test_action_parameters.py +2 -3
  91. package/tests/test_advanced_memory_behavior.py +0 -2
  92. package/tests/test_advanced_memory_flag.py +0 -2
  93. package/tests/test_advanced_planning_behavior.py +11 -5
  94. package/tests/test_autonomy.py +11 -1
  95. package/tests/test_runtime.py +8 -17
  96. package/tests/test_schedule_follow_up_action.py +260 -0
  97. package/tests/test_send_message_action_targets.py +114 -0
  98. package/tests/test_settings_crypto.py +0 -2
  99. package/uv.lock +1565 -0
@@ -2,12 +2,12 @@ from __future__ import annotations
2
2
 
3
3
  import contextlib
4
4
  from dataclasses import dataclass, field
5
- from typing import TYPE_CHECKING
6
- from uuid import UUID
5
+ from typing import TYPE_CHECKING, Any
7
6
 
8
7
  from elizaos.generated.spec_helpers import require_action_spec
9
8
  from elizaos.types import Action, ActionExample, ActionResult, Content
10
9
  from elizaos.types.memory import Memory as MemoryType
10
+ from elizaos.types.primitives import UUID, as_uuid
11
11
 
12
12
  if TYPE_CHECKING:
13
13
  from elizaos.types import HandlerCallback, HandlerOptions, IAgentRuntime, Memory, State
@@ -36,6 +36,54 @@ def _convert_spec_examples() -> list[list[ActionExample]]:
36
36
  return []
37
37
 
38
38
 
39
+ def _parse_uuid(value: object) -> UUID | None:
40
+ if not isinstance(value, str) or not value.strip():
41
+ return None
42
+ with contextlib.suppress(Exception):
43
+ return as_uuid(value.strip())
44
+ return None
45
+
46
+
47
+ def _normalize_parameters(options: HandlerOptions | None) -> dict[str, Any]:
48
+ raw = getattr(options, "parameters", None)
49
+ if isinstance(raw, dict):
50
+ return raw
51
+
52
+ if raw is None:
53
+ return {}
54
+
55
+ if hasattr(raw, "items"):
56
+ try:
57
+ return {str(k): v for k, v in raw.items()}
58
+ except Exception:
59
+ return {}
60
+
61
+ return {}
62
+
63
+
64
+ def _coerce_entity_name(entity: object) -> list[str]:
65
+ if isinstance(entity, dict):
66
+ names = entity.get("names")
67
+ if isinstance(names, list):
68
+ return [str(n).strip() for n in names if isinstance(n, str) and n.strip()]
69
+ name = entity.get("name")
70
+ if isinstance(name, str) and name.strip():
71
+ return [name.strip()]
72
+ return []
73
+
74
+ names = getattr(entity, "names", None)
75
+ if isinstance(names, list):
76
+ clean = [str(n).strip() for n in names if isinstance(n, str) and str(n).strip()]
77
+ if clean:
78
+ return clean
79
+
80
+ name = getattr(entity, "name", None)
81
+ if isinstance(name, str) and name.strip():
82
+ return [name.strip()]
83
+
84
+ return []
85
+
86
+
39
87
  @dataclass
40
88
  class SendMessageAction:
41
89
  name: str = _spec["name"]
@@ -58,9 +106,14 @@ class SendMessageAction:
58
106
  callback: HandlerCallback | None = None,
59
107
  responses: list[Memory] | None = None,
60
108
  ) -> ActionResult:
61
- message_text = ""
62
- if responses and responses[0].content:
63
- message_text = str(responses[0].content.text or "")
109
+ params = _normalize_parameters(options)
110
+
111
+ text_param = params.get("text")
112
+ message_text = str(text_param).strip() if isinstance(text_param, str) else ""
113
+ if not message_text and responses and responses[0].content:
114
+ message_text = str(responses[0].content.text or "").strip()
115
+ if not message_text and message.content and isinstance(message.content.text, str):
116
+ message_text = message.content.text.strip()
64
117
 
65
118
  if not message_text:
66
119
  return ActionResult(
@@ -72,18 +125,91 @@ class SendMessageAction:
72
125
 
73
126
  target_room_id = message.room_id
74
127
  target_entity_id: UUID | None = None
128
+ target_type = "room"
129
+
130
+ target_type_param = params.get("targetType") or params.get("target_type")
131
+ target_param = params.get("target")
132
+ source_param = params.get("source")
133
+
134
+ source = (
135
+ source_param.strip()
136
+ if isinstance(source_param, str) and source_param.strip()
137
+ else (
138
+ message.content.source
139
+ if message.content and isinstance(message.content.source, str)
140
+ else "agent"
141
+ )
142
+ )
143
+
144
+ if isinstance(target_type_param, str):
145
+ normalized_target_type = target_type_param.strip().lower()
146
+ if normalized_target_type in {"user", "entity"}:
147
+ target_type = "user"
148
+ elif normalized_target_type == "room":
149
+ target_type = "room"
150
+
151
+ if isinstance(target_param, str) and target_param.strip():
152
+ target_value = target_param.strip()
153
+ if target_type == "room":
154
+ parsed_room = _parse_uuid(target_value)
155
+ if parsed_room:
156
+ target_room_id = parsed_room
157
+ else:
158
+ world_id = None
159
+ room_data = (
160
+ getattr(getattr(state, "data", None), "room", None) if state else None
161
+ )
162
+ if room_data is not None:
163
+ world_id = getattr(room_data, "world_id", None) or getattr(
164
+ room_data, "worldId", None
165
+ )
166
+ if world_id is None:
167
+ with contextlib.suppress(Exception):
168
+ current_room = await runtime.get_room(message.room_id)
169
+ if current_room:
170
+ world_id = getattr(current_room, "world_id", None) or getattr(
171
+ current_room, "worldId", None
172
+ )
173
+
174
+ if world_id is not None:
175
+ with contextlib.suppress(Exception):
176
+ rooms = await runtime.get_rooms(world_id)
177
+ for room in rooms:
178
+ room_name = getattr(room, "name", None)
179
+ if (
180
+ isinstance(room_name, str)
181
+ and room_name.strip().lower() == target_value.lower()
182
+ ):
183
+ room_id = getattr(room, "id", None)
184
+ if room_id is not None:
185
+ target_room_id = as_uuid(str(room_id))
186
+ break
187
+ else:
188
+ parsed_entity = _parse_uuid(target_value)
189
+ if parsed_entity:
190
+ target_entity_id = parsed_entity
191
+ else:
192
+ with contextlib.suppress(Exception):
193
+ entities = await runtime.get_entities_for_room(message.room_id)
194
+ for entity in entities:
195
+ names = _coerce_entity_name(entity)
196
+ if any(name.lower() == target_value.lower() for name in names):
197
+ entity_id = getattr(entity, "id", None)
198
+ if entity_id is not None:
199
+ target_entity_id = as_uuid(str(entity_id))
200
+ break
75
201
 
76
202
  if message.content and message.content.target:
77
203
  target = message.content.target
78
204
  if isinstance(target, dict):
79
205
  room_str = target.get("roomId")
80
206
  entity_str = target.get("entityId")
81
- if room_str:
82
- with contextlib.suppress(ValueError):
83
- target_room_id = UUID(room_str)
84
- if entity_str:
85
- with contextlib.suppress(ValueError):
86
- target_entity_id = UUID(entity_str)
207
+ if room_str and target_type == "room":
208
+ with contextlib.suppress(Exception):
209
+ target_room_id = as_uuid(room_str)
210
+ if entity_str and target_type == "user":
211
+ with contextlib.suppress(Exception):
212
+ target_entity_id = as_uuid(entity_str)
87
213
 
88
214
  if not target_room_id:
89
215
  return ActionResult(
@@ -95,16 +221,28 @@ class SendMessageAction:
95
221
 
96
222
  message_content = Content(
97
223
  text=message_text,
98
- source="agent",
224
+ source=source,
99
225
  actions=["SEND_MESSAGE"],
100
226
  )
101
227
 
228
+ send_message_to_target = getattr(runtime, "send_message_to_target", None)
229
+ if callable(send_message_to_target):
230
+ with contextlib.suppress(Exception):
231
+ from elizaos.types.runtime import TargetInfo
232
+
233
+ await send_message_to_target(
234
+ TargetInfo(
235
+ roomId=str(target_room_id),
236
+ entityId=str(target_entity_id) if target_entity_id else None,
237
+ source=source,
238
+ ),
239
+ message_content,
240
+ )
241
+
102
242
  # Create the message memory
103
243
  import time
104
244
  import uuid as uuid_module
105
245
 
106
- from elizaos.types.primitives import as_uuid
107
-
108
246
  message_memory = MemoryType(
109
247
  id=as_uuid(str(uuid_module.uuid4())),
110
248
  entity_id=runtime.agent_id,
@@ -142,16 +280,25 @@ class SendMessageAction:
142
280
  if callback:
143
281
  await callback(response_content)
144
282
 
283
+ target_id = (
284
+ target_entity_id if target_type == "user" and target_entity_id else target_room_id
285
+ )
145
286
  return ActionResult(
146
- text="Message sent to room",
287
+ text="Message sent",
147
288
  values={
148
289
  "success": True,
149
290
  "messageSent": True,
291
+ "targetType": target_type,
292
+ "target": str(target_id),
293
+ "source": source,
150
294
  "targetRoomId": str(target_room_id),
151
295
  "targetEntityId": str(target_entity_id) if target_entity_id else None,
152
296
  },
153
297
  data={
154
298
  "actionName": "SEND_MESSAGE",
299
+ "targetType": target_type,
300
+ "target": str(target_id),
301
+ "source": source,
155
302
  "targetRoomId": str(target_room_id),
156
303
  "messagePreview": message_text[:100],
157
304
  },
@@ -11,6 +11,7 @@ import contextlib
11
11
  import logging
12
12
  import time
13
13
  import uuid
14
+ from collections.abc import Iterable
14
15
  from typing import TYPE_CHECKING, Any
15
16
 
16
17
  from elizaos.bootstrap.services.task import Task
@@ -23,7 +24,7 @@ from elizaos.prompts import (
23
24
  from elizaos.types.environment import Room, World
24
25
  from elizaos.types.events import EventType
25
26
  from elizaos.types.memory import Memory
26
- from elizaos.types.primitives import UUID, Content, as_uuid
27
+ from elizaos.types.primitives import UUID, Content, as_uuid, string_to_uuid
27
28
  from elizaos.types.service import Service
28
29
 
29
30
  from .types import AutonomyStatus
@@ -40,8 +41,7 @@ AUTONOMY_SERVICE_TYPE = "AUTONOMY"
40
41
  AUTONOMY_TASK_NAME = "AUTONOMY_THINK"
41
42
 
42
43
  # Tags used for autonomy tasks (parity with TypeScript).
43
- # Note: TypeScript uses ["repeat", "autonomy", "internal"] without "queue".
44
- AUTONOMY_TASK_TAGS = ["repeat", "autonomy", "internal"]
44
+ AUTONOMY_TASK_TAGS = ["queue", "repeat", "autonomy"]
45
45
 
46
46
  # Default interval in milliseconds
47
47
  DEFAULT_INTERVAL_MS = 30_000
@@ -112,8 +112,10 @@ class AutonomyService(Service):
112
112
  self._interval_ms = DEFAULT_INTERVAL_MS
113
113
  self._task_registered = False
114
114
  self._settings_monitor_task: asyncio.Task[None] | None = None
115
- self._autonomous_room_id = as_uuid(str(uuid.uuid4()))
115
+ # Placeholder; replaced with a deterministic ID during _initialize().
116
+ self._autonomous_room_id = as_uuid("00000000-0000-0000-0000-000000000000")
116
117
  self._autonomous_world_id = as_uuid("00000000-0000-0000-0000-000000000001")
118
+ self._autonomy_entity_id = as_uuid("00000000-0000-0000-0000-000000000002")
117
119
 
118
120
  def _log(self, level: str, msg: str) -> None:
119
121
  if self._runtime:
@@ -135,6 +137,9 @@ class AutonomyService(Service):
135
137
  if not self._runtime:
136
138
  return
137
139
 
140
+ self._autonomous_room_id = as_uuid(
141
+ string_to_uuid(f"autonomy-room-{self._runtime.agent_id}")
142
+ )
138
143
  self._log("info", f"Using autonomous room ID: {self._autonomous_room_id}")
139
144
 
140
145
  # Ensure autonomous context exists
@@ -289,35 +294,223 @@ class AutonomyService(Service):
289
294
  except Exception:
290
295
  return None
291
296
 
297
+ @staticmethod
298
+ def _coerce_name(entity: object) -> str | None:
299
+ if isinstance(entity, dict):
300
+ names = entity.get("names")
301
+ if isinstance(names, list):
302
+ for name in names:
303
+ if isinstance(name, str) and name.strip():
304
+ return name.strip()
305
+ name = entity.get("name")
306
+ if isinstance(name, str) and name.strip():
307
+ return name.strip()
308
+ return None
309
+
310
+ names = getattr(entity, "names", None)
311
+ if isinstance(names, list):
312
+ for name in names:
313
+ if isinstance(name, str) and name.strip():
314
+ return name.strip()
315
+
316
+ name = getattr(entity, "name", None)
317
+ if isinstance(name, str) and name.strip():
318
+ return name.strip()
319
+
320
+ return None
321
+
322
+ @staticmethod
323
+ def _memory_text(memory: Memory) -> str:
324
+ if memory.content and isinstance(memory.content.text, str):
325
+ return memory.content.text.strip()
326
+ return ""
327
+
328
+ async def _build_entity_name_lookup(self, entity_ids: Iterable[UUID]) -> dict[UUID, str]:
329
+ if not self._runtime:
330
+ return {}
331
+
332
+ ids = list({entity_id for entity_id in entity_ids if entity_id})
333
+ if not ids:
334
+ return {}
335
+
336
+ getter = getattr(self._runtime, "get_entities_by_ids", None)
337
+ if not callable(getter):
338
+ return {}
339
+
340
+ try:
341
+ entities = await getter(ids)
342
+ except Exception:
343
+ return {}
344
+
345
+ name_by_id: dict[UUID, str] = {}
346
+ for entity in entities or []:
347
+ entity_id = None
348
+ if isinstance(entity, dict):
349
+ raw_id = entity.get("id")
350
+ if isinstance(raw_id, str):
351
+ with contextlib.suppress(Exception):
352
+ entity_id = as_uuid(raw_id)
353
+ else:
354
+ raw_id = getattr(entity, "id", None)
355
+ if raw_id is not None:
356
+ with contextlib.suppress(Exception):
357
+ entity_id = as_uuid(str(raw_id))
358
+
359
+ if not entity_id:
360
+ continue
361
+
362
+ entity_name = self._coerce_name(entity)
363
+ if entity_name:
364
+ name_by_id[entity_id] = entity_name
365
+
366
+ return name_by_id
367
+
368
+ @staticmethod
369
+ def _dedupe_memories_by_id_keep_earliest(memories: list[Memory]) -> list[Memory]:
370
+ by_id: dict[str, Memory] = {}
371
+ without_id: list[Memory] = []
372
+
373
+ for memory in memories:
374
+ mem_id = str(memory.id) if memory.id else ""
375
+ if not mem_id:
376
+ without_id.append(memory)
377
+ continue
378
+
379
+ existing = by_id.get(mem_id)
380
+ if existing is None or (memory.created_at or 0) < (existing.created_at or 0):
381
+ by_id[mem_id] = memory
382
+
383
+ return [*without_id, *by_id.values()]
384
+
292
385
  async def _get_target_room_context_text(self) -> str:
293
386
  if not self._runtime:
294
- return "(no target room configured)"
387
+ return "(no rooms configured)"
388
+
295
389
  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"}
390
+
391
+ ordered_room_ids: list[UUID] = []
392
+ if target_room_id:
393
+ ordered_room_ids.append(target_room_id)
394
+
395
+ get_participant_rooms = getattr(self._runtime, "get_rooms_for_participant", None)
396
+ if callable(get_participant_rooms):
397
+ with contextlib.suppress(Exception):
398
+ participant_rooms = await get_participant_rooms(self._runtime.agent_id)
399
+ for room_id in participant_rooms or []:
400
+ if room_id not in ordered_room_ids:
401
+ ordered_room_ids.append(room_id)
402
+
403
+ if not ordered_room_ids:
404
+ return "(no rooms configured)"
405
+
406
+ room_name_by_id: dict[UUID, str] = {}
407
+ get_rooms_by_ids = getattr(self._runtime, "get_rooms_by_ids", None)
408
+ if callable(get_rooms_by_ids):
409
+ with contextlib.suppress(Exception):
410
+ rooms = await get_rooms_by_ids(ordered_room_ids)
411
+ for room in rooms or []:
412
+ if room and room.id:
413
+ room_name_by_id[room.id] = (
414
+ room.name if isinstance(room.name, str) and room.name else str(room.id)
415
+ )
416
+
417
+ message_room_ids = [rid for rid in ordered_room_ids if rid != self._autonomous_room_id]
418
+ per_room_limit = 10
419
+
420
+ fetched_messages: list[Memory] = []
421
+ if message_room_ids:
422
+ get_memories_by_room_ids = getattr(self._runtime, "get_memories_by_room_ids", None)
423
+ if callable(get_memories_by_room_ids):
424
+ with contextlib.suppress(Exception):
425
+ fetched_messages = await get_memories_by_room_ids(
426
+ {
427
+ "roomIds": message_room_ids,
428
+ "limit": per_room_limit * len(message_room_ids),
429
+ "tableName": "messages",
430
+ }
431
+ )
432
+ if not fetched_messages:
433
+ for room_id in message_room_ids:
434
+ with contextlib.suppress(Exception):
435
+ fetched_messages.extend(
436
+ await self._runtime.get_memories(
437
+ {
438
+ "roomId": room_id,
439
+ "count": per_room_limit,
440
+ "tableName": "messages",
441
+ }
442
+ )
443
+ )
444
+
445
+ autonomy_memories = await self._runtime.get_memories(
446
+ {"roomId": self._autonomous_room_id, "count": per_room_limit, "tableName": "memories"}
300
447
  )
301
- messages_table = await self._runtime.get_memories(
302
- {"roomId": target_room_id, "count": 15, "tableName": "messages"}
448
+
449
+ external_messages = [
450
+ m
451
+ for m in fetched_messages
452
+ if m and m.entity_id and m.entity_id != self._runtime.agent_id
453
+ ]
454
+ entity_name_by_id = await self._build_entity_name_lookup(
455
+ memory.entity_id for memory in external_messages
303
456
  )
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:
457
+
458
+ messages_by_room: dict[UUID, list[Memory]] = {}
459
+ sorted_messages = sorted(
460
+ self._dedupe_memories_by_id_keep_earliest(external_messages),
461
+ key=lambda m: m.created_at or 0,
462
+ reverse=True,
463
+ )
464
+ for memory in sorted_messages:
465
+ bucket = messages_by_room.setdefault(memory.room_id, [])
466
+ if len(bucket) >= per_room_limit:
308
467
  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)"
468
+ bucket.append(memory)
469
+
470
+ room_sections: list[str] = []
471
+ for room_id in message_room_ids:
472
+ room_name = room_name_by_id.get(room_id, str(room_id))
473
+ room_messages = list(reversed(messages_by_room.get(room_id, [])))
474
+ if not room_messages:
475
+ room_sections.append(f"Room: {room_name}\n(no recent messages)")
476
+ continue
477
+
478
+ lines: list[str] = []
479
+ for memory in room_messages:
480
+ text = self._memory_text(memory)
481
+ if not text:
482
+ continue
483
+ author = entity_name_by_id.get(memory.entity_id, str(memory.entity_id))
484
+ lines.append(f"{author}: {text}")
485
+
486
+ if lines:
487
+ room_sections.append(f"Room: {room_name}\n" + "\n".join(lines))
488
+ else:
489
+ room_sections.append(f"Room: {room_name}\n(no recent messages)")
490
+
491
+ autonomy_entries: list[str] = []
492
+ for memory in autonomy_memories:
493
+ text = self._memory_text(memory)
494
+ if not text:
495
+ continue
496
+
497
+ metadata_obj = memory.content.data if memory.content else None
498
+ metadata: dict[str, object] = metadata_obj if isinstance(metadata_obj, dict) else {}
499
+ entry_type = metadata.get("type")
500
+
501
+ if memory.entity_id == self._runtime.agent_id and entry_type == "autonomous-response":
502
+ autonomy_entries.append(f"Thought: {text}")
503
+ elif (
504
+ memory.entity_id == self._autonomy_entity_id and entry_type == "autonomous-trigger"
505
+ ):
506
+ autonomy_entries.append(f"Trigger: {text}")
507
+
508
+ if autonomy_entries:
509
+ autonomy_section = "Autonomous context:\n" + "\n".join(autonomy_entries)
510
+ else:
511
+ autonomy_section = "Autonomous context: (none)"
512
+
513
+ return "\n\n".join([*room_sections, autonomy_section])
321
514
 
322
515
  async def _settings_monitoring(self) -> None:
323
516
  while not self._is_stopped:
@@ -412,7 +605,7 @@ class AutonomyService(Service):
412
605
  else self._create_continuous_prompt(last_thought, is_first_thought, target_context)
413
606
  )
414
607
 
415
- entity_id = agent_entity.id if agent_entity.id else self._runtime.agent_id
608
+ entity_id = self._autonomy_entity_id
416
609
  current_time_ms = int(time.time() * 1000)
417
610
  autonomous_message = Memory(
418
611
  id=as_uuid(str(uuid.uuid4())),
@@ -420,6 +613,15 @@ class AutonomyService(Service):
420
613
  content=Content(
421
614
  text=autonomy_prompt,
422
615
  source="autonomy-service",
616
+ data={
617
+ "type": "autonomous-prompt",
618
+ "isAutonomous": True,
619
+ "isInternalThought": True,
620
+ "autonomyMode": mode,
621
+ "channelId": "autonomous",
622
+ "timestamp": current_time_ms,
623
+ "isContinuation": not is_first_thought,
624
+ },
423
625
  ),
424
626
  room_id=self._autonomous_room_id,
425
627
  agent_id=self._runtime.agent_id,