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.

agnt5/context.py ADDED
@@ -0,0 +1,210 @@
1
+ """Context implementation for AGNT5 SDK."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import contextvars
6
+ import logging
7
+ from typing import Any, Awaitable, Callable, Dict, List, Optional, TypeVar, Union
8
+
9
+ T = TypeVar("T")
10
+
11
+ # Task-local context variable for automatic context propagation
12
+ # This is NOT a global variable - contextvars provide task-isolated storage
13
+ # Each asyncio task gets its own independent copy, preventing cross-contamination
14
+ _current_context: contextvars.ContextVar[Optional["Context"]] = contextvars.ContextVar(
15
+ "_current_context", default=None
16
+ )
17
+
18
+
19
+ class _CorrelationFilter(logging.Filter):
20
+ """Inject correlation IDs (run_id, trace_id, span_id) and streaming context into every log record."""
21
+
22
+ def __init__(
23
+ self,
24
+ runtime_context: Any,
25
+ is_streaming: bool = False,
26
+ tenant_id: Optional[str] = None,
27
+ deployment_id: Optional[str] = None,
28
+ ) -> None:
29
+ super().__init__()
30
+ self.runtime_context = runtime_context
31
+ self.is_streaming = is_streaming
32
+ self.tenant_id = tenant_id
33
+ self.deployment_id = deployment_id
34
+
35
+ def filter(self, record: logging.LogRecord) -> bool:
36
+ """Add correlation IDs and streaming context as extra fields to the log record."""
37
+ record.run_id = self.runtime_context.run_id
38
+ if self.runtime_context.trace_id:
39
+ record.trace_id = self.runtime_context.trace_id
40
+ if self.runtime_context.span_id:
41
+ record.span_id = self.runtime_context.span_id
42
+ # Add streaming context for journal export
43
+ record.is_streaming = self.is_streaming
44
+ record.tenant_id = self.tenant_id
45
+ record.deployment_id = self.deployment_id
46
+ return True
47
+
48
+
49
+ class Context:
50
+ """
51
+ Base context providing common functionality.
52
+
53
+ Provides:
54
+ - Logging with correlation IDs
55
+ - Execution metadata (run_id, attempt)
56
+ - Runtime context for tracing
57
+
58
+ Extended by:
59
+ - FunctionContext: Minimal context for stateless functions
60
+ - WorkflowContext: Context for durable workflows
61
+ """
62
+
63
+ def __init__(
64
+ self,
65
+ run_id: str,
66
+ attempt: int = 0,
67
+ runtime_context: Optional[Any] = None,
68
+ is_streaming: bool = False,
69
+ tenant_id: Optional[str] = None,
70
+ deployment_id: Optional[str] = None,
71
+ ) -> None:
72
+ """
73
+ Initialize base context.
74
+
75
+ Args:
76
+ run_id: Unique execution identifier
77
+ attempt: Retry attempt number (0-indexed)
78
+ runtime_context: RuntimeContext for trace correlation
79
+ is_streaming: Whether this is a streaming request (for real-time SSE log delivery)
80
+ tenant_id: Tenant ID for multi-tenant deployments
81
+ deployment_id: Deployment ID for tracking deployments
82
+ """
83
+ self._run_id = run_id
84
+ self._attempt = attempt
85
+ self._runtime_context = runtime_context
86
+ self._is_streaming = is_streaming
87
+ self._tenant_id = tenant_id
88
+ self._deployment_id = deployment_id
89
+
90
+ # Create logger with correlation
91
+ self._logger = logging.getLogger(f"agnt5.{run_id}")
92
+ from ._telemetry import setup_context_logger
93
+ setup_context_logger(self._logger)
94
+
95
+ if runtime_context:
96
+ self._logger.addFilter(_CorrelationFilter(runtime_context, is_streaming, tenant_id, deployment_id))
97
+
98
+ @property
99
+ def run_id(self) -> str:
100
+ """Unique execution identifier."""
101
+ return self._run_id
102
+
103
+ @property
104
+ def attempt(self) -> int:
105
+ """Current retry attempt (0-indexed)."""
106
+ return self._attempt
107
+
108
+ @property
109
+ def logger(self) -> logging.Logger:
110
+ """Full logger for .debug(), .warning(), .error(), etc."""
111
+ return self._logger
112
+
113
+ @property
114
+ def tenant_id(self) -> Optional[str]:
115
+ """Tenant identifier for multi-tenant deployments."""
116
+ return self._tenant_id
117
+
118
+ @property
119
+ def deployment_id(self) -> Optional[str]:
120
+ """Deployment identifier for tracking deployments."""
121
+ return self._deployment_id
122
+
123
+
124
+ def get_current_context() -> Optional[Context]:
125
+ """
126
+ Get the current execution context from task-local storage.
127
+
128
+ This function retrieves the context that was set by the nearest enclosing
129
+ decorator (@function, @workflow) or Agent.run() call in the current asyncio task.
130
+
131
+ Returns:
132
+ Current Context if available (WorkflowContext, FunctionContext, AgentContext),
133
+ None if no context is set (e.g., running outside AGNT5 execution)
134
+
135
+ Example:
136
+ >>> ctx = get_current_context()
137
+ >>> if ctx:
138
+ ... ctx.logger.info("Logging from anywhere in the call stack!")
139
+ ... runtime = ctx._runtime_context # Access tracing context
140
+
141
+ Note:
142
+ This uses Python's contextvars which provide task-local (NOT global) storage.
143
+ Each asyncio task has its own isolated context, preventing cross-contamination
144
+ between concurrent executions.
145
+ """
146
+ return _current_context.get()
147
+
148
+
149
+ def set_current_context(ctx: Context) -> contextvars.Token:
150
+ """
151
+ Set the current execution context in task-local storage.
152
+
153
+ This is typically called by decorators and framework code, not by user code.
154
+ Returns a token that can be used to reset the context to its previous value.
155
+
156
+ Args:
157
+ ctx: Context to set as current
158
+
159
+ Returns:
160
+ Token for resetting the context later (use with contextvars.Token.reset())
161
+
162
+ Example:
163
+ >>> token = set_current_context(my_context)
164
+ >>> try:
165
+ ... # Context is available via get_current_context()
166
+ ... do_work()
167
+ >>> finally:
168
+ ... _current_context.reset(token) # Restore previous context
169
+
170
+ Note:
171
+ Always use try/finally to ensure context is properly reset, even if
172
+ an exception occurs. This prevents context leakage between executions.
173
+ """
174
+ return _current_context.set(ctx)
175
+
176
+
177
+ def get_workflow_context() -> Optional["WorkflowContext"]:
178
+ """
179
+ Get the WorkflowContext from the current context or its parent chain.
180
+
181
+ This function traverses the context hierarchy to find a WorkflowContext,
182
+ which is needed for emitting workflow checkpoints from nested contexts
183
+ like AgentContext or FunctionContext.
184
+
185
+ Returns:
186
+ WorkflowContext if found in the context chain, None otherwise
187
+
188
+ Example:
189
+ >>> # Inside an agent called from a workflow
190
+ >>> workflow_ctx = get_workflow_context()
191
+ >>> if workflow_ctx:
192
+ ... workflow_ctx._send_checkpoint("workflow.lm.started", {...})
193
+ """
194
+ from .workflow import WorkflowContext
195
+
196
+ ctx = get_current_context()
197
+
198
+ # Traverse up the context chain looking for WorkflowContext
199
+ while ctx is not None:
200
+ if isinstance(ctx, WorkflowContext):
201
+ return ctx
202
+ # Check if this context has a parent_context attribute
203
+ if hasattr(ctx, 'parent_context'):
204
+ ctx = ctx.parent_context
205
+ else:
206
+ break
207
+
208
+ return None
209
+
210
+