@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
@@ -1,8 +1,14 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import TYPE_CHECKING
3
+ import contextlib
4
+ from typing import TYPE_CHECKING, TypeVar
4
5
 
5
6
  from elizaos.action_docs import get_canonical_action_example_calls
7
+ from elizaos.deterministic import (
8
+ build_conversation_seed,
9
+ build_deterministic_seed,
10
+ deterministic_int,
11
+ )
6
12
  from elizaos.generated.spec_helpers import require_provider_spec
7
13
  from elizaos.types import Provider, ProviderResult
8
14
  from elizaos.types.components import ActionExample
@@ -21,10 +27,6 @@ if TYPE_CHECKING:
21
27
  _spec = require_provider_spec("ACTIONS")
22
28
 
23
29
 
24
- def format_action_names(actions: list[Action]) -> str:
25
- return ", ".join(action.name for action in actions)
26
-
27
-
28
30
  def _format_parameter_type(schema: ActionParameterSchema) -> str:
29
31
  if schema.type == "number" and (schema.minimum is not None or schema.maximum is not None):
30
32
  min_val = schema.minimum if schema.minimum is not None else "∞"
@@ -62,9 +64,23 @@ def _format_action_parameters(parameters: list[ActionParameter]) -> str:
62
64
  return "\n".join(lines)
63
65
 
64
66
 
65
- def format_actions(actions: list[Action]) -> str:
67
+ T = TypeVar("T")
68
+
69
+
70
+ def _deterministic_shuffle(items: list[T], seed: str, surface: str = "shuffle") -> list[T]:
71
+ shuffled = list(items)
72
+ for i in range(len(shuffled) - 1, 0, -1):
73
+ j = deterministic_int(seed, f"{surface}:{i}", i + 1)
74
+ shuffled[i], shuffled[j] = shuffled[j], shuffled[i]
75
+ return shuffled
76
+
77
+
78
+ def format_actions(actions: list[Action], seed: str | None = None) -> str:
79
+ deterministic_seed = seed or build_deterministic_seed(
80
+ ["actions-format", ",".join(action.name for action in actions)]
81
+ )
66
82
  lines: list[str] = []
67
- for action in actions:
83
+ for action in _deterministic_shuffle(actions, deterministic_seed, "actions"):
68
84
  line = f"- **{action.name}**: {action.description or 'No description'}"
69
85
  if action.parameters:
70
86
  params_text = _format_action_parameters(action.parameters)
@@ -81,7 +97,24 @@ def _replace_name_placeholders(text: str) -> str:
81
97
  return text
82
98
 
83
99
 
