agnt5 0.3.0a8__cp310-abi3-manylinux_2_17_x86_64.manylinux2014_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.

@@ -0,0 +1,48 @@
1
+ """Agent module - AI agents with streaming execution.
2
+
3
+ This module provides the core agent primitives for building AI-powered
4
+ applications with tool orchestration and multi-agent collaboration.
5
+
6
+ Example:
7
+ ```python
8
+ from agnt5.agent import Agent, AgentResult, handoff
9
+
10
+ # Create an agent
11
+ agent = Agent(
12
+ name="researcher",
13
+ model="openai/gpt-4o",
14
+ instructions="You are a research assistant.",
15
+ )
16
+
17
+ # Streaming execution (recommended)
18
+ async for event in agent.run("Find recent AI papers"):
19
+ if event.event_type == EventType.LM_MESSAGE_DELTA:
20
+ print(event.data, end="") # data is raw content string for deltas
21
+
22
+ # Non-streaming execution
23
+ result = await agent.run_sync("Find recent AI papers")
24
+ print(result.output)
25
+ ```
26
+ """
27
+
28
+ # Import from split modules
29
+ from .context import AgentContext
30
+ from .result import AgentResult
31
+ from .handoff import Handoff, handoff
32
+ from .registry import AgentRegistry
33
+ from .core import Agent
34
+ from .decorator import agent
35
+
36
+ __all__ = [
37
+ # Core classes
38
+ "Agent",
39
+ "AgentContext",
40
+ "AgentResult",
41
+ # Handoff support
42
+ "Handoff",
43
+ "handoff",
44
+ # Registry
45
+ "AgentRegistry",
46
+ # Decorator
47
+ "agent",
48
+ ]
agnt5/agent/context.py ADDED
@@ -0,0 +1,458 @@
1
+ """Agent execution context with conversation state management."""
2
+
3
+ import logging
4
+ import time
5
+ from typing import Any, Dict, List, Optional, TYPE_CHECKING
6
+
7
+ from ..context import Context
8
+ from ..lm import Message
9
+
10
+ if TYPE_CHECKING:
11
+ pass
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ class AgentContext(Context):
17
+ """
18
+ Context for agent execution with conversation state management.
19
+
20
+ Extends base Context with:
21
+ - State management via EntityStateManager
22
+ - Conversation history persistence
23
+ - Context inheritance (child agents share parent's state)
24
+
25
+ Three initialization modes:
26
+ 1. Standalone: Creates own state manager (playground testing)
27
+ 2. Inherit WorkflowContext: Shares parent's state manager
28
+ 3. Inherit parent AgentContext: Shares parent's state manager
29
+
30
+ Example:
31
+ ```python
32
+ # Standalone agent with conversation history
33
+ ctx = AgentContext(run_id="session-1", agent_name="tutor")
34
+ result = await agent.run_sync("Hello", context=ctx)
35
+ result = await agent.run_sync("Continue", context=ctx) # Remembers previous message
36
+
37
+ # Agent in workflow - shares workflow state
38
+ @workflow
39
+ async def research_workflow(ctx: WorkflowContext):
40
+ agent_result = await research_agent.run_sync("Find AI trends", context=ctx)
41
+ # Agent has access to workflow state via inherited context
42
+ ```
43
+ """
44
+
45
+ def __init__(
46
+ self,
47
+ run_id: str,
48
+ agent_name: str,
49
+ session_id: Optional[str] = None,
50
+ state_manager: Optional[Any] = None,
51
+ parent_context: Optional[Context] = None,
52
+ attempt: int = 0,
53
+ runtime_context: Optional[Any] = None,
54
+ is_streaming: bool = False,
55
+ tenant_id: Optional[str] = None,
56
+ ):
57
+ """
58
+ Initialize agent context.
59
+
60
+ Args:
61
+ run_id: Unique execution identifier
62
+ agent_name: Name of the agent
63
+ session_id: Session identifier for conversation history (default: run_id)
64
+ state_manager: Optional state manager (for context inheritance)
65
+ parent_context: Parent context to inherit state from
66
+ attempt: Retry attempt number
67
+ runtime_context: RuntimeContext for trace correlation
68
+ is_streaming: Whether this is a streaming request (for real-time SSE log delivery)
69
+ tenant_id: Tenant identifier for multi-tenant deployments
70
+ """
71
+ # Inherit is_streaming and tenant_id from parent context if not explicitly provided
72
+ if parent_context and not is_streaming:
73
+ is_streaming = getattr(parent_context, '_is_streaming', False)
74
+ if parent_context and not tenant_id:
75
+ tenant_id = getattr(parent_context, '_tenant_id', None)
76
+
77
+ super().__init__(run_id, attempt, runtime_context, is_streaming, tenant_id)
78
+
79
+ self._agent_name = agent_name
80
+ self._session_id = session_id or run_id
81
+ self.parent_context = parent_context # Store for context chain traversal
82
+
83
+ # Determine state adapter based on parent context
84
+ from ..entity import EntityStateAdapter, _get_state_adapter
85
+
86
+ if state_manager:
87
+ # Explicit state adapter provided (parameter name kept for backward compat)
88
+ self._state_adapter = state_manager
89
+ elif parent_context:
90
+ # Try to inherit state adapter from parent
91
+ try:
92
+ # Check if parent is WorkflowContext or AgentContext
93
+ if hasattr(parent_context, '_workflow_entity'):
94
+ # WorkflowContext - get state adapter from worker context
95
+ self._state_adapter = _get_state_adapter()
96
+ elif hasattr(parent_context, '_state_adapter'):
97
+ # Parent AgentContext - share state adapter
98
+ self._state_adapter = parent_context._state_adapter
99
+ elif hasattr(parent_context, '_state_manager'):
100
+ # Backward compatibility: parent has old _state_manager
101
+ self._state_adapter = parent_context._state_manager
102
+ else:
103
+ # FunctionContext or base Context - create new state adapter
104
+ self._state_adapter = EntityStateAdapter()
105
+ except RuntimeError:
106
+ # _get_state_adapter() failed (not in worker context) - create standalone
107
+ self._state_adapter = EntityStateAdapter()
108
+ else:
109
+ # Try to get from worker context first
110
+ try:
111
+ self._state_adapter = _get_state_adapter()
112
+ except RuntimeError:
113
+ # Standalone - create new state adapter
114
+ self._state_adapter = EntityStateAdapter()
115
+
116
+ # Conversation key for state storage (used for in-memory state)
117
+ self._conversation_key = f"agent:{agent_name}:{self._session_id}:messages"
118
+ # Entity key for database persistence (without :messages suffix to match API expectations)
119
+ self._entity_key = f"agent:{agent_name}:{self._session_id}"
120
+
121
+ # Determine storage mode: "workflow" if parent is WorkflowContext, else "standalone"
122
+ self._storage_mode = "standalone" # Default mode
123
+ self._workflow_entity = None
124
+
125
+ if parent_context and hasattr(parent_context, '_workflow_entity'):
126
+ # Agent is running within a workflow - store conversation in workflow state
127
+ self._storage_mode = "workflow"
128
+ self._workflow_entity = parent_context._workflow_entity
129
+ logger.debug(
130
+ f"Agent '{agent_name}' using workflow storage mode "
131
+ f"(workflow entity: {self._workflow_entity.key})"
132
+ )
133
+
134
+ @property
135
+ def state(self):
136
+ """
137
+ Get state interface for agent state management.
138
+
139
+ Note: This is a simplified in-memory state interface for agent-specific data.
140
+ Conversation history is managed separately via get_conversation_history() and
141
+ save_conversation_history() which use the Rust-backed persistence layer.
142
+
143
+ Returns:
144
+ Dict-like object for state operations
145
+
146
+ Example:
147
+ # Store agent-specific data (in-memory only)
148
+ ctx.state["research_results"] = data
149
+ ctx.state["iteration_count"] = 5
150
+ """
151
+ # Simple dict-based state for agent-specific data
152
+ # This is in-memory only and not persisted to platform
153
+ if not hasattr(self, '_agent_state'):
154
+ self._agent_state = {}
155
+ return self._agent_state
156
+
157
+ @property
158
+ def session_id(self) -> str:
159
+ """Get session identifier for this agent context."""
160
+ return self._session_id
161
+
162
+ async def get_conversation_history(self) -> List[Message]:
163
+ """
164
+ Retrieve conversation history from state, loading from database if needed.
165
+
166
+ Uses the EntityStateAdapter which delegates to Rust core for cache-first loading.
167
+ If running within a workflow, loads from workflow entity state instead.
168
+
169
+ Returns:
170
+ List of Message objects from conversation history
171
+ """
172
+ if self._storage_mode == "workflow":
173
+ return await self._load_from_workflow_state()
174
+ else:
175
+ return await self._load_from_entity_storage()
176
+
177
+ async def _load_from_workflow_state(self) -> List[Message]:
178
+ """Load conversation history from workflow entity state."""
179
+ key = f"agent.{self._agent_name}"
180
+ agent_data = self._workflow_entity.state.get(key, {})
181
+ messages_data = agent_data.get("messages", [])
182
+
183
+ # Convert dict representations back to Message objects
184
+ return self._convert_dicts_to_messages(messages_data)
185
+
186
+ async def _load_from_entity_storage(self) -> List[Message]:
187
+ """Load conversation history from AgentSession entity (standalone mode)."""
188
+ entity_type = "AgentSession"
189
+ entity_key = self._entity_key
190
+
191
+ # Load session data via adapter (Rust handles cache + platform load)
192
+ # Use session scope with session_id for proper entity isolation
193
+ session_data = await self._state_adapter.load_state(
194
+ entity_type,
195
+ entity_key,
196
+ scope="session",
197
+ scope_id=self._session_id,
198
+ )
199
+
200
+ # Extract messages from session object
201
+ if isinstance(session_data, dict) and "messages" in session_data:
202
+ # New format with session metadata
203
+ messages_data = session_data["messages"]
204
+ elif isinstance(session_data, list):
205
+ # Old format - just messages array
206
+ messages_data = session_data
207
+ else:
208
+ # No messages found
209
+ messages_data = []
210
+
211
+ # Convert dict representations back to Message objects
212
+ return self._convert_dicts_to_messages(messages_data)
213
+
214
+ def _convert_dicts_to_messages(self, messages_data: list) -> List[Message]:
215
+ """Convert list of message dicts to Message objects."""
216
+ from ..lm import MessageRole
217
+
218
+ messages = []
219
+ for msg_dict in messages_data:
220
+ if isinstance(msg_dict, dict):
221
+ role = msg_dict.get("role", "user")
222
+ content = msg_dict.get("content", "")
223
+ if role == "user":
224
+ messages.append(Message.user(content))
225
+ elif role == "assistant":
226
+ messages.append(Message.assistant(content))
227
+ else:
228
+ # Generic message - create with MessageRole enum
229
+ msg_role = (
230
+ MessageRole(role)
231
+ if role in ("user", "assistant", "system")
232
+ else MessageRole.USER
233
+ )
234
+ msg = Message(role=msg_role, content=content)
235
+ messages.append(msg)
236
+ else:
237
+ # Already a Message object
238
+ messages.append(msg_dict)
239
+
240
+ return messages
241
+
242
+ async def save_conversation_history(self, messages: List[Message]) -> None:
243
+ """
244
+ Save conversation history to state and persist to database.
245
+
246
+ Uses the EntityStateAdapter which delegates to Rust core for version-checked saves.
247
+ If running within a workflow, saves to workflow entity state instead.
248
+
249
+ Args:
250
+ messages: List of Message objects to persist
251
+ """
252
+ if self._storage_mode == "workflow":
253
+ await self._save_to_workflow_state(messages)
254
+ else:
255
+ await self._save_to_entity_storage(messages)
256
+
257
+ async def _save_to_workflow_state(self, messages: List[Message]) -> None:
258
+ """Save conversation history to workflow entity state."""
259
+ # Convert Message objects to dict for JSON serialization
260
+ messages_data = []
261
+ for msg in messages:
262
+ messages_data.append({
263
+ "role": msg.role.value if hasattr(msg.role, 'value') else str(msg.role),
264
+ "content": msg.content,
265
+ "timestamp": time.time()
266
+ })
267
+
268
+ # Build agent data structure
269
+ key = f"agent.{self._agent_name}"
270
+ current_data = self._workflow_entity.state.get(key, {})
271
+ now = time.time()
272
+
273
+ agent_data = {
274
+ "session_id": self._session_id,
275
+ "agent_name": self._agent_name,
276
+ "created_at": current_data.get("created_at", now),
277
+ "last_message_time": now,
278
+ "message_count": len(messages_data),
279
+ "messages": messages_data,
280
+ "metadata": getattr(self, '_custom_metadata', {})
281
+ }
282
+
283
+ # Store in workflow state (WorkflowEntity handles persistence)
284
+ self._workflow_entity.state.set(key, agent_data)
285
+ logger.info(f"Saved conversation to workflow state: {key} ({len(messages_data)} messages)")
286
+
287
+ async def _save_to_entity_storage(self, messages: List[Message]) -> None:
288
+ """Save conversation history to AgentSession entity (standalone mode)."""
289
+ # Convert Message objects to dict for JSON serialization
290
+ messages_data = []
291
+ for msg in messages:
292
+ messages_data.append({
293
+ "role": msg.role.value if hasattr(msg.role, 'value') else str(msg.role),
294
+ "content": msg.content,
295
+ "timestamp": time.time() # Add timestamp for each message
296
+ })
297
+
298
+ entity_type = "AgentSession"
299
+ entity_key = self._entity_key
300
+
301
+ # Load current state with version for optimistic locking
302
+ # Use session scope with session_id for proper entity isolation
303
+ current_state, current_version = await self._state_adapter.load_with_version(
304
+ entity_type,
305
+ entity_key,
306
+ scope="session",
307
+ scope_id=self._session_id,
308
+ )
309
+
310
+ # Build session object with metadata
311
+ now = time.time()
312
+
313
+ # Get custom metadata from instance variable or preserve from loaded state
314
+ custom_metadata = getattr(self, '_custom_metadata', current_state.get("metadata", {}))
315
+
316
+ session_data = {
317
+ "session_id": self._session_id,
318
+ "agent_name": self._agent_name,
319
+ "created_at": current_state.get("created_at", now), # Preserve existing or set new
320
+ "last_message_time": now,
321
+ "message_count": len(messages_data),
322
+ "messages": messages_data,
323
+ "metadata": custom_metadata # Save custom metadata
324
+ }
325
+
326
+ # Save to platform via adapter (Rust handles optimistic locking)
327
+ # Use session scope with session_id for proper entity isolation
328
+ try:
329
+ new_version = await self._state_adapter.save_state(
330
+ entity_type,
331
+ entity_key,
332
+ session_data,
333
+ current_version,
334
+ scope="session",
335
+ scope_id=self._session_id,
336
+ )
337
+ logger.info(
338
+ f"Persisted conversation history: {entity_key} "
339
+ f"(version {current_version} -> {new_version})"
340
+ )
341
+ except Exception as e:
342
+ logger.error(f"Failed to persist conversation history to database: {e}")
343
+ # Don't fail - conversation is still in memory for this execution
344
+
345
+ async def get_metadata(self) -> Dict[str, Any]:
346
+ """
347
+ Get conversation session metadata.
348
+
349
+ Returns session metadata including:
350
+ - created_at: Timestamp of first message (float, Unix timestamp)
351
+ - last_activity: Timestamp of last message (float, Unix timestamp)
352
+ - message_count: Number of messages in conversation (int)
353
+ - custom: Dict of user-provided custom metadata
354
+
355
+ Returns:
356
+ Dictionary with metadata. If no conversation exists yet, returns defaults.
357
+
358
+ Example:
359
+ ```python
360
+ metadata = await context.get_metadata()
361
+ print(f"Session created: {metadata['created_at']}")
362
+ print(f"User ID: {metadata['custom'].get('user_id')}")
363
+ ```
364
+ """
365
+ if self._storage_mode == "workflow":
366
+ return await self._get_metadata_from_workflow()
367
+ else:
368
+ return await self._get_metadata_from_entity()
369
+
370
+ async def _get_metadata_from_workflow(self) -> Dict[str, Any]:
371
+ """Get metadata from workflow entity state."""
372
+ key = f"agent.{self._agent_name}"
373
+ agent_data = self._workflow_entity.state.get(key, {})
374
+
375
+ if not agent_data:
376
+ # No conversation exists yet - return defaults
377
+ return {
378
+ "created_at": None,
379
+ "last_activity": None,
380
+ "message_count": 0,
381
+ "custom": getattr(self, '_custom_metadata', {})
382
+ }
383
+
384
+ messages = agent_data.get("messages", [])
385
+ return {
386
+ "created_at": agent_data.get("created_at"),
387
+ "last_activity": agent_data.get("last_message_time"),
388
+ "message_count": len(messages),
389
+ "custom": agent_data.get("metadata", {})
390
+ }
391
+
392
+ async def _get_metadata_from_entity(self) -> Dict[str, Any]:
393
+ """Get metadata from AgentSession entity (standalone mode)."""
394
+ entity_type = "AgentSession"
395
+ entity_key = self._entity_key
396
+
397
+ # Load session data with session scope
398
+ session_data = await self._state_adapter.load_state(
399
+ entity_type,
400
+ entity_key,
401
+ scope="session",
402
+ scope_id=self._session_id,
403
+ )
404
+
405
+ if not session_data:
406
+ # No conversation exists yet - return defaults
407
+ return {
408
+ "created_at": None,
409
+ "last_activity": None,
410
+ "message_count": 0,
411
+ "custom": getattr(self, '_custom_metadata', {})
412
+ }
413
+
414
+ messages = session_data.get("messages", [])
415
+
416
+ # Derive timestamps from messages if available
417
+ created_at = session_data.get("created_at")
418
+ last_activity = session_data.get("last_message_time")
419
+
420
+ return {
421
+ "created_at": created_at,
422
+ "last_activity": last_activity,
423
+ "message_count": len(messages),
424
+ "custom": session_data.get("metadata", {})
425
+ }
426
+
427
+ def update_metadata(self, **kwargs) -> None:
428
+ """
429
+ Update custom session metadata.
430
+
431
+ Metadata will be persisted alongside conversation history on next save.
432
+ Use this to store application-specific data like user_id, preferences, etc.
433
+
434
+ Args:
435
+ **kwargs: Key-value pairs to store as metadata
436
+
437
+ Example:
438
+ ```python
439
+ # Store user identification and preferences
440
+ context.update_metadata(
441
+ user_id="user-123",
442
+ subscription_tier="premium",
443
+ preferences={"theme": "dark", "language": "en"}
444
+ )
445
+
446
+ # Later retrieve it
447
+ metadata = await context.get_metadata()
448
+ user_id = metadata["custom"]["user_id"]
449
+ ```
450
+
451
+ Note:
452
+ - Metadata is merged with existing metadata (doesn't replace)
453
+ - Changes persist on next save_conversation_history() call
454
+ - Use simple JSON-serializable types (str, int, float, dict, list)
455
+ """
456
+ if not hasattr(self, '_custom_metadata'):
457
+ self._custom_metadata = {}
458
+ self._custom_metadata.update(kwargs)