agnt5 0.3.2a1__cp310-abi3-manylinux_2_34_aarch64.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/tracing.py ADDED
@@ -0,0 +1,300 @@
1
+ """
2
+ User-facing tracing API for AGNT5 SDK.
3
+
4
+ Provides decorators and context managers for instrumenting Python code with
5
+ OpenTelemetry spans. All spans are created via Rust FFI and exported through
6
+ the centralized Rust OpenTelemetry system.
7
+
8
+ This module uses Python's contextvars for async-safe span context propagation,
9
+ ensuring proper parent-child relationships even with asyncio.gather() and
10
+ other parallel async operations.
11
+
12
+ Example:
13
+ ```python
14
+ from agnt5.tracing import span
15
+
16
+ @span("my_operation")
17
+ async def my_function(ctx, data):
18
+ # Your code here
19
+ return result
20
+
21
+ # Or use context manager
22
+ from agnt5.tracing import span_context
23
+
24
+ async def process():
25
+ with span_context("processing", user_id="123") as s:
26
+ data = await fetch_data()
27
+ s.set_attribute("records", str(len(data)))
28
+ return data
29
+ ```
30
+ """
31
+
32
+ import functools
33
+ import inspect
34
+ from contextvars import ContextVar
35
+ from contextlib import contextmanager
36
+ from dataclasses import dataclass
37
+ from typing import Any, Callable, Dict, Optional, Tuple
38
+
39
+ from ._core import create_span as _rust_create_span
40
+
41
+
42
+ @dataclass
43
+ class SpanInfo:
44
+ """Information about the current span for context propagation."""
45
+ trace_id: str
46
+ span_id: str
47
+
48
+
49
+ # Async-safe contextvar for tracking the current span
50
+ # This is task-local in asyncio, ensuring proper isolation for parallel operations
51
+ _current_span: ContextVar[Optional[SpanInfo]] = ContextVar('current_span', default=None)
52
+
53
+
54
+ def get_current_span_info() -> Optional[SpanInfo]:
55
+ """Get the current span info from the contextvar (if any)."""
56
+ return _current_span.get()
57
+
58
+
59
+ class SpanContextManager:
60
+ """
61
+ Wrapper around PySpan that manages the contextvar for proper async-safe
62
+ parent-child span linking.
63
+
64
+ When entering this context manager:
65
+ 1. Saves the previous span info
66
+ 2. Sets this span as the current span in the contextvar
67
+
68
+ When exiting:
69
+ 1. Restores the previous span info
70
+ 2. Calls PySpan.__exit__ to end the span
71
+ """
72
+
73
+ def __init__(self, py_span):
74
+ self._py_span = py_span
75
+ self._token = None
76
+
77
+ def __enter__(self):
78
+ # Enter the underlying PySpan first
79
+ self._py_span.__enter__()
80
+
81
+ # Set this span as the current span in the contextvar
82
+ # The token allows us to restore the previous value on exit
83
+ span_info = SpanInfo(
84
+ trace_id=self._py_span.trace_id,
85
+ span_id=self._py_span.span_id
86
+ )
87
+ self._token = _current_span.set(span_info)
88
+
89
+ return self._py_span
90
+
91
+ def __exit__(self, exc_type, exc_val, exc_tb):
92
+ # Restore the previous span context first
93
+ if self._token is not None:
94
+ _current_span.reset(self._token)
95
+ self._token = None
96
+
97
+ # Then exit the underlying PySpan (ends the span, handles exceptions)
98
+ return self._py_span.__exit__(exc_type, exc_val, exc_tb)
99
+
100
+
101
+ def create_span(
102
+ name: str,
103
+ component_type: str = "operation",
104
+ runtime_context: Optional[Any] = None,
105
+ attributes: Optional[Dict[str, str]] = None,
106
+ ) -> SpanContextManager:
107
+ """
108
+ Create a span with proper async-safe context propagation.
109
+
110
+ This function checks the contextvar for the current span and passes
111
+ the parent trace_id/span_id to Rust for proper parent-child linking.
112
+
113
+ Args:
114
+ name: Span name
115
+ component_type: Component type (e.g., "function", "task", "agent")
116
+ runtime_context: Optional RuntimeContext for initial trace context
117
+ attributes: Optional span attributes
118
+
119
+ Returns:
120
+ SpanContextManager that can be used as a context manager
121
+ """
122
+ # Get the current span from contextvar (async-safe parent lookup)
123
+ current_span = _current_span.get()
124
+
125
+ parent_trace_id = None
126
+ parent_span_id = None
127
+ if current_span is not None:
128
+ parent_trace_id = current_span.trace_id
129
+ parent_span_id = current_span.span_id
130
+
131
+ # Create the Rust span with parent IDs for proper linking
132
+ py_span = _rust_create_span(
133
+ name,
134
+ component_type,
135
+ runtime_context,
136
+ attributes or {},
137
+ parent_trace_id,
138
+ parent_span_id,
139
+ )
140
+
141
+ # Wrap in our context manager for contextvar management
142
+ return SpanContextManager(py_span)
143
+
144
+
145
+ def span(
146
+ name: Optional[str] = None,
147
+ component_type: str = "function",
148
+ runtime_context: Optional[Any] = None,
149
+ **attributes: str
150
+ ):
151
+ """
152
+ Decorator to automatically create spans for functions.
153
+
154
+ Args:
155
+ name: Span name (defaults to function name)
156
+ component_type: Component type (default: "function")
157
+ runtime_context: Optional RuntimeContext for trace linking
158
+ **attributes: Additional span attributes
159
+
160
+ Example:
161
+ ```python
162
+ @span("fetch_user_data", user_type="premium")
163
+ async def fetch_user(user_id: str):
164
+ return await db.get_user(user_id)
165
+ ```
166
+ """
167
+ def decorator(func: Callable) -> Callable:
168
+ span_name = name or func.__name__
169
+
170
+ if inspect.iscoroutinefunction(func):
171
+ @functools.wraps(func)
172
+ async def async_wrapper(*args, **kwargs):
173
+ # Try to extract runtime_context from first arg if it's a Context
174
+ ctx = runtime_context
175
+ if ctx is None and args:
176
+ from .context import Context
177
+ if isinstance(args[0], Context):
178
+ ctx = args[0]._runtime_context
179
+
180
+ with create_span(span_name, component_type, ctx, attributes) as s:
181
+ try:
182
+ result = await func(*args, **kwargs)
183
+ # Span automatically marked as OK on success
184
+ return result
185
+ except Exception as e:
186
+ # Exception automatically recorded by PySpan.__exit__
187
+ raise
188
+ return async_wrapper
189
+ else:
190
+ @functools.wraps(func)
191
+ def sync_wrapper(*args, **kwargs):
192
+ # Try to extract runtime_context from first arg if it's a Context
193
+ ctx = runtime_context
194
+ if ctx is None and args:
195
+ from .context import Context
196
+ if isinstance(args[0], Context):
197
+ ctx = args[0]._runtime_context
198
+
199
+ with create_span(span_name, component_type, ctx, attributes) as s:
200
+ try:
201
+ result = func(*args, **kwargs)
202
+ return result
203
+ except Exception as e:
204
+ raise
205
+ return sync_wrapper
206
+
207
+ return decorator
208
+
209
+
210
+ @contextmanager
211
+ def span_context(
212
+ name: str,
213
+ component_type: str = "operation",
214
+ runtime_context: Optional[Any] = None,
215
+ **attributes: str
216
+ ):
217
+ """
218
+ Context manager for creating spans around code blocks.
219
+
220
+ Args:
221
+ name: Span name
222
+ component_type: Component type (default: "operation")
223
+ runtime_context: Optional RuntimeContext for trace linking
224
+ **attributes: Span attributes
225
+
226
+ Yields:
227
+ PySpan object with set_attribute() and record_exception() methods
228
+
229
+ Example:
230
+ ```python
231
+ with span_context("db_query", runtime_context=ctx._runtime_context, table="users") as s:
232
+ results = query_database()
233
+ s.set_attribute("result_count", str(len(results)))
234
+ ```
235
+ """
236
+ with create_span(name, component_type, runtime_context, attributes) as s:
237
+ yield s
238
+
239
+
240
+ def create_task_span(name: str, runtime_context: Optional[Any] = None, **attributes: str):
241
+ """
242
+ Create a span for task execution.
243
+
244
+ Args:
245
+ name: Task name
246
+ runtime_context: Optional RuntimeContext for trace linking
247
+ **attributes: Task attributes
248
+
249
+ Returns:
250
+ SpanContextManager to use as context manager
251
+
252
+ Example:
253
+ ```python
254
+ with create_task_span("process_data", runtime_context=ctx._runtime_context, batch_size="100") as s:
255
+ result = await process()
256
+ ```
257
+ """
258
+ return create_span(name, "task", runtime_context, attributes)
259
+
260
+
261
+ def create_workflow_span(name: str, runtime_context: Optional[Any] = None, **attributes: str):
262
+ """
263
+ Create a span for workflow execution.
264
+
265
+ Args:
266
+ name: Workflow name
267
+ runtime_context: Optional RuntimeContext for trace linking
268
+ **attributes: Workflow attributes
269
+
270
+ Returns:
271
+ SpanContextManager to use as context manager
272
+ """
273
+ return create_span(name, "workflow", runtime_context, attributes)
274
+
275
+
276
+ def create_agent_span(name: str, runtime_context: Optional[Any] = None, **attributes: str):
277
+ """
278
+ Create a span for agent execution.
279
+
280
+ Args:
281
+ name: Agent name
282
+ runtime_context: Optional RuntimeContext for trace linking
283
+ **attributes: Agent attributes
284
+
285
+ Returns:
286
+ SpanContextManager to use as context manager
287
+ """
288
+ return create_span(name, "agent", runtime_context, attributes)
289
+
290
+
291
+ __all__ = [
292
+ "span",
293
+ "span_context",
294
+ "create_span",
295
+ "create_task_span",
296
+ "create_workflow_span",
297
+ "create_agent_span",
298
+ "get_current_span_info",
299
+ "SpanInfo",
300
+ ]
agnt5/types.py ADDED
@@ -0,0 +1,111 @@
1
+ """Type definitions and protocols for AGNT5 SDK."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from enum import Enum
7
+ from typing import Any, Awaitable, Callable, Dict, List, Optional, Protocol, TypeVar, Union
8
+
9
+ # Type aliases
10
+ JSON = Union[Dict[str, Any], List[Any], str, int, float, bool, None]
11
+ HandlerFunc = Callable[..., Awaitable[Any]]
12
+
13
+ T = TypeVar("T")
14
+
15
+
16
+ class BackoffType(str, Enum):
17
+ """Backoff strategy for retry policies."""
18
+
19
+ CONSTANT = "constant"
20
+ LINEAR = "linear"
21
+ EXPONENTIAL = "exponential"
22
+
23
+
24
+ @dataclass
25
+ class RetryPolicy:
26
+ """Configuration for function retry behavior."""
27
+
28
+ max_attempts: int = 3
29
+ initial_interval_ms: int = 1000
30
+ max_interval_ms: int = 60000
31
+
32
+ def __post_init__(self) -> None:
33
+ if self.max_attempts < 1:
34
+ raise ValueError("max_attempts must be at least 1")
35
+ if self.initial_interval_ms < 0:
36
+ raise ValueError("initial_interval_ms must be non-negative")
37
+ if self.max_interval_ms < self.initial_interval_ms:
38
+ raise ValueError("max_interval_ms must be >= initial_interval_ms")
39
+
40
+
41
+ @dataclass
42
+ class BackoffPolicy:
43
+ """Configuration for retry backoff strategy."""
44
+
45
+ type: BackoffType = BackoffType.EXPONENTIAL
46
+ multiplier: float = 2.0
47
+
48
+ def __post_init__(self) -> None:
49
+ if self.multiplier <= 0:
50
+ raise ValueError("multiplier must be positive")
51
+
52
+
53
+ @dataclass
54
+ class FunctionConfig:
55
+ """Configuration for a function handler."""
56
+
57
+ name: str
58
+ handler: HandlerFunc
59
+ retries: Optional[RetryPolicy] = None
60
+ backoff: Optional[BackoffPolicy] = None
61
+ timeout_ms: Optional[int] = None
62
+ input_schema: Optional[Dict[str, Any]] = None
63
+ output_schema: Optional[Dict[str, Any]] = None
64
+ metadata: Optional[Dict[str, str]] = None
65
+
66
+
67
+ @dataclass
68
+ class WorkflowConfig:
69
+ """Configuration for a workflow handler."""
70
+
71
+ name: str
72
+ handler: HandlerFunc
73
+ input_schema: Optional[Dict[str, Any]] = None
74
+ output_schema: Optional[Dict[str, Any]] = None
75
+ metadata: Optional[Dict[str, str]] = None
76
+
77
+
78
+ class ContextProtocol(Protocol):
79
+ """Protocol defining the Context interface."""
80
+
81
+ @property
82
+ def run_id(self) -> str:
83
+ """Workflow/run identifier."""
84
+ ...
85
+
86
+ @property
87
+ def step_id(self) -> Optional[str]:
88
+ """Current step identifier."""
89
+ ...
90
+
91
+ @property
92
+ def attempt(self) -> int:
93
+ """Retry attempt number."""
94
+ ...
95
+
96
+ @property
97
+ def component_type(self) -> str:
98
+ """Component type: 'function', 'entity', 'workflow'."""
99
+ ...
100
+
101
+ async def get(self, key: str, default: Any = None) -> Any:
102
+ """Get value from state."""
103
+ ...
104
+
105
+ def set(self, key: str, value: Any) -> None:
106
+ """Set value in state."""
107
+ ...
108
+
109
+ def delete(self, key: str) -> None:
110
+ """Delete key from state."""
111
+ ...
agnt5/version.py ADDED
@@ -0,0 +1,19 @@
1
+ """Version information for agnt5 SDK."""
2
+
3
+
4
+ def _get_version() -> str:
5
+ """Get package version from installed metadata.
6
+
7
+ This uses importlib.metadata (Python 3.8+) to read the version from
8
+ the installed package metadata, maintaining pyproject.toml as the
9
+ single source of truth.
10
+
11
+ Returns:
12
+ Package version string, or "0.0.0+dev" for development installs.
13
+ """
14
+ try:
15
+ from importlib.metadata import version
16
+ return version("agnt5")
17
+ except Exception:
18
+ # Development/editable install fallback
19
+ return "0.0.0+dev"