84
- def format_action_examples(actions: list[Action], max_examples: int = 10) -> str:
100
+ def _replace_name_placeholders_seeded(text: str, seed: str, example_index: int) -> str:
101
+ names = ["Alex", "Jordan", "Sam", "Taylor", "Riley"]
102
+ output = text
103
+ for placeholder_index in range(1, 6):
104
+ name_index = deterministic_int(
105
+ seed,
106
+ f"example:{example_index}:name:{placeholder_index}",
107
+ len(names),
108
+ )
109
+ output = output.replace(f"{{{{name{placeholder_index}}}}}", names[name_index])
110
+ return output
111
+
112
+
113
+ def format_action_examples(
114
+ actions: list[Action],
115
+ max_examples: int = 10,
116
+ seed: str | None = None,
117
+ ) -> str:
85
118
  """
86
119
  Format a deterministic subset of action examples for prompt context.
87
120
 
@@ -90,27 +123,60 @@ def format_action_examples(actions: list[Action], max_examples: int = 10) -> str
90
123
  if max_examples <= 0:
91
124
  return ""
92
125
 
93
- examples: list[list[ActionExample]] = []
94
- for action in sorted(actions, key=lambda a: a.name):
95
- if not action.examples:
96
- continue
97
- for ex in action.examples:
98
- if isinstance(ex, list) and ex:
99
- examples.append(ex)
100
- if len(examples) >= max_examples:
101
- break
102
- if len(examples) >= max_examples:
103
- break
104
-
105
- if not examples:
126
+ actions_with_examples = [
127
+ action
128
+ for action in actions
129
+ if action.examples and isinstance(action.examples, list) and len(action.examples) > 0
130
+ ]
131
+ if not actions_with_examples:
106
132
  return ""
107
133
 
134
+ examples_copy: list[list[list[ActionExample]]] = [
135
+ [example for example in (action.examples or []) if isinstance(example, list) and example]
136
+ for action in actions_with_examples
137
+ ]
138
+ available_action_indices = [
139
+ idx for idx, action_examples in enumerate(examples_copy) if action_examples
140
+ ]
141
+
142
+ selection_seed = seed or build_deterministic_seed(
143
+ [
144
+ "action-examples",
145
+ ",".join(action.name for action in actions_with_examples),
146
+ max_examples,
147
+ ]
148
+ )
149
+
150
+ selected_examples: list[list[ActionExample]] = []
151
+ iteration = 0
152
+ while len(selected_examples) < max_examples and available_action_indices:
153
+ random_index = deterministic_int(
154
+ selection_seed,
155
+ f"action-index:{iteration}",
156
+ len(available_action_indices),
157
+ )
158
+ action_index = available_action_indices[random_index]
159
+ action_examples = examples_copy[action_index]
160
+
161
+ example_index = deterministic_int(
162
+ selection_seed,
163
+ f"example-index:{iteration}",
164
+ len(action_examples),
165
+ )
166
+ selected_examples.append(action_examples.pop(example_index))
167
+ iteration += 1
168
+
169
+ if not action_examples:
170
+ available_action_indices.pop(random_index)
171
+
108
172
  blocks: list[str] = []
109
- for ex in examples:
173
+ for example_index, ex in enumerate(selected_examples):
110
174
  lines: list[str] = []
111
175
  for msg in ex:
112
176
  msg_text = msg.content.text if msg.content and msg.content.text else ""
113
- lines.append(f"{msg.name}: {_replace_name_placeholders(msg_text)}")
177
+ lines.append(
178
+ f"{msg.name}: {_replace_name_placeholders_seeded(msg_text, selection_seed, example_index)}"
179
+ )
114
180
  blocks.append("\n".join(lines))
115
181
 
116
182
  return "\n\n".join(blocks)
@@ -180,6 +246,17 @@ def format_action_call_examples(actions: list[Action], max_examples: int = 5) ->
180
246
  return "\n\n".join(blocks)
181
247
 
182
248
 
249
+ def format_action_names(actions: list[Action], seed: str | None = None) -> str:
250
+ if not actions:
251
+ return ""
252
+
253
+ deterministic_seed = seed or build_deterministic_seed(
254
+ ["action-names", ",".join(action.name for action in actions)]
255
+ )
256
+ shuffled = _deterministic_shuffle(actions, deterministic_seed, "actions")
257
+ return ", ".join(action.name for action in shuffled)
258
+
259
+
183
260
  async def get_actions(
184
261
  runtime: IAgentRuntime,
185
262
  message: Memory,
@@ -189,13 +266,27 @@ async def get_actions(
189
266
 
190
267
  for action in runtime.actions:
191
268
  validate_fn = getattr(action, "validate", None) or getattr(action, "validate_fn", None)
192
- is_valid = await validate_fn(runtime, message, state) if validate_fn else True
269
+ if not validate_fn:
270
+ is_valid = True
271
+ else:
272
+ try:
273
+ is_valid = await validate_fn(runtime, message, state)
274
+ except Exception:
275
+ if hasattr(runtime, "logger"):
276
+ with contextlib.suppress(Exception):
277
+ runtime.logger.warning(
278
+ f"Action validation failed for {action.name}; excluding from prompt"
279
+ )
280
+ is_valid = False
193
281
  if is_valid:
194
282
  validated_actions.append(action)
195
283
 
196
- action_names = format_action_names(validated_actions)
197
- actions_text = format_actions(validated_actions)
198
- examples_text = format_action_examples(validated_actions, max_examples=10)
284
+ action_seed = build_conversation_seed(runtime, message, state, "provider:actions")
285
+ action_names = format_action_names(validated_actions, seed=f"{action_seed}:names")
286
+ actions_text = format_actions(validated_actions, seed=f"{action_seed}:descriptions")
287
+ examples_text = format_action_examples(
288
+ validated_actions, max_examples=10, seed=f"{action_seed}:examples"
289
+ )
199
290
  call_examples_text = format_action_call_examples(validated_actions, max_examples=5)
200
291
 
201
292
  text_parts: list[str] = [f"Possible response actions: {action_names}"]
@@ -60,4 +60,5 @@ agent_settings_provider = Provider(
60
60
  description=_spec["description"],
61
61
  get=get_agent_settings_context,
62
62
  dynamic=_spec.get("dynamic", True),
63
+ position=_spec.get("position"),
63
64
  )
@@ -73,4 +73,5 @@ attachments_provider = Provider(
73
73
  description=_spec["description"],
74
74
  get=get_attachments,
75
75
  dynamic=_spec.get("dynamic", True),
76
+ position=_spec.get("position"),
76
77
  )
@@ -63,4 +63,5 @@ capabilities_provider = Provider(
63
63
  description=_spec["description"],
64
64
  get=get_capabilities,
65
65
  dynamic=_spec.get("dynamic", False),
66
+ position=_spec.get("position"),
66
67
  )
@@ -125,4 +125,5 @@ character_provider = Provider(
125
125
  description=_spec["description"],
126
126
  get=get_character_context,
127
127
  dynamic=_spec.get("dynamic", False),
128
+ position=_spec.get("position"),
128
129
  )
@@ -74,4 +74,5 @@ choice_provider = Provider(
74
74
  description=_spec["description"],
75
75
  get=get_choice_options,
76
76
  dynamic=_spec.get("dynamic", True),
77
+ position=_spec.get("position"),
77
78
  )
@@ -75,4 +75,5 @@ contacts_provider = Provider(
75
75
  description=_spec["description"],
76
76
  get=get_contacts_context,
77
77
  dynamic=_spec.get("dynamic", True),
78
+ position=_spec.get("position"),
78
79
  )
@@ -1,8 +1,8 @@
1
1
  from __future__ import annotations
2
2
 
3
- from datetime import UTC, datetime
4
3
  from typing import TYPE_CHECKING
5
4
 
5
+ from elizaos.deterministic import get_prompt_reference_datetime
6
6
  from elizaos.generated.spec_helpers import require_provider_spec
7
7
  from elizaos.types import Provider, ProviderResult
8
8
 
@@ -18,7 +18,12 @@ async def get_current_time_context(
18
18
  message: Memory,
19
19
  state: State | None = None,
20
20
  ) -> ProviderResult:
21
- now = datetime.now(UTC)
21
+ now = get_prompt_reference_datetime(
22
+ runtime,
23
+ message,
24
+ state,
25
+ "provider:current_time",
26
+ )
22
27
 
23
28
  iso_timestamp = now.isoformat()
24
29
  human_readable = now.strftime("%A, %B %d, %Y at %H:%M:%S UTC")
@@ -53,4 +58,5 @@ current_time_provider = Provider(
53
58
  description=_spec["description"],
54
59
  get=get_current_time_context,
55
60
  dynamic=_spec.get("dynamic", True),
61
+ position=_spec.get("position"),
56
62
  )
@@ -96,4 +96,5 @@ entities_provider = Provider(
96
96
  description=_spec["description"],
97
97
  get=get_entities_context,
98
98
  dynamic=_spec.get("dynamic", True),
99
+ position=_spec.get("position"),
99
100
  )
@@ -55,4 +55,5 @@ evaluators_provider = Provider(
55
55
  description=_spec["description"],
56
56
  get=get_evaluators,
57
57
  dynamic=_spec.get("dynamic", False),
58
+ position=_spec.get("position"),
58
59
  )
@@ -83,4 +83,5 @@ facts_provider = Provider(
83
83
  description=_spec["description"],
84
84
  get=get_facts_context,
85
85
  dynamic=_spec.get("dynamic", True),
86
+ position=_spec.get("position"),
86
87
  )
@@ -113,4 +113,5 @@ follow_ups_provider = Provider(
113
113
  description=_spec["description"],
114
114
  get=get_follow_ups_context,
115
115
  dynamic=_spec.get("dynamic", True),
116
+ position=_spec.get("position"),
116
117
  )
@@ -70,4 +70,5 @@ knowledge_provider = Provider(
70
70
  description=_spec["description"],
71
71
  get=get_knowledge_context,
72
72
  dynamic=_spec.get("dynamic", True),
73
+ position=_spec.get("position"),
73
74
  )
@@ -56,4 +56,5 @@ providers_list_provider = Provider(
56
56
  description=_spec["description"],
57
57
  get=get_providers_list,
58
58
  dynamic=_spec.get("dynamic", False),
59
+ position=_spec.get("position"),
59
60
  )
@@ -103,4 +103,5 @@ relationships_provider = Provider(
103
103
  description=_spec["description"],
104
104
  get=get_relationships,
105
105
  dynamic=_spec.get("dynamic", True),
106
+ position=_spec.get("position"),
106
107
  )
@@ -92,4 +92,5 @@ roles_provider = Provider(
92
92
  description=_spec["description"],
93
93
  get=get_roles,
94
94
  dynamic=_spec.get("dynamic", True),
95
+ position=_spec.get("position"),
95
96
  )
@@ -52,4 +52,5 @@ settings_provider = Provider(
52
52
  description=_spec["description"],
53
53
  get=get_settings_context,
54
54
  dynamic=_spec.get("dynamic", True),
55
+ position=_spec.get("position"),
55
56
  )
@@ -1,8 +1,8 @@
1
1
  from __future__ import annotations
2
2
 
3
- from datetime import UTC, datetime
4
3
  from typing import TYPE_CHECKING
5
4
 
5
+ from elizaos.deterministic import get_prompt_reference_datetime
6
6
  from elizaos.generated.spec_helpers import require_provider_spec
7
7
  from elizaos.types import Provider, ProviderResult
8
8
 
@@ -18,9 +18,12 @@ async def get_time_context(
18
18
  message: Memory,
19
19
  state: State | None = None,
20
20
  ) -> ProviderResult:
21
- _ = runtime, message, state
22
-
23
- now = datetime.now(UTC)
21
+ now = get_prompt_reference_datetime(
22
+ runtime,
23
+ message,
24
+ state,
25
+ "provider:time",
26
+ )
24
27
  iso_string = now.isoformat()
25
28
  timestamp_ms = int(now.timestamp() * 1000)
26
29
  human_readable = now.strftime("%A, %B %d, %Y at %H:%M:%S UTC")
@@ -42,4 +45,5 @@ time_provider = Provider(
42
45
  description=_spec["description"],
43
46
  get=get_time_context,
44
47
  dynamic=_spec.get("dynamic", True),
48
+ position=_spec.get("position"),
45
49
  )
@@ -94,4 +94,5 @@ world_provider = Provider(
94
94
  description=_spec["description"],
95
95
  get=get_world_context,
96
96
  dynamic=_spec.get("dynamic", True),
97
+ position=_spec.get("position"),
97
98
  )
@@ -0,0 +1,193 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import UTC, datetime
4
+ from hashlib import sha256
5
+ from typing import Protocol
6
+ from uuid import UUID
7
+
8
+ DEFAULT_TIME_BUCKET_MS = 5 * 60 * 1000
9
+
10
+ SeedPart = str | int | float | bool | None
11
+
12
+
13
+ class RuntimeLike(Protocol):
14
+ @property
15
+ def agent_id(self) -> object: ...
16
+
17
+ @property
18
+ def character(self) -> object: ...
19
+
20
+ def get_setting(self, key: str) -> object | None: ...
21
+
22
+
23
+ def _normalize_seed_part(part: SeedPart) -> str:
24
+ if part is None:
25
+ return "none"
26
+ return str(part)
27
+
28
+
29
+ def _coerce_non_empty_string(value: object | None) -> str | None:
30
+ if value is None:
31
+ return None
32
+ as_text = str(value).strip()
33
+ if not as_text:
34
+ return None
35
+ return as_text
36
+
37
+
38
+ def _get_field(obj: object | None, *names: str) -> object | None:
39
+ if obj is None:
40
+ return None
41
+
42
+ if isinstance(obj, dict):
43
+ for name in names:
44
+ if name in obj:
45
+ value = obj[name]
46
+ if value is not None and value != "":
47
+ return value
48
+ return None
49
+
50
+ for name in names:
51
+ value = getattr(obj, name, None)
52
+ if value is not None and value != "":
53
+ return value
54
+ return None
55
+
56
+
57
+ def build_deterministic_seed(parts: list[SeedPart]) -> str:
58
+ return "|".join(_normalize_seed_part(part) for part in parts)
59
+
60
+
61
+ def deterministic_hex(seed: str, surface: str, length: int = 16) -> str:
62
+ if length <= 0:
63
+ return ""
64
+
65
+ output = ""
66
+ counter = 0
67
+ while len(output) < length:
68
+ payload = f"{seed}|{surface}|{counter}".encode()
69
+ output += sha256(payload).hexdigest()
70
+ counter += 1
71
+ return output[:length]
72
+
73
+
74
+ def deterministic_int(seed: str, surface: str, max_exclusive: int) -> int:
75
+ if max_exclusive <= 1:
76
+ return 0
77
+ value = int(deterministic_hex(seed, surface, 12), 16)
78
+ return value % max_exclusive
79
+
80
+
81
+ def deterministic_uuid(seed: str, surface: str) -> str:
82
+ return str(UUID(hex=deterministic_hex(seed, surface, 32)))
83
+
84
+
85
+ def parse_boolean_setting(value: object | None) -> bool:
86
+ if isinstance(value, bool):
87
+ return value
88
+ if isinstance(value, (int, float)):
89
+ return value != 0
90
+ if isinstance(value, str):
91
+ normalized = value.strip().lower()
92
+ return normalized in ("1", "true", "yes", "on", "enabled")
93
+ return False
94
+
95
+
96
+ def parse_positive_int_setting(value: object | None, fallback: int) -> int:
97
+ if isinstance(value, bool):
98
+ return fallback
99
+ if isinstance(value, (int, float)):
100
+ numeric = int(value)
101
+ return numeric if numeric > 0 else fallback
102
+ if isinstance(value, str):
103
+ try:
104
+ numeric = int(float(value))
105
+ return numeric if numeric > 0 else fallback
106
+ except ValueError:
107
+ return fallback
108
+ return fallback
109
+
110
+
111
+ def build_conversation_seed(
112
+ runtime: RuntimeLike,
113
+ message: object | None,
114
+ state: object | None,
115
+ surface: str,
116
+ *,
117
+ bucket_ms: int | None = None,
118
+ now_ms: int | None = None,
119
+ ) -> str:
120
+ now_ms_value = now_ms if now_ms is not None else int(datetime.now(UTC).timestamp() * 1000)
121
+
122
+ state_data = _get_field(state, "data")
123
+ room_obj = _get_field(state_data, "room")
124
+ world_obj = _get_field(state_data, "world")
125
+
126
+ room_id = (
127
+ _coerce_non_empty_string(_get_field(room_obj, "id"))
128
+ or _coerce_non_empty_string(_get_field(state_data, "room_id", "roomId"))
129
+ or _coerce_non_empty_string(_get_field(message, "room_id", "roomId"))
130
+ or "room:none"
131
+ )
132
+ world_id = (
133
+ _coerce_non_empty_string(_get_field(world_obj, "id"))
134
+ or _coerce_non_empty_string(_get_field(room_obj, "world_id", "worldId"))
135
+ or _coerce_non_empty_string(_get_field(state_data, "world_id", "worldId"))
136
+ or _coerce_non_empty_string(_get_field(message, "world_id", "worldId"))
137
+ or "world:none"
138
+ )
139
+
140
+ character_obj = _get_field(runtime, "character")
141
+ character_id = (
142
+ _coerce_non_empty_string(_get_field(character_obj, "id"))
143
+ or _coerce_non_empty_string(_get_field(runtime, "agent_id"))
144
+ or "agent:none"
145
+ )
146
+
147
+ epoch_bucket = 0
148
+ if bucket_ms and bucket_ms > 0:
149
+ epoch_bucket = now_ms_value // bucket_ms
150
+
151
+ return build_deterministic_seed(
152
+ [
153
+ "eliza-prompt-cache-v1",
154
+ world_id,
155
+ room_id,
156
+ character_id,
157
+ epoch_bucket,
158
+ surface,
159
+ ]
160
+ )
161
+
162
+
163
+ def get_prompt_reference_datetime(
164
+ runtime: RuntimeLike,
165
+ message: object | None,
166
+ state: object | None,
167
+ surface: str,
168
+ *,
169
+ now: datetime | None = None,
170
+ ) -> datetime:
171
+ now_utc = now.astimezone(UTC) if now is not None else datetime.now(UTC)
172
+ deterministic_enabled = parse_boolean_setting(
173
+ runtime.get_setting("PROMPT_CACHE_DETERMINISTIC_TIME")
174
+ )
175
+ if not deterministic_enabled:
176
+ return now_utc
177
+
178
+ bucket_ms = parse_positive_int_setting(
179
+ runtime.get_setting("PROMPT_CACHE_TIME_BUCKET_MS"),
180
+ DEFAULT_TIME_BUCKET_MS,
181
+ )
182
+ now_ms = int(now_utc.timestamp() * 1000)
183
+ seed = build_conversation_seed(
184
+ runtime,
185
+ message,
186
+ state,
187
+ surface,
188
+ bucket_ms=bucket_ms,
189
+ now_ms=now_ms,
190
+ )
191
+ bucket_start = (now_ms // bucket_ms) * bucket_ms
192
+ offset = deterministic_int(seed, "time-offset-ms", bucket_ms)
193
+ return datetime.fromtimestamp((bucket_start + offset) / 1000, tz=UTC)
@@ -0,0 +1 @@
1
+ """Auto-generated module package."""