agnt5 0.2.8a2__cp310-abi3-macosx_10_12_x86_64.whl → 0.2.8a4__cp310-abi3-macosx_10_12_x86_64.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 -2
- agnt5/_core.abi3.so +0 -0
- agnt5/agent.py +202 -48
- agnt5/entity.py +233 -135
- agnt5/worker.py +70 -56
- agnt5/workflow.py +3 -5
- {agnt5-0.2.8a2.dist-info → agnt5-0.2.8a4.dist-info}/METADATA +1 -1
- {agnt5-0.2.8a2.dist-info → agnt5-0.2.8a4.dist-info}/RECORD +9 -9
- {agnt5-0.2.8a2.dist-info → agnt5-0.2.8a4.dist-info}/WHEEL +0 -0
agnt5/__init__.py
CHANGED
|
@@ -14,7 +14,7 @@ from .workflow import WorkflowContext
|
|
|
14
14
|
from .entity import (
|
|
15
15
|
Entity,
|
|
16
16
|
EntityRegistry,
|
|
17
|
-
|
|
17
|
+
EntityStateAdapter,
|
|
18
18
|
EntityType,
|
|
19
19
|
create_entity_context,
|
|
20
20
|
with_entity_context,
|
|
@@ -54,7 +54,7 @@ __all__ = [
|
|
|
54
54
|
"Entity",
|
|
55
55
|
"EntityType",
|
|
56
56
|
"EntityRegistry",
|
|
57
|
-
"
|
|
57
|
+
"EntityStateAdapter",
|
|
58
58
|
"with_entity_context",
|
|
59
59
|
"create_entity_context",
|
|
60
60
|
"workflow",
|
agnt5/_core.abi3.so
CHANGED
|
Binary file
|
agnt5/agent.py
CHANGED
|
@@ -79,78 +79,109 @@ 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
|
|
105
|
-
except RuntimeError:
|
|
106
|
-
#
|
|
107
|
-
self.
|
|
108
|
-
logger.debug(f"AgentContext created standalone 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)")
|
|
109
|
+
except RuntimeError as e:
|
|
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
|
-
#
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
114
|
+
# Try to get from worker context first
|
|
115
|
+
try:
|
|
116
|
+
self._state_adapter = _get_state_adapter()
|
|
117
|
+
logger.debug(f"AgentContext got state adapter from worker context")
|
|
118
|
+
except RuntimeError as e:
|
|
119
|
+
# Standalone - create new state adapter
|
|
120
|
+
self._state_adapter = EntityStateAdapter()
|
|
121
|
+
logger.debug(f"AgentContext created standalone state adapter")
|
|
122
|
+
|
|
123
|
+
# Conversation key for state storage (used for in-memory state)
|
|
115
124
|
self._conversation_key = f"agent:{agent_name}:{self._session_id}:messages"
|
|
125
|
+
# Entity key for database persistence (without :messages suffix to match API expectations)
|
|
126
|
+
self._entity_key = f"agent:{agent_name}:{self._session_id}"
|
|
127
|
+
logger.debug(f"AgentContext initialized - session_id={self._session_id}")
|
|
116
128
|
|
|
117
129
|
@property
|
|
118
130
|
def state(self):
|
|
119
131
|
"""
|
|
120
132
|
Get state interface for agent state management.
|
|
121
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
|
+
|
|
122
138
|
Returns:
|
|
123
|
-
|
|
139
|
+
Dict-like object for state operations
|
|
124
140
|
|
|
125
141
|
Example:
|
|
126
|
-
# Store
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
ctx.state.set(f"agent:{agent_name}:{session_id}:messages", messages)
|
|
130
|
-
|
|
131
|
-
# Store agent-specific data
|
|
132
|
-
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
|
|
133
145
|
"""
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
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
|
|
140
151
|
|
|
141
152
|
@property
|
|
142
153
|
def session_id(self) -> str:
|
|
143
154
|
"""Get session identifier for this agent context."""
|
|
144
155
|
return self._session_id
|
|
145
156
|
|
|
146
|
-
def get_conversation_history(self) -> List[Message]:
|
|
157
|
+
async def get_conversation_history(self) -> List[Message]:
|
|
147
158
|
"""
|
|
148
|
-
Retrieve conversation history from state.
|
|
159
|
+
Retrieve conversation history from state, loading from database if needed.
|
|
160
|
+
|
|
161
|
+
Uses the EntityStateAdapter which delegates to Rust core for cache-first loading.
|
|
149
162
|
|
|
150
163
|
Returns:
|
|
151
164
|
List of Message objects from conversation history
|
|
152
165
|
"""
|
|
153
|
-
|
|
166
|
+
entity_type = "AgentSession"
|
|
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}")
|
|
154
185
|
|
|
155
186
|
# Convert dict representations back to Message objects
|
|
156
187
|
messages = []
|
|
@@ -174,13 +205,17 @@ class AgentContext(Context):
|
|
|
174
205
|
|
|
175
206
|
return messages
|
|
176
207
|
|
|
177
|
-
def save_conversation_history(self, messages: List[Message]) -> None:
|
|
208
|
+
async def save_conversation_history(self, messages: List[Message]) -> None:
|
|
178
209
|
"""
|
|
179
|
-
Save conversation history to state.
|
|
210
|
+
Save conversation history to state and persist to database.
|
|
211
|
+
|
|
212
|
+
Uses the EntityStateAdapter which delegates to Rust core for version-checked saves.
|
|
180
213
|
|
|
181
214
|
Args:
|
|
182
215
|
messages: List of Message objects to persist
|
|
183
216
|
"""
|
|
217
|
+
logger.debug(f"Saving {len(messages)} messages to conversation history")
|
|
218
|
+
|
|
184
219
|
# Convert Message objects to dict for JSON serialization
|
|
185
220
|
messages_data = []
|
|
186
221
|
for msg in messages:
|
|
@@ -189,7 +224,126 @@ class AgentContext(Context):
|
|
|
189
224
|
"content": msg.content
|
|
190
225
|
})
|
|
191
226
|
|
|
192
|
-
|
|
227
|
+
import time
|
|
228
|
+
entity_type = "AgentSession"
|
|
229
|
+
entity_key = self._entity_key
|
|
230
|
+
|
|
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
|
+
)
|
|
235
|
+
|
|
236
|
+
# Build session object with metadata
|
|
237
|
+
now = time.time()
|
|
238
|
+
|
|
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
|
|
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)
|
|
193
347
|
|
|
194
348
|
|
|
195
349
|
class Handoff:
|
|
@@ -665,11 +819,11 @@ class Agent:
|
|
|
665
819
|
|
|
666
820
|
# Load conversation history from state (if AgentContext)
|
|
667
821
|
if isinstance(context, AgentContext):
|
|
668
|
-
messages: List[Message] = context.get_conversation_history()
|
|
822
|
+
messages: List[Message] = await context.get_conversation_history()
|
|
669
823
|
# Add new user message
|
|
670
824
|
messages.append(Message.user(user_message))
|
|
671
825
|
# Save updated conversation
|
|
672
|
-
context.save_conversation_history(messages)
|
|
826
|
+
await context.save_conversation_history(messages)
|
|
673
827
|
else:
|
|
674
828
|
# Fallback for non-AgentContext (shouldn't happen with code above)
|
|
675
829
|
messages = [Message.user(user_message)]
|
|
@@ -795,7 +949,7 @@ class Agent:
|
|
|
795
949
|
)
|
|
796
950
|
# Save conversation before returning
|
|
797
951
|
if isinstance(context, AgentContext):
|
|
798
|
-
context.save_conversation_history(messages)
|
|
952
|
+
await context.save_conversation_history(messages)
|
|
799
953
|
# Return immediately with handoff result
|
|
800
954
|
return AgentResult(
|
|
801
955
|
output=result["output"],
|
|
@@ -835,7 +989,7 @@ class Agent:
|
|
|
835
989
|
self.logger.debug(f"Agent completed after {iteration + 1} iterations")
|
|
836
990
|
# Save conversation before returning
|
|
837
991
|
if isinstance(context, AgentContext):
|
|
838
|
-
context.save_conversation_history(messages)
|
|
992
|
+
await context.save_conversation_history(messages)
|
|
839
993
|
return AgentResult(
|
|
840
994
|
output=response.text,
|
|
841
995
|
tool_calls=all_tool_calls,
|
|
@@ -847,7 +1001,7 @@ class Agent:
|
|
|
847
1001
|
final_output = messages[-1].content if messages else "No output generated"
|
|
848
1002
|
# Save conversation before returning
|
|
849
1003
|
if isinstance(context, AgentContext):
|
|
850
|
-
context.save_conversation_history(messages)
|
|
1004
|
+
await context.save_conversation_history(messages)
|
|
851
1005
|
return AgentResult(
|
|
852
1006
|
output=final_output,
|
|
853
1007
|
tool_calls=all_tool_calls,
|
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 []
|
|
149
232
|
|
|
233
|
+
keys = []
|
|
234
|
+
for (etype, ekey) in self._standalone_states.keys():
|
|
235
|
+
if etype == entity_type:
|
|
236
|
+
keys.append(ekey)
|
|
237
|
+
return keys
|
|
150
238
|
|
|
151
|
-
|
|
239
|
+
|
|
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,26 +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
|
-
#
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
# state_key, result.state_json, result.version
|
|
508
|
-
# )
|
|
509
|
-
|
|
510
|
-
# Get or create state for this entity instance
|
|
511
|
-
state_dict = state_manager.get_or_create_state(state_key)
|
|
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)
|
|
595
|
+
|
|
596
|
+
logger.debug(
|
|
597
|
+
"Loaded state for %s:%s (version %d)",
|
|
598
|
+
entity_type, self._key, current_version
|
|
599
|
+
)
|
|
512
600
|
|
|
513
601
|
# Set up EntityState on instance for method access
|
|
514
602
|
self._state = EntityState(state_dict)
|
|
@@ -519,16 +607,26 @@ def _create_entity_method_wrapper(entity_type: str, method):
|
|
|
519
607
|
result = await method(self, *args, **kwargs)
|
|
520
608
|
logger.debug("Completed %s:%s.%s", entity_type, self._key, method.__name__)
|
|
521
609
|
|
|
522
|
-
#
|
|
523
|
-
#
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
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
|
|
532
630
|
|
|
533
631
|
return result
|
|
534
632
|
|
agnt5/worker.py
CHANGED
|
@@ -126,6 +126,10 @@ 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
135
|
from ._core import PyWorker, PyWorkerConfig, PyComponentInfo
|
|
@@ -148,9 +152,17 @@ class Worker:
|
|
|
148
152
|
# Create Rust worker instance
|
|
149
153
|
self._rust_worker = self._PyWorker(self._rust_config)
|
|
150
154
|
|
|
151
|
-
# Create worker-scoped entity state
|
|
152
|
-
from .entity import
|
|
153
|
-
|
|
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)
|
|
161
|
+
|
|
162
|
+
# Create Python adapter (thin wrapper around Rust core)
|
|
163
|
+
self._entity_state_adapter = EntityStateAdapter(rust_core=rust_core)
|
|
164
|
+
|
|
165
|
+
logger.info("Created EntityStateAdapter with Rust core for state management")
|
|
154
166
|
|
|
155
167
|
# Component registration: auto-discover or explicit
|
|
156
168
|
if auto_register:
|
|
@@ -746,7 +758,7 @@ class Worker:
|
|
|
746
758
|
"""Execute a workflow handler with automatic replay support."""
|
|
747
759
|
import json
|
|
748
760
|
from .workflow import WorkflowEntity, WorkflowContext
|
|
749
|
-
from .entity import
|
|
761
|
+
from .entity import _get_state_adapter
|
|
750
762
|
from ._core import PyExecuteComponentResponse
|
|
751
763
|
|
|
752
764
|
try:
|
|
@@ -787,10 +799,15 @@ class Worker:
|
|
|
787
799
|
logger.debug(f"Loaded {len(completed_steps)} completed steps into workflow entity")
|
|
788
800
|
|
|
789
801
|
if initial_state:
|
|
790
|
-
# Load initial state into entity's state
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
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)")
|
|
794
811
|
|
|
795
812
|
# Create WorkflowContext with entity and runtime_context for trace correlation
|
|
796
813
|
ctx = WorkflowContext(
|
|
@@ -925,11 +942,11 @@ class Worker:
|
|
|
925
942
|
"""Execute an entity method."""
|
|
926
943
|
import json
|
|
927
944
|
from .context import Context
|
|
928
|
-
from .entity import EntityType, Entity,
|
|
945
|
+
from .entity import EntityType, Entity, _entity_state_adapter_ctx
|
|
929
946
|
from ._core import PyExecuteComponentResponse
|
|
930
947
|
|
|
931
|
-
# Set entity state
|
|
932
|
-
|
|
948
|
+
# Set entity state adapter in context for Entity instances to access
|
|
949
|
+
_entity_state_adapter_ctx.set(self._entity_state_adapter)
|
|
933
950
|
|
|
934
951
|
try:
|
|
935
952
|
# Parse input data
|
|
@@ -944,23 +961,8 @@ class Worker:
|
|
|
944
961
|
if not method_name:
|
|
945
962
|
raise ValueError("Entity invocation requires 'method' parameter")
|
|
946
963
|
|
|
947
|
-
#
|
|
948
|
-
|
|
949
|
-
if hasattr(request, 'metadata') and request.metadata:
|
|
950
|
-
if "entity_state" in request.metadata:
|
|
951
|
-
platform_state_json = request.metadata["entity_state"]
|
|
952
|
-
platform_version = int(request.metadata.get("state_version", "0"))
|
|
953
|
-
|
|
954
|
-
# Load platform state into state manager
|
|
955
|
-
self._entity_state_manager.load_state_from_platform(
|
|
956
|
-
state_key,
|
|
957
|
-
platform_state_json,
|
|
958
|
-
platform_version
|
|
959
|
-
)
|
|
960
|
-
logger.info(
|
|
961
|
-
f"Loaded entity state from platform: {entity_type.name}/{entity_key} "
|
|
962
|
-
f"(version {platform_version})"
|
|
963
|
-
)
|
|
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
|
|
964
966
|
|
|
965
967
|
# Create entity instance using the stored class reference
|
|
966
968
|
entity_instance = entity_type.entity_class(key=entity_key)
|
|
@@ -971,32 +973,15 @@ class Worker:
|
|
|
971
973
|
|
|
972
974
|
method = getattr(entity_instance, method_name)
|
|
973
975
|
|
|
974
|
-
# Execute method
|
|
976
|
+
# Execute method (entity method wrapper handles state load/save automatically)
|
|
975
977
|
result = await method(**input_dict)
|
|
976
978
|
|
|
977
979
|
# Serialize result
|
|
978
980
|
output_data = json.dumps(result).encode("utf-8")
|
|
979
981
|
|
|
980
|
-
#
|
|
981
|
-
|
|
982
|
-
self._entity_state_manager.get_state_for_persistence(state_key)
|
|
983
|
-
|
|
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
|
|
984
984
|
metadata = {}
|
|
985
|
-
if state_dict:
|
|
986
|
-
# Serialize state as JSON string for platform persistence
|
|
987
|
-
state_json = json.dumps(state_dict)
|
|
988
|
-
# Pass in metadata for Worker Coordinator to publish
|
|
989
|
-
metadata = {
|
|
990
|
-
"entity_state": state_json,
|
|
991
|
-
"entity_type": entity_type.name,
|
|
992
|
-
"entity_key": entity_key,
|
|
993
|
-
"expected_version": str(expected_version),
|
|
994
|
-
"new_version": str(new_version),
|
|
995
|
-
}
|
|
996
|
-
logger.info(
|
|
997
|
-
f"Captured entity state: {entity_type.name}/{entity_key} "
|
|
998
|
-
f"(version {expected_version} → {new_version})"
|
|
999
|
-
)
|
|
1000
985
|
|
|
1001
986
|
return PyExecuteComponentResponse(
|
|
1002
987
|
invocation_id=request.invocation_id,
|
|
@@ -1027,11 +1012,16 @@ class Worker:
|
|
|
1027
1012
|
)
|
|
1028
1013
|
|
|
1029
1014
|
async def _execute_agent(self, agent, input_data: bytes, request):
|
|
1030
|
-
"""Execute an agent."""
|
|
1015
|
+
"""Execute an agent with session support for multi-turn conversations."""
|
|
1031
1016
|
import json
|
|
1032
|
-
|
|
1017
|
+
import uuid
|
|
1018
|
+
from .agent import AgentContext
|
|
1019
|
+
from .entity import _entity_state_adapter_ctx
|
|
1033
1020
|
from ._core import PyExecuteComponentResponse
|
|
1034
1021
|
|
|
1022
|
+
# Set entity state adapter in context so AgentContext can access it
|
|
1023
|
+
_entity_state_adapter_ctx.set(self._entity_state_adapter)
|
|
1024
|
+
|
|
1035
1025
|
try:
|
|
1036
1026
|
# Parse input data
|
|
1037
1027
|
input_dict = json.loads(input_data.decode("utf-8")) if input_data else {}
|
|
@@ -1041,16 +1031,30 @@ class Worker:
|
|
|
1041
1031
|
if not user_message:
|
|
1042
1032
|
raise ValueError("Agent invocation requires 'message' parameter")
|
|
1043
1033
|
|
|
1044
|
-
#
|
|
1045
|
-
|
|
1046
|
-
|
|
1034
|
+
# Extract or generate session_id for multi-turn conversation support
|
|
1035
|
+
# If session_id is provided, the agent will load previous conversation history
|
|
1036
|
+
# If not provided, a new session is created with auto-generated ID
|
|
1037
|
+
session_id = input_dict.get("session_id")
|
|
1038
|
+
|
|
1039
|
+
if not session_id:
|
|
1040
|
+
session_id = str(uuid.uuid4())
|
|
1041
|
+
logger.info(f"Created new agent session: {session_id}")
|
|
1042
|
+
else:
|
|
1043
|
+
logger.info(f"Using existing agent session: {session_id}")
|
|
1044
|
+
|
|
1045
|
+
# Create AgentContext with session support for conversation persistence
|
|
1046
|
+
# AgentContext automatically loads/saves conversation history based on session_id
|
|
1047
|
+
ctx = AgentContext(
|
|
1048
|
+
run_id=request.invocation_id,
|
|
1049
|
+
agent_name=agent.name,
|
|
1050
|
+
session_id=session_id,
|
|
1047
1051
|
runtime_context=request.runtime_context,
|
|
1048
1052
|
)
|
|
1049
1053
|
|
|
1050
|
-
# Execute agent
|
|
1054
|
+
# Execute agent - conversation history is automatically included
|
|
1051
1055
|
agent_result = await agent.run(user_message, context=ctx)
|
|
1052
1056
|
|
|
1053
|
-
# Build response
|
|
1057
|
+
# Build response with agent output and tool calls
|
|
1054
1058
|
result = {
|
|
1055
1059
|
"output": agent_result.output,
|
|
1056
1060
|
"tool_calls": agent_result.tool_calls,
|
|
@@ -1059,13 +1063,16 @@ class Worker:
|
|
|
1059
1063
|
# Serialize result
|
|
1060
1064
|
output_data = json.dumps(result).encode("utf-8")
|
|
1061
1065
|
|
|
1066
|
+
# Return session_id in metadata so UI can persist it
|
|
1067
|
+
metadata = {"session_id": session_id}
|
|
1068
|
+
|
|
1062
1069
|
return PyExecuteComponentResponse(
|
|
1063
1070
|
invocation_id=request.invocation_id,
|
|
1064
1071
|
success=True,
|
|
1065
1072
|
output_data=output_data,
|
|
1066
1073
|
state_update=None,
|
|
1067
1074
|
error_message=None,
|
|
1068
|
-
metadata=
|
|
1075
|
+
metadata=metadata,
|
|
1069
1076
|
is_chunk=False,
|
|
1070
1077
|
done=True,
|
|
1071
1078
|
chunk_index=0,
|
|
@@ -1127,6 +1134,13 @@ class Worker:
|
|
|
1127
1134
|
if self.metadata:
|
|
1128
1135
|
self._rust_worker.set_service_metadata(self.metadata)
|
|
1129
1136
|
|
|
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")
|
|
1143
|
+
|
|
1130
1144
|
# Get the current event loop to pass to Rust for concurrent Python async execution
|
|
1131
1145
|
# This allows Rust to execute Python async functions on the same event loop
|
|
1132
1146
|
# without spawn_blocking overhead, enabling true concurrency
|
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,15 +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=eZxluNosN814qqPoiTnB1GwMrzvnR_7wE3Goh6BHRg8,105
|
|
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=Iu_CWOP5qpeKyRacdNxRlODj9hqEIJD0WWNhPhZSu3Q,12876984
|
|
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=
|
|
9
|
+
agnt5/agent.py,sha256=aBrhtPaUAHOHv3-h_Yb2UMqFHertr1P2hJ7fA_4IXcw,43225
|
|
10
10
|
agnt5/client.py,sha256=kXksazgxdVXWaG9OkjJA4cWruNtcS-ENhtnkrIdw-Nk,23212
|
|
11
11
|
agnt5/context.py,sha256=S2OzPkhn_jnqSWfT21mSYOux8vHaLKQxcAvggZDHQek,2378
|
|
12
|
-
agnt5/entity.py,sha256=
|
|
12
|
+
agnt5/entity.py,sha256=AlHmSHVxQD5EYBvkmERKUkwv0ERrKaT8rvRK611hv_I,28941
|
|
13
13
|
agnt5/exceptions.py,sha256=mZ0q-NK6OKhYxgwBJpIbgpgzk-CJaFIHDbp1EE-pS7I,925
|
|
14
14
|
agnt5/function.py,sha256=f1vaAlJRwuo8cxCOGEd8XPido00mOhlPS8UJJx-6hJI,11041
|
|
15
15
|
agnt5/lm.py,sha256=9dFjd6eQ3f3lFZe7H7rWZherYiP_58MT1F5xpwD8PCg,23195
|
|
@@ -17,6 +17,6 @@ agnt5/tool.py,sha256=uc4L-Q9QyLzQDe-MZKk2Wo3o5e-mK8tfaQwVDgQdouQ,13133
|
|
|
17
17
|
agnt5/tracing.py,sha256=Mh2-OfnQM61lM_P8gxJstafdsUA8Gxoo1lP-Joxhub8,5980
|
|
18
18
|
agnt5/types.py,sha256=Zb71ZMwvrt1p4SH18cAKunp2y5tao_W5_jGYaPDejQo,2840
|
|
19
19
|
agnt5/version.py,sha256=rOq1mObLihnnKgKqBrwZA0zwOPudEKVFcW1a48ynkqc,573
|
|
20
|
-
agnt5/worker.py,sha256=
|
|
21
|
-
agnt5/workflow.py,sha256=
|
|
22
|
-
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,,
|
|
File without changes
|