@elizaos/python 2.0.0-alpha.3 → 2.0.0-alpha.30
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.
- package/elizaos/__init__.py +0 -1
- package/elizaos/advanced_capabilities/__init__.py +6 -41
- package/elizaos/advanced_capabilities/actions/__init__.py +1 -21
- package/elizaos/advanced_capabilities/actions/add_contact.py +24 -13
- package/elizaos/advanced_capabilities/actions/follow_room.py +29 -29
- package/elizaos/advanced_capabilities/actions/image_generation.py +15 -28
- package/elizaos/advanced_capabilities/actions/mute_room.py +15 -28
- package/elizaos/advanced_capabilities/actions/remove_contact.py +17 -3
- package/elizaos/advanced_capabilities/actions/roles.py +17 -30
- package/elizaos/advanced_capabilities/actions/schedule_follow_up.py +70 -15
- package/elizaos/advanced_capabilities/actions/search_contacts.py +17 -3
- package/elizaos/advanced_capabilities/actions/send_message.py +184 -51
- package/elizaos/advanced_capabilities/actions/settings.py +17 -3
- package/elizaos/advanced_capabilities/actions/unfollow_room.py +15 -28
- package/elizaos/advanced_capabilities/actions/unmute_room.py +15 -28
- package/elizaos/advanced_capabilities/actions/update_contact.py +17 -3
- package/elizaos/advanced_capabilities/actions/update_entity.py +17 -3
- package/elizaos/advanced_capabilities/evaluators/__init__.py +2 -9
- package/elizaos/advanced_capabilities/evaluators/reflection.py +3 -132
- package/elizaos/advanced_capabilities/evaluators/relationship_extraction.py +5 -201
- package/elizaos/advanced_capabilities/providers/__init__.py +1 -12
- package/elizaos/advanced_capabilities/providers/knowledge.py +23 -3
- package/elizaos/advanced_capabilities/services/__init__.py +2 -9
- package/elizaos/advanced_capabilities/services/rolodex.py +2 -2
- package/elizaos/advanced_memory/actions/reset_session.py +143 -0
- package/elizaos/advanced_memory/evaluators/reflection.py +134 -0
- package/elizaos/advanced_memory/evaluators/relationship_extraction.py +203 -0
- package/elizaos/advanced_memory/memory_service.py +69 -27
- package/elizaos/advanced_memory/plugin.py +2 -1
- package/elizaos/advanced_memory/test_advanced_memory.py +357 -0
- package/elizaos/advanced_memory/types.py +2 -2
- package/elizaos/advanced_planning/actions/schedule_follow_up.py +222 -0
- package/elizaos/advanced_planning/planning_service.py +26 -14
- package/elizaos/basic_capabilities/__init__.py +0 -2
- package/elizaos/basic_capabilities/providers/__init__.py +0 -3
- package/elizaos/basic_capabilities/providers/actions.py +118 -29
- package/elizaos/basic_capabilities/providers/agent_settings.py +64 -0
- package/elizaos/basic_capabilities/providers/character.py +19 -21
- package/elizaos/basic_capabilities/providers/contacts.py +79 -0
- package/elizaos/basic_capabilities/providers/current_time.py +7 -4
- package/elizaos/basic_capabilities/providers/facts.py +87 -0
- package/elizaos/basic_capabilities/providers/follow_ups.py +117 -0
- package/elizaos/basic_capabilities/providers/knowledge.py +96 -0
- package/elizaos/basic_capabilities/providers/recent_messages.py +5 -0
- package/elizaos/basic_capabilities/providers/relationships.py +113 -0
- package/elizaos/basic_capabilities/providers/roles.py +96 -0
- package/elizaos/basic_capabilities/providers/settings.py +56 -0
- package/elizaos/basic_capabilities/providers/time.py +7 -4
- package/elizaos/basic_capabilities/services/embedding.py +10 -7
- package/elizaos/basic_capabilities/services/task.py +3 -3
- package/elizaos/bootstrap/__init__.py +21 -2
- package/elizaos/bootstrap/actions/__init__.py +3 -0
- package/elizaos/bootstrap/actions/reset_session.py +3 -0
- package/elizaos/bootstrap/actions/roles.py +5 -4
- package/elizaos/bootstrap/actions/schedule_follow_up.py +65 -7
- package/elizaos/bootstrap/actions/send_message.py +162 -15
- package/elizaos/bootstrap/autonomy/__init__.py +5 -1
- package/elizaos/bootstrap/autonomy/action.py +161 -0
- package/elizaos/bootstrap/autonomy/evaluators.py +217 -0
- package/elizaos/bootstrap/autonomy/service.py +238 -28
- package/elizaos/bootstrap/plugin.py +7 -0
- package/elizaos/bootstrap/providers/actions.py +118 -27
- package/elizaos/bootstrap/providers/agent_settings.py +1 -0
- package/elizaos/bootstrap/providers/attachments.py +1 -0
- package/elizaos/bootstrap/providers/capabilities.py +1 -0
- package/elizaos/bootstrap/providers/character.py +1 -0
- package/elizaos/bootstrap/providers/choice.py +1 -0
- package/elizaos/bootstrap/providers/contacts.py +1 -0
- package/elizaos/bootstrap/providers/current_time.py +8 -2
- package/elizaos/bootstrap/providers/entities.py +1 -0
- package/elizaos/bootstrap/providers/evaluators.py +1 -0
- package/elizaos/bootstrap/providers/facts.py +1 -0
- package/elizaos/bootstrap/providers/follow_ups.py +1 -0
- package/elizaos/bootstrap/providers/knowledge.py +26 -3
- package/elizaos/bootstrap/providers/providers_list.py +1 -0
- package/elizaos/bootstrap/providers/recent_messages.py +5 -0
- package/elizaos/bootstrap/providers/relationships.py +20 -13
- package/elizaos/bootstrap/providers/roles.py +1 -0
- package/elizaos/bootstrap/providers/settings.py +1 -0
- package/elizaos/bootstrap/providers/time.py +8 -4
- package/elizaos/bootstrap/providers/world.py +1 -0
- package/elizaos/bootstrap/services/embedding.py +206 -8
- package/elizaos/bootstrap/services/rolodex.py +2 -2
- package/elizaos/bootstrap/services/task.py +3 -3
- package/elizaos/deterministic.py +193 -0
- package/elizaos/generated/__init__.py +1 -0
- package/elizaos/generated/action_docs.py +3181 -0
- package/elizaos/generated/spec_helpers.py +175 -0
- package/elizaos/media/mime.py +4 -4
- package/elizaos/media/search.py +23 -23
- package/elizaos/runtime.py +223 -64
- package/elizaos/services/hook_service.py +3 -3
- package/elizaos/services/message_service.py +175 -29
- package/elizaos/types/components.py +2 -2
- package/elizaos/types/generated/__init__.py +12 -0
- package/elizaos/types/generated/eliza/v1/agent_pb2.py +63 -0
- package/elizaos/types/generated/eliza/v1/agent_pb2.pyi +159 -0
- package/elizaos/types/generated/eliza/v1/components_pb2.py +65 -0
- package/elizaos/types/generated/eliza/v1/components_pb2.pyi +160 -0
- package/elizaos/types/generated/eliza/v1/database_pb2.py +78 -0
- package/elizaos/types/generated/eliza/v1/database_pb2.pyi +305 -0
- package/elizaos/types/generated/eliza/v1/environment_pb2.py +58 -0
- package/elizaos/types/generated/eliza/v1/environment_pb2.pyi +135 -0
- package/elizaos/types/generated/eliza/v1/events_pb2.py +82 -0
- package/elizaos/types/generated/eliza/v1/events_pb2.pyi +322 -0
- package/elizaos/types/generated/eliza/v1/ipc_pb2.py +113 -0
- package/elizaos/types/generated/eliza/v1/ipc_pb2.pyi +367 -0
- package/elizaos/types/generated/eliza/v1/knowledge_pb2.py +41 -0
- package/elizaos/types/generated/eliza/v1/knowledge_pb2.pyi +26 -0
- package/elizaos/types/generated/eliza/v1/memory_pb2.py +55 -0
- package/elizaos/types/generated/eliza/v1/memory_pb2.pyi +111 -0
- package/elizaos/types/generated/eliza/v1/message_service_pb2.py +48 -0
- package/elizaos/types/generated/eliza/v1/message_service_pb2.pyi +69 -0
- package/elizaos/types/generated/eliza/v1/messaging_pb2.py +51 -0
- package/elizaos/types/generated/eliza/v1/messaging_pb2.pyi +97 -0
- package/elizaos/types/generated/eliza/v1/model_pb2.py +84 -0
- package/elizaos/types/generated/eliza/v1/model_pb2.pyi +280 -0
- package/elizaos/types/generated/eliza/v1/payment_pb2.py +44 -0
- package/elizaos/types/generated/eliza/v1/payment_pb2.pyi +70 -0
- package/elizaos/types/generated/eliza/v1/plugin_pb2.py +68 -0
- package/elizaos/types/generated/eliza/v1/plugin_pb2.pyi +145 -0
- package/elizaos/types/generated/eliza/v1/primitives_pb2.py +48 -0
- package/elizaos/types/generated/eliza/v1/primitives_pb2.pyi +92 -0
- package/elizaos/types/generated/eliza/v1/prompts_pb2.py +52 -0
- package/elizaos/types/generated/eliza/v1/prompts_pb2.pyi +74 -0
- package/elizaos/types/generated/eliza/v1/service_interfaces_pb2.py +211 -0
- package/elizaos/types/generated/eliza/v1/service_interfaces_pb2.pyi +1296 -0
- package/elizaos/types/generated/eliza/v1/service_pb2.py +42 -0
- package/elizaos/types/generated/eliza/v1/service_pb2.pyi +69 -0
- package/elizaos/types/generated/eliza/v1/settings_pb2.py +58 -0
- package/elizaos/types/generated/eliza/v1/settings_pb2.pyi +85 -0
- package/elizaos/types/generated/eliza/v1/state_pb2.py +60 -0
- package/elizaos/types/generated/eliza/v1/state_pb2.pyi +114 -0
- package/elizaos/types/generated/eliza/v1/task_pb2.py +42 -0
- package/elizaos/types/generated/eliza/v1/task_pb2.pyi +58 -0
- package/elizaos/types/generated/eliza/v1/tee_pb2.py +52 -0
- package/elizaos/types/generated/eliza/v1/tee_pb2.pyi +90 -0
- package/elizaos/types/generated/eliza/v1/testing_pb2.py +39 -0
- package/elizaos/types/generated/eliza/v1/testing_pb2.pyi +23 -0
- package/elizaos/types/model.py +33 -3
- package/elizaos/types/primitives.py +3 -3
- package/elizaos/types/runtime.py +17 -3
- package/elizaos/types/state.py +2 -2
- package/elizaos/utils/streaming.py +3 -3
- package/elizaos/utils/validation.py +76 -0
- package/package.json +4 -3
- package/pyproject.toml +1 -2
- package/requirements-dev.lock +2 -2
- package/requirements.in +1 -2
- package/requirements.lock +2 -2
- package/tests/test_action_parameters.py +2 -3
- package/tests/test_actions_provider_examples.py +58 -1
- package/tests/test_advanced_memory_behavior.py +0 -2
- package/tests/test_advanced_memory_flag.py +0 -2
- package/tests/test_advanced_planning_behavior.py +11 -5
- package/tests/test_async_embedding.py +124 -0
- package/tests/test_autonomy.py +24 -3
- package/tests/test_history_compaction.py +104 -0
- package/tests/test_memory_bounds.py +115 -0
- package/tests/test_runtime.py +8 -17
- package/tests/test_schedule_follow_up_action.py +260 -0
- package/tests/test_send_message_action_targets.py +114 -0
- package/tests/test_settings_crypto.py +0 -2
- package/tests/test_validation.py +141 -0
- package/tests/verify_memory_architecture.py +192 -0
- package/uv.lock +1565 -0
- package/elizaos/basic_capabilities/providers/capabilities.py +0 -62
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
"""Post-action evaluator for the autonomous loop.
|
|
2
|
+
|
|
3
|
+
After actions complete during an autonomous cycle, this evaluator asks the LLM
|
|
4
|
+
whether the agent has satisfied its goal or should continue with more actions.
|
|
5
|
+
If the LLM says CONTINUE, the evaluator recursively triggers another full
|
|
6
|
+
message-handling cycle so the agent can pick and execute additional actions.
|
|
7
|
+
This repeats until the LLM says PAUSE or the safety limit is reached.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import logging
|
|
13
|
+
from typing import TYPE_CHECKING, Any
|
|
14
|
+
|
|
15
|
+
from elizaos.types.components import ActionResult, Evaluator, HandlerOptions
|
|
16
|
+
from elizaos.types.memory import Memory
|
|
17
|
+
from elizaos.types.model import ModelType
|
|
18
|
+
from elizaos.types.primitives import Content
|
|
19
|
+
|
|
20
|
+
from .service import AUTONOMY_SERVICE_TYPE, AutonomyService
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from collections.abc import Awaitable, Callable
|
|
24
|
+
|
|
25
|
+
from elizaos.types.runtime import IAgentRuntime
|
|
26
|
+
from elizaos.types.state import State
|
|
27
|
+
|
|
28
|
+
_logger = logging.getLogger(__name__)
|
|
29
|
+
|
|
30
|
+
# Safety limit – the evaluator will stop recursing after this many
|
|
31
|
+
# consecutive CONTINUE decisions within a single evaluation chain.
|
|
32
|
+
MAX_CONTINUATION_DEPTH = 10
|
|
33
|
+
|
|
34
|
+
POST_ACTION_EVALUATION_TEMPLATE = """\
|
|
35
|
+
You are evaluating whether an autonomous agent has completed its current objective.
|
|
36
|
+
|
|
37
|
+
Recent actions and their results:
|
|
38
|
+
{action_results}
|
|
39
|
+
|
|
40
|
+
Recent context:
|
|
41
|
+
{recent_context}
|
|
42
|
+
|
|
43
|
+
Based on the above, decide:
|
|
44
|
+
- If the agent has completed everything it set out to do, or there is nothing \
|
|
45
|
+
more it can meaningfully do right now, respond with exactly: PAUSE
|
|
46
|
+
- If there are remaining steps, errors that need retrying, or follow-up actions \
|
|
47
|
+
the agent should take immediately, respond with exactly: CONTINUE
|
|
48
|
+
|
|
49
|
+
Respond with a single word: CONTINUE or PAUSE
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
async def _validate_post_action(
|
|
54
|
+
runtime: IAgentRuntime,
|
|
55
|
+
message: Memory,
|
|
56
|
+
_state: State | None = None,
|
|
57
|
+
) -> bool:
|
|
58
|
+
"""Run when the message originated from the autonomy service."""
|
|
59
|
+
if not message.content:
|
|
60
|
+
return False
|
|
61
|
+
|
|
62
|
+
# Check content.data for autonomy markers
|
|
63
|
+
data = message.content.data
|
|
64
|
+
if isinstance(data, dict):
|
|
65
|
+
if data.get("isAutonomous") is True or data.get("source") == "autonomy-service":
|
|
66
|
+
return True
|
|
67
|
+
|
|
68
|
+
# Check content.source field
|
|
69
|
+
source = getattr(message.content, "source", None)
|
|
70
|
+
return bool(isinstance(source, str) and source == "autonomy-service")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _collect_action_results_text(runtime: IAgentRuntime, message: Memory) -> str:
|
|
74
|
+
"""Build a human-readable summary of most recent action results."""
|
|
75
|
+
if not message.id:
|
|
76
|
+
return "(no action results available)"
|
|
77
|
+
|
|
78
|
+
results = runtime.get_action_results(message.id)
|
|
79
|
+
if not results:
|
|
80
|
+
return "(no action results available)"
|
|
81
|
+
|
|
82
|
+
lines: list[str] = []
|
|
83
|
+
for r in results:
|
|
84
|
+
name = ""
|
|
85
|
+
data = getattr(r, "data", None)
|
|
86
|
+
if isinstance(data, dict):
|
|
87
|
+
v = data.get("actionName")
|
|
88
|
+
if isinstance(v, str):
|
|
89
|
+
name = v
|
|
90
|
+
success = getattr(r, "success", True)
|
|
91
|
+
text = getattr(r, "text", "")
|
|
92
|
+
status = "success" if success else "failed"
|
|
93
|
+
lines.append(f"- {name} ({status}): {text}")
|
|
94
|
+
|
|
95
|
+
return "\n".join(lines) if lines else "(no action results available)"
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
async def _collect_recent_context(runtime: IAgentRuntime, room_id: Any) -> str:
|
|
99
|
+
"""Gather a short snippet of recent memories for the evaluation prompt."""
|
|
100
|
+
try:
|
|
101
|
+
recent_memories = await runtime.get_memories(
|
|
102
|
+
{"roomId": room_id, "count": 5, "tableName": "memories"}
|
|
103
|
+
)
|
|
104
|
+
except Exception:
|
|
105
|
+
return "(no recent context)"
|
|
106
|
+
|
|
107
|
+
if not recent_memories:
|
|
108
|
+
return "(no recent context)"
|
|
109
|
+
|
|
110
|
+
ctx_lines: list[str] = []
|
|
111
|
+
for m in recent_memories:
|
|
112
|
+
if m.content and m.content.text:
|
|
113
|
+
ctx_lines.append(m.content.text[:200])
|
|
114
|
+
|
|
115
|
+
return "\n".join(ctx_lines[-3:]) if ctx_lines else "(no recent context)"
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
async def _handle_post_action(
|
|
119
|
+
runtime: IAgentRuntime,
|
|
120
|
+
message: Memory,
|
|
121
|
+
state: State | None = None,
|
|
122
|
+
options: HandlerOptions | None = None,
|
|
123
|
+
callback: Callable[[Content], Awaitable[None]] | None = None,
|
|
124
|
+
responses: list[Memory] | None = None,
|
|
125
|
+
) -> ActionResult | None:
|
|
126
|
+
"""Evaluate whether the agent should continue with more actions.
|
|
127
|
+
|
|
128
|
+
Runs after ``processActions`` completes during an autonomy cycle.
|
|
129
|
+
If the LLM says CONTINUE, it recursively triggers another autonomous
|
|
130
|
+
think iteration. This repeats until the LLM says PAUSE or the
|
|
131
|
+
safety depth limit is reached.
|
|
132
|
+
"""
|
|
133
|
+
autonomy_service = runtime.get_service(AUTONOMY_SERVICE_TYPE)
|
|
134
|
+
if not autonomy_service or not isinstance(autonomy_service, AutonomyService):
|
|
135
|
+
return None
|
|
136
|
+
|
|
137
|
+
# Don't evaluate if autonomy is not actually running
|
|
138
|
+
if not autonomy_service.is_loop_running():
|
|
139
|
+
return None
|
|
140
|
+
|
|
141
|
+
# Track recursion depth via an attribute on the service instance
|
|
142
|
+
depth: int = getattr(autonomy_service, "_eval_depth", 0)
|
|
143
|
+
|
|
144
|
+
action_results_text = _collect_action_results_text(runtime, message)
|
|
145
|
+
|
|
146
|
+
room_id = autonomy_service.get_autonomous_room_id() or message.room_id
|
|
147
|
+
recent_context = await _collect_recent_context(runtime, room_id)
|
|
148
|
+
|
|
149
|
+
prompt = POST_ACTION_EVALUATION_TEMPLATE.format(
|
|
150
|
+
action_results=action_results_text,
|
|
151
|
+
recent_context=recent_context,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
try:
|
|
155
|
+
result = await runtime.use_model(
|
|
156
|
+
ModelType.TEXT_SMALL,
|
|
157
|
+
{"prompt": prompt, "temperature": 0.1, "maxTokens": 10},
|
|
158
|
+
)
|
|
159
|
+
decision = str(result).strip().upper()
|
|
160
|
+
except Exception as e:
|
|
161
|
+
_logger.warning(f"Post-action evaluation LLM call failed: {e}")
|
|
162
|
+
autonomy_service._eval_depth = 0 # type: ignore[attr-defined]
|
|
163
|
+
return None
|
|
164
|
+
|
|
165
|
+
if "CONTINUE" in decision:
|
|
166
|
+
depth += 1
|
|
167
|
+
|
|
168
|
+
if depth >= MAX_CONTINUATION_DEPTH:
|
|
169
|
+
runtime.logger.warning(
|
|
170
|
+
f"[post-action-evaluator] Safety limit reached ({MAX_CONTINUATION_DEPTH} "
|
|
171
|
+
"consecutive actions). Pausing."
|
|
172
|
+
)
|
|
173
|
+
autonomy_service._eval_depth = 0 # type: ignore[attr-defined]
|
|
174
|
+
return ActionResult(
|
|
175
|
+
success=True,
|
|
176
|
+
text=f"Paused after {MAX_CONTINUATION_DEPTH} consecutive actions",
|
|
177
|
+
data={"decision": "PAUSE", "reason": "depth_limit", "depth": depth},
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
runtime.logger.info(
|
|
181
|
+
f"[post-action-evaluator] CONTINUE (depth {depth}) – "
|
|
182
|
+
"triggering another autonomous think cycle"
|
|
183
|
+
)
|
|
184
|
+
autonomy_service._eval_depth = depth # type: ignore[attr-defined]
|
|
185
|
+
|
|
186
|
+
# Recurse: trigger another full think → actions → evaluate cycle
|
|
187
|
+
await autonomy_service.perform_autonomous_think()
|
|
188
|
+
|
|
189
|
+
return ActionResult(
|
|
190
|
+
success=True,
|
|
191
|
+
text=f"Continued with additional actions (depth {depth})",
|
|
192
|
+
data={"decision": "CONTINUE", "depth": depth},
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
# PAUSE (or unrecognised → default to pause)
|
|
196
|
+
runtime.logger.info(
|
|
197
|
+
f"[post-action-evaluator] PAUSE after {depth} continuation(s) – agent is satisfied"
|
|
198
|
+
)
|
|
199
|
+
autonomy_service._eval_depth = 0 # type: ignore[attr-defined]
|
|
200
|
+
|
|
201
|
+
return ActionResult(
|
|
202
|
+
success=True,
|
|
203
|
+
text="Agent is satisfied, pausing",
|
|
204
|
+
data={"decision": "PAUSE", "depth": depth},
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
post_action_evaluator = Evaluator(
|
|
209
|
+
name="POST_ACTION_EVALUATOR",
|
|
210
|
+
description=(
|
|
211
|
+
"Evaluates after autonomous actions complete to determine if the agent "
|
|
212
|
+
"should recursively continue with more actions or pause."
|
|
213
|
+
),
|
|
214
|
+
validate=_validate_post_action,
|
|
215
|
+
handler=_handle_post_action,
|
|
216
|
+
always_run=True,
|
|
217
|
+
)
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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,231 @@ 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
|
|
387
|
+
return "(no rooms configured)"
|
|
388
|
+
|
|
295
389
|
target_room_id = self._get_target_room_id()
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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
|
-
|
|
302
|
-
|
|
448
|
+
|
|
449
|
+
# ── Recent-context cutoff: ignore messages older than 1 hour ──
|
|
450
|
+
one_hour_ms = 3_600_000
|
|
451
|
+
now_ms = int(time.time() * 1000)
|
|
452
|
+
cutoff_ms = now_ms - one_hour_ms
|
|
453
|
+
|
|
454
|
+
fetched_messages = [m for m in fetched_messages if (m.created_at or 0) >= cutoff_ms]
|
|
455
|
+
autonomy_memories = [m for m in autonomy_memories if (m.created_at or 0) >= cutoff_ms]
|
|
456
|
+
|
|
457
|
+
external_messages = [
|
|
458
|
+
m
|
|
459
|
+
for m in fetched_messages
|
|
460
|
+
if m and m.entity_id and m.entity_id != self._runtime.agent_id
|
|
461
|
+
]
|
|
462
|
+
entity_name_by_id = await self._build_entity_name_lookup(
|
|
463
|
+
memory.entity_id for memory in external_messages
|
|
303
464
|
)
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
465
|
+
|
|
466
|
+
messages_by_room: dict[UUID, list[Memory]] = {}
|
|
467
|
+
sorted_messages = sorted(
|
|
468
|
+
self._dedupe_memories_by_id_keep_earliest(external_messages),
|
|
469
|
+
key=lambda m: m.created_at or 0,
|
|
470
|
+
reverse=True,
|
|
471
|
+
)
|
|
472
|
+
for memory in sorted_messages:
|
|
473
|
+
bucket = messages_by_room.setdefault(memory.room_id, [])
|
|
474
|
+
if len(bucket) >= per_room_limit:
|
|
308
475
|
continue
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
476
|
+
bucket.append(memory)
|
|
477
|
+
|
|
478
|
+
room_sections: list[str] = []
|
|
479
|
+
for room_id in message_room_ids:
|
|
480
|
+
room_name = room_name_by_id.get(room_id, str(room_id))
|
|
481
|
+
room_messages = list(reversed(messages_by_room.get(room_id, [])))
|
|
482
|
+
if not room_messages:
|
|
483
|
+
room_sections.append(f"Room: {room_name}\n(no recent messages)")
|
|
484
|
+
continue
|
|
485
|
+
|
|
486
|
+
lines: list[str] = []
|
|
487
|
+
for memory in room_messages:
|
|
488
|
+
text = self._memory_text(memory)
|
|
489
|
+
if not text:
|
|
490
|
+
continue
|
|
491
|
+
author = entity_name_by_id.get(memory.entity_id, str(memory.entity_id))
|
|
492
|
+
lines.append(f"{author}: {text}")
|
|
493
|
+
|
|
494
|
+
if lines:
|
|
495
|
+
room_sections.append(f"Room: {room_name}\n" + "\n".join(lines))
|
|
496
|
+
else:
|
|
497
|
+
room_sections.append(f"Room: {room_name}\n(no recent messages)")
|
|
498
|
+
|
|
499
|
+
autonomy_entries: list[str] = []
|
|
500
|
+
for memory in autonomy_memories:
|
|
501
|
+
text = self._memory_text(memory)
|
|
502
|
+
if not text:
|
|
503
|
+
continue
|
|
504
|
+
|
|
505
|
+
metadata_obj = memory.content.data if memory.content else None
|
|
506
|
+
metadata: dict[str, object] = metadata_obj if isinstance(metadata_obj, dict) else {}
|
|
507
|
+
entry_type = metadata.get("type")
|
|
508
|
+
|
|
509
|
+
if memory.entity_id == self._runtime.agent_id and entry_type == "autonomous-response":
|
|
510
|
+
autonomy_entries.append(f"Thought: {text}")
|
|
511
|
+
elif (
|
|
512
|
+
memory.entity_id == self._autonomy_entity_id and entry_type == "autonomous-trigger"
|
|
513
|
+
):
|
|
514
|
+
autonomy_entries.append(f"Trigger: {text}")
|
|
515
|
+
|
|
516
|
+
if autonomy_entries:
|
|
517
|
+
autonomy_section = "Autonomous context:\n" + "\n".join(autonomy_entries)
|
|
518
|
+
else:
|
|
519
|
+
autonomy_section = "Autonomous context: (none)"
|
|
520
|
+
|
|
521
|
+
return "\n\n".join([*room_sections, autonomy_section])
|
|
321
522
|
|
|
322
523
|
async def _settings_monitoring(self) -> None:
|
|
323
524
|
while not self._is_stopped:
|
|
@@ -412,7 +613,7 @@ class AutonomyService(Service):
|
|
|
412
613
|
else self._create_continuous_prompt(last_thought, is_first_thought, target_context)
|
|
413
614
|
)
|
|
414
615
|
|
|
415
|
-
entity_id =
|
|
616
|
+
entity_id = self._autonomy_entity_id
|
|
416
617
|
current_time_ms = int(time.time() * 1000)
|
|
417
618
|
autonomous_message = Memory(
|
|
418
619
|
id=as_uuid(str(uuid.uuid4())),
|
|
@@ -420,6 +621,15 @@ class AutonomyService(Service):
|
|
|
420
621
|
content=Content(
|
|
421
622
|
text=autonomy_prompt,
|
|
422
623
|
source="autonomy-service",
|
|
624
|
+
data={
|
|
625
|
+
"type": "autonomous-prompt",
|
|
626
|
+
"isAutonomous": True,
|
|
627
|
+
"isInternalThought": True,
|
|
628
|
+
"autonomyMode": mode,
|
|
629
|
+
"channelId": "autonomous",
|
|
630
|
+
"timestamp": current_time_ms,
|
|
631
|
+
"isContinuation": not is_first_thought,
|
|
632
|
+
},
|
|
423
633
|
),
|
|
424
634
|
room_id=self._autonomous_room_id,
|
|
425
635
|
agent_id=self._runtime.agent_id,
|
|
@@ -21,6 +21,9 @@ from .autonomy import (
|
|
|
21
21
|
AutonomyService,
|
|
22
22
|
admin_chat_provider,
|
|
23
23
|
autonomy_status_provider,
|
|
24
|
+
disable_autonomy_action,
|
|
25
|
+
enable_autonomy_action,
|
|
26
|
+
post_action_evaluator,
|
|
24
27
|
send_to_admin_action,
|
|
25
28
|
)
|
|
26
29
|
from .types import CapabilityConfig
|
|
@@ -63,6 +66,8 @@ def _get_actions(config: CapabilityConfig) -> list:
|
|
|
63
66
|
result.extend(EXTENDED_ACTIONS)
|
|
64
67
|
if config.enable_autonomy:
|
|
65
68
|
result.append(send_to_admin_action)
|
|
69
|
+
result.append(enable_autonomy_action)
|
|
70
|
+
result.append(disable_autonomy_action)
|
|
66
71
|
return result
|
|
67
72
|
|
|
68
73
|
|
|
@@ -73,6 +78,8 @@ def _get_evaluators(config: CapabilityConfig) -> list:
|
|
|
73
78
|
result.extend(BASIC_EVALUATORS)
|
|
74
79
|
if config.enable_extended:
|
|
75
80
|
result.extend(EXTENDED_EVALUATORS)
|
|
81
|
+
if config.enable_autonomy:
|
|
82
|
+
result.append(post_action_evaluator)
|
|
76
83
|
return result
|
|
77
84
|
|
|
78
85
|
|