agnt5 0.2.2__cp39-abi3-macosx_11_0_arm64.whl → 0.2.4__cp39-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 +12 -12
- agnt5/_core.abi3.so +0 -0
- agnt5/_retry_utils.py +169 -0
- agnt5/_schema_utils.py +312 -0
- agnt5/_telemetry.py +28 -7
- agnt5/agent.py +153 -140
- agnt5/client.py +50 -12
- agnt5/context.py +36 -756
- agnt5/entity.py +368 -1160
- agnt5/function.py +208 -235
- agnt5/lm.py +71 -12
- agnt5/tool.py +25 -11
- agnt5/tracing.py +196 -0
- agnt5/worker.py +205 -173
- agnt5/workflow.py +444 -20
- {agnt5-0.2.2.dist-info → agnt5-0.2.4.dist-info}/METADATA +2 -1
- agnt5-0.2.4.dist-info/RECORD +22 -0
- agnt5-0.2.2.dist-info/RECORD +0 -19
- {agnt5-0.2.2.dist-info → agnt5-0.2.4.dist-info}/WHEEL +0 -0
agnt5/context.py
CHANGED
|
@@ -3,802 +3,82 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import logging
|
|
6
|
-
from typing import Any, Awaitable, Callable, Dict, List, Optional, TypeVar
|
|
7
|
-
|
|
8
|
-
from .exceptions import NotImplementedError as AGNT5NotImplementedError
|
|
9
|
-
from .exceptions import StateError
|
|
6
|
+
from typing import Any, Awaitable, Callable, Dict, List, Optional, TypeVar, Union
|
|
10
7
|
|
|
11
8
|
T = TypeVar("T")
|
|
12
9
|
|
|
13
10
|
|
|
14
|
-
class
|
|
15
|
-
"""
|
|
16
|
-
Workflow state management client for durable workflows.
|
|
17
|
-
|
|
18
|
-
Provides simple key-value state storage scoped to workflow execution.
|
|
19
|
-
State changes are tracked and persisted for durability.
|
|
20
|
-
|
|
21
|
-
Example:
|
|
22
|
-
```python
|
|
23
|
-
@workflow
|
|
24
|
-
async def order_flow(ctx: Context, order_id: str):
|
|
25
|
-
await ctx.state.set("status", "processing")
|
|
26
|
-
await ctx.state.set("order_id", order_id)
|
|
27
|
-
|
|
28
|
-
# State persists across steps
|
|
29
|
-
result = await ctx.task("service", "process", input=order_id)
|
|
30
|
-
|
|
31
|
-
status = await ctx.state.get("status") # "processing"
|
|
32
|
-
return {"status": status, "result": result}
|
|
33
|
-
```
|
|
34
|
-
"""
|
|
35
|
-
|
|
36
|
-
def __init__(self, ctx: "Context") -> None:
|
|
37
|
-
"""Initialize state client."""
|
|
38
|
-
self._ctx = ctx
|
|
39
|
-
self._state: Dict[str, Any] = {} # Current state
|
|
40
|
-
self._state_changes: List[Dict[str, Any]] = [] # Track changes for events
|
|
41
|
-
|
|
42
|
-
async def get(self, key: str, default: Any = None) -> Any:
|
|
43
|
-
"""
|
|
44
|
-
Get workflow state value.
|
|
45
|
-
|
|
46
|
-
Args:
|
|
47
|
-
key: State key
|
|
48
|
-
default: Default value if key doesn't exist
|
|
49
|
-
|
|
50
|
-
Returns:
|
|
51
|
-
Value associated with key, or default
|
|
52
|
-
|
|
53
|
-
Example:
|
|
54
|
-
```python
|
|
55
|
-
retry_count = await ctx.state.get("retry_count", 0)
|
|
56
|
-
```
|
|
57
|
-
"""
|
|
58
|
-
return self._state.get(key, default)
|
|
59
|
-
|
|
60
|
-
async def set(self, key: str, value: Any) -> None:
|
|
61
|
-
"""
|
|
62
|
-
Set workflow state value.
|
|
63
|
-
|
|
64
|
-
Args:
|
|
65
|
-
key: State key
|
|
66
|
-
value: Value to store (must be JSON-serializable)
|
|
67
|
-
|
|
68
|
-
Example:
|
|
69
|
-
```python
|
|
70
|
-
await ctx.state.set("status", "completed")
|
|
71
|
-
await ctx.state.set("retry_count", 3)
|
|
72
|
-
```
|
|
73
|
-
"""
|
|
74
|
-
self._state[key] = value
|
|
75
|
-
self._state_changes.append({"key": key, "value": value, "deleted": False})
|
|
76
|
-
|
|
77
|
-
async def delete(self, key: str) -> None:
|
|
78
|
-
"""
|
|
79
|
-
Delete workflow state key.
|
|
80
|
-
|
|
81
|
-
Args:
|
|
82
|
-
key: State key to delete
|
|
83
|
-
|
|
84
|
-
Example:
|
|
85
|
-
```python
|
|
86
|
-
await ctx.state.delete("temporary_data")
|
|
87
|
-
```
|
|
88
|
-
"""
|
|
89
|
-
self._state.pop(key, None)
|
|
90
|
-
self._state_changes.append({"key": key, "value": None, "deleted": True})
|
|
91
|
-
|
|
92
|
-
async def clear(self) -> None:
|
|
93
|
-
"""
|
|
94
|
-
Clear all workflow state.
|
|
95
|
-
|
|
96
|
-
Example:
|
|
97
|
-
```python
|
|
98
|
-
await ctx.state.clear()
|
|
99
|
-
```
|
|
100
|
-
"""
|
|
101
|
-
self._state.clear()
|
|
102
|
-
self._state_changes.append({"key": "__clear__", "value": None, "deleted": True})
|
|
11
|
+
class _CorrelationFilter(logging.Filter):
|
|
12
|
+
"""Inject correlation IDs (run_id, trace_id, span_id) into every log record."""
|
|
103
13
|
|
|
104
|
-
def
|
|
105
|
-
|
|
106
|
-
|
|
14
|
+
def __init__(self, runtime_context: Any) -> None:
|
|
15
|
+
super().__init__()
|
|
16
|
+
self.runtime_context = runtime_context
|
|
107
17
|
|
|
108
|
-
def
|
|
109
|
-
"""
|
|
110
|
-
|
|
18
|
+
def filter(self, record: logging.LogRecord) -> bool:
|
|
19
|
+
"""Add correlation IDs as extra fields to the log record."""
|
|
20
|
+
record.run_id = self.runtime_context.run_id
|
|
21
|
+
if self.runtime_context.trace_id:
|
|
22
|
+
record.trace_id = self.runtime_context.trace_id
|
|
23
|
+
if self.runtime_context.span_id:
|
|
24
|
+
record.span_id = self.runtime_context.span_id
|
|
25
|
+
return True
|
|
111
26
|
|
|
112
27
|
|
|
113
28
|
class Context:
|
|
114
29
|
"""
|
|
115
|
-
|
|
30
|
+
Base context providing common functionality.
|
|
116
31
|
|
|
117
|
-
Provides
|
|
118
|
-
-
|
|
119
|
-
-
|
|
120
|
-
-
|
|
121
|
-
|
|
122
|
-
|
|
32
|
+
Provides:
|
|
33
|
+
- Logging with correlation IDs
|
|
34
|
+
- Execution metadata (run_id, attempt)
|
|
35
|
+
- Runtime context for tracing
|
|
36
|
+
|
|
37
|
+
Extended by:
|
|
38
|
+
- FunctionContext: Minimal context for stateless functions
|
|
39
|
+
- WorkflowContext: Context for durable workflows
|
|
123
40
|
"""
|
|
124
41
|
|
|
125
42
|
def __init__(
|
|
126
43
|
self,
|
|
127
44
|
run_id: str,
|
|
128
|
-
component_type: str = "function",
|
|
129
|
-
step_id: Optional[str] = None,
|
|
130
45
|
attempt: int = 0,
|
|
131
|
-
|
|
132
|
-
method_name: Optional[str] = None,
|
|
133
|
-
completed_steps: Optional[Dict[str, Any]] = None, # Phase 6B: Replay support
|
|
134
|
-
initial_state: Optional[Dict[str, Any]] = None, # Phase 6B: State replay
|
|
46
|
+
runtime_context: Optional[Any] = None,
|
|
135
47
|
) -> None:
|
|
136
|
-
"""
|
|
48
|
+
"""
|
|
49
|
+
Initialize base context.
|
|
137
50
|
|
|
138
51
|
Args:
|
|
139
|
-
run_id: Unique
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
attempt: Retry attempt number (default 0)
|
|
143
|
-
object_id: Entity key for entity components (optional)
|
|
144
|
-
method_name: Entity method name (optional)
|
|
145
|
-
completed_steps: Pre-recorded steps for workflow replay (Phase 6B)
|
|
146
|
-
initial_state: Initial workflow state for replay (Phase 6B)
|
|
52
|
+
run_id: Unique execution identifier
|
|
53
|
+
attempt: Retry attempt number (0-indexed)
|
|
54
|
+
runtime_context: RuntimeContext for trace correlation
|
|
147
55
|
"""
|
|
148
56
|
self._run_id = run_id
|
|
149
|
-
self._component_type = component_type
|
|
150
|
-
self._step_id = step_id
|
|
151
57
|
self._attempt = attempt
|
|
152
|
-
self.
|
|
153
|
-
self._method_name = method_name
|
|
154
|
-
self._state: Dict[str, Any] = {}
|
|
155
|
-
self._checkpoints: Dict[str, Any] = {}
|
|
58
|
+
self._runtime_context = runtime_context
|
|
156
59
|
|
|
157
|
-
# Create logger with
|
|
60
|
+
# Create logger with correlation
|
|
158
61
|
self._logger = logging.getLogger(f"agnt5.{run_id}")
|
|
159
62
|
from ._telemetry import setup_context_logger
|
|
160
63
|
setup_context_logger(self._logger)
|
|
161
64
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
self._completed_steps: Dict[str, Any] = completed_steps or {} # Cache for replay (Phase 6B)
|
|
165
|
-
self._step_events: List[Dict[str, Any]] = [] # Collect events to publish
|
|
166
|
-
|
|
167
|
-
# Phase 6B: Track if we're replaying from cached steps
|
|
168
|
-
self._is_replay = len(self._completed_steps) > 0
|
|
169
|
-
if self._is_replay:
|
|
170
|
-
self._logger.info(f"🔄 Replaying workflow with {len(self._completed_steps)} cached steps")
|
|
171
|
-
|
|
172
|
-
# Phase 6B: Initialize state client with initial state for replay
|
|
173
|
-
if initial_state:
|
|
174
|
-
self._state_client = StateClient(self)
|
|
175
|
-
self._state_client._state = initial_state.copy()
|
|
176
|
-
self._logger.info(f"🔄 Loaded workflow state: {len(initial_state)} keys")
|
|
177
|
-
|
|
178
|
-
# === Execution Metadata ===
|
|
65
|
+
if runtime_context:
|
|
66
|
+
self._logger.addFilter(_CorrelationFilter(runtime_context))
|
|
179
67
|
|
|
180
68
|
@property
|
|
181
69
|
def run_id(self) -> str:
|
|
182
|
-
"""
|
|
70
|
+
"""Unique execution identifier."""
|
|
183
71
|
return self._run_id
|
|
184
72
|
|
|
185
|
-
@property
|
|
186
|
-
def step_id(self) -> Optional[str]:
|
|
187
|
-
"""Current step identifier."""
|
|
188
|
-
return self._step_id
|
|
189
|
-
|
|
190
73
|
@property
|
|
191
74
|
def attempt(self) -> int:
|
|
192
|
-
"""
|
|
75
|
+
"""Current retry attempt (0-indexed)."""
|
|
193
76
|
return self._attempt
|
|
194
77
|
|
|
195
|
-
@property
|
|
196
|
-
def component_type(self) -> str:
|
|
197
|
-
"""Component type: 'function', 'entity', 'workflow'."""
|
|
198
|
-
return self._component_type
|
|
199
|
-
|
|
200
|
-
@property
|
|
201
|
-
def object_id(self) -> Optional[str]:
|
|
202
|
-
"""Entity key (for entities only)."""
|
|
203
|
-
return self._object_id
|
|
204
|
-
|
|
205
|
-
@property
|
|
206
|
-
def method_name(self) -> Optional[str]:
|
|
207
|
-
"""Entity method name (for entities only)."""
|
|
208
|
-
return self._method_name
|
|
209
|
-
|
|
210
|
-
# === State Management ===
|
|
211
|
-
|
|
212
|
-
def get(self, key: str, default: Any = None) -> Any:
|
|
213
|
-
"""
|
|
214
|
-
Get value from state (synchronous).
|
|
215
|
-
|
|
216
|
-
For method-based entities (@entity.method):
|
|
217
|
-
count = ctx.get("count", 0)
|
|
218
|
-
"""
|
|
219
|
-
return self._state.get(key, default)
|
|
220
|
-
|
|
221
|
-
def set(self, key: str, value: Any) -> None:
|
|
222
|
-
"""
|
|
223
|
-
Set value in state (synchronous).
|
|
224
|
-
|
|
225
|
-
For method-based entities (@entity.method):
|
|
226
|
-
ctx.set("count", 5)
|
|
227
|
-
"""
|
|
228
|
-
self._state[key] = value
|
|
229
|
-
|
|
230
|
-
def delete(self, key: str) -> None:
|
|
231
|
-
"""
|
|
232
|
-
Delete key from state (synchronous).
|
|
233
|
-
|
|
234
|
-
For method-based entities (@entity.method):
|
|
235
|
-
ctx.delete("count")
|
|
236
|
-
"""
|
|
237
|
-
try:
|
|
238
|
-
del self._state[key]
|
|
239
|
-
except KeyError:
|
|
240
|
-
raise StateError(f"Key '{key}' not found in state")
|
|
241
|
-
|
|
242
|
-
async def get_async(self, key: str, default: Any = None) -> Any:
|
|
243
|
-
"""
|
|
244
|
-
Get value from state (async version for DurableEntity).
|
|
245
|
-
|
|
246
|
-
For class-based entities (DurableEntity):
|
|
247
|
-
count = await self.ctx.get_async("count", 0)
|
|
248
|
-
# Or use the alias: await self.ctx.get(...)
|
|
249
|
-
"""
|
|
250
|
-
return self._state.get(key, default)
|
|
251
|
-
|
|
252
|
-
async def set_async(self, key: str, value: Any) -> None:
|
|
253
|
-
"""
|
|
254
|
-
Set value in state (async version for DurableEntity).
|
|
255
|
-
|
|
256
|
-
For class-based entities (DurableEntity):
|
|
257
|
-
await self.ctx.set_async("count", 5)
|
|
258
|
-
# Or use the alias: await self.ctx.set(...)
|
|
259
|
-
"""
|
|
260
|
-
self._state[key] = value
|
|
261
|
-
|
|
262
|
-
async def delete_async(self, key: str) -> None:
|
|
263
|
-
"""
|
|
264
|
-
Delete key from state (async version for DurableEntity).
|
|
265
|
-
|
|
266
|
-
For class-based entities (DurableEntity):
|
|
267
|
-
await self.ctx.delete_async("count")
|
|
268
|
-
# Or use the alias: await self.ctx.delete(...)
|
|
269
|
-
"""
|
|
270
|
-
try:
|
|
271
|
-
del self._state[key]
|
|
272
|
-
except KeyError:
|
|
273
|
-
raise StateError(f"Key '{key}' not found in state")
|
|
274
|
-
|
|
275
|
-
async def clear_all(self) -> None:
|
|
276
|
-
"""Clear all state (async)."""
|
|
277
|
-
self._state.clear()
|
|
278
|
-
|
|
279
|
-
# === Checkpointing ===
|
|
280
|
-
|
|
281
|
-
async def step(self, name: str, func: Callable[[], Awaitable[T]]) -> T:
|
|
282
|
-
"""
|
|
283
|
-
Checkpoint expensive operations.
|
|
284
|
-
|
|
285
|
-
If function crashes, won't re-execute this step on retry.
|
|
286
|
-
"""
|
|
287
|
-
# Check if step already executed
|
|
288
|
-
if name in self._checkpoints:
|
|
289
|
-
return self._checkpoints[name]
|
|
290
|
-
|
|
291
|
-
# Execute and checkpoint
|
|
292
|
-
result = await func()
|
|
293
|
-
self._checkpoints[name] = result
|
|
294
|
-
return result
|
|
295
|
-
|
|
296
|
-
# === Orchestration (Workflows Only) ===
|
|
297
|
-
|
|
298
|
-
async def task(
|
|
299
|
-
self,
|
|
300
|
-
service_name: str,
|
|
301
|
-
handler_name: str,
|
|
302
|
-
input: Any = None,
|
|
303
|
-
) -> Any:
|
|
304
|
-
"""
|
|
305
|
-
Execute a function and wait for result (workflows only).
|
|
306
|
-
|
|
307
|
-
Phase 1: Calls local @function handlers directly.
|
|
308
|
-
Phase 6: Records step completions for workflow durability.
|
|
309
|
-
Phase 2: Will support distributed function execution across services.
|
|
310
|
-
|
|
311
|
-
Args:
|
|
312
|
-
service_name: Service name (ignored in Phase 1)
|
|
313
|
-
handler_name: Function handler name
|
|
314
|
-
input: Input data for the function
|
|
315
|
-
|
|
316
|
-
Returns:
|
|
317
|
-
Function result
|
|
318
|
-
|
|
319
|
-
Example:
|
|
320
|
-
```python
|
|
321
|
-
result = await ctx.task(
|
|
322
|
-
service_name="data-processor",
|
|
323
|
-
handler_name="process_data",
|
|
324
|
-
input={"data": [1, 2, 3]}
|
|
325
|
-
)
|
|
326
|
-
```
|
|
327
|
-
"""
|
|
328
|
-
from .function import FunctionRegistry
|
|
329
|
-
|
|
330
|
-
# Phase 6: Generate unique step name for durability
|
|
331
|
-
step_name = f"{handler_name}_{self._step_counter}"
|
|
332
|
-
self._step_counter += 1
|
|
333
|
-
|
|
334
|
-
# Phase 6B: Check if we're in replay mode (step already completed)
|
|
335
|
-
if step_name in self._completed_steps:
|
|
336
|
-
# REPLAY: Return cached result without re-executing
|
|
337
|
-
result = self._completed_steps[step_name]
|
|
338
|
-
self._logger.info(f"🔄 Replaying cached step: {step_name} (skipping execution)")
|
|
339
|
-
# Don't record again - already in database
|
|
340
|
-
return result
|
|
341
|
-
|
|
342
|
-
# NORMAL EXECUTION: Look up function in registry
|
|
343
|
-
self._logger.info(f"▶️ Executing new step: {step_name}")
|
|
344
|
-
func_config = FunctionRegistry.get(handler_name)
|
|
345
|
-
if func_config is None:
|
|
346
|
-
raise ValueError(f"Function '{handler_name}' not found in registry")
|
|
347
|
-
|
|
348
|
-
# Create child context for function execution
|
|
349
|
-
child_ctx = Context(
|
|
350
|
-
run_id=f"{self.run_id}:task:{handler_name}",
|
|
351
|
-
component_type="function",
|
|
352
|
-
)
|
|
353
|
-
|
|
354
|
-
# Execute function with input
|
|
355
|
-
if input is not None:
|
|
356
|
-
result = await func_config.handler(child_ctx, input)
|
|
357
|
-
else:
|
|
358
|
-
result = await func_config.handler(child_ctx)
|
|
359
|
-
|
|
360
|
-
# Phase 6: Record step completion for durability
|
|
361
|
-
self._record_step_completion(step_name, handler_name, input, result)
|
|
362
|
-
|
|
363
|
-
return result
|
|
364
|
-
|
|
365
|
-
def _record_step_completion(
|
|
366
|
-
self, step_name: str, handler_name: str, input: Any, result: Any
|
|
367
|
-
) -> None:
|
|
368
|
-
"""
|
|
369
|
-
Record step completion event for workflow durability (internal use).
|
|
370
|
-
|
|
371
|
-
Args:
|
|
372
|
-
step_name: Unique step identifier
|
|
373
|
-
handler_name: Function handler name
|
|
374
|
-
input: Input data passed to function
|
|
375
|
-
result: Function result
|
|
376
|
-
"""
|
|
377
|
-
self._step_events.append({
|
|
378
|
-
"step_name": step_name,
|
|
379
|
-
"handler_name": handler_name,
|
|
380
|
-
"input": input,
|
|
381
|
-
"result": result,
|
|
382
|
-
})
|
|
383
|
-
self._logger.debug(f"Recorded step completion: {step_name}")
|
|
384
|
-
|
|
385
|
-
async def parallel(self, *tasks: Awaitable[T]) -> List[T]:
|
|
386
|
-
"""
|
|
387
|
-
Run multiple tasks in parallel (workflows only).
|
|
388
|
-
|
|
389
|
-
Phase 1: Uses asyncio.gather() for in-process parallelism.
|
|
390
|
-
Phase 2: Will add distributed execution across services.
|
|
391
|
-
|
|
392
|
-
Args:
|
|
393
|
-
*tasks: Async tasks to run in parallel
|
|
394
|
-
|
|
395
|
-
Returns:
|
|
396
|
-
List of results in the same order as tasks
|
|
397
|
-
|
|
398
|
-
Example:
|
|
399
|
-
```python
|
|
400
|
-
result1, result2 = await ctx.parallel(
|
|
401
|
-
fetch_data(source1),
|
|
402
|
-
fetch_data(source2)
|
|
403
|
-
)
|
|
404
|
-
```
|
|
405
|
-
"""
|
|
406
|
-
import asyncio
|
|
407
|
-
return list(await asyncio.gather(*tasks))
|
|
408
|
-
|
|
409
|
-
async def gather(self, **tasks: Awaitable[T]) -> Dict[str, T]:
|
|
410
|
-
"""
|
|
411
|
-
Run tasks in parallel with named results (workflows only).
|
|
412
|
-
|
|
413
|
-
Phase 1: Uses asyncio.gather() for in-process parallelism.
|
|
414
|
-
Phase 2: Will add distributed execution across services.
|
|
415
|
-
|
|
416
|
-
Args:
|
|
417
|
-
**tasks: Named async tasks to run in parallel
|
|
418
|
-
|
|
419
|
-
Returns:
|
|
420
|
-
Dictionary mapping names to results
|
|
421
|
-
|
|
422
|
-
Example:
|
|
423
|
-
```python
|
|
424
|
-
results = await ctx.gather(
|
|
425
|
-
db=query_database(),
|
|
426
|
-
api=fetch_api(),
|
|
427
|
-
cache=check_cache()
|
|
428
|
-
)
|
|
429
|
-
# Access: results["db"], results["api"], results["cache"]
|
|
430
|
-
```
|
|
431
|
-
"""
|
|
432
|
-
import asyncio
|
|
433
|
-
keys = list(tasks.keys())
|
|
434
|
-
values = list(tasks.values())
|
|
435
|
-
results = await asyncio.gather(*values)
|
|
436
|
-
return dict(zip(keys, results))
|
|
437
|
-
|
|
438
|
-
def spawn(self, handler: Callable[..., Awaitable[T]], *args: Any, **kwargs: Any) -> Any:
|
|
439
|
-
"""
|
|
440
|
-
Spawn a child function without waiting.
|
|
441
|
-
|
|
442
|
-
Raises:
|
|
443
|
-
AGNT5NotImplementedError: Not yet implemented in Phase 1
|
|
444
|
-
"""
|
|
445
|
-
raise AGNT5NotImplementedError(
|
|
446
|
-
"ctx.spawn() will be implemented in Phase 2 with platform integration"
|
|
447
|
-
)
|
|
448
|
-
|
|
449
|
-
# === Coordination ===
|
|
450
|
-
|
|
451
|
-
async def signal(
|
|
452
|
-
self,
|
|
453
|
-
name: str,
|
|
454
|
-
timeout_ms: Optional[int] = None,
|
|
455
|
-
default: Any = None,
|
|
456
|
-
) -> Any:
|
|
457
|
-
"""
|
|
458
|
-
Wait for external signal.
|
|
459
|
-
|
|
460
|
-
Phase 1: Uses asyncio.Event for in-process signaling.
|
|
461
|
-
Phase 2: Will add durable cross-process signal support.
|
|
462
|
-
|
|
463
|
-
Args:
|
|
464
|
-
name: Signal name to wait for
|
|
465
|
-
timeout_ms: Optional timeout in milliseconds
|
|
466
|
-
default: Default value if timeout occurs
|
|
467
|
-
|
|
468
|
-
Returns:
|
|
469
|
-
Signal payload or default value
|
|
470
|
-
|
|
471
|
-
Example:
|
|
472
|
-
```python
|
|
473
|
-
# In one task:
|
|
474
|
-
await ctx.signal_send("approval", {"approved": True})
|
|
475
|
-
|
|
476
|
-
# In another task:
|
|
477
|
-
result = await ctx.signal("approval", timeout_ms=5000)
|
|
478
|
-
```
|
|
479
|
-
"""
|
|
480
|
-
import asyncio
|
|
481
|
-
|
|
482
|
-
# Get or create event for this signal
|
|
483
|
-
if not hasattr(self, "_signals"):
|
|
484
|
-
self._signals: Dict[str, tuple[asyncio.Event, Any]] = {}
|
|
485
|
-
|
|
486
|
-
if name not in self._signals:
|
|
487
|
-
event = asyncio.Event()
|
|
488
|
-
self._signals[name] = (event, None)
|
|
489
|
-
|
|
490
|
-
event, payload = self._signals[name]
|
|
491
|
-
|
|
492
|
-
# Wait for signal with optional timeout
|
|
493
|
-
try:
|
|
494
|
-
if timeout_ms:
|
|
495
|
-
await asyncio.wait_for(event.wait(), timeout=timeout_ms / 1000)
|
|
496
|
-
else:
|
|
497
|
-
await event.wait()
|
|
498
|
-
return self._signals[name][1] # Return payload
|
|
499
|
-
except asyncio.TimeoutError:
|
|
500
|
-
return default
|
|
501
|
-
|
|
502
|
-
def signal_send(self, name: str, payload: Any = None) -> None:
|
|
503
|
-
"""
|
|
504
|
-
Send a signal to waiting tasks.
|
|
505
|
-
|
|
506
|
-
Phase 1: Uses asyncio.Event for in-process signaling.
|
|
507
|
-
Phase 2: Will add durable cross-process signal support.
|
|
508
|
-
|
|
509
|
-
Args:
|
|
510
|
-
name: Signal name
|
|
511
|
-
payload: Data to send with signal
|
|
512
|
-
|
|
513
|
-
Example:
|
|
514
|
-
```python
|
|
515
|
-
# Send approval signal
|
|
516
|
-
ctx.signal_send("approval", {"approved": True, "by": "admin"})
|
|
517
|
-
```
|
|
518
|
-
"""
|
|
519
|
-
import asyncio
|
|
520
|
-
|
|
521
|
-
# Get or create event for this signal
|
|
522
|
-
if not hasattr(self, "_signals"):
|
|
523
|
-
self._signals: Dict[str, tuple[asyncio.Event, Any]] = {}
|
|
524
|
-
|
|
525
|
-
if name not in self._signals:
|
|
526
|
-
event = asyncio.Event()
|
|
527
|
-
self._signals[name] = (event, payload)
|
|
528
|
-
else:
|
|
529
|
-
# Update payload and set event
|
|
530
|
-
event = self._signals[name][0]
|
|
531
|
-
self._signals[name] = (event, payload)
|
|
532
|
-
|
|
533
|
-
event.set()
|
|
534
|
-
|
|
535
|
-
async def timer(
|
|
536
|
-
self,
|
|
537
|
-
delay_ms: Optional[int] = None,
|
|
538
|
-
cron: Optional[str] = None,
|
|
539
|
-
) -> None:
|
|
540
|
-
"""
|
|
541
|
-
Wait for delay or scheduled time.
|
|
542
|
-
|
|
543
|
-
Phase 1: Supports delay_ms using asyncio.sleep (cron not supported).
|
|
544
|
-
Phase 2: Will add durable timers and cron scheduling.
|
|
545
|
-
|
|
546
|
-
Args:
|
|
547
|
-
delay_ms: Delay in milliseconds
|
|
548
|
-
cron: Cron expression (Phase 2 only)
|
|
549
|
-
|
|
550
|
-
Example:
|
|
551
|
-
```python
|
|
552
|
-
await ctx.timer(delay_ms=5000) # Wait 5 seconds
|
|
553
|
-
```
|
|
554
|
-
"""
|
|
555
|
-
if cron is not None:
|
|
556
|
-
raise AGNT5NotImplementedError(
|
|
557
|
-
"ctx.timer(cron=...) will be implemented in Phase 2 with cron support"
|
|
558
|
-
)
|
|
559
|
-
|
|
560
|
-
if delay_ms is not None:
|
|
561
|
-
import asyncio
|
|
562
|
-
await asyncio.sleep(delay_ms / 1000)
|
|
563
|
-
|
|
564
|
-
async def sleep(self, seconds: int) -> None:
|
|
565
|
-
"""
|
|
566
|
-
Durable sleep (alternative to timer).
|
|
567
|
-
|
|
568
|
-
Phase 1: Uses asyncio.sleep for in-process delays.
|
|
569
|
-
Phase 2: Will add durable sleep that survives restarts.
|
|
570
|
-
|
|
571
|
-
Args:
|
|
572
|
-
seconds: Number of seconds to sleep
|
|
573
|
-
|
|
574
|
-
Example:
|
|
575
|
-
```python
|
|
576
|
-
await ctx.sleep(5) # Sleep 5 seconds
|
|
577
|
-
```
|
|
578
|
-
"""
|
|
579
|
-
import asyncio
|
|
580
|
-
await asyncio.sleep(seconds)
|
|
581
|
-
|
|
582
|
-
# === Observability ===
|
|
583
|
-
|
|
584
|
-
def log(self) -> logging.Logger:
|
|
585
|
-
"""Get structured logger for this context."""
|
|
586
|
-
return self._logger
|
|
587
|
-
|
|
588
78
|
@property
|
|
589
79
|
def logger(self) -> logging.Logger:
|
|
590
|
-
"""
|
|
80
|
+
"""Full logger for .debug(), .warning(), .error(), etc."""
|
|
591
81
|
return self._logger
|
|
592
82
|
|
|
593
|
-
def metrics(self) -> "MetricsClient":
|
|
594
|
-
"""
|
|
595
|
-
Get metrics client.
|
|
596
|
-
|
|
597
|
-
Raises:
|
|
598
|
-
AGNT5NotImplementedError: Not yet implemented in Phase 1
|
|
599
|
-
"""
|
|
600
|
-
raise AGNT5NotImplementedError(
|
|
601
|
-
"ctx.metrics() will be implemented in Phase 2 with observability support"
|
|
602
|
-
)
|
|
603
|
-
|
|
604
|
-
def trace_span(self) -> "TraceSpan":
|
|
605
|
-
"""
|
|
606
|
-
Create trace span.
|
|
607
|
-
|
|
608
|
-
Raises:
|
|
609
|
-
AGNT5NotImplementedError: Not yet implemented in Phase 1
|
|
610
|
-
"""
|
|
611
|
-
raise AGNT5NotImplementedError(
|
|
612
|
-
"ctx.trace_span() will be implemented in Phase 2 with observability support"
|
|
613
|
-
)
|
|
614
|
-
|
|
615
|
-
# === AI Integration ===
|
|
616
|
-
|
|
617
|
-
@property
|
|
618
|
-
def llm(self) -> "LLMClient":
|
|
619
|
-
"""
|
|
620
|
-
Get LLM client for generate/stream.
|
|
621
|
-
|
|
622
|
-
Raises:
|
|
623
|
-
AGNT5NotImplementedError: Not yet implemented in Phase 1
|
|
624
|
-
"""
|
|
625
|
-
raise AGNT5NotImplementedError(
|
|
626
|
-
"ctx.llm will be implemented in Phase 2 with LLM support"
|
|
627
|
-
)
|
|
628
|
-
|
|
629
|
-
@property
|
|
630
|
-
def tools(self) -> "ToolsClient":
|
|
631
|
-
"""
|
|
632
|
-
Get tools client for registration.
|
|
633
|
-
|
|
634
|
-
Raises:
|
|
635
|
-
AGNT5NotImplementedError: Not yet implemented in Phase 1
|
|
636
|
-
"""
|
|
637
|
-
raise AGNT5NotImplementedError(
|
|
638
|
-
"ctx.tools will be implemented in Phase 2 with tools support"
|
|
639
|
-
)
|
|
640
|
-
|
|
641
|
-
# === Memory (Alternative State API) ===
|
|
642
|
-
|
|
643
|
-
@property
|
|
644
|
-
def state(self) -> StateClient:
|
|
645
|
-
"""
|
|
646
|
-
Get state client for durable workflow state.
|
|
647
|
-
|
|
648
|
-
Available for workflows only. Provides key-value state storage
|
|
649
|
-
that persists across workflow steps and survives crashes.
|
|
650
|
-
|
|
651
|
-
Returns:
|
|
652
|
-
StateClient instance
|
|
653
|
-
|
|
654
|
-
Example:
|
|
655
|
-
```python
|
|
656
|
-
@workflow
|
|
657
|
-
async def my_workflow(ctx: Context):
|
|
658
|
-
await ctx.state.set("status", "processing")
|
|
659
|
-
result = await ctx.task("service", "process", input={})
|
|
660
|
-
status = await ctx.state.get("status")
|
|
661
|
-
return {"status": status}
|
|
662
|
-
```
|
|
663
|
-
"""
|
|
664
|
-
if not hasattr(self, '_state_client'):
|
|
665
|
-
self._state_client = StateClient(self)
|
|
666
|
-
return self._state_client
|
|
667
|
-
|
|
668
|
-
@property
|
|
669
|
-
def memory(self) -> "MemoryClient":
|
|
670
|
-
"""
|
|
671
|
-
Get memory client for durable state operations.
|
|
672
|
-
|
|
673
|
-
Raises:
|
|
674
|
-
AGNT5NotImplementedError: Not yet implemented in Phase 1
|
|
675
|
-
"""
|
|
676
|
-
raise AGNT5NotImplementedError(
|
|
677
|
-
"ctx.memory will be implemented in Phase 2 with memory support"
|
|
678
|
-
)
|
|
679
|
-
|
|
680
|
-
# === Configuration & Secrets ===
|
|
681
|
-
|
|
682
|
-
def secrets(self) -> "SecretsClient":
|
|
683
|
-
"""
|
|
684
|
-
Access secrets securely.
|
|
685
|
-
|
|
686
|
-
Raises:
|
|
687
|
-
AGNT5NotImplementedError: Not yet implemented in Phase 1
|
|
688
|
-
"""
|
|
689
|
-
raise AGNT5NotImplementedError(
|
|
690
|
-
"ctx.secrets() will be implemented in Phase 2 with secrets support"
|
|
691
|
-
)
|
|
692
|
-
|
|
693
|
-
def config(self) -> "ConfigClient":
|
|
694
|
-
"""
|
|
695
|
-
Access configuration and feature flags.
|
|
696
|
-
|
|
697
|
-
Raises:
|
|
698
|
-
AGNT5NotImplementedError: Not yet implemented in Phase 1
|
|
699
|
-
"""
|
|
700
|
-
raise AGNT5NotImplementedError(
|
|
701
|
-
"ctx.config() will be implemented in Phase 2 with config support"
|
|
702
|
-
)
|
|
703
|
-
|
|
704
|
-
def headers(self) -> Dict[str, str]:
|
|
705
|
-
"""
|
|
706
|
-
Access request headers.
|
|
707
|
-
|
|
708
|
-
Raises:
|
|
709
|
-
AGNT5NotImplementedError: Not yet implemented in Phase 1
|
|
710
|
-
"""
|
|
711
|
-
raise AGNT5NotImplementedError(
|
|
712
|
-
"ctx.headers() will be implemented in Phase 2 with request context"
|
|
713
|
-
)
|
|
714
|
-
|
|
715
|
-
# === Messaging ===
|
|
716
|
-
|
|
717
|
-
async def send_to(
|
|
718
|
-
self,
|
|
719
|
-
target: str,
|
|
720
|
-
message: Any,
|
|
721
|
-
metadata: Optional[Dict[str, Any]] = None,
|
|
722
|
-
) -> str:
|
|
723
|
-
"""
|
|
724
|
-
Send message to another participant.
|
|
725
|
-
|
|
726
|
-
Raises:
|
|
727
|
-
AGNT5NotImplementedError: Not yet implemented in Phase 1
|
|
728
|
-
"""
|
|
729
|
-
raise AGNT5NotImplementedError(
|
|
730
|
-
"ctx.send_to() will be implemented in Phase 2 with messaging support"
|
|
731
|
-
)
|
|
732
|
-
|
|
733
|
-
async def subscribe(self, participant_id: str) -> Any:
|
|
734
|
-
"""
|
|
735
|
-
Subscribe to messages.
|
|
736
|
-
|
|
737
|
-
Raises:
|
|
738
|
-
AGNT5NotImplementedError: Not yet implemented in Phase 1
|
|
739
|
-
"""
|
|
740
|
-
raise AGNT5NotImplementedError(
|
|
741
|
-
"ctx.subscribe() will be implemented in Phase 2 with messaging support"
|
|
742
|
-
)
|
|
743
|
-
|
|
744
|
-
# === Entity Methods ===
|
|
745
|
-
|
|
746
|
-
def entity(self, entity_type: str, key: str) -> "EntityProxy":
|
|
747
|
-
"""
|
|
748
|
-
Get entity proxy for method calls.
|
|
749
|
-
|
|
750
|
-
Raises:
|
|
751
|
-
AGNT5NotImplementedError: Entities are Phase 2 (Q1 2025)
|
|
752
|
-
"""
|
|
753
|
-
raise AGNT5NotImplementedError(
|
|
754
|
-
"ctx.entity() will be implemented in Phase 2 - Entities (Q1 2025)"
|
|
755
|
-
)
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
# Placeholder classes for type hints
|
|
759
|
-
class MetricsClient:
|
|
760
|
-
"""Placeholder for metrics client."""
|
|
761
|
-
|
|
762
|
-
pass
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
class TraceSpan:
|
|
766
|
-
"""Placeholder for trace span."""
|
|
767
|
-
|
|
768
|
-
pass
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
class LLMClient:
|
|
772
|
-
"""Placeholder for LLM client."""
|
|
773
|
-
|
|
774
|
-
pass
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
class ToolsClient:
|
|
778
|
-
"""Placeholder for tools client."""
|
|
779
|
-
|
|
780
|
-
pass
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
class MemoryClient:
|
|
784
|
-
"""Placeholder for memory client."""
|
|
785
|
-
|
|
786
|
-
pass
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
class SecretsClient:
|
|
790
|
-
"""Placeholder for secrets client."""
|
|
791
|
-
|
|
792
|
-
pass
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
class ConfigClient:
|
|
796
|
-
"""Placeholder for config client."""
|
|
797
|
-
|
|
798
|
-
pass
|
|
799
|
-
|
|
800
83
|
|
|
801
|
-
class EntityProxy:
|
|
802
|
-
"""Placeholder for entity proxy."""
|
|
803
84
|
|
|
804
|
-
pass
|