agnt5 0.2.8a3__cp310-abi3-macosx_11_0_arm64.whl → 0.2.8a4__cp310-abi3-macosx_11_0_arm64.whl
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.
Potentially problematic release.
This version of agnt5 might be problematic. Click here for more details.
- agnt5/__init__.py +2 -4
- agnt5/_core.abi3.so +0 -0
- agnt5/agent.py +177 -107
- agnt5/entity.py +232 -153
- agnt5/worker.py +41 -61
- agnt5/workflow.py +3 -5
- {agnt5-0.2.8a3.dist-info → agnt5-0.2.8a4.dist-info}/METADATA +1 -1
- {agnt5-0.2.8a3.dist-info → agnt5-0.2.8a4.dist-info}/RECORD +9 -10
- agnt5/agent_session.py +0 -110
- {agnt5-0.2.8a3.dist-info → agnt5-0.2.8a4.dist-info}/WHEEL +0 -0
agnt5/__init__.py
CHANGED
|
@@ -7,7 +7,6 @@ with built-in durability guarantees and state management.
|
|
|
7
7
|
|
|
8
8
|
from ._compat import _import_error, _rust_available
|
|
9
9
|
from .agent import Agent, AgentContext, AgentRegistry, AgentResult, Handoff, agent, handoff
|
|
10
|
-
from .agent_session import AgentSession
|
|
11
10
|
from .client import Client, RunError
|
|
12
11
|
from .context import Context
|
|
13
12
|
from .function import FunctionContext
|
|
@@ -15,7 +14,7 @@ from .workflow import WorkflowContext
|
|
|
15
14
|
from .entity import (
|
|
16
15
|
Entity,
|
|
17
16
|
EntityRegistry,
|
|
18
|
-
|
|
17
|
+
EntityStateAdapter,
|
|
19
18
|
EntityType,
|
|
20
19
|
create_entity_context,
|
|
21
20
|
with_entity_context,
|
|
@@ -55,7 +54,7 @@ __all__ = [
|
|
|
55
54
|
"Entity",
|
|
56
55
|
"EntityType",
|
|
57
56
|
"EntityRegistry",
|
|
58
|
-
"
|
|
57
|
+
"EntityStateAdapter",
|
|
59
58
|
"with_entity_context",
|
|
60
59
|
"create_entity_context",
|
|
61
60
|
"workflow",
|
|
@@ -67,7 +66,6 @@ __all__ = [
|
|
|
67
66
|
"Agent",
|
|
68
67
|
"AgentRegistry",
|
|
69
68
|
"AgentResult",
|
|
70
|
-
"AgentSession",
|
|
71
69
|
"Handoff",
|
|
72
70
|
"handoff",
|
|
73
71
|
# Types
|
agnt5/_core.abi3.so
CHANGED
|
Binary file
|
agnt5/agent.py
CHANGED
|
@@ -79,42 +79,46 @@ class AgentContext(Context):
|
|
|
79
79
|
self._agent_name = agent_name
|
|
80
80
|
self._session_id = session_id or run_id
|
|
81
81
|
|
|
82
|
-
# Determine state
|
|
83
|
-
from .entity import
|
|
82
|
+
# Determine state adapter based on parent context
|
|
83
|
+
from .entity import EntityStateAdapter, _get_state_adapter
|
|
84
84
|
|
|
85
85
|
if state_manager:
|
|
86
|
-
# Explicit state
|
|
87
|
-
self.
|
|
88
|
-
logger.debug(f"AgentContext using provided state
|
|
86
|
+
# Explicit state adapter provided (parameter name kept for backward compat)
|
|
87
|
+
self._state_adapter = state_manager
|
|
88
|
+
logger.debug(f"AgentContext using provided state adapter")
|
|
89
89
|
elif parent_context:
|
|
90
|
-
# Try to inherit state
|
|
90
|
+
# Try to inherit state adapter from parent
|
|
91
91
|
try:
|
|
92
92
|
# Check if parent is WorkflowContext or AgentContext
|
|
93
93
|
if hasattr(parent_context, '_workflow_entity'):
|
|
94
|
-
# WorkflowContext - get state
|
|
95
|
-
self.
|
|
94
|
+
# WorkflowContext - get state adapter from worker context
|
|
95
|
+
self._state_adapter = _get_state_adapter()
|
|
96
96
|
logger.debug(f"AgentContext inheriting state from WorkflowContext")
|
|
97
|
-
elif hasattr(parent_context, '
|
|
98
|
-
# Parent AgentContext - share state
|
|
99
|
-
self.
|
|
97
|
+
elif hasattr(parent_context, '_state_adapter'):
|
|
98
|
+
# Parent AgentContext - share state adapter
|
|
99
|
+
self._state_adapter = parent_context._state_adapter
|
|
100
100
|
logger.debug(f"AgentContext inheriting state from parent AgentContext")
|
|
101
|
+
elif hasattr(parent_context, '_state_manager'):
|
|
102
|
+
# Backward compatibility: parent has old _state_manager
|
|
103
|
+
self._state_adapter = parent_context._state_manager
|
|
104
|
+
logger.debug(f"AgentContext inheriting state from parent (legacy)")
|
|
101
105
|
else:
|
|
102
|
-
# FunctionContext or base Context - create new state
|
|
103
|
-
self.
|
|
104
|
-
logger.debug(f"AgentContext created new state
|
|
106
|
+
# FunctionContext or base Context - create new state adapter
|
|
107
|
+
self._state_adapter = EntityStateAdapter()
|
|
108
|
+
logger.debug(f"AgentContext created new state adapter (parent has no state)")
|
|
105
109
|
except RuntimeError as e:
|
|
106
|
-
#
|
|
107
|
-
self.
|
|
108
|
-
logger.debug(f"AgentContext created standalone state
|
|
110
|
+
# _get_state_adapter() failed (not in worker context) - create standalone
|
|
111
|
+
self._state_adapter = EntityStateAdapter()
|
|
112
|
+
logger.debug(f"AgentContext created standalone state adapter (not in worker context)")
|
|
109
113
|
else:
|
|
110
114
|
# Try to get from worker context first
|
|
111
115
|
try:
|
|
112
|
-
self.
|
|
113
|
-
logger.debug(f"AgentContext got state
|
|
116
|
+
self._state_adapter = _get_state_adapter()
|
|
117
|
+
logger.debug(f"AgentContext got state adapter from worker context")
|
|
114
118
|
except RuntimeError as e:
|
|
115
|
-
# Standalone - create new state
|
|
116
|
-
self.
|
|
117
|
-
logger.debug(f"AgentContext created standalone state
|
|
119
|
+
# Standalone - create new state adapter
|
|
120
|
+
self._state_adapter = EntityStateAdapter()
|
|
121
|
+
logger.debug(f"AgentContext created standalone state adapter")
|
|
118
122
|
|
|
119
123
|
# Conversation key for state storage (used for in-memory state)
|
|
120
124
|
self._conversation_key = f"agent:{agent_name}:{self._session_id}:messages"
|
|
@@ -127,24 +131,23 @@ class AgentContext(Context):
|
|
|
127
131
|
"""
|
|
128
132
|
Get state interface for agent state management.
|
|
129
133
|
|
|
134
|
+
Note: This is a simplified in-memory state interface for agent-specific data.
|
|
135
|
+
Conversation history is managed separately via get_conversation_history() and
|
|
136
|
+
save_conversation_history() which use the Rust-backed persistence layer.
|
|
137
|
+
|
|
130
138
|
Returns:
|
|
131
|
-
|
|
139
|
+
Dict-like object for state operations
|
|
132
140
|
|
|
133
141
|
Example:
|
|
134
|
-
# Store
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
ctx.state.set(f"agent:{agent_name}:{session_id}:messages", messages)
|
|
138
|
-
|
|
139
|
-
# Store agent-specific data
|
|
140
|
-
ctx.state.set("research_results", data)
|
|
142
|
+
# Store agent-specific data (in-memory only)
|
|
143
|
+
ctx.state["research_results"] = data
|
|
144
|
+
ctx.state["iteration_count"] = 5
|
|
141
145
|
"""
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
return EntityState(state_dict)
|
|
146
|
+
# Simple dict-based state for agent-specific data
|
|
147
|
+
# This is in-memory only and not persisted to platform
|
|
148
|
+
if not hasattr(self, '_agent_state'):
|
|
149
|
+
self._agent_state = {}
|
|
150
|
+
return self._agent_state
|
|
148
151
|
|
|
149
152
|
@property
|
|
150
153
|
def session_id(self) -> str:
|
|
@@ -155,45 +158,30 @@ class AgentContext(Context):
|
|
|
155
158
|
"""
|
|
156
159
|
Retrieve conversation history from state, loading from database if needed.
|
|
157
160
|
|
|
161
|
+
Uses the EntityStateAdapter which delegates to Rust core for cache-first loading.
|
|
162
|
+
|
|
158
163
|
Returns:
|
|
159
164
|
List of Message objects from conversation history
|
|
160
165
|
"""
|
|
161
|
-
# Try to load from database first if not in memory
|
|
162
166
|
entity_type = "AgentSession"
|
|
163
|
-
entity_key = self._entity_key
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
messages_data = session_data["messages"]
|
|
182
|
-
elif isinstance(session_data, list):
|
|
183
|
-
# Old format - just messages array
|
|
184
|
-
messages_data = session_data
|
|
185
|
-
else:
|
|
186
|
-
messages_data = []
|
|
187
|
-
|
|
188
|
-
# Load messages into in-memory state (store just messages, not full session object)
|
|
189
|
-
self._state_manager.load_state_from_platform(state_key, json.dumps(messages_data), version)
|
|
190
|
-
logger.info(f"Loaded conversation history from database: {entity_key} (version {version})")
|
|
191
|
-
except Exception as e:
|
|
192
|
-
logger.warning(f"Failed to load conversation history from database: {e}")
|
|
193
|
-
|
|
194
|
-
# Get from in-memory state
|
|
195
|
-
messages_data = self.state.get(self._conversation_key, [])
|
|
196
|
-
logger.debug(f"Loaded {len(messages_data)} messages from conversation history")
|
|
167
|
+
entity_key = self._entity_key
|
|
168
|
+
|
|
169
|
+
# Load session data via adapter (Rust handles cache + platform load)
|
|
170
|
+
session_data = await self._state_adapter.load_state(entity_type, entity_key)
|
|
171
|
+
|
|
172
|
+
# Extract messages from session object
|
|
173
|
+
if isinstance(session_data, dict) and "messages" in session_data:
|
|
174
|
+
# New format with session metadata
|
|
175
|
+
messages_data = session_data["messages"]
|
|
176
|
+
logger.debug(f"Loaded {len(messages_data)} messages from session {entity_key}")
|
|
177
|
+
elif isinstance(session_data, list):
|
|
178
|
+
# Old format - just messages array
|
|
179
|
+
messages_data = session_data
|
|
180
|
+
logger.debug(f"Loaded {len(messages_data)} messages (legacy format)")
|
|
181
|
+
else:
|
|
182
|
+
# No messages found
|
|
183
|
+
messages_data = []
|
|
184
|
+
logger.debug(f"No conversation history found for {entity_key}")
|
|
197
185
|
|
|
198
186
|
# Convert dict representations back to Message objects
|
|
199
187
|
messages = []
|
|
@@ -221,6 +209,8 @@ class AgentContext(Context):
|
|
|
221
209
|
"""
|
|
222
210
|
Save conversation history to state and persist to database.
|
|
223
211
|
|
|
212
|
+
Uses the EntityStateAdapter which delegates to Rust core for version-checked saves.
|
|
213
|
+
|
|
224
214
|
Args:
|
|
225
215
|
messages: List of Message objects to persist
|
|
226
216
|
"""
|
|
@@ -234,46 +224,126 @@ class AgentContext(Context):
|
|
|
234
224
|
"content": msg.content
|
|
235
225
|
})
|
|
236
226
|
|
|
237
|
-
|
|
238
|
-
|
|
227
|
+
import time
|
|
228
|
+
entity_type = "AgentSession"
|
|
229
|
+
entity_key = self._entity_key
|
|
239
230
|
|
|
240
|
-
#
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
import time
|
|
245
|
-
entity_type = "AgentSession"
|
|
246
|
-
entity_key = self._entity_key # Use entity_key without :messages suffix
|
|
247
|
-
state_key = (entity_type, entity_key)
|
|
248
|
-
|
|
249
|
-
# Get current version
|
|
250
|
-
expected_version = self._state_manager._versions.get(state_key, 0)
|
|
251
|
-
|
|
252
|
-
# Build session object with metadata for the sessions API
|
|
253
|
-
now = time.time()
|
|
254
|
-
session_data = {
|
|
255
|
-
"session_id": self._session_id,
|
|
256
|
-
"agent_name": self._agent_name,
|
|
257
|
-
"created_at": now if expected_version == 0 else None, # Only set on first save
|
|
258
|
-
"last_message_time": now,
|
|
259
|
-
"message_count": len(messages_data),
|
|
260
|
-
"messages": messages_data,
|
|
261
|
-
"metadata": {}
|
|
262
|
-
}
|
|
231
|
+
# Load current state with version for optimistic locking
|
|
232
|
+
current_state, current_version = await self._state_adapter.load_with_version(
|
|
233
|
+
entity_type, entity_key
|
|
234
|
+
)
|
|
263
235
|
|
|
264
|
-
|
|
265
|
-
|
|
236
|
+
# Build session object with metadata
|
|
237
|
+
now = time.time()
|
|
266
238
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
239
|
+
# Get custom metadata from instance variable or preserve from loaded state
|
|
240
|
+
custom_metadata = getattr(self, '_custom_metadata', current_state.get("metadata", {}))
|
|
241
|
+
|
|
242
|
+
session_data = {
|
|
243
|
+
"session_id": self._session_id,
|
|
244
|
+
"agent_name": self._agent_name,
|
|
245
|
+
"created_at": current_state.get("created_at", now), # Preserve existing or set new
|
|
246
|
+
"last_message_time": now,
|
|
247
|
+
"message_count": len(messages_data),
|
|
248
|
+
"messages": messages_data,
|
|
249
|
+
"metadata": custom_metadata # Save custom metadata
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
# Save to platform via adapter (Rust handles optimistic locking)
|
|
253
|
+
try:
|
|
254
|
+
new_version = await self._state_adapter.save_state(
|
|
255
|
+
entity_type,
|
|
256
|
+
entity_key,
|
|
257
|
+
session_data,
|
|
258
|
+
current_version
|
|
259
|
+
)
|
|
260
|
+
logger.info(
|
|
261
|
+
f"Persisted conversation history: {entity_key} (version {current_version} -> {new_version})"
|
|
262
|
+
)
|
|
263
|
+
except Exception as e:
|
|
264
|
+
logger.error(f"Failed to persist conversation history to database: {e}")
|
|
265
|
+
# Don't fail - conversation is still in memory for this execution
|
|
266
|
+
|
|
267
|
+
async def get_metadata(self) -> Dict[str, Any]:
|
|
268
|
+
"""
|
|
269
|
+
Get conversation session metadata.
|
|
270
|
+
|
|
271
|
+
Returns session metadata including:
|
|
272
|
+
- created_at: Timestamp of first message (float, Unix timestamp)
|
|
273
|
+
- last_activity: Timestamp of last message (float, Unix timestamp)
|
|
274
|
+
- message_count: Number of messages in conversation (int)
|
|
275
|
+
- custom: Dict of user-provided custom metadata
|
|
271
276
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
+
Returns:
|
|
278
|
+
Dictionary with metadata. If no conversation exists yet, returns defaults.
|
|
279
|
+
|
|
280
|
+
Example:
|
|
281
|
+
```python
|
|
282
|
+
metadata = await context.get_metadata()
|
|
283
|
+
print(f"Session created: {metadata['created_at']}")
|
|
284
|
+
print(f"User ID: {metadata['custom'].get('user_id')}")
|
|
285
|
+
```
|
|
286
|
+
"""
|
|
287
|
+
entity_type = "AgentSession"
|
|
288
|
+
entity_key = self._entity_key
|
|
289
|
+
|
|
290
|
+
# Load session data
|
|
291
|
+
session_data = await self._state_adapter.load_state(entity_type, entity_key)
|
|
292
|
+
|
|
293
|
+
if not session_data:
|
|
294
|
+
# No conversation exists yet - return defaults
|
|
295
|
+
return {
|
|
296
|
+
"created_at": None,
|
|
297
|
+
"last_activity": None,
|
|
298
|
+
"message_count": 0,
|
|
299
|
+
"custom": getattr(self, '_custom_metadata', {})
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
messages = session_data.get("messages", [])
|
|
303
|
+
|
|
304
|
+
# Derive timestamps from messages if available
|
|
305
|
+
created_at = session_data.get("created_at")
|
|
306
|
+
last_activity = session_data.get("last_message_time")
|
|
307
|
+
|
|
308
|
+
return {
|
|
309
|
+
"created_at": created_at,
|
|
310
|
+
"last_activity": last_activity,
|
|
311
|
+
"message_count": len(messages),
|
|
312
|
+
"custom": session_data.get("metadata", {})
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
def update_metadata(self, **kwargs) -> None:
|
|
316
|
+
"""
|
|
317
|
+
Update custom session metadata.
|
|
318
|
+
|
|
319
|
+
Metadata will be persisted alongside conversation history on next save.
|
|
320
|
+
Use this to store application-specific data like user_id, preferences, etc.
|
|
321
|
+
|
|
322
|
+
Args:
|
|
323
|
+
**kwargs: Key-value pairs to store as metadata
|
|
324
|
+
|
|
325
|
+
Example:
|
|
326
|
+
```python
|
|
327
|
+
# Store user identification and preferences
|
|
328
|
+
context.update_metadata(
|
|
329
|
+
user_id="user-123",
|
|
330
|
+
subscription_tier="premium",
|
|
331
|
+
preferences={"theme": "dark", "language": "en"}
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
# Later retrieve it
|
|
335
|
+
metadata = await context.get_metadata()
|
|
336
|
+
user_id = metadata["custom"]["user_id"]
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
Note:
|
|
340
|
+
- Metadata is merged with existing metadata (doesn't replace)
|
|
341
|
+
- Changes persist on next save_conversation_history() call
|
|
342
|
+
- Use simple JSON-serializable types (str, int, float, dict, list)
|
|
343
|
+
"""
|
|
344
|
+
if not hasattr(self, '_custom_metadata'):
|
|
345
|
+
self._custom_metadata = {}
|
|
346
|
+
self._custom_metadata.update(kwargs)
|
|
277
347
|
|
|
278
348
|
|
|
279
349
|
class Handoff:
|
agnt5/entity.py
CHANGED
|
@@ -15,156 +15,245 @@ from ._telemetry import setup_module_logger
|
|
|
15
15
|
|
|
16
16
|
logger = setup_module_logger(__name__)
|
|
17
17
|
|
|
18
|
-
# Context variable for worker-scoped state
|
|
18
|
+
# Context variable for worker-scoped state adapter
|
|
19
19
|
# This is set by Worker before entity execution and accessed by Entity instances
|
|
20
|
-
|
|
21
|
-
contextvars.ContextVar('
|
|
20
|
+
_entity_state_adapter_ctx: contextvars.ContextVar[Optional["EntityStateAdapter"]] = \
|
|
21
|
+
contextvars.ContextVar('_entity_state_adapter', default=None)
|
|
22
22
|
|
|
23
23
|
# Global entity registry
|
|
24
24
|
_ENTITY_REGISTRY: Dict[str, "EntityType"] = {}
|
|
25
25
|
|
|
26
26
|
|
|
27
|
-
class
|
|
27
|
+
class EntityStateAdapter:
|
|
28
28
|
"""
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
This
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
29
|
+
Thin Python adapter providing Pythonic interface to Rust EntityStateManager core.
|
|
30
|
+
|
|
31
|
+
This adapter provides language-specific concerns only:
|
|
32
|
+
- Worker-local asyncio.Lock for coarse-grained coordination
|
|
33
|
+
- Type conversions between Python dict and JSON bytes
|
|
34
|
+
- Pythonic async/await API over Rust core
|
|
35
|
+
|
|
36
|
+
All business logic (caching, version tracking, retry logic, gRPC) lives in the Rust core.
|
|
37
|
+
This keeps the Python layer simple (~150 LOC) and enables sharing business logic across SDKs.
|
|
38
38
|
"""
|
|
39
39
|
|
|
40
|
-
def __init__(self,
|
|
40
|
+
def __init__(self, rust_core=None):
|
|
41
41
|
"""
|
|
42
|
-
Initialize
|
|
42
|
+
Initialize entity state adapter.
|
|
43
43
|
|
|
44
44
|
Args:
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
rust_core: Rust EntityStateManager instance (from _core module).
|
|
46
|
+
If None, operates in standalone/testing mode with in-memory state.
|
|
47
47
|
"""
|
|
48
|
-
self.
|
|
49
|
-
|
|
50
|
-
self.
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
48
|
+
self._rust_core = rust_core
|
|
49
|
+
# Worker-local locks for coarse-grained coordination within this worker
|
|
50
|
+
self._local_locks: Dict[Tuple[str, str], asyncio.Lock] = {}
|
|
51
|
+
|
|
52
|
+
# Standalone mode: in-memory state storage when no Rust core
|
|
53
|
+
# This enables testing without the full platform stack
|
|
54
|
+
if rust_core is None:
|
|
55
|
+
self._standalone_states: Dict[Tuple[str, str], Dict[str, Any]] = {}
|
|
56
|
+
self._standalone_versions: Dict[Tuple[str, str], int] = {}
|
|
57
|
+
logger.debug("Created EntityStateAdapter in standalone mode (in-memory state)")
|
|
58
|
+
else:
|
|
59
|
+
logger.debug("Created EntityStateAdapter with Rust core")
|
|
60
|
+
|
|
61
|
+
def get_local_lock(self, state_key: Tuple[str, str]) -> asyncio.Lock:
|
|
55
62
|
"""
|
|
56
|
-
Get
|
|
63
|
+
Get worker-local asyncio.Lock for single-writer guarantee within this worker.
|
|
64
|
+
|
|
65
|
+
This provides coarse-grained coordination for operations within the same worker.
|
|
66
|
+
Cross-worker conflicts are handled by the Rust core via optimistic concurrency.
|
|
57
67
|
|
|
58
68
|
Args:
|
|
59
69
|
state_key: Tuple of (entity_type, entity_key)
|
|
60
70
|
|
|
61
71
|
Returns:
|
|
62
|
-
|
|
72
|
+
asyncio.Lock for this worker-local operation
|
|
63
73
|
"""
|
|
64
|
-
if state_key not in self.
|
|
65
|
-
self.
|
|
66
|
-
return self.
|
|
74
|
+
if state_key not in self._local_locks:
|
|
75
|
+
self._local_locks[state_key] = asyncio.Lock()
|
|
76
|
+
return self._local_locks[state_key]
|
|
67
77
|
|
|
68
|
-
def
|
|
78
|
+
async def load_state(self, entity_type: str, entity_key: str) -> Dict[str, Any]:
|
|
69
79
|
"""
|
|
70
|
-
|
|
80
|
+
Load entity state (Rust handles cache-first logic and platform load).
|
|
81
|
+
|
|
82
|
+
In standalone mode (no Rust core), uses in-memory state storage.
|
|
71
83
|
|
|
72
84
|
Args:
|
|
73
|
-
|
|
85
|
+
entity_type: Type of entity (e.g., "ShoppingCart", "Counter")
|
|
86
|
+
entity_key: Unique key for entity instance
|
|
74
87
|
|
|
75
88
|
Returns:
|
|
76
|
-
|
|
89
|
+
State dictionary (empty dict if not found)
|
|
77
90
|
"""
|
|
78
|
-
if
|
|
79
|
-
|
|
80
|
-
|
|
91
|
+
if not self._rust_core:
|
|
92
|
+
# Standalone mode - return from in-memory storage
|
|
93
|
+
state_key = (entity_type, entity_key)
|
|
94
|
+
return self._standalone_states.get(state_key, {}).copy()
|
|
81
95
|
|
|
82
|
-
|
|
96
|
+
try:
|
|
97
|
+
# Rust checks cache first, loads from platform if needed
|
|
98
|
+
state_json_bytes, version = await self._rust_core.py_get_cached_or_load(entity_type, entity_key)
|
|
99
|
+
|
|
100
|
+
# Convert bytes to dict
|
|
101
|
+
if state_json_bytes:
|
|
102
|
+
state_json = state_json_bytes.decode('utf-8') if isinstance(state_json_bytes, bytes) else state_json_bytes
|
|
103
|
+
return json.loads(state_json)
|
|
104
|
+
else:
|
|
105
|
+
return {}
|
|
106
|
+
except Exception as e:
|
|
107
|
+
logger.warning(f"Failed to load state for {entity_type}:{entity_key}: {e}")
|
|
108
|
+
return {}
|
|
109
|
+
|
|
110
|
+
async def save_state(
|
|
83
111
|
self,
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
112
|
+
entity_type: str,
|
|
113
|
+
entity_key: str,
|
|
114
|
+
state: Dict[str, Any],
|
|
115
|
+
expected_version: int
|
|
116
|
+
) -> int:
|
|
88
117
|
"""
|
|
89
|
-
|
|
118
|
+
Save entity state (Rust handles version check and platform save).
|
|
119
|
+
|
|
120
|
+
In standalone mode (no Rust core), stores in-memory with version tracking.
|
|
90
121
|
|
|
91
122
|
Args:
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
123
|
+
entity_type: Type of entity
|
|
124
|
+
entity_key: Unique key for entity instance
|
|
125
|
+
state: State dictionary to save
|
|
126
|
+
expected_version: Expected current version (for optimistic locking)
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
New version number after save
|
|
130
|
+
|
|
131
|
+
Raises:
|
|
132
|
+
RuntimeError: If version conflict or platform error
|
|
95
133
|
"""
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
self.
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
self._states[state_key] = {}
|
|
107
|
-
self._versions[state_key] = 0
|
|
134
|
+
if not self._rust_core:
|
|
135
|
+
# Standalone mode - store in memory with version tracking
|
|
136
|
+
state_key = (entity_type, entity_key)
|
|
137
|
+
current_version = self._standalone_versions.get(state_key, 0)
|
|
138
|
+
|
|
139
|
+
# Optimistic locking check (even in standalone mode for consistency)
|
|
140
|
+
if current_version != expected_version:
|
|
141
|
+
raise RuntimeError(
|
|
142
|
+
f"Version conflict: expected {expected_version}, got {current_version}"
|
|
143
|
+
)
|
|
108
144
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
145
|
+
# Store state and increment version
|
|
146
|
+
new_version = expected_version + 1
|
|
147
|
+
self._standalone_states[state_key] = state.copy()
|
|
148
|
+
self._standalone_versions[state_key] = new_version
|
|
149
|
+
return new_version
|
|
150
|
+
|
|
151
|
+
# Convert dict to JSON bytes
|
|
152
|
+
state_json = json.dumps(state).encode('utf-8')
|
|
153
|
+
|
|
154
|
+
# Rust handles optimistic locking and platform save
|
|
155
|
+
new_version = await self._rust_core.py_save_state(
|
|
156
|
+
entity_type,
|
|
157
|
+
entity_key,
|
|
158
|
+
state_json,
|
|
159
|
+
expected_version
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
return new_version
|
|
163
|
+
|
|
164
|
+
async def load_with_version(self, entity_type: str, entity_key: str) -> Tuple[Dict[str, Any], int]:
|
|
113
165
|
"""
|
|
114
|
-
|
|
166
|
+
Load entity state with version (for update operations).
|
|
167
|
+
|
|
168
|
+
In standalone mode (no Rust core), loads from in-memory storage with version.
|
|
115
169
|
|
|
116
170
|
Args:
|
|
117
|
-
|
|
171
|
+
entity_type: Type of entity
|
|
172
|
+
entity_key: Unique key for entity instance
|
|
118
173
|
|
|
119
174
|
Returns:
|
|
120
|
-
Tuple of (state_dict,
|
|
175
|
+
Tuple of (state_dict, version)
|
|
176
|
+
"""
|
|
177
|
+
if not self._rust_core:
|
|
178
|
+
# Standalone mode - return from in-memory storage with version
|
|
179
|
+
state_key = (entity_type, entity_key)
|
|
180
|
+
state = self._standalone_states.get(state_key, {}).copy()
|
|
181
|
+
version = self._standalone_versions.get(state_key, 0)
|
|
182
|
+
return state, version
|
|
183
|
+
|
|
184
|
+
try:
|
|
185
|
+
state_json_bytes, version = await self._rust_core.py_get_cached_or_load(entity_type, entity_key)
|
|
186
|
+
|
|
187
|
+
if state_json_bytes:
|
|
188
|
+
state_json = state_json_bytes.decode('utf-8') if isinstance(state_json_bytes, bytes) else state_json_bytes
|
|
189
|
+
state = json.loads(state_json)
|
|
190
|
+
else:
|
|
191
|
+
state = {}
|
|
192
|
+
|
|
193
|
+
return state, version
|
|
194
|
+
except Exception as e:
|
|
195
|
+
logger.warning(f"Failed to load state with version for {entity_type}:{entity_key}: {e}")
|
|
196
|
+
return {}, 0
|
|
197
|
+
|
|
198
|
+
async def invalidate_cache(self, entity_type: str, entity_key: str) -> None:
|
|
121
199
|
"""
|
|
122
|
-
|
|
123
|
-
expected_version = self._versions.get(state_key, 0)
|
|
124
|
-
new_version = expected_version + 1
|
|
200
|
+
Invalidate cache entry for specific entity.
|
|
125
201
|
|
|
126
|
-
|
|
127
|
-
|
|
202
|
+
Args:
|
|
203
|
+
entity_type: Type of entity
|
|
204
|
+
entity_key: Unique key for entity instance
|
|
205
|
+
"""
|
|
206
|
+
if self._rust_core:
|
|
207
|
+
await self._rust_core.py_invalidate_cache(entity_type, entity_key)
|
|
128
208
|
|
|
129
|
-
|
|
209
|
+
async def clear_cache(self) -> None:
|
|
210
|
+
"""Clear entire cache (useful for testing)."""
|
|
211
|
+
if self._rust_core:
|
|
212
|
+
await self._rust_core.py_clear_cache()
|
|
130
213
|
|
|
131
214
|
def clear_all(self) -> None:
|
|
132
|
-
"""Clear all
|
|
133
|
-
self.
|
|
134
|
-
|
|
135
|
-
self._versions.clear()
|
|
136
|
-
logger.debug("Cleared EntityStateManager")
|
|
215
|
+
"""Clear all local locks (for testing)."""
|
|
216
|
+
self._local_locks.clear()
|
|
217
|
+
logger.debug("Cleared EntityStateAdapter local locks")
|
|
137
218
|
|
|
138
|
-
def get_state(self, entity_type: str, key: str) -> Optional[Dict[str, Any]]:
|
|
219
|
+
async def get_state(self, entity_type: str, key: str) -> Optional[Dict[str, Any]]:
|
|
139
220
|
"""Get state for debugging/testing."""
|
|
140
|
-
|
|
141
|
-
return
|
|
221
|
+
state, _ = await self.load_with_version(entity_type, key)
|
|
222
|
+
return state if state else None
|
|
142
223
|
|
|
143
224
|
def get_all_keys(self, entity_type: str) -> list[str]:
|
|
144
|
-
"""
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
225
|
+
"""
|
|
226
|
+
Get all keys for an entity type (testing/debugging only).
|
|
227
|
+
|
|
228
|
+
Only works in standalone mode. Returns empty list in production mode.
|
|
229
|
+
"""
|
|
230
|
+
if not hasattr(self, '_standalone_states'):
|
|
231
|
+
return []
|
|
232
|
+
|
|
233
|
+
keys = []
|
|
234
|
+
for (etype, ekey) in self._standalone_states.keys():
|
|
235
|
+
if etype == entity_type:
|
|
236
|
+
keys.append(ekey)
|
|
237
|
+
return keys
|
|
149
238
|
|
|
150
239
|
|
|
151
|
-
def
|
|
240
|
+
def _get_state_adapter() -> EntityStateAdapter:
|
|
152
241
|
"""
|
|
153
|
-
Get the current entity state
|
|
242
|
+
Get the current entity state adapter from context.
|
|
154
243
|
|
|
155
|
-
The state
|
|
244
|
+
The state adapter must be set by Worker before entity execution.
|
|
156
245
|
This ensures proper worker-scoped state isolation.
|
|
157
246
|
|
|
158
247
|
Returns:
|
|
159
|
-
|
|
248
|
+
EntityStateAdapter instance
|
|
160
249
|
|
|
161
250
|
Raises:
|
|
162
|
-
RuntimeError: If called outside of Worker context (state
|
|
251
|
+
RuntimeError: If called outside of Worker context (state adapter not set)
|
|
163
252
|
"""
|
|
164
|
-
|
|
165
|
-
if
|
|
253
|
+
adapter = _entity_state_adapter_ctx.get()
|
|
254
|
+
if adapter is None:
|
|
166
255
|
raise RuntimeError(
|
|
167
|
-
"Entity requires state
|
|
256
|
+
"Entity requires state adapter context.\n\n"
|
|
168
257
|
"In production:\n"
|
|
169
258
|
" Entities run automatically through Worker.\n\n"
|
|
170
259
|
"In tests, use one of:\n"
|
|
@@ -179,7 +268,9 @@ def _get_state_manager() -> EntityStateManager:
|
|
|
179
268
|
" await cart.add_item(...)\n\n"
|
|
180
269
|
"See: https://docs.agnt5.dev/sdk/entities#testing"
|
|
181
270
|
)
|
|
182
|
-
return
|
|
271
|
+
return adapter
|
|
272
|
+
|
|
273
|
+
|
|
183
274
|
|
|
184
275
|
|
|
185
276
|
# ============================================================================
|
|
@@ -188,7 +279,7 @@ def _get_state_manager() -> EntityStateManager:
|
|
|
188
279
|
|
|
189
280
|
def with_entity_context(func):
|
|
190
281
|
"""
|
|
191
|
-
Decorator that sets up entity state
|
|
282
|
+
Decorator that sets up entity state adapter for tests.
|
|
192
283
|
|
|
193
284
|
Usage:
|
|
194
285
|
@with_entity_context
|
|
@@ -199,13 +290,13 @@ def with_entity_context(func):
|
|
|
199
290
|
"""
|
|
200
291
|
@functools.wraps(func)
|
|
201
292
|
async def wrapper(*args, **kwargs):
|
|
202
|
-
|
|
203
|
-
token =
|
|
293
|
+
adapter = EntityStateAdapter()
|
|
294
|
+
token = _entity_state_adapter_ctx.set(adapter)
|
|
204
295
|
try:
|
|
205
296
|
return await func(*args, **kwargs)
|
|
206
297
|
finally:
|
|
207
|
-
|
|
208
|
-
|
|
298
|
+
_entity_state_adapter_ctx.reset(token)
|
|
299
|
+
adapter.clear_all()
|
|
209
300
|
return wrapper
|
|
210
301
|
|
|
211
302
|
|
|
@@ -219,16 +310,16 @@ def create_entity_context():
|
|
|
219
310
|
|
|
220
311
|
@pytest.fixture
|
|
221
312
|
def entity_context():
|
|
222
|
-
|
|
223
|
-
yield
|
|
313
|
+
adapter, token = create_entity_context()
|
|
314
|
+
yield adapter
|
|
224
315
|
# Cleanup happens automatically
|
|
225
316
|
|
|
226
317
|
Returns:
|
|
227
|
-
Tuple of (
|
|
318
|
+
Tuple of (EntityStateAdapter, context_token)
|
|
228
319
|
"""
|
|
229
|
-
|
|
230
|
-
token =
|
|
231
|
-
return
|
|
320
|
+
adapter = EntityStateAdapter()
|
|
321
|
+
token = _entity_state_adapter_ctx.set(adapter)
|
|
322
|
+
return adapter, token
|
|
232
323
|
|
|
233
324
|
|
|
234
325
|
def extract_state_schema(entity_class: type) -> Optional[Dict[str, Any]]:
|
|
@@ -473,12 +564,12 @@ def _create_entity_method_wrapper(entity_type: str, method):
|
|
|
473
564
|
"""
|
|
474
565
|
Create a wrapper for an entity method that provides single-writer consistency.
|
|
475
566
|
|
|
476
|
-
This wrapper:
|
|
477
|
-
1.
|
|
478
|
-
2.
|
|
479
|
-
3.
|
|
480
|
-
4.
|
|
481
|
-
5.
|
|
567
|
+
This wrapper implements hybrid locking:
|
|
568
|
+
1. Local lock (asyncio.Lock) for worker-scoped single-writer guarantee
|
|
569
|
+
2. Optimistic concurrency (via Rust) for cross-worker conflicts
|
|
570
|
+
3. Loads state via adapter (Rust handles cache + platform)
|
|
571
|
+
4. Executes the method with clean EntityState interface
|
|
572
|
+
5. Saves state via adapter (Rust handles version check + retry)
|
|
482
573
|
|
|
483
574
|
Args:
|
|
484
575
|
entity_type: Name of the entity type (class name)
|
|
@@ -489,36 +580,23 @@ def _create_entity_method_wrapper(entity_type: str, method):
|
|
|
489
580
|
"""
|
|
490
581
|
@functools.wraps(method)
|
|
491
582
|
async def entity_method_wrapper(self, *args, **kwargs):
|
|
492
|
-
"""Execute entity method with
|
|
583
|
+
"""Execute entity method with hybrid locking (local + optimistic)."""
|
|
493
584
|
state_key = (entity_type, self._key)
|
|
494
585
|
|
|
495
|
-
# Get state
|
|
496
|
-
|
|
497
|
-
|
|
586
|
+
# Get state adapter
|
|
587
|
+
adapter = _get_state_adapter()
|
|
588
|
+
|
|
589
|
+
# Local lock for worker-scoped single-writer guarantee
|
|
590
|
+
lock = adapter.get_local_lock(state_key)
|
|
498
591
|
|
|
499
592
|
async with lock:
|
|
500
|
-
# Load state
|
|
501
|
-
|
|
502
|
-
try:
|
|
503
|
-
# Call Rust manager to load state from platform
|
|
504
|
-
result = await state_manager._rust_manager.py_load_state(
|
|
505
|
-
entity_type, self._key
|
|
506
|
-
)
|
|
507
|
-
# result is a tuple: (found, state_json, version)
|
|
508
|
-
found, state_json_bytes, version = result
|
|
509
|
-
if found:
|
|
510
|
-
# Decode and load state
|
|
511
|
-
state_json = state_json_bytes.decode('utf-8') if isinstance(state_json_bytes, bytes) else state_json_bytes
|
|
512
|
-
state_manager.load_state_from_platform(
|
|
513
|
-
state_key, state_json, version
|
|
514
|
-
)
|
|
515
|
-
logger.info(f"Loaded entity state from platform: {entity_type}:{self._key} (version {version})")
|
|
516
|
-
except Exception as e:
|
|
517
|
-
logger.warning(f"Failed to load entity state from platform: {e}")
|
|
518
|
-
# Continue with empty state
|
|
593
|
+
# Load state with version (Rust handles cache-first + platform load)
|
|
594
|
+
state_dict, current_version = await adapter.load_with_version(entity_type, self._key)
|
|
519
595
|
|
|
520
|
-
|
|
521
|
-
|
|
596
|
+
logger.debug(
|
|
597
|
+
"Loaded state for %s:%s (version %d)",
|
|
598
|
+
entity_type, self._key, current_version
|
|
599
|
+
)
|
|
522
600
|
|
|
523
601
|
# Set up EntityState on instance for method access
|
|
524
602
|
self._state = EntityState(state_dict)
|
|
@@ -529,25 +607,26 @@ def _create_entity_method_wrapper(entity_type: str, method):
|
|
|
529
607
|
result = await method(self, *args, **kwargs)
|
|
530
608
|
logger.debug("Completed %s:%s.%s", entity_type, self._key, method.__name__)
|
|
531
609
|
|
|
532
|
-
# Save state
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
610
|
+
# Save state after successful execution
|
|
611
|
+
# Rust handles optimistic locking (version check)
|
|
612
|
+
try:
|
|
613
|
+
new_version = await adapter.save_state(
|
|
614
|
+
entity_type,
|
|
615
|
+
self._key,
|
|
616
|
+
state_dict,
|
|
617
|
+
current_version
|
|
618
|
+
)
|
|
619
|
+
logger.info(
|
|
620
|
+
"Saved state for %s:%s (version %d -> %d)",
|
|
621
|
+
entity_type, self._key, current_version, new_version
|
|
622
|
+
)
|
|
623
|
+
except Exception as e:
|
|
624
|
+
logger.error(
|
|
625
|
+
"Failed to save state for %s:%s: %s",
|
|
626
|
+
entity_type, self._key, e
|
|
627
|
+
)
|
|
628
|
+
# Don't fail the method execution just because persistence failed
|
|
629
|
+
# The state is still in the local dict for this execution
|
|
551
630
|
|
|
552
631
|
return result
|
|
553
632
|
|
agnt5/worker.py
CHANGED
|
@@ -126,13 +126,16 @@ class Worker:
|
|
|
126
126
|
self.runtime = runtime
|
|
127
127
|
self.metadata = metadata or {}
|
|
128
128
|
|
|
129
|
+
# Get tenant_id from environment (required for entity state management)
|
|
130
|
+
import os
|
|
131
|
+
self._tenant_id = os.getenv("AGNT5_TENANT_ID", "default-tenant")
|
|
132
|
+
|
|
129
133
|
# Import Rust worker
|
|
130
134
|
try:
|
|
131
|
-
from ._core import PyWorker, PyWorkerConfig, PyComponentInfo
|
|
135
|
+
from ._core import PyWorker, PyWorkerConfig, PyComponentInfo
|
|
132
136
|
self._PyWorker = PyWorker
|
|
133
137
|
self._PyWorkerConfig = PyWorkerConfig
|
|
134
138
|
self._PyComponentInfo = PyComponentInfo
|
|
135
|
-
self._RustEntityStateManager = RustEntityStateManager
|
|
136
139
|
except ImportError as e:
|
|
137
140
|
raise ImportError(
|
|
138
141
|
f"Failed to import Rust core worker: {e}. "
|
|
@@ -149,16 +152,17 @@ class Worker:
|
|
|
149
152
|
# Create Rust worker instance
|
|
150
153
|
self._rust_worker = self._PyWorker(self._rust_config)
|
|
151
154
|
|
|
152
|
-
#
|
|
153
|
-
import
|
|
154
|
-
|
|
155
|
+
# Create worker-scoped entity state adapter with Rust core
|
|
156
|
+
from .entity import EntityStateAdapter
|
|
157
|
+
from ._core import EntityStateManager as RustEntityStateManager
|
|
158
|
+
|
|
159
|
+
# Create Rust core for entity state management
|
|
160
|
+
rust_core = RustEntityStateManager(tenant_id=self._tenant_id)
|
|
155
161
|
|
|
156
|
-
# Create
|
|
157
|
-
self.
|
|
162
|
+
# Create Python adapter (thin wrapper around Rust core)
|
|
163
|
+
self._entity_state_adapter = EntityStateAdapter(rust_core=rust_core)
|
|
158
164
|
|
|
159
|
-
|
|
160
|
-
from .entity import EntityStateManager
|
|
161
|
-
self._entity_state_manager = EntityStateManager(rust_entity_state_manager=self._rust_entity_state_manager)
|
|
165
|
+
logger.info("Created EntityStateAdapter with Rust core for state management")
|
|
162
166
|
|
|
163
167
|
# Component registration: auto-discover or explicit
|
|
164
168
|
if auto_register:
|
|
@@ -754,7 +758,7 @@ class Worker:
|
|
|
754
758
|
"""Execute a workflow handler with automatic replay support."""
|
|
755
759
|
import json
|
|
756
760
|
from .workflow import WorkflowEntity, WorkflowContext
|
|
757
|
-
from .entity import
|
|
761
|
+
from .entity import _get_state_adapter
|
|
758
762
|
from ._core import PyExecuteComponentResponse
|
|
759
763
|
|
|
760
764
|
try:
|
|
@@ -795,10 +799,15 @@ class Worker:
|
|
|
795
799
|
logger.debug(f"Loaded {len(completed_steps)} completed steps into workflow entity")
|
|
796
800
|
|
|
797
801
|
if initial_state:
|
|
798
|
-
# Load initial state into entity's state
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
+
# Load initial state into entity's state adapter
|
|
803
|
+
state_adapter = _get_state_adapter()
|
|
804
|
+
if hasattr(state_adapter, '_standalone_states'):
|
|
805
|
+
# Standalone mode - set state directly
|
|
806
|
+
state_adapter._standalone_states[workflow_entity._state_key] = initial_state
|
|
807
|
+
logger.debug(f"Loaded initial state with {len(initial_state)} keys into workflow entity (standalone)")
|
|
808
|
+
else:
|
|
809
|
+
# Production mode - state is managed by Rust core
|
|
810
|
+
logger.debug(f"Initial state will be loaded from platform (production mode)")
|
|
802
811
|
|
|
803
812
|
# Create WorkflowContext with entity and runtime_context for trace correlation
|
|
804
813
|
ctx = WorkflowContext(
|
|
@@ -933,11 +942,11 @@ class Worker:
|
|
|
933
942
|
"""Execute an entity method."""
|
|
934
943
|
import json
|
|
935
944
|
from .context import Context
|
|
936
|
-
from .entity import EntityType, Entity,
|
|
945
|
+
from .entity import EntityType, Entity, _entity_state_adapter_ctx
|
|
937
946
|
from ._core import PyExecuteComponentResponse
|
|
938
947
|
|
|
939
|
-
# Set entity state
|
|
940
|
-
|
|
948
|
+
# Set entity state adapter in context for Entity instances to access
|
|
949
|
+
_entity_state_adapter_ctx.set(self._entity_state_adapter)
|
|
941
950
|
|
|
942
951
|
try:
|
|
943
952
|
# Parse input data
|
|
@@ -952,23 +961,8 @@ class Worker:
|
|
|
952
961
|
if not method_name:
|
|
953
962
|
raise ValueError("Entity invocation requires 'method' parameter")
|
|
954
963
|
|
|
955
|
-
#
|
|
956
|
-
|
|
957
|
-
if hasattr(request, 'metadata') and request.metadata:
|
|
958
|
-
if "entity_state" in request.metadata:
|
|
959
|
-
platform_state_json = request.metadata["entity_state"]
|
|
960
|
-
platform_version = int(request.metadata.get("state_version", "0"))
|
|
961
|
-
|
|
962
|
-
# Load platform state into state manager
|
|
963
|
-
self._entity_state_manager.load_state_from_platform(
|
|
964
|
-
state_key,
|
|
965
|
-
platform_state_json,
|
|
966
|
-
platform_version
|
|
967
|
-
)
|
|
968
|
-
logger.info(
|
|
969
|
-
f"Loaded entity state from platform: {entity_type.name}/{entity_key} "
|
|
970
|
-
f"(version {platform_version})"
|
|
971
|
-
)
|
|
964
|
+
# Note: State loading is now handled automatically by the entity method wrapper
|
|
965
|
+
# via EntityStateAdapter which uses the Rust core for cache + platform persistence
|
|
972
966
|
|
|
973
967
|
# Create entity instance using the stored class reference
|
|
974
968
|
entity_instance = entity_type.entity_class(key=entity_key)
|
|
@@ -979,32 +973,15 @@ class Worker:
|
|
|
979
973
|
|
|
980
974
|
method = getattr(entity_instance, method_name)
|
|
981
975
|
|
|
982
|
-
# Execute method
|
|
976
|
+
# Execute method (entity method wrapper handles state load/save automatically)
|
|
983
977
|
result = await method(**input_dict)
|
|
984
978
|
|
|
985
979
|
# Serialize result
|
|
986
980
|
output_data = json.dumps(result).encode("utf-8")
|
|
987
981
|
|
|
988
|
-
#
|
|
989
|
-
|
|
990
|
-
self._entity_state_manager.get_state_for_persistence(state_key)
|
|
991
|
-
|
|
982
|
+
# Note: State persistence is now handled automatically by the entity method wrapper
|
|
983
|
+
# via EntityStateAdapter which uses Rust core for optimistic locking + version tracking
|
|
992
984
|
metadata = {}
|
|
993
|
-
if state_dict:
|
|
994
|
-
# Serialize state as JSON string for platform persistence
|
|
995
|
-
state_json = json.dumps(state_dict)
|
|
996
|
-
# Pass in metadata for Worker Coordinator to publish
|
|
997
|
-
metadata = {
|
|
998
|
-
"entity_state": state_json,
|
|
999
|
-
"entity_type": entity_type.name,
|
|
1000
|
-
"entity_key": entity_key,
|
|
1001
|
-
"expected_version": str(expected_version),
|
|
1002
|
-
"new_version": str(new_version),
|
|
1003
|
-
}
|
|
1004
|
-
logger.info(
|
|
1005
|
-
f"Captured entity state: {entity_type.name}/{entity_key} "
|
|
1006
|
-
f"(version {expected_version} → {new_version})"
|
|
1007
|
-
)
|
|
1008
985
|
|
|
1009
986
|
return PyExecuteComponentResponse(
|
|
1010
987
|
invocation_id=request.invocation_id,
|
|
@@ -1039,11 +1016,11 @@ class Worker:
|
|
|
1039
1016
|
import json
|
|
1040
1017
|
import uuid
|
|
1041
1018
|
from .agent import AgentContext
|
|
1042
|
-
from .entity import
|
|
1019
|
+
from .entity import _entity_state_adapter_ctx
|
|
1043
1020
|
from ._core import PyExecuteComponentResponse
|
|
1044
1021
|
|
|
1045
|
-
# Set entity state
|
|
1046
|
-
|
|
1022
|
+
# Set entity state adapter in context so AgentContext can access it
|
|
1023
|
+
_entity_state_adapter_ctx.set(self._entity_state_adapter)
|
|
1047
1024
|
|
|
1048
1025
|
try:
|
|
1049
1026
|
# Parse input data
|
|
@@ -1157,9 +1134,12 @@ class Worker:
|
|
|
1157
1134
|
if self.metadata:
|
|
1158
1135
|
self._rust_worker.set_service_metadata(self.metadata)
|
|
1159
1136
|
|
|
1160
|
-
#
|
|
1161
|
-
logger.info("Configuring
|
|
1162
|
-
|
|
1137
|
+
# Configure entity state manager on Rust worker for database persistence
|
|
1138
|
+
logger.info("Configuring Rust EntityStateManager for database persistence")
|
|
1139
|
+
# Access the Rust core from the adapter
|
|
1140
|
+
if hasattr(self._entity_state_adapter, '_rust_core') and self._entity_state_adapter._rust_core:
|
|
1141
|
+
self._rust_worker.set_entity_state_manager(self._entity_state_adapter._rust_core)
|
|
1142
|
+
logger.info("Successfully configured Rust EntityStateManager")
|
|
1163
1143
|
|
|
1164
1144
|
# Get the current event loop to pass to Rust for concurrent Python async execution
|
|
1165
1145
|
# This allows Rust to execute Python async functions on the same event loop
|
agnt5/workflow.py
CHANGED
|
@@ -11,7 +11,7 @@ from typing import Any, Callable, Dict, Optional, TypeVar, cast
|
|
|
11
11
|
|
|
12
12
|
from ._schema_utils import extract_function_metadata, extract_function_schemas
|
|
13
13
|
from .context import Context
|
|
14
|
-
from .entity import Entity, EntityState,
|
|
14
|
+
from .entity import Entity, EntityState, _get_state_adapter
|
|
15
15
|
from .function import FunctionContext
|
|
16
16
|
from .types import HandlerFunc, WorkflowConfig
|
|
17
17
|
from ._telemetry import setup_module_logger
|
|
@@ -346,10 +346,8 @@ class WorkflowEntity(Entity):
|
|
|
346
346
|
for debugging and replay of AI workflows.
|
|
347
347
|
"""
|
|
348
348
|
if self._state is None:
|
|
349
|
-
#
|
|
350
|
-
|
|
351
|
-
state_dict = state_manager.get_or_create_state(self._state_key)
|
|
352
|
-
self._state = WorkflowState(state_dict, self)
|
|
349
|
+
# Initialize with empty state dict - will be populated by entity system
|
|
350
|
+
self._state = WorkflowState({}, self)
|
|
353
351
|
return self._state
|
|
354
352
|
|
|
355
353
|
|
|
@@ -1,16 +1,15 @@
|
|
|
1
|
-
agnt5-0.2.
|
|
2
|
-
agnt5-0.2.
|
|
3
|
-
agnt5/__init__.py,sha256=
|
|
1
|
+
agnt5-0.2.8a4.dist-info/METADATA,sha256=Dp8yJo_dx5B79OM0ptGgR80TjCUbyovpSc4Pt57dfuY,996
|
|
2
|
+
agnt5-0.2.8a4.dist-info/WHEEL,sha256=-lwEpi49KOTCcgx48T3fLSP8Dxynwa-iRMZNo-JZaqc,103
|
|
3
|
+
agnt5/__init__.py,sha256=ACkK91EPdnv5tYip09QCZ9rfV4iBKzNjGfYVLJD1XGg,2045
|
|
4
4
|
agnt5/_compat.py,sha256=BGuy3v5VDOHVa5f3Z-C22iMN19lAt0mPmXwF3qSSWxI,369
|
|
5
|
-
agnt5/_core.abi3.so,sha256=
|
|
5
|
+
agnt5/_core.abi3.so,sha256=zjKPxbDlFR0otveH4mWRbL_O6hWq6vXDDFj-WY10xRs,12598432
|
|
6
6
|
agnt5/_retry_utils.py,sha256=loHsWY5BR4wZy57IzcDEjQAy88DHVwVIr25Cn1d9GPA,5801
|
|
7
7
|
agnt5/_schema_utils.py,sha256=MR67RW757T4Oq2Jqf4kB61H_b51zwaf3CLWELnkngRo,9572
|
|
8
8
|
agnt5/_telemetry.py,sha256=bIY9AvBRjJBTHoBPbfR6X1OgaiUf-T0vCoi0_snsWXA,5957
|
|
9
|
-
agnt5/agent.py,sha256=
|
|
10
|
-
agnt5/agent_session.py,sha256=mZ61mVax6Wt1ArocwdVEx4Kju6veFYtqxtSNWeKOve0,3161
|
|
9
|
+
agnt5/agent.py,sha256=aBrhtPaUAHOHv3-h_Yb2UMqFHertr1P2hJ7fA_4IXcw,43225
|
|
11
10
|
agnt5/client.py,sha256=kXksazgxdVXWaG9OkjJA4cWruNtcS-ENhtnkrIdw-Nk,23212
|
|
12
11
|
agnt5/context.py,sha256=S2OzPkhn_jnqSWfT21mSYOux8vHaLKQxcAvggZDHQek,2378
|
|
13
|
-
agnt5/entity.py,sha256=
|
|
12
|
+
agnt5/entity.py,sha256=AlHmSHVxQD5EYBvkmERKUkwv0ERrKaT8rvRK611hv_I,28941
|
|
14
13
|
agnt5/exceptions.py,sha256=mZ0q-NK6OKhYxgwBJpIbgpgzk-CJaFIHDbp1EE-pS7I,925
|
|
15
14
|
agnt5/function.py,sha256=f1vaAlJRwuo8cxCOGEd8XPido00mOhlPS8UJJx-6hJI,11041
|
|
16
15
|
agnt5/lm.py,sha256=9dFjd6eQ3f3lFZe7H7rWZherYiP_58MT1F5xpwD8PCg,23195
|
|
@@ -18,6 +17,6 @@ agnt5/tool.py,sha256=uc4L-Q9QyLzQDe-MZKk2Wo3o5e-mK8tfaQwVDgQdouQ,13133
|
|
|
18
17
|
agnt5/tracing.py,sha256=Mh2-OfnQM61lM_P8gxJstafdsUA8Gxoo1lP-Joxhub8,5980
|
|
19
18
|
agnt5/types.py,sha256=Zb71ZMwvrt1p4SH18cAKunp2y5tao_W5_jGYaPDejQo,2840
|
|
20
19
|
agnt5/version.py,sha256=rOq1mObLihnnKgKqBrwZA0zwOPudEKVFcW1a48ynkqc,573
|
|
21
|
-
agnt5/worker.py,sha256=
|
|
22
|
-
agnt5/workflow.py,sha256=
|
|
23
|
-
agnt5-0.2.
|
|
20
|
+
agnt5/worker.py,sha256=NflbueeL2LT8NGywTQnEv1r-N8f54AENWcZARJ5wO8o,47975
|
|
21
|
+
agnt5/workflow.py,sha256=3s9CY6a4UkJZ9YyHv2SAkY3UeCVBlfVi7jxJMFi8Dhg,19488
|
|
22
|
+
agnt5-0.2.8a4.dist-info/RECORD,,
|
agnt5/agent_session.py
DELETED
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
"""Agent session management for multi-turn conversations.
|
|
2
|
-
|
|
3
|
-
Provides AgentSession entity to track and persist conversation metadata
|
|
4
|
-
across multiple agent invocations. Session state is backed by the entity
|
|
5
|
-
infrastructure for durability.
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
from __future__ import annotations
|
|
9
|
-
|
|
10
|
-
import time
|
|
11
|
-
from typing import Optional, Dict, Any
|
|
12
|
-
|
|
13
|
-
from .entity import Entity
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
class AgentSession(Entity):
|
|
17
|
-
"""
|
|
18
|
-
Entity representing an agent conversation session.
|
|
19
|
-
|
|
20
|
-
Tracks metadata about multi-turn conversations including:
|
|
21
|
-
- Session creation and last activity timestamps
|
|
22
|
-
- Message count
|
|
23
|
-
- Agent identification
|
|
24
|
-
- Custom metadata
|
|
25
|
-
|
|
26
|
-
The actual conversation messages are stored separately in AgentContext
|
|
27
|
-
using the session_id as the key. This entity only tracks session metadata.
|
|
28
|
-
|
|
29
|
-
Example:
|
|
30
|
-
```python
|
|
31
|
-
# Create new session
|
|
32
|
-
session = AgentSession("session-123")
|
|
33
|
-
session.create(agent_name="tutor", metadata={"user_id": "user-456"})
|
|
34
|
-
|
|
35
|
-
# Update on each message
|
|
36
|
-
session.add_message()
|
|
37
|
-
|
|
38
|
-
# Get session summary
|
|
39
|
-
summary = session.get_summary()
|
|
40
|
-
```
|
|
41
|
-
"""
|
|
42
|
-
|
|
43
|
-
def __init__(self, session_id: str):
|
|
44
|
-
"""
|
|
45
|
-
Initialize agent session entity.
|
|
46
|
-
|
|
47
|
-
Args:
|
|
48
|
-
session_id: Unique session identifier
|
|
49
|
-
"""
|
|
50
|
-
self.session_id = session_id
|
|
51
|
-
self.agent_name: str = ""
|
|
52
|
-
self.created_at: float = 0.0
|
|
53
|
-
self.last_message_time: float = 0.0
|
|
54
|
-
self.message_count: int = 0
|
|
55
|
-
self.metadata: Dict[str, Any] = {}
|
|
56
|
-
|
|
57
|
-
def create(self, agent_name: str, metadata: Optional[Dict[str, Any]] = None) -> None:
|
|
58
|
-
"""
|
|
59
|
-
Initialize a new session.
|
|
60
|
-
|
|
61
|
-
Args:
|
|
62
|
-
agent_name: Name of the agent for this session
|
|
63
|
-
metadata: Optional additional session metadata
|
|
64
|
-
"""
|
|
65
|
-
current_time = time.time()
|
|
66
|
-
self.agent_name = agent_name
|
|
67
|
-
self.created_at = current_time
|
|
68
|
-
self.last_message_time = current_time
|
|
69
|
-
self.message_count = 0
|
|
70
|
-
self.metadata = metadata or {}
|
|
71
|
-
|
|
72
|
-
def add_message(self) -> None:
|
|
73
|
-
"""
|
|
74
|
-
Record a new message in the session.
|
|
75
|
-
|
|
76
|
-
Updates message count and last activity timestamp.
|
|
77
|
-
"""
|
|
78
|
-
self.message_count += 1
|
|
79
|
-
self.last_message_time = time.time()
|
|
80
|
-
|
|
81
|
-
def get_summary(self) -> Dict[str, Any]:
|
|
82
|
-
"""
|
|
83
|
-
Get session metadata summary.
|
|
84
|
-
|
|
85
|
-
Returns:
|
|
86
|
-
Dictionary with session metadata including:
|
|
87
|
-
- session_id
|
|
88
|
-
- agent_name
|
|
89
|
-
- created_at
|
|
90
|
-
- last_message_time
|
|
91
|
-
- message_count
|
|
92
|
-
- metadata
|
|
93
|
-
"""
|
|
94
|
-
return {
|
|
95
|
-
"session_id": self.session_id,
|
|
96
|
-
"agent_name": self.agent_name,
|
|
97
|
-
"created_at": self.created_at,
|
|
98
|
-
"last_message_time": self.last_message_time,
|
|
99
|
-
"message_count": self.message_count,
|
|
100
|
-
"metadata": self.metadata,
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
def update_metadata(self, metadata: Dict[str, Any]) -> None:
|
|
104
|
-
"""
|
|
105
|
-
Update session metadata.
|
|
106
|
-
|
|
107
|
-
Args:
|
|
108
|
-
metadata: Metadata to merge into existing metadata
|
|
109
|
-
"""
|
|
110
|
-
self.metadata.update(metadata)
|
|
File without changes
|